1use crate::draw::{parse_svg_animations, usvg_to_lyon};
3use crate::heim::SundrPacker;
4use crate::kvasir;
5use crate::types::*;
6use crate::vertex::*;
7use crate::{
8 WGSL_BIFROST, WGSL_BLOOM, WGSL_COLOR_BLIND, WGSL_COMMON, WGSL_MATERIAL_GLASS,
9 WGSL_MATERIAL_OPAQUE, WGSL_PARTICLES, WGSL_SHAPES, WGSL_TONEMAP,
10};
11use bytemuck;
12use cvkg_core::Rect;
13use cvkg_core::Renderer;
14use cvkg_core::{ColorTheme, SceneUniforms};
15use lru::LruCache;
16use lyon::tessellation::{
17 BuffersBuilder, FillOptions, FillTessellator, StrokeOptions, StrokeTessellator, VertexBuffers,
18};
19use std::collections::VecDeque;
20use std::num::NonZeroUsize;
21
22pub(crate) mod material_id {
25 pub const OPAQUE: u32 = 0;
27 pub const ELLIPSE: u32 = 4;
29 pub const TOP_UI: u32 = 6;
31 pub const GLASS: u32 = 7;
33 pub const BLEND_START: u32 = 8;
35 pub const BLEND_END: u32 = 22;
36 pub const RADIAL_GRADIENT: u32 = 16;
38 pub const SQUIRCLE_STROKE: u32 = 17;
40 pub const DROP_SHADOW: u32 = 18;
42 pub const DASHED_STROKE: u32 = 19;
44 pub const MESH_3D: u32 = 21;
46}
47use std::sync::Arc;
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum QualityLevel {
56 High,
57 Medium,
58 Low,
59}
60
61impl QualityLevel {
62 pub fn msaa_sample_count(self) -> u32 {
64 match self {
65 QualityLevel::High => 4,
66 QualityLevel::Medium => 2,
67 QualityLevel::Low => 1,
68 }
69 }
70}
71
72impl Default for QualityLevel {
73 fn default() -> Self {
74 QualityLevel::High
75 }
76}
77
78pub struct SurtrRenderer {
92 pub(crate) instance: Arc<wgpu::Instance>,
93 pub(crate) adapter: Arc<wgpu::Adapter>,
94 pub(crate) device: Arc<wgpu::Device>,
95 pub(crate) queue: Arc<wgpu::Queue>,
96
97 pub(crate) registry: crate::kvasir::registry::ResourceRegistry,
99
100 pub(crate) active_offscreens: Vec<crate::types::OffscreenEffectConfig>,
101 pub(crate) effect_pipelines: std::collections::HashMap<String, wgpu::RenderPipeline>,
102 pub(crate) effect_params_buffer: wgpu::Buffer,
103 pub(crate) effect_params_bind_group: wgpu::BindGroup,
104 pub(crate) linear_sampler: wgpu::Sampler,
105 pub ai_material_rx: Option<
107 std::sync::mpsc::Receiver<
108 Result<crate::material::CompiledMaterial, crate::ai::GeneratorError>,
109 >,
110 >,
111
112 pub(crate) surfaces: std::collections::HashMap<winit::window::WindowId, SurfaceContext>,
114 pub(crate) current_window: Option<winit::window::WindowId>,
115 pub headless_context: Option<HeadlessContext>,
116
117 pub(crate) text: crate::types::TextSubsystem,
122 pub(crate) mega_heim_tex: wgpu::Texture,
123 pub(crate) mega_heim_bind_group: wgpu::BindGroup,
124 pub(crate) heim_packer: SundrPacker,
125 pub(crate) image_uv_registry: LruCache<String, Rect>,
126 pub(crate) texture_registry: LruCache<String, u32>,
127 pub(crate) texture_views: Vec<wgpu::TextureView>,
128 pub(crate) dummy_sampler: wgpu::Sampler,
129 pub(crate) svg: crate::types::SvgSubsystem,
133
134 pub(crate) dummy_texture_bind_group: wgpu::BindGroup,
136 pub(crate) dummy_env_bind_group: wgpu::BindGroup,
137 pub(crate) texture_bind_group_layout: wgpu::BindGroupLayout,
138 pub(crate) texture_bind_groups: Vec<wgpu::BindGroup>,
139 pub(crate) shared_elements: LruCache<String, cvkg_core::Rect>,
140
141 pub(crate) geometry_buffers: crate::types::GeometryBuffers,
147 pub(crate) vertices: Vec<Vertex>,
148 pub(crate) indices: Vec<u32>,
149 pub(crate) instance_data: Vec<InstanceData>,
150 pub(crate) staging_belt: wgpu::util::StagingBelt,
151 pub(crate) staging_command_buffers: Vec<wgpu::CommandBuffer>,
152 pub(crate) draw_calls: Vec<DrawCall>,
153 pub(crate) current_texture_id: Option<u32>,
154
155 pub(crate) opacity_stack: Vec<f32>,
157 pub(crate) clip_stack: Vec<Rect>,
158 pub(crate) slice_stack: Vec<(f32, f32)>,
159 pub(crate) shadow_stack: Vec<ShadowState>,
160
161 pub(crate) theme_buffer: wgpu::Buffer,
163 pub(crate) scene_buffer: wgpu::Buffer,
164 pub(crate) berserker_bind_group: wgpu::BindGroup,
165 pub(crate) berserker_bind_group_layout: wgpu::BindGroupLayout,
166 pub(crate) start_time: std::time::Instant,
167 pub(crate) current_theme: ColorTheme,
168 pub(crate) current_scene: SceneUniforms,
169 pub(crate) current_z: f32,
170
171 pub(crate) default_background_color: [f32; 4],
175
176 pub(crate) app_drew_background: bool,
179
180 pub(crate) frame_rendered: bool,
183
184 pub(crate) current_draw_order: i32,
187
188 pub(crate) pipeline: wgpu::RenderPipeline,
190 pub(crate) opaque_pipeline: wgpu::RenderPipeline,
192 pub(crate) ui_pipeline: wgpu::RenderPipeline,
195 pub(crate) glass_pipeline: wgpu::RenderPipeline,
197 pub(crate) background_pipeline: wgpu::RenderPipeline,
198 pub(crate) bloom_extract_pipeline: wgpu::RenderPipeline,
199 pub(crate) copy_pipeline: wgpu::RenderPipeline,
201 pub(crate) composite_pipeline: wgpu::RenderPipeline,
202 pub(crate) color_blind_pipeline: wgpu::RenderPipeline,
204 pub(crate) volumetric_pipeline: wgpu::RenderPipeline,
206 pub(crate) volumetric_bind_group_layout: wgpu::BindGroupLayout,
208 pub(crate) volumetric_uniform_buffer: wgpu::Buffer,
210 pub(crate) volumetric_depth_sampler: wgpu::Sampler,
212 pub(crate) hologram_instances: Vec<HologramInstance>,
215 pub(crate) kawase_down_pipeline: wgpu::RenderPipeline,
217 pub(crate) kawase_up_pipeline: wgpu::RenderPipeline,
219 pub(crate) kawase_bind_group_layout: wgpu::BindGroupLayout,
221 pub(crate) kawase_uniform: wgpu::Buffer,
223 pub(crate) kawase_uniform_buffers: Vec<wgpu::Buffer>,
225 pub(crate) env_bind_group_layout: wgpu::BindGroupLayout,
227
228 pub telemetry: cvkg_core::TelemetryData,
230
231 pub(crate) pipeline_cache: Option<wgpu::PipelineCache>,
234
235 pub frame_budget: cvkg_core::FrameBudget,
237 pub(crate) capture_staging_buffer: Option<wgpu::Buffer>,
239 pub last_redraw_start: std::time::Instant,
241 pub last_frame_start: std::time::Instant,
243
244 pub(crate) vram_buffers_bytes: u64,
246 pub(crate) vram_textures_bytes: u64,
247
248 pub(crate) _debug_layout: bool,
250
251 pub(crate) transform_stack: Vec<glam::Mat3>,
253 pub redraw_requested: bool,
255 pub(crate) compositor_index_cursor: u32,
257
258 pub bloom_enabled: bool,
260 pub volumetric_enabled: bool,
262
263 pub(crate) path_geometry_cache: lru::LruCache<u64, (Vec<Vertex>, Vec<u32>)>,
269 pub(crate) color_blind_bind_group_layout: wgpu::BindGroupLayout,
271 pub(crate) color_blind_uniform_buffer: wgpu::Buffer,
273 pub color_blind_mode: crate::color_blindness::ColorBlindMode,
275 pub color_blind_intensity: f32,
277 pub(crate) sampler: wgpu::Sampler,
279
280 pub(crate) skuld_queries: Option<wgpu::QuerySet>,
282 pub(crate) skuld_buffer: Option<wgpu::Buffer>,
283 pub(crate) skuld_read_buffer: Option<wgpu::Buffer>,
284 pub(crate) skuld_period: f32,
285 pub last_gpu_time_ns: u64,
286
287 pub(crate) particle_compute_pipeline: wgpu::ComputePipeline,
290 pub(crate) particle_compute_bgl: wgpu::BindGroupLayout,
292 pub(crate) particle_buffer: wgpu::Buffer,
294 pub(crate) particle_uniform_buffer: wgpu::Buffer,
296 pub(crate) particles: crate::types::ParticleSubsystem,
301 pub(crate) particle_render_pipeline: wgpu::RenderPipeline,
303 pub(crate) particle_render_bgl: wgpu::BindGroupLayout,
305 pub(crate) particle_render_bind_group: Option<wgpu::BindGroup>,
307 pub(crate) particle_compute_bind_group: Option<wgpu::BindGroup>,
309
310 pub(crate) vnode_stack: Vec<(Rect, &'static str)>,
312
313 pub(crate) event_handlers: std::collections::HashMap<
316 String,
317 Vec<std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>>,
318 >,
319
320 pub(crate) glass_output_bind_group_layout: wgpu::BindGroupLayout,
322 pub(crate) current_draw_material: cvkg_core::DrawMaterial,
324
325 pub(crate) portal_regions: std::collections::VecDeque<cvkg_core::Rect>,
328
329 pub(crate) cached_graph_plan: Option<kvasir::graph_cache::CachedGraphPlan>,
332 pub(crate) material_compilation_hash: u64,
338 pub(crate) memo_cache: std::collections::HashMap<u64, crate::types::MemoEntry>,
341 pub(crate) frame_generation: u64,
344 pub(crate) config: crate::subsystems::SurtrConfig,
349 pub(crate) quality_level: QualityLevel,
353 pub(crate) bind_group_cache: std::sync::Mutex<
356 std::collections::HashMap<
357 (crate::kvasir::resource::ResourceId, u32, bool),
358 wgpu::BindGroup,
359 >,
360 >,
361 pub(crate) texture_view_cache: std::sync::Mutex<
364 std::collections::HashMap<(crate::kvasir::resource::ResourceId, u32), wgpu::TextureView>,
365 >,
366}
367
368#[cfg(target_arch = "wasm32")]
424unsafe impl Send for SurtrRenderer {}
425#[cfg(target_arch = "wasm32")]
426unsafe impl Sync for SurtrRenderer {}
427
428pub(crate) struct TessellateParams<'a> {
430 fill_tessellator: &'a mut FillTessellator,
431 stroke_tessellator: &'a mut StrokeTessellator,
432 vertices: &'a mut Vec<Vertex>,
433 indices: &'a mut Vec<u32>,
434 parsed_animations: &'a [SvgAnimation],
435 finalized_animations: &'a mut Vec<SvgAnimation>,
436 paths: &'a mut Vec<crate::types::SvgPath>,
437}
438
439#[derive(Debug, Clone)]
442pub struct HologramInstance {
443 pub rect: cvkg_core::Rect,
445 pub id_hash: u32,
447 pub time: f32,
449}
450
451pub trait ClearInto {
458 fn clear_into(&mut self);
459}
460
461impl<K, V, S> ClearInto for std::collections::HashMap<K, V, S>
462where
463 S: std::hash::BuildHasher,
464{
465 fn clear_into(&mut self) {
466 self.clear();
467 }
468}
469
470impl<T> ClearInto for Vec<T> {
471 fn clear_into(&mut self) {
472 self.clear();
473 }
474}
475
476fn load_pipeline_cache_with_integrity_check(
494 cache_path: &std::path::Path,
495) -> Result<Option<Vec<u8>>, String> {
496 let cache_data = match std::fs::read(cache_path) {
498 Ok(d) => d,
499 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
500 Err(e) => return Err(format!("read failed: {e}")),
501 };
502
503 let hash_path = cache_path.with_extension("bin.sha256");
504 let expected_hash = match std::fs::read_to_string(&hash_path) {
505 Ok(s) => s.trim().to_lowercase(),
506 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
507 return Err(format!(
508 "sidecar hash file missing at {}",
509 hash_path.display()
510 ))
511 }
512 Err(e) => return Err(format!("sidecar read failed: {e}")),
513 };
514
515 let actual = compute_sha256(&cache_data);
517 let actual_hex = format!(
518 "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
519 actual[0], actual[1], actual[2], actual[3],
520 actual[4], actual[5], actual[6], actual[7]
521 );
522
523 if actual_hex != expected_hash {
524 return Err(format!(
525 "hash mismatch: expected {expected_hash}, got {actual_hex}"
526 ));
527 }
528
529 Ok(Some(cache_data))
530}
531
532fn compute_sha256(data: &[u8]) -> [u8; 32] {
535 let mut hasher = Sha256::new();
536 hasher.update(data);
537 hasher.finalize()
538}
539
540fn compute_mip_levels(width: u32, height: u32) -> u32 {
547 let max_dim = width.max(height);
548 if max_dim <= 1 {
549 return 1;
550 }
551 let mips = (32 - max_dim.leading_zeros()).clamp(2, 8);
553 mips
554}
555
556impl SurtrRenderer {
557 pub fn hologram_instances(&self) -> &[HologramInstance] {
559 &self.hologram_instances
560 }
561
562 pub fn set_quality_level(&mut self, level: QualityLevel) {
573 self.quality_level = level;
574 }
575
576 pub fn set_config(&mut self, config: crate::subsystems::SurtrConfig) {
587 self.config = config;
588 }
589
590 pub fn config(&self) -> &crate::subsystems::SurtrConfig {
592 &self.config
593 }
594
595 pub fn quality_level(&self) -> QualityLevel {
597 self.quality_level
598 }
599
600 pub(crate) fn lock_or_clear_cache<'a, T: ClearInto>(
613 mutex: &'a std::sync::Mutex<T>,
614 ) -> std::sync::MutexGuard<'a, T> {
615 match mutex.lock() {
616 Ok(g) => g,
617 Err(poisoned) => {
618 log::warn!(
619 "[GPU] poisoned cache mutex recovered; clearing data to avoid stale state"
620 );
621 let mut g = poisoned.into_inner();
622 g.clear_into();
623 g
624 }
625 }
626 }
627
628 pub fn update_mouse(&mut self, mouse: [f32; 2], velocity: [f32; 2]) {
634 self.current_scene.mouse = mouse;
635 self.current_scene.mouse_velocity = velocity;
636 }
637
638 pub fn invalidate_material_cache(&mut self) {
646 self.material_compilation_hash = self.material_compilation_hash.wrapping_add(1);
650 }
651
652 pub fn invalidate_all_caches(&mut self) -> usize {
675 let mut total = 0;
676
677 total += self.text.shaped_cache.len();
680 self.text.shaped_cache.clear();
681 self.svg.clear_filter_batches();
686 total += self.image_uv_registry.len();
692 self.image_uv_registry.clear();
693
694 total += self.texture_registry.len();
696 self.texture_registry.clear();
697 total += self.shared_elements.len();
706 self.shared_elements.clear();
707
708 log::info!(
709 "[Surtr] invalidate_all_caches: cleared {} entries across all caches",
710 total
711 );
712 total
713 }
714
715 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 Some(shaped) = self.text.engine.shape_layout(
735 &spans,
736 None,
737 cvkg_runic_text::TextAlign::Start,
738 cvkg_runic_text::TextOverflow::Visible,
739 ).ok() {
740 self.text.shaped_cache.put(cache_key, std::sync::Arc::new(shaped));
741 count += 1;
742 }
743 }
744 if count > 0 {
745 log::info!("[Surtr] prewarm_text_cache: pre-shaped {} labels", count);
746 }
747 }
748
749 pub(crate) fn select_best_surface_format(
753 formats: &[wgpu::TextureFormat],
754 ) -> wgpu::TextureFormat {
755 if formats.is_empty() {
756 return wgpu::TextureFormat::Rgba8Unorm;
759 }
760 let preferred_formats = [
771 wgpu::TextureFormat::Rgba16Float, wgpu::TextureFormat::Rgba8Unorm, wgpu::TextureFormat::Bgra8UnormSrgb,
774 wgpu::TextureFormat::Rgba8UnormSrgb,
775 wgpu::TextureFormat::Bgra8Unorm,
777 wgpu::TextureFormat::Rgba8Unorm,
778 wgpu::TextureFormat::Rgba8Unorm,
780 ];
781 for preferred in &preferred_formats {
782 if formats.contains(preferred) {
783 return *preferred;
784 }
785 }
786 if formats.contains(&wgpu::TextureFormat::Rgba8Unorm) {
792 return wgpu::TextureFormat::Rgba8Unorm;
793 }
794 formats[0]
795 }
796
797 pub async fn forge(window: Arc<winit::window::Window>) -> Self {
804 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
805 backends: wgpu::Backends::all(),
806 flags: wgpu::InstanceFlags::default(),
807 backend_options: wgpu::BackendOptions::default(),
808 display: None,
809 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
810 });
811
812 let surface = instance
813 .create_surface(window.clone())
814 .expect("Failed to create surface");
815
816 log::info!("[GPU] Requesting HighPerformance adapter...");
818
819 let mut adapter = None;
820
821 #[cfg(not(target_arch = "wasm32"))]
822 if let Ok(filter) = std::env::var("WGPU_ADAPTER_NAME") {
823 let adapters = instance.enumerate_adapters(wgpu::Backends::all()).await;
824 log::info!("[GPU] Available adapters:");
825 for a in &adapters {
826 let info = a.get_info();
827 log::info!(
828 " - Name: '{}' | Driver: '{}' | Backend: {:?}",
829 info.name,
830 info.driver,
831 info.backend
832 );
833 }
834
835 adapter = adapters.into_iter().find(|a| {
836 let info = a.get_info();
837 let match_found = info.name.to_lowercase().contains(&filter.to_lowercase())
838 || info.driver.to_lowercase().contains(&filter.to_lowercase());
839 if match_found {
840 log::info!(
841 "[GPU] Manual selection match: {} | Driver: {}",
842 info.name,
843 info.driver
844 );
845 }
846 match_found
847 });
848
849 if adapter.is_some() {
850 log::info!(
851 "[GPU] Forced adapter selection via WGPU_ADAPTER_NAME='{}'",
852 filter
853 );
854 } else {
855 log::warn!(
856 "[GPU] WGPU_ADAPTER_NAME='{}' provided but no matching adapter found. Falling back...",
857 filter
858 );
859 }
860 }
861
862 if adapter.is_none() {
863 adapter = instance
864 .request_adapter(&wgpu::RequestAdapterOptions {
865 power_preference: wgpu::PowerPreference::HighPerformance,
866 compatible_surface: Some(&surface),
867 force_fallback_adapter: false,
868 })
869 .await
870 .ok();
871 }
872
873 if adapter.is_none() {
874 log::warn!(
875 "[GPU] HighPerformance adapter failed (possible Bumblebee/Optimus), trying LowPower..."
876 );
877 adapter = instance
878 .request_adapter(&wgpu::RequestAdapterOptions {
879 power_preference: wgpu::PowerPreference::LowPower,
880 compatible_surface: Some(&surface),
881 force_fallback_adapter: false,
882 })
883 .await
884 .ok();
885 }
886
887 if adapter.is_none() {
888 log::warn!("[GPU] Hardware adapters failed, trying Software fallback...");
889 adapter = instance
890 .request_adapter(&wgpu::RequestAdapterOptions {
891 power_preference: wgpu::PowerPreference::LowPower,
892 compatible_surface: Some(&surface),
893 force_fallback_adapter: true,
894 })
895 .await
896 .ok();
897 }
898
899 let adapter = adapter.expect("Failed to find a suitable GPU for Surtr");
900 let info = adapter.get_info();
901 let caps = crate::subsystems::GpuCapabilities::detect(
904 &info.name,
905 format!("{:?}", info.backend),
906 );
907 log::info!(
908 "[GPU] Selected adapter: {} ({:?}) on backend: {:?} -- detected as {}",
909 info.name,
910 info.device_type,
911 info.backend,
912 caps.vendor
913 );
914 log::info!("[GPU] Driver info: {} - {}", info.driver, info.driver_info);
915 let supports_timestamps = adapter.features().contains(wgpu::Features::TIMESTAMP_QUERY);
916 let supports_pipeline_cache = adapter.features().contains(wgpu::Features::PIPELINE_CACHE);
917 #[cfg(not(target_arch = "wasm32"))]
918 let mut required_features =
919 wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
920 | wgpu::Features::TEXTURE_BINDING_ARRAY;
921
922 #[cfg(target_arch = "wasm32")]
923 let mut required_features = wgpu::Features::empty(); if supports_timestamps {
925 required_features |= wgpu::Features::TIMESTAMP_QUERY;
926 }
927 if supports_pipeline_cache {
928 required_features |= wgpu::Features::PIPELINE_CACHE;
929 }
930 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
932 {
933 log::info!("[GPU] Validation layer enabled (debug build)");
934 }
935
936 let (device, queue) = adapter
937 .request_device(&wgpu::DeviceDescriptor {
938 label: Some("Surtr Forge"),
939 required_features,
940 required_limits: wgpu::Limits {
941 max_bindings_per_bind_group: 256,
942 max_binding_array_elements_per_shader_stage: 256,
943 ..wgpu::Limits::default()
944 },
945 memory_hints: wgpu::MemoryHints::default(),
946 experimental_features: wgpu::ExperimentalFeatures::disabled(),
947 trace: wgpu::Trace::Off,
948 })
949 .await
950 .expect("Failed to create Surtr device");
951
952 let instance = Arc::new(instance);
953 let adapter = Arc::new(adapter);
954
955 device.on_uncaptured_error(Arc::new(|error| {
956 log::error!(
957 "[GPU] Uncaptured device error (Device Lost or Panic): {:?}",
958 error
959 );
960 }));
962
963 let device = Arc::new(device);
964 let queue = Arc::new(queue);
965
966 let size = window.inner_size();
967 let width = if size.width > 0 { size.width } else { 1280 };
969 let height = if size.height > 0 { size.height } else { 720 };
970 let surface_caps = surface.get_capabilities(&adapter);
971 let surface_format = Self::select_best_surface_format(&surface_caps.formats);
975
976 log::info!("[GPU] Available present modes: {:?}", surface_caps.present_modes);
982 log::info!("[GPU] Adapter: {} ({:?})", adapter.get_info().name, adapter.get_info().backend);
983 let present_mode = if surface_caps
984 .present_modes
985 .contains(&wgpu::PresentMode::Immediate)
986 {
987 log::info!("[GPU] Selected: Immediate (no vsync, uncapped)");
988 wgpu::PresentMode::Immediate
989 } else if surface_caps
990 .present_modes
991 .contains(&wgpu::PresentMode::Mailbox)
992 {
993 log::info!("[GPU] Selected: Mailbox (no vsync)");
994 wgpu::PresentMode::Mailbox
995 } else {
996 log::info!("[GPU] Selected: Fifo (V-Sync capped at compositor rate)");
997 wgpu::PresentMode::Fifo
998 };
999
1000 let alpha_mode = if surface_caps
1001 .alpha_modes
1002 .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
1003 {
1004 wgpu::CompositeAlphaMode::PostMultiplied
1005 } else if surface_caps
1006 .alpha_modes
1007 .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
1008 {
1009 wgpu::CompositeAlphaMode::PreMultiplied
1010 } else {
1011 surface_caps.alpha_modes[0]
1012 };
1013
1014 log::info!(
1015 "[GPU] Configuring surface: {}x{} | {:?} | {:?}",
1016 width,
1017 height,
1018 present_mode,
1019 alpha_mode
1020 );
1021
1022 let config = wgpu::SurfaceConfiguration {
1023 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1024 format: surface_format,
1025 width,
1026 height,
1027 present_mode,
1028 alpha_mode,
1029 view_formats: vec![],
1030 desired_maximum_frame_latency: 1,
1031 };
1032 surface.configure(&device, &config);
1033 log::info!("[GPU] Surface configuration successful.");
1034
1035 let renderer = Self::forge_internal(
1036 instance,
1037 adapter,
1038 device,
1039 queue,
1040 Some((window, surface, config)),
1041 None,
1042 )
1043 .await;
1044 log::info!("[GPU] Forge internal complete.");
1045 renderer
1046 }
1047
1048 pub(crate) async fn forge_internal(
1058 instance: Arc<wgpu::Instance>,
1059 adapter: Arc<wgpu::Adapter>,
1060 device: Arc<wgpu::Device>,
1061 queue: Arc<wgpu::Queue>,
1062 surface_info: Option<(
1063 Arc<winit::window::Window>,
1064 wgpu::Surface<'static>,
1065 wgpu::SurfaceConfiguration,
1066 )>,
1067 headless_info: Option<(u32, u32, wgpu::TextureFormat)>,
1068 ) -> Self {
1069 let format = if let Some((_, _, ref config)) = surface_info {
1070 config.format
1071 } else if let Some((_, _, f)) = headless_info {
1072 f
1073 } else {
1074 wgpu::TextureFormat::Rgba8UnormSrgb
1075 };
1076
1077 let supports_timestamps = adapter.features().contains(wgpu::Features::TIMESTAMP_QUERY);
1078 let skuld_period = queue.get_timestamp_period();
1079 let (skuld_queries, skuld_buffer, skuld_read_buffer) = if supports_timestamps {
1080 let q = device.create_query_set(&wgpu::QuerySetDescriptor {
1081 label: Some("Skuld Timestamp Queries"),
1082 count: 2,
1083 ty: wgpu::QueryType::Timestamp,
1084 });
1085 let b = device.create_buffer(&wgpu::BufferDescriptor {
1086 label: Some("Skuld Query Buffer"),
1087 size: 16,
1088 usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
1089 mapped_at_creation: false,
1090 });
1091 let rb = device.create_buffer(&wgpu::BufferDescriptor {
1092 label: Some("Skuld Read Buffer"),
1093 size: 16,
1094 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1095 mapped_at_creation: false,
1096 });
1097 (Some(q), Some(b), Some(rb))
1098 } else {
1099 (None, None, None)
1100 };
1101
1102 let pipeline_cache = if device.features().contains(wgpu::Features::PIPELINE_CACHE) {
1117 let cache_dir = std::env::current_exe()
1118 .ok()
1119 .and_then(|p| p.parent().map(|d| d.join("pipeline_cache")))
1120 .unwrap_or_else(|| std::env::temp_dir().join("cvkg_pipeline_cache"));
1121 let _ = std::fs::create_dir_all(&cache_dir);
1122 let cache_path = cache_dir.join("cvkg_render_gpu.bin");
1123 let cache_data = match load_pipeline_cache_with_integrity_check(&cache_path) {
1124 Ok(data) => data,
1125 Err(reason) => {
1126 log::warn!(
1127 "[GPU] pipeline cache integrity check failed: {reason}; using empty cache"
1128 );
1129 None
1130 }
1131 };
1132 Some(unsafe {
1139 device.create_pipeline_cache(&wgpu::PipelineCacheDescriptor {
1140 label: Some("CVKG Pipeline Cache"),
1141 data: cache_data.as_deref(),
1142 fallback: true,
1143 })
1144 })
1145 } else {
1146 log::debug!(
1147 "[GPU] device does not expose PIPELINE_CACHE; compiling pipelines without cache"
1148 );
1149 None
1150 };
1151 let materials_generated = crate::material::generate_builtins_wgsl();
1152
1153 let wgsl_src = format!(
1164 "{}{}{}{}{}{}",
1165 WGSL_COMMON,
1166 WGSL_SHAPES,
1167 WGSL_BIFROST,
1168 WGSL_BLOOM,
1169 WGSL_COLOR_BLIND,
1170 materials_generated
1171 );
1172 let wgsl_opaque = format!(
1173 "{}{}{}{}{}{}",
1174 WGSL_COMMON,
1175 WGSL_MATERIAL_OPAQUE,
1176 WGSL_BIFROST,
1177 WGSL_BLOOM,
1178 WGSL_COLOR_BLIND,
1179 materials_generated
1180 );
1181 let wgsl_glass = format!(
1182 "{}{}{}{}{}{}",
1183 WGSL_COMMON,
1184 WGSL_MATERIAL_GLASS,
1185 WGSL_BIFROST,
1186 WGSL_BLOOM,
1187 WGSL_COLOR_BLIND,
1188 materials_generated
1189 );
1190
1191 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1192 label: Some("Surtr Main Shader"),
1193 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(wgsl_src)),
1194 });
1195
1196 #[cfg(target_arch = "wasm32")]
1200 let texture_array_count: Option<std::num::NonZeroU32> = None;
1201 #[cfg(not(target_arch = "wasm32"))]
1202 let texture_array_count: Option<std::num::NonZeroU32> =
1203 std::num::NonZeroU32::new(32);
1204
1205 let texture_bind_group_layout =
1206 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1207 entries: &[
1208 wgpu::BindGroupLayoutEntry {
1209 binding: 0,
1210 visibility: wgpu::ShaderStages::FRAGMENT,
1211 ty: wgpu::BindingType::Texture {
1212 multisampled: false,
1213 view_dimension: wgpu::TextureViewDimension::D2,
1214 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1215 },
1216 count: texture_array_count,
1217 },
1218 wgpu::BindGroupLayoutEntry {
1219 binding: 1,
1220 visibility: wgpu::ShaderStages::FRAGMENT,
1221 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1222 count: None,
1223 },
1224 ],
1225 label: Some("Niflheim Texture Bind Group Layout"),
1226 });
1227
1228 let env_bind_group_layout =
1231 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1232 entries: &[
1233 wgpu::BindGroupLayoutEntry {
1234 binding: 0,
1235 visibility: wgpu::ShaderStages::FRAGMENT,
1236 ty: wgpu::BindingType::Texture {
1237 multisampled: false,
1238 view_dimension: wgpu::TextureViewDimension::D2,
1239 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1240 },
1241 count: None,
1242 },
1243 wgpu::BindGroupLayoutEntry {
1244 binding: 1,
1245 visibility: wgpu::ShaderStages::FRAGMENT,
1246 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1247 count: None,
1248 },
1249 ],
1250 label: Some("Surtr Environment Bind Group Layout"),
1251 });
1252
1253 let berserker_bind_group_layout =
1254 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1255 entries: &[
1256 wgpu::BindGroupLayoutEntry {
1257 binding: 0,
1258 visibility: wgpu::ShaderStages::FRAGMENT,
1259 ty: wgpu::BindingType::Buffer {
1260 ty: wgpu::BufferBindingType::Uniform,
1261 has_dynamic_offset: false,
1262 min_binding_size: None,
1263 },
1264 count: None,
1265 },
1266 wgpu::BindGroupLayoutEntry {
1267 binding: 1,
1268 visibility: wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::VERTEX,
1269 ty: wgpu::BindingType::Buffer {
1270 ty: wgpu::BufferBindingType::Uniform,
1271 has_dynamic_offset: false,
1272 min_binding_size: None,
1273 },
1274 count: None,
1275 },
1276 ],
1277 label: Some("Surtr Berserker Bind Group Layout"),
1278 });
1279
1280 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1282 label: Some("Surtr Main Pipeline Layout"),
1283 bind_group_layouts: &[
1284 Some(&texture_bind_group_layout),
1285 Some(&env_bind_group_layout),
1286 Some(&berserker_bind_group_layout),
1287 ],
1288 immediate_size: 0,
1289 });
1290
1291 let post_process_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1293 label: Some("Muspelheim Post Process Layout"),
1294 bind_group_layouts: &[
1295 Some(&texture_bind_group_layout),
1296 Some(&env_bind_group_layout),
1297 Some(&berserker_bind_group_layout),
1298 ],
1299 immediate_size: 0,
1300 });
1301
1302 let composite_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1304 label: Some("Muspelheim Composite Layout"),
1305 bind_group_layouts: &[
1306 Some(&texture_bind_group_layout),
1307 Some(&env_bind_group_layout),
1308 Some(&berserker_bind_group_layout),
1309 ],
1310 immediate_size: 0,
1311 });
1312
1313 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1314 label: Some("Surtr Main Pipeline"),
1315 layout: Some(&pipeline_layout),
1316 vertex: wgpu::VertexState {
1317 module: &shader,
1318 entry_point: Some("vs_main"),
1319 buffers: &[Vertex::desc(), InstanceData::desc()],
1320 compilation_options: wgpu::PipelineCompilationOptions::default(),
1321 },
1322 fragment: Some(wgpu::FragmentState {
1323 module: &shader,
1324 entry_point: Some("fs_main"),
1325 targets: &[Some(wgpu::ColorTargetState {
1326 format: wgpu::TextureFormat::Rgba16Float,
1327 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1328 write_mask: wgpu::ColorWrites::ALL,
1329 })],
1330 compilation_options: wgpu::PipelineCompilationOptions::default(),
1331 }),
1332 primitive: wgpu::PrimitiveState::default(),
1333 depth_stencil: Some(wgpu::DepthStencilState {
1334 format: wgpu::TextureFormat::Depth32Float,
1335 depth_write_enabled: Some(true),
1336 depth_compare: Some(wgpu::CompareFunction::LessEqual),
1337 stencil: wgpu::StencilState::default(),
1338 bias: wgpu::DepthBiasState::default(),
1339 }),
1340 multisample: wgpu::MultisampleState {
1341 count: 4,
1342 mask: !0,
1343 alpha_to_coverage_enabled: false,
1344 },
1345 multiview_mask: None,
1346 cache: pipeline_cache.as_ref(),
1347 });
1348
1349 let background_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1350 label: Some("Surtr Background Pipeline"),
1351 layout: Some(&pipeline_layout),
1352 vertex: wgpu::VertexState {
1353 module: &shader,
1354 entry_point: Some("vs_fullscreen"),
1355 buffers: &[],
1356 compilation_options: wgpu::PipelineCompilationOptions::default(),
1357 },
1358 fragment: Some(wgpu::FragmentState {
1359 module: &shader,
1360 entry_point: Some("fs_background"),
1361 targets: &[Some(wgpu::ColorTargetState {
1362 format: wgpu::TextureFormat::Rgba16Float,
1363 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1364 write_mask: wgpu::ColorWrites::ALL,
1365 })],
1366 compilation_options: wgpu::PipelineCompilationOptions::default(),
1367 }),
1368 primitive: wgpu::PrimitiveState::default(),
1369 depth_stencil: Some(wgpu::DepthStencilState {
1370 format: wgpu::TextureFormat::Depth32Float,
1371 depth_write_enabled: Some(false),
1372 depth_compare: Some(wgpu::CompareFunction::Always),
1373 stencil: wgpu::StencilState::default(),
1374 bias: wgpu::DepthBiasState::default(),
1375 }),
1376 multisample: wgpu::MultisampleState {
1377 count: 4,
1378 mask: !0,
1379 alpha_to_coverage_enabled: false,
1380 },
1381 multiview_mask: None,
1382 cache: pipeline_cache.as_ref(),
1383 });
1384
1385 let opaque_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1387 label: Some("Muspelheim Opaque"),
1388 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(wgsl_opaque)),
1389 });
1390 let glass_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1391 label: Some("Muspelheim Glass"),
1392 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(wgsl_glass)),
1393 });
1394
1395 let opaque_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1396 label: Some("Muspelheim Opaque"),
1397 layout: Some(&pipeline_layout),
1398 vertex: wgpu::VertexState {
1399 module: &opaque_shader,
1400 entry_point: Some("vs_main"),
1401 buffers: &[Vertex::desc(), InstanceData::desc()],
1402 compilation_options: wgpu::PipelineCompilationOptions::default(),
1403 },
1404 fragment: Some(wgpu::FragmentState {
1405 module: &opaque_shader,
1406 entry_point: Some("fs_main"),
1407 targets: &[Some(wgpu::ColorTargetState {
1408 format: wgpu::TextureFormat::Rgba16Float,
1409 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1410 write_mask: wgpu::ColorWrites::ALL,
1411 })],
1412 compilation_options: wgpu::PipelineCompilationOptions::default(),
1413 }),
1414 primitive: wgpu::PrimitiveState::default(),
1415 depth_stencil: Some(wgpu::DepthStencilState {
1416 format: wgpu::TextureFormat::Depth32Float,
1417 depth_write_enabled: Some(true),
1418 depth_compare: Some(wgpu::CompareFunction::LessEqual),
1419 stencil: wgpu::StencilState::default(),
1420 bias: wgpu::DepthBiasState::default(),
1421 }),
1422 multisample: wgpu::MultisampleState {
1423 count: 4,
1424 mask: !0,
1425 alpha_to_coverage_enabled: false,
1426 },
1427 multiview_mask: None,
1428 cache: pipeline_cache.as_ref(),
1429 });
1430 let ui_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1431 label: Some("Muspelheim UI"),
1432 layout: Some(&pipeline_layout),
1433 vertex: wgpu::VertexState {
1434 module: &opaque_shader,
1435 entry_point: Some("vs_main"),
1436 buffers: &[Vertex::desc(), InstanceData::desc()],
1437 compilation_options: wgpu::PipelineCompilationOptions::default(),
1438 },
1439 fragment: Some(wgpu::FragmentState {
1440 module: &opaque_shader,
1441 entry_point: Some("fs_main"),
1442 targets: &[Some(wgpu::ColorTargetState {
1443 format: wgpu::TextureFormat::Rgba16Float,
1444 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1445 write_mask: wgpu::ColorWrites::ALL,
1446 })],
1447 compilation_options: wgpu::PipelineCompilationOptions::default(),
1448 }),
1449 primitive: wgpu::PrimitiveState::default(),
1450 depth_stencil: None,
1451 multisample: wgpu::MultisampleState {
1452 count: 1,
1453 mask: !0,
1454 alpha_to_coverage_enabled: false,
1455 },
1456 multiview_mask: None,
1457 cache: pipeline_cache.as_ref(),
1458 });
1459 let glass_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1460 label: Some("Muspelheim Glass"),
1461 layout: Some(&pipeline_layout),
1462 vertex: wgpu::VertexState {
1463 module: &opaque_shader,
1464 entry_point: Some("vs_main"),
1465 buffers: &[Vertex::desc(), InstanceData::desc()],
1466 compilation_options: wgpu::PipelineCompilationOptions::default(),
1467 },
1468 fragment: Some(wgpu::FragmentState {
1469 module: &glass_shader,
1470 entry_point: Some("fs_main"),
1471 targets: &[Some(wgpu::ColorTargetState {
1472 format: wgpu::TextureFormat::Rgba16Float,
1473 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1474 write_mask: wgpu::ColorWrites::ALL,
1475 })],
1476 compilation_options: wgpu::PipelineCompilationOptions::default(),
1477 }),
1478 primitive: wgpu::PrimitiveState::default(),
1479 depth_stencil: None,
1480 multisample: wgpu::MultisampleState {
1481 count: 1,
1482 mask: !0,
1483 alpha_to_coverage_enabled: false,
1484 },
1485 multiview_mask: None,
1486 cache: pipeline_cache.as_ref(),
1487 });
1488
1489 let bloom_extract_pipeline =
1491 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1492 label: Some("Muspelheim Bloom Extract"),
1493 layout: Some(&post_process_layout),
1494 vertex: wgpu::VertexState {
1495 module: &shader,
1496 entry_point: Some("vs_fullscreen"),
1497 buffers: &[],
1498 compilation_options: wgpu::PipelineCompilationOptions::default(),
1499 },
1500 fragment: Some(wgpu::FragmentState {
1501 module: &shader,
1502 entry_point: Some("fs_bloom_extract"),
1503 targets: &[Some(wgpu::ColorTargetState {
1504 format,
1505 blend: None,
1506 write_mask: wgpu::ColorWrites::ALL,
1507 })],
1508 compilation_options: wgpu::PipelineCompilationOptions::default(),
1509 }),
1510 primitive: wgpu::PrimitiveState::default(),
1511 depth_stencil: None,
1512 multisample: wgpu::MultisampleState::default(),
1513 multiview_mask: None,
1514 cache: pipeline_cache.as_ref(),
1515 });
1516
1517 let copy_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1519 label: Some("Muspelheim Copy"),
1520 layout: Some(&post_process_layout),
1521 vertex: wgpu::VertexState {
1522 module: &shader,
1523 entry_point: Some("vs_fullscreen"),
1524 buffers: &[],
1525 compilation_options: wgpu::PipelineCompilationOptions::default(),
1526 },
1527 fragment: Some(wgpu::FragmentState {
1528 module: &shader,
1529 entry_point: Some("fs_copy"),
1530 targets: &[Some(wgpu::ColorTargetState {
1531 format,
1532 blend: None,
1533 write_mask: wgpu::ColorWrites::ALL,
1534 })],
1535 compilation_options: wgpu::PipelineCompilationOptions::default(),
1536 }),
1537 primitive: wgpu::PrimitiveState::default(),
1538 depth_stencil: None,
1539 multisample: wgpu::MultisampleState::default(),
1540 multiview_mask: None,
1541 cache: pipeline_cache.as_ref(),
1542 });
1543
1544 let kawase_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1549 label: Some("Kawase Blur Pyramid"),
1550 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
1551 "shaders/blur_pyramid.wgsl"
1552 ))),
1553 });
1554 let kawase_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1555 label: Some("Kawase Blur BGL"),
1556 entries: &[
1557 wgpu::BindGroupLayoutEntry {
1558 binding: 0,
1559 visibility: wgpu::ShaderStages::FRAGMENT,
1560 ty: wgpu::BindingType::Buffer {
1561 ty: wgpu::BufferBindingType::Uniform,
1562 has_dynamic_offset: false,
1563 min_binding_size: wgpu::BufferSize::new(32),
1564 },
1565 count: None,
1566 },
1567 wgpu::BindGroupLayoutEntry {
1568 binding: 1,
1569 visibility: wgpu::ShaderStages::FRAGMENT,
1570 ty: wgpu::BindingType::Texture {
1571 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1572 view_dimension: wgpu::TextureViewDimension::D2,
1573 multisampled: false,
1574 },
1575 count: None,
1576 },
1577 wgpu::BindGroupLayoutEntry {
1578 binding: 2,
1579 visibility: wgpu::ShaderStages::FRAGMENT,
1580 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1581 count: None,
1582 },
1583 ],
1584 });
1585 let kawase_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1586 label: Some("Kawase Pipeline Layout"),
1587 bind_group_layouts: &[Some(&kawase_bgl)],
1588 immediate_size: 0,
1589 });
1590 let kawase_down_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1591 label: Some("Kawase Downsample"),
1592 layout: Some(&kawase_layout),
1593 vertex: wgpu::VertexState {
1594 module: &kawase_shader,
1595 entry_point: Some("vs_blur"),
1596 buffers: &[],
1597 compilation_options: wgpu::PipelineCompilationOptions::default(),
1598 },
1599 fragment: Some(wgpu::FragmentState {
1600 module: &kawase_shader,
1601 entry_point: Some("fs_kawase_down"),
1602 targets: &[Some(wgpu::ColorTargetState {
1603 format,
1604 blend: None,
1605 write_mask: wgpu::ColorWrites::ALL,
1606 })],
1607 compilation_options: wgpu::PipelineCompilationOptions::default(),
1608 }),
1609 primitive: wgpu::PrimitiveState::default(),
1610 depth_stencil: None,
1611 multisample: wgpu::MultisampleState::default(),
1612 multiview_mask: None,
1613 cache: pipeline_cache.as_ref(),
1614 });
1615 let kawase_up_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1616 label: Some("Kawase Upsample"),
1617 layout: Some(&kawase_layout),
1618 vertex: wgpu::VertexState {
1619 module: &kawase_shader,
1620 entry_point: Some("vs_blur"),
1621 buffers: &[],
1622 compilation_options: wgpu::PipelineCompilationOptions::default(),
1623 },
1624 fragment: Some(wgpu::FragmentState {
1625 module: &kawase_shader,
1626 entry_point: Some("fs_kawase_up"),
1627 targets: &[Some(wgpu::ColorTargetState {
1628 format,
1629 blend: None,
1630 write_mask: wgpu::ColorWrites::ALL,
1631 })],
1632 compilation_options: wgpu::PipelineCompilationOptions::default(),
1633 }),
1634 primitive: wgpu::PrimitiveState::default(),
1635 depth_stencil: None,
1636 multisample: wgpu::MultisampleState::default(),
1637 multiview_mask: None,
1638 cache: pipeline_cache.as_ref(),
1639 });
1640
1641 let composite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1643 label: Some("Muspelheim Composite"),
1644 layout: Some(&composite_layout),
1645 vertex: wgpu::VertexState {
1646 module: &shader,
1647 entry_point: Some("vs_fullscreen"),
1648 buffers: &[],
1649 compilation_options: wgpu::PipelineCompilationOptions::default(),
1650 },
1651 fragment: Some(wgpu::FragmentState {
1652 module: &shader,
1653 entry_point: Some("fs_composite"),
1654 targets: &[Some(wgpu::ColorTargetState {
1655 format,
1656 blend: Some(wgpu::BlendState {
1658 color: wgpu::BlendComponent {
1659 src_factor: wgpu::BlendFactor::One,
1660 dst_factor: wgpu::BlendFactor::One,
1661 operation: wgpu::BlendOperation::Add,
1662 },
1663 alpha: wgpu::BlendComponent {
1664 src_factor: wgpu::BlendFactor::SrcAlpha,
1665 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
1666 operation: wgpu::BlendOperation::Add,
1667 },
1668 }),
1669 write_mask: wgpu::ColorWrites::ALL,
1670 })],
1671 compilation_options: wgpu::PipelineCompilationOptions::default(),
1672 }),
1673 primitive: wgpu::PrimitiveState::default(),
1674 depth_stencil: None,
1675 multisample: wgpu::MultisampleState::default(),
1676 multiview_mask: None,
1677 cache: pipeline_cache.as_ref(),
1678 });
1679
1680 let mega_heim_tex = device.create_texture(&wgpu::TextureDescriptor {
1682 label: Some("Surtr Mega-Heim"),
1683 size: wgpu::Extent3d {
1684 width: 4096,
1685 height: 4096,
1686 depth_or_array_layers: 1,
1687 },
1688 mip_level_count: 1,
1689 sample_count: 1,
1690 dimension: wgpu::TextureDimension::D2,
1691 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1692 usage: wgpu::TextureUsages::TEXTURE_BINDING
1693 | wgpu::TextureUsages::COPY_DST
1694 | wgpu::TextureUsages::COPY_SRC,
1695 view_formats: &[],
1696 });
1697 let mega_heim_view_obj = mega_heim_tex.create_view(&wgpu::TextureViewDescriptor::default());
1698 let text_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1699 address_mode_u: wgpu::AddressMode::ClampToEdge,
1700 address_mode_v: wgpu::AddressMode::ClampToEdge,
1701 mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear,
1703 ..Default::default()
1704 });
1705
1706 let dummy_size = wgpu::Extent3d {
1708 width: 1,
1709 height: 1,
1710 depth_or_array_layers: 1,
1711 };
1712 let dummy_texture = device.create_texture(&wgpu::TextureDescriptor {
1713 label: Some("Niflheim Dummy Texture"),
1714 size: dummy_size,
1715 mip_level_count: 1,
1716 sample_count: 1,
1717 dimension: wgpu::TextureDimension::D2,
1718 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1719 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1720 view_formats: &[],
1721 });
1722 queue.write_texture(
1723 wgpu::TexelCopyTextureInfo {
1724 texture: &dummy_texture,
1725 mip_level: 0,
1726 origin: wgpu::Origin3d::ZERO,
1727 aspect: wgpu::TextureAspect::All,
1728 },
1729 &[255, 255, 255, 255],
1730 wgpu::TexelCopyBufferLayout {
1731 offset: 0,
1732 bytes_per_row: Some(4),
1733 rows_per_image: Some(1),
1734 },
1735 dummy_size,
1736 );
1737
1738 let dummy_view = dummy_texture.create_view(&wgpu::TextureViewDescriptor::default());
1739 let dummy_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1740 address_mode_u: wgpu::AddressMode::ClampToEdge,
1741 address_mode_v: wgpu::AddressMode::ClampToEdge,
1742 address_mode_w: wgpu::AddressMode::ClampToEdge,
1743 mag_filter: wgpu::FilterMode::Linear,
1744 min_filter: wgpu::FilterMode::Nearest,
1745 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
1746 ..Default::default()
1747 });
1748
1749 let mut texture_views_list: Vec<wgpu::TextureView> =
1750 (0..32).map(|_| dummy_view.clone()).collect();
1751 texture_views_list[0] = mega_heim_view_obj.clone();
1752
1753 let views_refs: Vec<&wgpu::TextureView> = texture_views_list.iter().collect();
1754 let mega_heim_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1755 layout: &texture_bind_group_layout,
1756 entries: &[
1757 wgpu::BindGroupEntry {
1758 binding: 0,
1759 resource: wgpu::BindingResource::TextureViewArray(&views_refs),
1760 },
1761 wgpu::BindGroupEntry {
1762 binding: 1,
1763 resource: wgpu::BindingResource::Sampler(&text_sampler),
1764 },
1765 ],
1766 label: Some("Mega-Heim Bind Group"),
1767 });
1768
1769 let dummy_views_refs: Vec<&wgpu::TextureView> = (0..32).map(|_| &dummy_view).collect();
1770 let dummy_texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1771 layout: &texture_bind_group_layout,
1772 entries: &[
1773 wgpu::BindGroupEntry {
1774 binding: 0,
1775 resource: wgpu::BindingResource::TextureViewArray(&dummy_views_refs),
1776 },
1777 wgpu::BindGroupEntry {
1778 binding: 1,
1779 resource: wgpu::BindingResource::Sampler(&dummy_sampler),
1780 },
1781 ],
1782 label: Some("Dummy Texture Bind Group"),
1783 });
1784
1785 let dummy_env_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1786 layout: &env_bind_group_layout,
1787 entries: &[
1788 wgpu::BindGroupEntry {
1789 binding: 0,
1790 resource: wgpu::BindingResource::TextureView(&dummy_view),
1791 },
1792 wgpu::BindGroupEntry {
1793 binding: 1,
1794 resource: wgpu::BindingResource::Sampler(&dummy_sampler),
1795 },
1796 ],
1797 label: Some("Dummy Env Bind Group"),
1798 });
1799
1800 let mut texture_registry = LruCache::new(NonZeroUsize::new(31).unwrap());
1801 let mut texture_bind_groups = Vec::new();
1802
1803 texture_registry.put("__mega_heim".to_string(), 0);
1805 texture_bind_groups.push(mega_heim_bind_group.clone());
1806
1807 let geometry_buffers =
1812 crate::types::GeometryBuffers::forge(&device, MAX_VERTICES, MAX_INDICES);
1813
1814 let current_theme = ColorTheme::default();
1816 use wgpu::util::DeviceExt;
1817 let theme_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1818 label: Some("Surtr Theme Buffer"),
1819 contents: bytemuck::bytes_of(¤t_theme),
1820 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1821 });
1822
1823 let (width, height, scale_factor) = if let Some((ref window, _, ref config)) = surface_info
1824 {
1825 (config.width, config.height, window.scale_factor() as f32)
1826 } else if let Some((w, h, _)) = headless_info {
1827 (w, h, 1.0)
1828 } else {
1829 (1280, 720, 1.0)
1830 };
1831
1832 let mut current_scene =
1833 SceneUniforms::new(width as f32 / scale_factor, height as f32 / scale_factor);
1834 current_scene.scale_factor = scale_factor;
1835 let msaa_sample_count = QualityLevel::default().msaa_sample_count();
1841 let scene_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1842 label: Some("Surtr Scene Buffer"),
1843 contents: bytemuck::bytes_of(¤t_scene),
1844 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1845 });
1846
1847 let berserker_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1848 layout: &berserker_bind_group_layout,
1849 entries: &[
1850 wgpu::BindGroupEntry {
1851 binding: 0,
1852 resource: theme_buffer.as_entire_binding(),
1853 },
1854 wgpu::BindGroupEntry {
1855 binding: 1,
1856 resource: scene_buffer.as_entire_binding(),
1857 },
1858 ],
1859 label: Some("Surtr Berserker Bind Group"),
1860 });
1861
1862 let mut registry = crate::kvasir::registry::ResourceRegistry::new();
1863 let mut surfaces = std::collections::HashMap::new();
1864 let mut current_window = None;
1865 let mut headless_context = None;
1866
1867 if let Some((window, surface, config)) = surface_info {
1868 let window_id = window.id();
1869 let ctx = Self::create_surface_context(
1870 &device,
1871 surface,
1872 config,
1873 &env_bind_group_layout,
1874 &texture_bind_group_layout,
1875 scale_factor,
1876 msaa_sample_count,
1877 &mut registry,
1878 );
1879 surfaces.insert(window_id, ctx);
1880 current_window = Some(window_id);
1881 } else if let Some((w, h, f)) = headless_info {
1882 headless_context = Some(Self::create_headless_context(
1883 &device,
1884 w,
1885 h,
1886 f,
1887 &env_bind_group_layout,
1888 &texture_bind_group_layout,
1889 &mut registry,
1890 msaa_sample_count,
1891 ));
1892 }
1893
1894 let staging_belt = wgpu::util::StagingBelt::new((*device).clone(), 1024 * 1024);
1895
1896 let glass_output_bind_group_layout = env_bind_group_layout.clone();
1897
1898 let color_blind_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1900 label: Some("Color Blind Bind Group Layout"),
1901 entries: &[
1902 wgpu::BindGroupLayoutEntry {
1903 binding: 0,
1904 visibility: wgpu::ShaderStages::FRAGMENT,
1905 ty: wgpu::BindingType::Texture {
1906 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1907 view_dimension: wgpu::TextureViewDimension::D2,
1908 multisampled: false,
1909 },
1910 count: None,
1911 },
1912 wgpu::BindGroupLayoutEntry {
1913 binding: 1,
1914 visibility: wgpu::ShaderStages::FRAGMENT,
1915 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1916 count: None,
1917 },
1918 wgpu::BindGroupLayoutEntry {
1919 binding: 2,
1920 visibility: wgpu::ShaderStages::FRAGMENT,
1921 ty: wgpu::BindingType::Buffer {
1922 ty: wgpu::BufferBindingType::Uniform,
1923 has_dynamic_offset: false,
1924 min_binding_size: wgpu::BufferSize::new(std::mem::size_of::<
1925 crate::color_blindness::ColorBlindUniforms,
1926 >() as u64),
1927 },
1928 count: None,
1929 },
1930 ],
1931 });
1932 let color_blind_pipeline_layout =
1933 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1934 label: Some("Color Blind Pipeline Layout"),
1935 bind_group_layouts: &[Some(&color_blind_bgl)],
1936 immediate_size: 0,
1937 });
1938
1939 let color_blind_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1941 label: Some("Surtr Color Blind Shader"),
1942 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
1943 crate::color_blindness::shader_source(),
1944 )),
1945 });
1946 let color_blind_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1947 label: Some("Surtr Color Blindness"),
1948 layout: Some(&color_blind_pipeline_layout),
1949 vertex: wgpu::VertexState {
1950 module: &color_blind_shader,
1951 entry_point: Some("fs_main_vs"),
1952 buffers: &[],
1953 compilation_options: wgpu::PipelineCompilationOptions::default(),
1954 },
1955 fragment: Some(wgpu::FragmentState {
1956 module: &color_blind_shader,
1957 entry_point: Some("fs_color_blind"),
1958 targets: &[Some(wgpu::ColorTargetState {
1959 format,
1960 blend: None,
1961 write_mask: wgpu::ColorWrites::ALL,
1962 })],
1963 compilation_options: wgpu::PipelineCompilationOptions::default(),
1964 }),
1965 primitive: wgpu::PrimitiveState::default(),
1966 depth_stencil: None,
1967 multisample: wgpu::MultisampleState::default(),
1968 multiview_mask: None,
1969 cache: pipeline_cache.as_ref(),
1970 });
1971
1972 let volumetric_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1976 label: Some("Surtr Volumetric Shader"),
1977 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
1978 "shaders/volumetric.wgsl"
1979 ))),
1980 });
1981 let volumetric_bgl =
1983 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1984 label: Some("Volumetric Bind Group Layout"),
1985 entries: &[
1986 wgpu::BindGroupLayoutEntry {
1988 binding: 0,
1989 visibility: wgpu::ShaderStages::FRAGMENT,
1990 ty: wgpu::BindingType::Buffer {
1991 ty: wgpu::BufferBindingType::Uniform,
1992 has_dynamic_offset: false,
1993 min_binding_size: wgpu::BufferSize::new(
1994 std::mem::size_of::<[f32; 24]>() as u64
1995 ),
1996 },
1997 count: None,
1998 },
1999 wgpu::BindGroupLayoutEntry {
2001 binding: 1,
2002 visibility: wgpu::ShaderStages::FRAGMENT,
2003 ty: wgpu::BindingType::Texture {
2004 sample_type: wgpu::TextureSampleType::Depth,
2005 view_dimension: wgpu::TextureViewDimension::D2,
2006 multisampled: false,
2007 },
2008 count: None,
2009 },
2010 wgpu::BindGroupLayoutEntry {
2012 binding: 2,
2013 visibility: wgpu::ShaderStages::FRAGMENT,
2014 ty: wgpu::BindingType::Texture {
2015 sample_type: wgpu::TextureSampleType::Depth,
2016 view_dimension: wgpu::TextureViewDimension::D2,
2017 multisampled: true,
2018 },
2019 count: None,
2020 },
2021 wgpu::BindGroupLayoutEntry {
2023 binding: 3,
2024 visibility: wgpu::ShaderStages::FRAGMENT,
2025 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
2026 count: None,
2027 },
2028 ],
2029 });
2030 let volumetric_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2031 label: Some("Surtr Volumetric Layout"),
2032 bind_group_layouts: &[Some(&volumetric_bgl)],
2033 immediate_size: 0,
2034 });
2035
2036 let volumetric_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2037 label: Some("Surtr Volumetric Raymarching"),
2038 layout: Some(&volumetric_layout),
2039 vertex: wgpu::VertexState {
2040 module: &volumetric_shader,
2041 entry_point: Some("vs_fullscreen"),
2042 buffers: &[],
2043 compilation_options: wgpu::PipelineCompilationOptions::default(),
2044 },
2045 fragment: Some(wgpu::FragmentState {
2046 module: &volumetric_shader,
2047 entry_point: Some("fs_main"),
2048 targets: &[Some(wgpu::ColorTargetState {
2049 format: wgpu::TextureFormat::Rgba16Float,
2050 blend: Some(wgpu::BlendState {
2051 color: wgpu::BlendComponent {
2052 src_factor: wgpu::BlendFactor::One,
2053 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
2054 operation: wgpu::BlendOperation::Add,
2055 },
2056 alpha: wgpu::BlendComponent {
2057 src_factor: wgpu::BlendFactor::One,
2058 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
2059 operation: wgpu::BlendOperation::Add,
2060 },
2061 }),
2062 write_mask: wgpu::ColorWrites::ALL,
2063 })],
2064 compilation_options: wgpu::PipelineCompilationOptions::default(),
2065 }),
2066 primitive: wgpu::PrimitiveState::default(),
2067 depth_stencil: None,
2068 multisample: wgpu::MultisampleState::default(),
2069 multiview_mask: None,
2070 cache: pipeline_cache.as_ref(),
2071 });
2072
2073 let tonemap_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2076 label: Some("Surtr ToneMap Shader"),
2077 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(WGSL_TONEMAP)),
2078 });
2079 let tonemap_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2080 label: Some("ToneMap Bind Group Layout"),
2081 entries: &[
2082 wgpu::BindGroupLayoutEntry {
2083 binding: 0,
2084 visibility: wgpu::ShaderStages::FRAGMENT,
2085 ty: wgpu::BindingType::Buffer {
2086 ty: wgpu::BufferBindingType::Uniform,
2087 has_dynamic_offset: false,
2088 min_binding_size: wgpu::BufferSize::new(
2089 std::mem::size_of::<[f32; 4]>() as u64
2090 ),
2091 },
2092 count: None,
2093 },
2094 wgpu::BindGroupLayoutEntry {
2095 binding: 1,
2096 visibility: wgpu::ShaderStages::FRAGMENT,
2097 ty: wgpu::BindingType::Texture {
2098 sample_type: wgpu::TextureSampleType::Float { filterable: true },
2099 view_dimension: wgpu::TextureViewDimension::D2,
2100 multisampled: false,
2101 },
2102 count: None,
2103 },
2104 wgpu::BindGroupLayoutEntry {
2105 binding: 2,
2106 visibility: wgpu::ShaderStages::FRAGMENT,
2107 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
2108 count: None,
2109 },
2110 ],
2111 });
2112 let tonemap_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2113 label: Some("Surtr ToneMap Layout"),
2114 bind_group_layouts: &[Some(&tonemap_bgl)],
2115 immediate_size: 0,
2116 });
2117 let tonemap_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2118 label: Some("Surtr ToneMapping"),
2119 layout: Some(&tonemap_layout),
2120 vertex: wgpu::VertexState {
2121 module: &tonemap_shader,
2122 entry_point: Some("vs_fullscreen"),
2123 buffers: &[],
2124 compilation_options: wgpu::PipelineCompilationOptions::default(),
2125 },
2126 fragment: Some(wgpu::FragmentState {
2127 module: &tonemap_shader,
2128 entry_point: Some("fs_main"),
2129 targets: &[Some(wgpu::ColorTargetState {
2130 format,
2131 blend: None,
2132 write_mask: wgpu::ColorWrites::ALL,
2133 })],
2134 compilation_options: wgpu::PipelineCompilationOptions::default(),
2135 }),
2136 primitive: wgpu::PrimitiveState::default(),
2137 depth_stencil: None,
2138 multisample: wgpu::MultisampleState::default(),
2139 multiview_mask: None,
2140 cache: pipeline_cache.as_ref(),
2141 });
2142
2143 let color_blind_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2145 label: Some("Color Blind Uniforms"),
2146 size: std::mem::size_of::<crate::color_blindness::ColorBlindUniforms>() as u64,
2147 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2148 mapped_at_creation: false,
2149 });
2150
2151 let volumetric_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2154 label: Some("Volumetric Uniforms"),
2155 size: std::mem::size_of::<[f32; 24]>() as u64,
2156 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2157 mapped_at_creation: false,
2158 });
2159
2160 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
2162 address_mode_u: wgpu::AddressMode::ClampToEdge,
2163 address_mode_v: wgpu::AddressMode::ClampToEdge,
2164 mag_filter: wgpu::FilterMode::Linear,
2165 min_filter: wgpu::FilterMode::Linear,
2166 ..Default::default()
2167 });
2168
2169 let volumetric_depth_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
2171 address_mode_u: wgpu::AddressMode::ClampToEdge,
2172 address_mode_v: wgpu::AddressMode::ClampToEdge,
2173 mag_filter: wgpu::FilterMode::Linear,
2174 min_filter: wgpu::FilterMode::Linear,
2175 compare: Some(wgpu::CompareFunction::Less),
2176 ..Default::default()
2177 });
2178
2179 let particle_compute_bgl =
2183 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2184 label: Some("Particle Compute BGL"),
2185 entries: &[
2186 wgpu::BindGroupLayoutEntry {
2187 binding: 0,
2188 visibility: wgpu::ShaderStages::COMPUTE,
2189 ty: wgpu::BindingType::Buffer {
2190 ty: wgpu::BufferBindingType::Storage { read_only: false },
2191 has_dynamic_offset: false,
2192 min_binding_size: wgpu::BufferSize::new(
2193 (MAX_PARTICLES * std::mem::size_of::<GpuParticle>()) as u64
2194 ),
2195 },
2196 count: None,
2197 },
2198 wgpu::BindGroupLayoutEntry {
2199 binding: 1,
2200 visibility: wgpu::ShaderStages::COMPUTE,
2201 ty: wgpu::BindingType::Buffer {
2202 ty: wgpu::BufferBindingType::Uniform,
2203 has_dynamic_offset: false,
2204 min_binding_size: wgpu::BufferSize::new(
2205 std::mem::size_of::<ParticleUniforms>() as u64
2206 ),
2207 },
2208 count: None,
2209 },
2210 ],
2211 });
2212 let particle_compute_layout =
2213 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2214 label: Some("Particle Compute Layout"),
2215 bind_group_layouts: &[Some(&particle_compute_bgl)],
2216 immediate_size: 0,
2217 });
2218 let particle_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2219 label: Some("Particles Compute Shader"),
2220 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(WGSL_PARTICLES)),
2221 });
2222 let particle_compute_pipeline =
2223 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
2224 label: Some("Particle Compute Pipeline"),
2225 layout: Some(&particle_compute_layout),
2226 module: &particle_shader,
2227 entry_point: Some("cs_main"),
2228 compilation_options: wgpu::PipelineCompilationOptions::default(),
2229 cache: pipeline_cache.as_ref(),
2230 });
2231
2232 let particle_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2234 label: Some("Particle Storage Buffer"),
2235 size: (MAX_PARTICLES * std::mem::size_of::<GpuParticle>()) as u64,
2236 usage: wgpu::BufferUsages::STORAGE
2237 | wgpu::BufferUsages::COPY_DST
2238 | wgpu::BufferUsages::VERTEX,
2239 mapped_at_creation: false,
2240 });
2241 let particle_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2243 label: Some("Particle Uniform Buffer"),
2244 size: std::mem::size_of::<ParticleUniforms>() as u64,
2245 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2246 mapped_at_creation: false,
2247 });
2248
2249 let particle_render_bgl =
2253 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2254 label: Some("Particle Render BGL"),
2255 entries: &[
2256 wgpu::BindGroupLayoutEntry {
2257 binding: 0,
2258 visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
2259 ty: wgpu::BindingType::Buffer {
2260 ty: wgpu::BufferBindingType::Storage { read_only: true },
2261 has_dynamic_offset: false,
2262 min_binding_size: wgpu::BufferSize::new(
2263 (MAX_PARTICLES * std::mem::size_of::<GpuParticle>()) as u64
2264 ),
2265 },
2266 count: None,
2267 },
2268 ],
2269 });
2270 let particle_render_layout =
2271 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2272 label: Some("Particle Render Layout"),
2273 bind_group_layouts: &[Some(&particle_render_bgl)],
2274 immediate_size: 0,
2275 });
2276 let particle_render_wgsl = "
2278struct Particle {
2279 pos_vel: vec4<f32>,
2280 color_life: vec4<f32>,
2281};
2282struct ParticleArray {
2283 particles: array<Particle>,
2284};
2285@group(0) @binding(0) var<storage, read> particles: ParticleArray;
2286
2287struct VsOut {
2288 @builtin(position) pos: vec4<f32>,
2289 @location(0) color: vec4<f32>,
2290};
2291
2292@vertex
2293fn vs_main(@builtin(vertex_index) vi: u32) -> VsOut {
2294 var out: VsOut;
2295 let p = particles.particles[vi];
2296 // pos_vel.xy is in logical pixels; convert to NDC.
2297 // For now, pass through as clip-space position (caller sets viewport).
2298 let life = p.color_life.w;
2299 if (life <= 0.0) {
2300 // Degenerate point (behind camera)
2301 out.pos = vec4<f32>(0.0, 0.0, 2.0, 1.0);
2302 out.color = vec4<f32>(0.0);
2303 } else {
2304 // Fade out near end of lifetime
2305 let alpha = min(life, 1.0);
2306 out.pos = vec4<f32>(p.pos_vel.xy, 0.0, 1.0);
2307 out.color = vec4<f32>(p.color_life.xyz, alpha);
2308 }
2309 return out;
2310}
2311
2312@fragment
2313fn fs_main(@location(0) color: vec4<f32>) -> @location(0) vec4<f32> {
2314 return color;
2315}
2316";
2317 let particle_render_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2318 label: Some("Particle Render Shader"),
2319 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(particle_render_wgsl)),
2320 });
2321 let particle_render_pipeline =
2322 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2323 label: Some("Particle Render Pipeline"),
2324 layout: Some(&particle_render_layout),
2325 vertex: wgpu::VertexState {
2326 module: &particle_render_shader,
2327 entry_point: Some("vs_main"),
2328 buffers: &[],
2329 compilation_options: wgpu::PipelineCompilationOptions::default(),
2330 },
2331 fragment: Some(wgpu::FragmentState {
2332 module: &particle_render_shader,
2333 entry_point: Some("fs_main"),
2334 targets: &[Some(wgpu::ColorTargetState {
2335 format,
2336 blend: Some(wgpu::BlendState {
2337 color: wgpu::BlendComponent {
2338 src_factor: wgpu::BlendFactor::SrcAlpha,
2339 dst_factor: wgpu::BlendFactor::One,
2340 operation: wgpu::BlendOperation::Add,
2341 },
2342 alpha: wgpu::BlendComponent {
2343 src_factor: wgpu::BlendFactor::One,
2344 dst_factor: wgpu::BlendFactor::One,
2345 operation: wgpu::BlendOperation::Add,
2346 },
2347 }),
2348 write_mask: wgpu::ColorWrites::ALL,
2349 })],
2350 compilation_options: wgpu::PipelineCompilationOptions::default(),
2351 }),
2352 primitive: wgpu::PrimitiveState {
2353 topology: wgpu::PrimitiveTopology::PointList,
2354 ..Default::default()
2355 },
2356 depth_stencil: None,
2357 multisample: wgpu::MultisampleState::default(),
2358 multiview_mask: None,
2359 cache: pipeline_cache.as_ref(),
2360 });
2361
2362 Self {
2363 registry,
2364 ai_material_rx: None,
2365 active_offscreens: Vec::new(),
2366 effect_pipelines: std::collections::HashMap::new(),
2367 effect_params_buffer: device.create_buffer(&wgpu::BufferDescriptor {
2368 label: Some("Dummy Effect Buffer"),
2369 size: 256,
2370 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2371 mapped_at_creation: false,
2372 }),
2373 effect_params_bind_group: device.create_bind_group(&wgpu::BindGroupDescriptor {
2374 label: Some("Dummy Effect Bind Group"),
2375 layout: &device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2376 label: None,
2377 entries: &[],
2378 }),
2379 entries: &[],
2380 }),
2381 linear_sampler: device.create_sampler(&wgpu::SamplerDescriptor {
2382 label: Some("Linear Sampler"),
2383 address_mode_u: wgpu::AddressMode::ClampToEdge,
2384 address_mode_v: wgpu::AddressMode::ClampToEdge,
2385 address_mode_w: wgpu::AddressMode::ClampToEdge,
2386 mag_filter: wgpu::FilterMode::Linear,
2387 min_filter: wgpu::FilterMode::Linear,
2388 mipmap_filter: wgpu::MipmapFilterMode::Linear,
2389 ..Default::default()
2390 }),
2391 instance,
2392 adapter,
2393 device: device.clone(),
2394 queue: queue.clone(),
2395
2396 surfaces,
2397 current_window,
2398 headless_context,
2399 pipeline,
2400 opaque_pipeline,
2401 ui_pipeline,
2402 glass_pipeline,
2403 bloom_extract_pipeline,
2404 copy_pipeline,
2405 composite_pipeline,
2406 env_bind_group_layout,
2407 mega_heim_tex,
2409 mega_heim_bind_group,
2410 config: crate::subsystems::SurtrConfig::default(),
2429 text: crate::types::TextSubsystem::forge(
2432 NonZeroUsize::new(8192).unwrap(),
2433 ),
2434 heim_packer: SundrPacker::new(4096, 4096),
2435 image_uv_registry: {
2436 let mut cache = LruCache::new(NonZeroUsize::new(256).unwrap());
2437 cache.put("__mega_heim".to_string(), cvkg_core::Rect { x: 0.0, y: 0.0, width: 1.0, height: 1.0 });
2438 cache
2439 },
2440 texture_registry,
2441 texture_views: texture_views_list,
2442 dummy_sampler,
2443 svg: crate::types::SvgSubsystem::forge(
2444 &device,
2445 &queue,
2446 NonZeroUsize::new(512).unwrap(),
2447 NonZeroUsize::new(512).unwrap(),
2448 ),
2449 dummy_texture_bind_group,
2450 dummy_env_bind_group,
2451 texture_bind_group_layout,
2452 texture_bind_groups,
2453 shared_elements: LruCache::new(NonZeroUsize::new(1024).unwrap()),
2454 geometry_buffers,
2458 vertices: Vec::with_capacity(MAX_VERTICES),
2459 indices: Vec::with_capacity(MAX_INDICES),
2460 instance_data: Vec::with_capacity(MAX_VERTICES / 4),
2461 draw_calls: Vec::new(),
2462 current_texture_id: None,
2463 opacity_stack: vec![1.0],
2464 clip_stack: Vec::new(),
2465 slice_stack: Vec::new(),
2466 shadow_stack: Vec::new(),
2467 theme_buffer,
2468 scene_buffer,
2469 berserker_bind_group,
2470 berserker_bind_group_layout,
2471 start_time: std::time::Instant::now(),
2472 current_theme,
2473 current_scene,
2474 background_pipeline,
2475 current_z: 0.0,
2476 default_background_color: [0.02, 0.02, 0.05, 1.0],
2477 app_drew_background: false,
2478 frame_rendered: false,
2479 current_draw_order: 0,
2480 telemetry: cvkg_core::TelemetryData::default(),
2481 last_frame_start: std::time::Instant::now(),
2482 last_redraw_start: std::time::Instant::now(),
2483 frame_budget: cvkg_core::FrameBudget::default(),
2484 capture_staging_buffer: None,
2485 compositor_index_cursor: 0,
2486 vram_buffers_bytes: 0,
2487 vram_textures_bytes: 0,
2488 _debug_layout: false,
2489 transform_stack: Vec::new(),
2490 redraw_requested: false,
2491 skuld_queries,
2492 skuld_buffer,
2493 skuld_read_buffer,
2494 skuld_period,
2495 last_gpu_time_ns: 0,
2496 particle_compute_pipeline,
2497 particle_compute_bgl,
2498 particle_buffer,
2499 particle_uniform_buffer,
2500 particles: crate::types::ParticleSubsystem::forge(),
2502 particle_render_pipeline,
2503 particle_render_bgl,
2504 particle_render_bind_group: None,
2505 particle_compute_bind_group: None,
2506 vnode_stack: Vec::new(),
2507 event_handlers: std::collections::HashMap::new(),
2508 staging_belt,
2509 staging_command_buffers: Vec::new(),
2510 glass_output_bind_group_layout,
2511 current_draw_material: cvkg_core::DrawMaterial::Opaque,
2512 portal_regions: VecDeque::new(),
2513 cached_graph_plan: None,
2514 material_compilation_hash: 0,
2515 memo_cache: std::collections::HashMap::new(),
2516 frame_generation: 0,
2517 quality_level: QualityLevel::default(),
2518 pipeline_cache,
2519 bloom_enabled: true,
2520 volumetric_enabled: false,
2521 path_geometry_cache: lru::LruCache::new(NonZeroUsize::new(64).unwrap()),
2522 color_blind_mode: crate::color_blindness::ColorBlindMode::Normal,
2523 color_blind_intensity: 1.0,
2524 color_blind_pipeline,
2525 volumetric_pipeline,
2526 volumetric_bind_group_layout: volumetric_bgl,
2527 volumetric_uniform_buffer,
2528 volumetric_depth_sampler,
2529 hologram_instances: Vec::new(),
2530 color_blind_bind_group_layout: color_blind_bgl,
2531 color_blind_uniform_buffer,
2532 sampler,
2533 kawase_down_pipeline,
2534 kawase_up_pipeline,
2535 kawase_bind_group_layout: kawase_bgl,
2536 kawase_uniform: device.create_buffer(&wgpu::BufferDescriptor {
2537 label: Some("Kawase Persistent Uniform"),
2538 size: 32,
2539 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2540 mapped_at_creation: false,
2541 }),
2542 kawase_uniform_buffers: (0..16)
2543 .map(|i| {
2544 device.create_buffer(&wgpu::BufferDescriptor {
2545 label: Some(&format!("Kawase Persistent Uniform {}", i)),
2546 size: 32,
2547 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2548 mapped_at_creation: false,
2549 })
2550 })
2551 .collect(),
2552 bind_group_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
2553 texture_view_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
2554
2555 }
2556 }
2557
2558 pub(crate) fn rebuild_texture_array_bind_group(&mut self) {
2559 let views: Vec<&wgpu::TextureView> = self.texture_views.iter().collect();
2560 self.mega_heim_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2561 layout: &self.texture_bind_group_layout,
2562 entries: &[
2563 wgpu::BindGroupEntry {
2564 binding: 0,
2565 resource: wgpu::BindingResource::TextureViewArray(&views),
2566 },
2567 wgpu::BindGroupEntry {
2568 binding: 1,
2569 resource: wgpu::BindingResource::Sampler(&self.dummy_sampler),
2570 },
2571 ],
2572 label: Some("Surtr Texture Array Bind Group"),
2573 });
2574 }
2575
2576 pub(crate) fn update_vram_telemetry(&mut self) {
2582 let mut buffer_bytes = 0;
2584 buffer_bytes += (MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64;
2585 buffer_bytes += (MAX_INDICES * std::mem::size_of::<u32>()) as u64;
2586 buffer_bytes += std::mem::size_of::<cvkg_core::ColorTheme>() as u64;
2587 buffer_bytes += std::mem::size_of::<cvkg_core::SceneUniforms>() as u64;
2588 self.vram_buffers_bytes = buffer_bytes;
2589
2590 let mut texture_bytes = 0u64;
2592 texture_bytes += 4096 * 4096 * 4; texture_bytes += 4; for ctx in self.surfaces.values() {
2596 let bpp = 4;
2597 let surface_bytes = (ctx.config.width * ctx.config.height * bpp) as u64;
2598 texture_bytes += surface_bytes * 3;
2600 }
2601
2602 let loaded_count = self.texture_registry.len() as u64;
2606 texture_bytes += loaded_count * 512 * 512 * 4;
2607
2608 self.vram_textures_bytes = texture_bytes;
2609
2610 self.telemetry.vram_buffers_mb = buffer_bytes as f32 / 1_048_576.0;
2611 self.telemetry.vram_textures_mb = texture_bytes as f32 / 1_048_576.0;
2612 self.telemetry.vram_pipelines_mb = 0.0;
2613 self.telemetry.vram_usage_mb =
2614 self.telemetry.vram_buffers_mb + self.telemetry.vram_textures_mb;
2615 }
2616
2617 pub fn get_telemetry(&self) -> cvkg_core::TelemetryData {
2619 self.telemetry.clone()
2620 }
2621
2622 pub fn resize(
2624 &mut self,
2625 window_id: winit::window::WindowId,
2626 width: u32,
2627 height: u32,
2628 scale_factor: f32,
2629 ) {
2630 if width > 0
2631 && height > 0
2632 && let Some(ctx) = self.surfaces.get_mut(&window_id)
2633 {
2634 if ctx.config.width == width && ctx.config.height == height {
2635 return;
2637 }
2638
2639 log::info!("[GPU] Reconfiguring surface: {}x{}", width, height);
2640 SurtrRenderer::lock_or_clear_cache(&self.bind_group_cache).clear();
2641 SurtrRenderer::lock_or_clear_cache(&self.texture_view_cache).clear();
2642 self.text.shaped_cache.clear();
2643 ctx.config.width = width;
2644 ctx.config.height = height;
2645 ctx.scale_factor = scale_factor;
2646 ctx.surface.configure(&self.device, &ctx.config);
2647
2648 let texture_desc = wgpu::TextureDescriptor {
2650 label: Some("Surtr Scene Texture"),
2651 size: wgpu::Extent3d {
2652 width,
2653 height,
2654 depth_or_array_layers: 1,
2655 },
2656 mip_level_count: 1,
2657 sample_count: 1,
2658 dimension: wgpu::TextureDimension::D2,
2659 format: wgpu::TextureFormat::Rgba16Float,
2660 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2661 | wgpu::TextureUsages::TEXTURE_BINDING,
2662 view_formats: &[],
2663 };
2664
2665 let scene_tex = self.device.create_texture(&texture_desc);
2666
2667 let msaa_desc = wgpu::TextureDescriptor {
2668 label: Some("Scene MSAA"),
2669 size: texture_desc.size,
2670 mip_level_count: 1,
2671 sample_count: self.quality_level.msaa_sample_count(),
2672 dimension: wgpu::TextureDimension::D2,
2673 format: wgpu::TextureFormat::Rgba16Float,
2674 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2675 view_formats: &[],
2676 };
2677 let scene_msaa_tex = self.device.create_texture(&msaa_desc);
2678 ctx.scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
2679 ctx.scene_msaa_texture =
2680 scene_msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
2681
2682 self.registry.remove_image(ctx.blur_tex_a);
2683 self.registry.remove_image(ctx.blur_tex_b);
2684 self.registry.remove_image(ctx.bloom_tex_a);
2685 self.registry.remove_image(ctx.bloom_tex_b);
2686
2687 let blur_width = (width / 2).max(1);
2688 let blur_height = (height / 2).max(1);
2689
2690 let blur_desc_a = crate::kvasir::resource::ResourceDescriptor {
2691 label: Some("Surtr Blur Texture A".into()),
2692 kind: crate::kvasir::resource::ResourceKind::Image {
2693 format: ctx.config.format,
2694 width: blur_width,
2695 height: blur_height,
2696 mip_level_count: compute_mip_levels(blur_width, blur_height),
2697 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2698 | wgpu::TextureUsages::TEXTURE_BINDING
2699 | wgpu::TextureUsages::COPY_SRC,
2700 },
2701 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2702 };
2703 ctx.blur_tex_a = self.registry.allocate_image(&self.device, &blur_desc_a);
2704
2705 let blur_desc_b = crate::kvasir::resource::ResourceDescriptor {
2706 label: Some("Surtr Blur Texture B".into()),
2707 kind: crate::kvasir::resource::ResourceKind::Image {
2708 format: ctx.config.format,
2709 width: blur_width,
2710 height: blur_height,
2711 mip_level_count: compute_mip_levels(blur_width, blur_height),
2712 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2713 | wgpu::TextureUsages::TEXTURE_BINDING
2714 | wgpu::TextureUsages::COPY_SRC,
2715 },
2716 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2717 };
2718 ctx.blur_tex_b = self.registry.allocate_image(&self.device, &blur_desc_b);
2719
2720 let bloom_desc_a = crate::kvasir::resource::ResourceDescriptor {
2721 label: Some("Surtr Bloom Texture A".into()),
2722 kind: crate::kvasir::resource::ResourceKind::Image {
2723 format: ctx.config.format,
2724 width: blur_width,
2725 height: blur_height,
2726 mip_level_count: compute_mip_levels(blur_width, blur_height),
2727 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2728 | wgpu::TextureUsages::TEXTURE_BINDING
2729 | wgpu::TextureUsages::COPY_SRC,
2730 },
2731 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2732 };
2733 ctx.bloom_tex_a = self.registry.allocate_image(&self.device, &bloom_desc_a);
2734
2735 let bloom_desc_b = crate::kvasir::resource::ResourceDescriptor {
2736 label: Some("Surtr Bloom Texture B".into()),
2737 kind: crate::kvasir::resource::ResourceKind::Image {
2738 format: ctx.config.format,
2739 width: blur_width,
2740 height: blur_height,
2741 mip_level_count: compute_mip_levels(blur_width, blur_height),
2742 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2743 | wgpu::TextureUsages::TEXTURE_BINDING
2744 | wgpu::TextureUsages::COPY_SRC,
2745 },
2746 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2747 };
2748 ctx.bloom_tex_b = self.registry.allocate_image(&self.device, &bloom_desc_b);
2749
2750 ctx.scene_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2752 layout: &self.env_bind_group_layout,
2753 entries: &[
2754 wgpu::BindGroupEntry {
2755 binding: 0,
2756 resource: wgpu::BindingResource::TextureView(&ctx.scene_texture),
2757 },
2758 wgpu::BindGroupEntry {
2759 binding: 1,
2760 resource: wgpu::BindingResource::Sampler(&ctx.sampler),
2761 },
2762 ],
2763 label: Some("Scene Bind Group Resize"),
2764 });
2765
2766 let scene_views: Vec<&wgpu::TextureView> =
2767 (0..32).map(|_| &ctx.scene_texture).collect();
2768 ctx.scene_texture_bind_group =
2769 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2770 layout: &self.texture_bind_group_layout,
2771 entries: &[
2772 wgpu::BindGroupEntry {
2773 binding: 0,
2774 resource: wgpu::BindingResource::TextureViewArray(&scene_views),
2775 },
2776 wgpu::BindGroupEntry {
2777 binding: 1,
2778 resource: wgpu::BindingResource::Sampler(&ctx.sampler),
2779 },
2780 ],
2781 label: Some("Scene Texture Bind Group Resize"),
2782 });
2783
2784 let depth_texture = self.device.create_texture(&wgpu::TextureDescriptor {
2785 label: Some("Surtr Depth Texture"),
2786 size: wgpu::Extent3d {
2787 width,
2788 height,
2789 depth_or_array_layers: 1,
2790 },
2791 mip_level_count: 1,
2792 sample_count: self.quality_level.msaa_sample_count(),
2793 dimension: wgpu::TextureDimension::D2,
2794 format: wgpu::TextureFormat::Depth32Float,
2795 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
2796 view_formats: &[],
2797 });
2798 ctx.depth_texture_view =
2799 depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
2800 }
2801 }
2802
2803 pub fn begin_frame_headless(&mut self) -> wgpu::CommandEncoder {
2805 self.current_window = None;
2806 self.compositor_index_cursor = self.indices.len() as u32;
2807 self.reset_frame_state();
2808
2809 self.staging_belt.recall();
2811
2812 let ctx = self
2813 .headless_context
2814 .as_ref()
2815 .expect("Headless context not initialized");
2816 let time = self.start_time.elapsed().as_secs_f32();
2817 let logical_w = ctx.width as f32 / ctx.scale_factor;
2818 let logical_h = ctx.height as f32 / ctx.scale_factor;
2819 let dt = time - self.current_scene.time;
2820 self.current_scene.time = time;
2821 self.current_scene.delta_time = dt;
2822 self.current_scene.resolution = [logical_w, logical_h];
2823 self.current_scene.scale_factor = ctx.scale_factor;
2824 self.current_scene.proj =
2825 glam::Mat4::orthographic_lh(0.0, logical_w, logical_h, 0.0, -1000.0, 1000.0);
2826
2827 self.queue.write_buffer(
2828 &self.scene_buffer,
2829 0,
2830 bytemuck::bytes_of(&self.current_scene),
2831 );
2832
2833 self.device
2834 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2835 label: Some("Surtr Headless Command Encoder"),
2836 })
2837 }
2838
2839 fn reset_frame_state(&mut self) {
2842 self.vertices.clear();
2843 self.indices.clear();
2844 self.instance_data.clear();
2845 self.draw_calls.clear();
2846 self.svg.clear_filter_batches();
2847 self.shared_elements.clear();
2848 self.current_texture_id = None;
2849 self.opacity_stack.clear();
2850 self.opacity_stack.push(1.0);
2851 self.clip_stack.clear();
2852 self.slice_stack.clear();
2853 self.transform_stack.clear();
2854 self.portal_regions.clear();
2855 self.hologram_instances.clear();
2856 self.current_z = 0.0;
2857 self.vnode_stack.clear();
2858 self.event_handlers.clear();
2859 let current_time = self.current_time();
2863 let resolution = [
2864 self.current_width() as f32,
2865 self.current_height() as f32,
2866 ];
2867 let time_uniform: [f32; 4] = [
2868 current_time,
2869 resolution[0],
2870 resolution[1],
2871 0.0, ];
2873 self.queue.write_buffer(
2874 &self.volumetric_uniform_buffer,
2875 0,
2876 bytemuck::cast_slice(&time_uniform),
2877 );
2878 self.frame_generation += 1;
2880 const MAX_MEMO_AGE: u64 = 1000;
2882 if self.frame_generation > MAX_MEMO_AGE {
2883 let cutoff = self.frame_generation - MAX_MEMO_AGE;
2884 self.memo_cache
2885 .retain(|_, entry| entry.frame_gen >= cutoff);
2886 }
2887 self.last_frame_start = std::time::Instant::now();
2888 self.telemetry.draw_calls = 0;
2889 self.telemetry.vertices = 0;
2890 }
2891
2892 pub fn begin_frame(&mut self, window_id: winit::window::WindowId) -> wgpu::CommandEncoder {
2894 self.begin_frame_internal(window_id, true)
2895 }
2896
2897 pub fn begin_frame_reuse(&mut self, window_id: winit::window::WindowId) -> wgpu::CommandEncoder {
2900 self.begin_frame_internal(window_id, false)
2901 }
2902
2903 fn begin_frame_internal(&mut self, window_id: winit::window::WindowId, reset_state: bool) -> wgpu::CommandEncoder {
2904 if let Some(rx) = &self.ai_material_rx {
2906 while let Ok(res) = rx.try_recv() {
2907 match res {
2908 Ok(_) => log::info!("[Surtr] Received AI generated material"),
2909 Err(e) => log::warn!("[Surtr] AI material generation error: {:?}", e),
2910 }
2911 }
2912 }
2913
2914 self.staging_belt.recall();
2918 self.current_window = Some(window_id);
2919 if reset_state {
2920 self.reset_frame_state();
2921 }
2922
2923 let ctx = self
2924 .surfaces
2925 .get(&window_id)
2926 .expect("Window not registered");
2927 let time = self.start_time.elapsed().as_secs_f32();
2928 let logical_w = ctx.config.width as f32 / ctx.scale_factor;
2929 let logical_h = ctx.config.height as f32 / ctx.scale_factor;
2930 let dt = time - self.current_scene.time;
2931 self.current_scene.time = time;
2932 self.current_scene.delta_time = dt;
2933 self.current_scene.resolution = [logical_w, logical_h];
2934 self.current_scene.scale_factor = ctx.scale_factor;
2935 self.current_scene.proj =
2936 glam::Mat4::orthographic_lh(0.0, logical_w, logical_h, 0.0, -1000.0, 1000.0);
2937
2938 self.queue.write_buffer(
2939 &self.scene_buffer,
2940 0,
2941 bytemuck::bytes_of(&self.current_scene),
2942 );
2943
2944 self.device
2945 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2946 label: Some("Surtr Command Encoder"),
2947 })
2948 }
2949
2950 pub fn register_window(&mut self, window: Arc<winit::window::Window>) {
2952 let size = window.inner_size();
2953 let surface = self
2954 .instance
2955 .create_surface(window.clone())
2956 .expect("Failed to create surface");
2957 let caps = surface.get_capabilities(&self.adapter);
2958 let format = caps.formats[0];
2959
2960 let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Mailbox) {
2962 wgpu::PresentMode::Mailbox
2963 } else {
2964 log::warn!("[GPU] Mailbox not supported, falling back to Fifo (V-Sync)");
2965 wgpu::PresentMode::Fifo
2966 };
2967
2968 let alpha_mode = if caps
2969 .alpha_modes
2970 .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
2971 {
2972 wgpu::CompositeAlphaMode::PostMultiplied
2973 } else if caps
2974 .alpha_modes
2975 .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
2976 {
2977 wgpu::CompositeAlphaMode::PreMultiplied
2978 } else {
2979 caps.alpha_modes[0]
2980 };
2981
2982 log::info!(
2983 "[GPU] Configuring surface: {}x{} | {:?} | {:?}",
2984 size.width,
2985 size.height,
2986 present_mode,
2987 alpha_mode
2988 );
2989
2990 let config = wgpu::SurfaceConfiguration {
2991 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2992 format,
2993 width: size.width,
2994 height: size.height,
2995 present_mode,
2996 alpha_mode,
2997 view_formats: vec![],
2998 desired_maximum_frame_latency: 1,
2999 };
3000 surface.configure(&self.device, &config);
3001
3002 let ctx = Self::create_surface_context(
3003 &self.device,
3004 surface,
3005 config,
3006 &self.env_bind_group_layout,
3007 &self.texture_bind_group_layout,
3008 window.scale_factor() as f32,
3009 self.quality_level.msaa_sample_count(),
3010 &mut self.registry,
3011 );
3012
3013 self.surfaces.insert(window.id(), ctx);
3014 }
3015
3016 pub(crate) fn create_headless_context(
3017 device: &wgpu::Device,
3018 width: u32,
3019 height: u32,
3020 format: wgpu::TextureFormat,
3021 env_bind_group_layout: &wgpu::BindGroupLayout,
3022 texture_bind_group_layout: &wgpu::BindGroupLayout,
3023 registry: &mut crate::kvasir::registry::ResourceRegistry,
3024 msaa_sample_count: u32,
3025 ) -> HeadlessContext {
3026 let texture_desc = wgpu::TextureDescriptor {
3027 label: Some("Surtr Headless Scene Texture"),
3028 size: wgpu::Extent3d {
3029 width,
3030 height,
3031 depth_or_array_layers: 1,
3032 },
3033 mip_level_count: 1,
3034 sample_count: 1,
3035 dimension: wgpu::TextureDimension::D2,
3036 format: wgpu::TextureFormat::Rgba16Float,
3037 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
3038 | wgpu::TextureUsages::TEXTURE_BINDING
3039 | wgpu::TextureUsages::COPY_SRC,
3040 view_formats: &[],
3041 };
3042
3043 let scene_tex = device.create_texture(&texture_desc);
3044
3045 let msaa_desc = wgpu::TextureDescriptor {
3046 label: Some("Scene MSAA"),
3047 size: texture_desc.size,
3048 mip_level_count: 1,
3049 sample_count: msaa_sample_count,
3050 dimension: wgpu::TextureDimension::D2,
3051 format: wgpu::TextureFormat::Rgba16Float,
3052 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
3053 view_formats: &[],
3054 };
3055 let scene_msaa_tex = device.create_texture(&msaa_desc);
3056 let scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
3057 let scene_msaa_texture =
3058 scene_msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
3059
3060 let blur_width = (width / 2).max(1);
3061 let blur_height = (height / 2).max(1);
3062 let blur_desc_a = crate::kvasir::resource::ResourceDescriptor {
3063 label: Some("Headless Blur Texture A".into()),
3064 kind: crate::kvasir::resource::ResourceKind::Image {
3065 format,
3066 width: blur_width,
3067 height: blur_height,
3068 mip_level_count: compute_mip_levels(blur_width, blur_height),
3069 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
3070 | wgpu::TextureUsages::TEXTURE_BINDING
3071 | wgpu::TextureUsages::COPY_SRC,
3072 },
3073 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
3074 };
3075 let blur_tex_a = registry.allocate_image(device, &blur_desc_a);
3076
3077 let blur_desc_b = crate::kvasir::resource::ResourceDescriptor {
3078 label: Some("Headless Blur Texture B".into()),
3079 kind: crate::kvasir::resource::ResourceKind::Image {
3080 format,
3081 width: blur_width,
3082 height: blur_height,
3083 mip_level_count: compute_mip_levels(blur_width, blur_height),
3084 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
3085 | wgpu::TextureUsages::TEXTURE_BINDING
3086 | wgpu::TextureUsages::COPY_SRC,
3087 },
3088 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
3089 };
3090 let blur_tex_b = registry.allocate_image(device, &blur_desc_b);
3091
3092 let bloom_desc_a = crate::kvasir::resource::ResourceDescriptor {
3093 label: Some("Headless Bloom Texture A".into()),
3094 kind: crate::kvasir::resource::ResourceKind::Image {
3095 format,
3096 width: blur_width,
3097 height: blur_height,
3098 mip_level_count: compute_mip_levels(blur_width, blur_height),
3099 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
3100 | wgpu::TextureUsages::TEXTURE_BINDING
3101 | wgpu::TextureUsages::COPY_SRC,
3102 },
3103 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
3104 };
3105 let bloom_tex_a = registry.allocate_image(device, &bloom_desc_a);
3106
3107 let bloom_desc_b = crate::kvasir::resource::ResourceDescriptor {
3108 label: Some("Headless Bloom Texture B".into()),
3109 kind: crate::kvasir::resource::ResourceKind::Image {
3110 format,
3111 width: blur_width,
3112 height: blur_height,
3113 mip_level_count: compute_mip_levels(blur_width, blur_height),
3114 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
3115 | wgpu::TextureUsages::TEXTURE_BINDING
3116 | wgpu::TextureUsages::COPY_SRC,
3117 },
3118 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
3119 };
3120 let bloom_tex_b = registry.allocate_image(device, &bloom_desc_b);
3121
3122 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
3123 address_mode_u: wgpu::AddressMode::ClampToEdge,
3124 address_mode_v: wgpu::AddressMode::ClampToEdge,
3125 mag_filter: wgpu::FilterMode::Linear,
3126 min_filter: wgpu::FilterMode::Linear,
3127 ..Default::default()
3128 });
3129
3130 let scene_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3131 layout: env_bind_group_layout,
3132 entries: &[
3133 wgpu::BindGroupEntry {
3134 binding: 0,
3135 resource: wgpu::BindingResource::TextureView(&scene_texture),
3136 },
3137 wgpu::BindGroupEntry {
3138 binding: 1,
3139 resource: wgpu::BindingResource::Sampler(&sampler),
3140 },
3141 ],
3142 label: Some("Headless Scene Bind Group"),
3143 });
3144
3145 let blur_view_a = registry
3149 .get_texture_view(blur_tex_a)
3150 .expect("headless: blur_tex_a view must exist after allocation");
3151 let blur_view_b = registry
3152 .get_texture_view(blur_tex_b)
3153 .expect("headless: blur_tex_b view must exist after allocation");
3154 let bloom_view_a = registry
3155 .get_texture_view(bloom_tex_a)
3156 .expect("headless: bloom_tex_a view must exist after allocation");
3157 let bloom_view_b = registry
3158 .get_texture_view(bloom_tex_b)
3159 .expect("headless: bloom_tex_b view must exist after allocation");
3160
3161 let blur_env_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
3162 layout: env_bind_group_layout,
3163 entries: &[
3164 wgpu::BindGroupEntry {
3165 binding: 0,
3166 resource: wgpu::BindingResource::TextureView(&blur_view_a),
3167 },
3168 wgpu::BindGroupEntry {
3169 binding: 1,
3170 resource: wgpu::BindingResource::Sampler(&sampler),
3171 },
3172 ],
3173 label: Some("Headless Blur Env Bind Group A"),
3174 });
3175 let blur_env_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
3176 layout: env_bind_group_layout,
3177 entries: &[
3178 wgpu::BindGroupEntry {
3179 binding: 0,
3180 resource: wgpu::BindingResource::TextureView(&blur_view_b),
3181 },
3182 wgpu::BindGroupEntry {
3183 binding: 1,
3184 resource: wgpu::BindingResource::Sampler(&sampler),
3185 },
3186 ],
3187 label: Some("Headless Blur Env Bind Group B"),
3188 });
3189 let bloom_env_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
3190 layout: env_bind_group_layout,
3191 entries: &[
3192 wgpu::BindGroupEntry {
3193 binding: 0,
3194 resource: wgpu::BindingResource::TextureView(&bloom_view_a),
3195 },
3196 wgpu::BindGroupEntry {
3197 binding: 1,
3198 resource: wgpu::BindingResource::Sampler(&sampler),
3199 },
3200 ],
3201 label: Some("Headless Bloom Env Bind Group A"),
3202 });
3203 let bloom_env_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
3204 layout: env_bind_group_layout,
3205 entries: &[
3206 wgpu::BindGroupEntry {
3207 binding: 0,
3208 resource: wgpu::BindingResource::TextureView(&bloom_view_b),
3209 },
3210 wgpu::BindGroupEntry {
3211 binding: 1,
3212 resource: wgpu::BindingResource::Sampler(&sampler),
3213 },
3214 ],
3215 label: Some("Headless Bloom Env Bind Group B"),
3216 });
3217
3218 let scene_views: Vec<&wgpu::TextureView> = (0..32).map(|_| &scene_texture).collect();
3219 let scene_texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3220 layout: texture_bind_group_layout,
3221 entries: &[
3222 wgpu::BindGroupEntry {
3223 binding: 0,
3224 resource: wgpu::BindingResource::TextureViewArray(&scene_views),
3225 },
3226 wgpu::BindGroupEntry {
3227 binding: 1,
3228 resource: wgpu::BindingResource::Sampler(&sampler),
3229 },
3230 ],
3231 label: Some("Headless Scene Texture Bind Group"),
3232 });
3233
3234 let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
3235 label: Some("Headless Depth Texture"),
3236 size: wgpu::Extent3d {
3237 width,
3238 height,
3239 depth_or_array_layers: 1,
3240 },
3241 mip_level_count: 1,
3242 sample_count: 4,
3243 dimension: wgpu::TextureDimension::D2,
3244 format: wgpu::TextureFormat::Depth32Float,
3245 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
3246 view_formats: &[],
3247 });
3248 let depth_texture_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
3249
3250 let output_texture = device.create_texture(&wgpu::TextureDescriptor {
3251 label: Some("Headless Output Texture"),
3252 size: wgpu::Extent3d {
3253 width,
3254 height,
3255 depth_or_array_layers: 1,
3256 },
3257 mip_level_count: 1,
3258 sample_count: 1,
3259 dimension: wgpu::TextureDimension::D2,
3260 format,
3261 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
3262 | wgpu::TextureUsages::COPY_DST
3263 | wgpu::TextureUsages::COPY_SRC,
3264 view_formats: &[],
3265 });
3266 let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
3267
3268 crate::types::HeadlessContext {
3269 scene_texture,
3270 scene_msaa_texture,
3271 scene_bind_group,
3272 scene_texture_bind_group,
3273 depth_texture_view,
3274 blur_tex_a,
3275 blur_tex_b,
3276 bloom_tex_a,
3277 bloom_tex_b,
3278 blur_env_bind_group_a,
3279 blur_env_bind_group_b,
3280 bloom_env_bind_group_a,
3281 bloom_env_bind_group_b,
3282 scale_factor: 1.0,
3283 sampler,
3284 width,
3285 height,
3286 output_texture,
3287 output_view,
3288 }
3289 }
3290
3291 pub(crate) fn create_surface_context(
3292 device: &wgpu::Device,
3293 surface: wgpu::Surface<'static>,
3294 config: wgpu::SurfaceConfiguration,
3295 env_bind_group_layout: &wgpu::BindGroupLayout,
3296 texture_bind_group_layout: &wgpu::BindGroupLayout,
3297 scale_factor: f32,
3298 msaa_sample_count: u32,
3299 registry: &mut crate::kvasir::registry::ResourceRegistry,
3300 ) -> SurfaceContext {
3301 let width = config.width;
3302 let height = config.height;
3303
3304 let texture_desc = wgpu::TextureDescriptor {
3305 label: Some("Surtr Scene Texture"),
3306 size: wgpu::Extent3d {
3307 width,
3308 height,
3309 depth_or_array_layers: 1,
3310 },
3311 mip_level_count: 1,
3312 sample_count: 1,
3313 dimension: wgpu::TextureDimension::D2,
3314 format: wgpu::TextureFormat::Rgba16Float,
3315 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
3316 view_formats: &[],
3317 };
3318
3319 let scene_tex = device.create_texture(&texture_desc);
3320
3321 let msaa_desc = wgpu::TextureDescriptor {
3322 label: Some("Scene MSAA"),
3323 size: texture_desc.size,
3324 mip_level_count: 1,
3325 sample_count: msaa_sample_count,
3326 dimension: wgpu::TextureDimension::D2,
3327 format: wgpu::TextureFormat::Rgba16Float,
3328 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
3329 view_formats: &[],
3330 };
3331 let scene_msaa_tex = device.create_texture(&msaa_desc);
3332 let scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
3333 let scene_msaa_texture =
3334 scene_msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
3335
3336 let blur_width = (config.width / 2).max(1);
3337 let blur_height = (config.height / 2).max(1);
3338 let blur_desc_a = crate::kvasir::resource::ResourceDescriptor {
3339 label: Some("Surface Blur Texture A".into()),
3340 kind: crate::kvasir::resource::ResourceKind::Image {
3341 format: config.format,
3342 width: blur_width,
3343 height: blur_height,
3344 mip_level_count: compute_mip_levels(blur_width, blur_height),
3345 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
3346 | wgpu::TextureUsages::TEXTURE_BINDING
3347 | wgpu::TextureUsages::COPY_SRC,
3348 },
3349 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
3350 };
3351 let blur_tex_a = registry.allocate_image(device, &blur_desc_a);
3352
3353 let blur_desc_b = crate::kvasir::resource::ResourceDescriptor {
3354 label: Some("Surface Blur Texture B".into()),
3355 kind: crate::kvasir::resource::ResourceKind::Image {
3356 format: config.format,
3357 width: blur_width,
3358 height: blur_height,
3359 mip_level_count: compute_mip_levels(blur_width, blur_height),
3360 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
3361 | wgpu::TextureUsages::TEXTURE_BINDING
3362 | wgpu::TextureUsages::COPY_SRC,
3363 },
3364 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
3365 };
3366 let blur_tex_b = registry.allocate_image(device, &blur_desc_b);
3367
3368 let bloom_desc_a = crate::kvasir::resource::ResourceDescriptor {
3369 label: Some("Surface Bloom Texture A".into()),
3370 kind: crate::kvasir::resource::ResourceKind::Image {
3371 format: config.format,
3372 width: blur_width,
3373 height: blur_height,
3374 mip_level_count: compute_mip_levels(blur_width, blur_height),
3375 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
3376 | wgpu::TextureUsages::TEXTURE_BINDING
3377 | wgpu::TextureUsages::COPY_SRC,
3378 },
3379 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
3380 };
3381 let bloom_tex_a = registry.allocate_image(device, &bloom_desc_a);
3382
3383 let bloom_desc_b = crate::kvasir::resource::ResourceDescriptor {
3384 label: Some("Surface Bloom Texture B".into()),
3385 kind: crate::kvasir::resource::ResourceKind::Image {
3386 format: config.format,
3387 width: blur_width,
3388 height: blur_height,
3389 mip_level_count: compute_mip_levels(blur_width, blur_height),
3390 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
3391 | wgpu::TextureUsages::TEXTURE_BINDING
3392 | wgpu::TextureUsages::COPY_SRC,
3393 },
3394 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
3395 };
3396 let bloom_tex_b = registry.allocate_image(device, &bloom_desc_b);
3397
3398 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
3399 address_mode_u: wgpu::AddressMode::ClampToEdge,
3400 address_mode_v: wgpu::AddressMode::ClampToEdge,
3401 mag_filter: wgpu::FilterMode::Linear,
3402 min_filter: wgpu::FilterMode::Linear,
3403 ..Default::default()
3404 });
3405
3406 let scene_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3407 layout: env_bind_group_layout,
3408 entries: &[
3409 wgpu::BindGroupEntry {
3410 binding: 0,
3411 resource: wgpu::BindingResource::TextureView(&scene_texture),
3412 },
3413 wgpu::BindGroupEntry {
3414 binding: 1,
3415 resource: wgpu::BindingResource::Sampler(&sampler),
3416 },
3417 ],
3418 label: Some("Scene Bind Group"),
3419 });
3420
3421 let blur_view_a = registry
3425 .get_texture_view(blur_tex_a)
3426 .expect("resize: blur_tex_a view must exist after allocation");
3427 let blur_view_b = registry
3428 .get_texture_view(blur_tex_b)
3429 .expect("resize: blur_tex_b view must exist after allocation");
3430 let bloom_view_a = registry
3431 .get_texture_view(bloom_tex_a)
3432 .expect("resize: bloom_tex_a view must exist after allocation");
3433 let bloom_view_b = registry
3434 .get_texture_view(bloom_tex_b)
3435 .expect("resize: bloom_tex_b view must exist after allocation");
3436
3437 let blur_env_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
3438 layout: env_bind_group_layout,
3439 entries: &[
3440 wgpu::BindGroupEntry {
3441 binding: 0,
3442 resource: wgpu::BindingResource::TextureView(&blur_view_a),
3443 },
3444 wgpu::BindGroupEntry {
3445 binding: 1,
3446 resource: wgpu::BindingResource::Sampler(&sampler),
3447 },
3448 ],
3449 label: Some("Blur Env Bind Group A"),
3450 });
3451 let blur_env_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
3452 layout: env_bind_group_layout,
3453 entries: &[
3454 wgpu::BindGroupEntry {
3455 binding: 0,
3456 resource: wgpu::BindingResource::TextureView(&blur_view_b),
3457 },
3458 wgpu::BindGroupEntry {
3459 binding: 1,
3460 resource: wgpu::BindingResource::Sampler(&sampler),
3461 },
3462 ],
3463 label: Some("Blur Env Bind Group B"),
3464 });
3465 let bloom_env_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
3466 layout: env_bind_group_layout,
3467 entries: &[
3468 wgpu::BindGroupEntry {
3469 binding: 0,
3470 resource: wgpu::BindingResource::TextureView(&bloom_view_a),
3471 },
3472 wgpu::BindGroupEntry {
3473 binding: 1,
3474 resource: wgpu::BindingResource::Sampler(&sampler),
3475 },
3476 ],
3477 label: Some("Bloom Env Bind Group A"),
3478 });
3479 let bloom_env_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
3480 layout: env_bind_group_layout,
3481 entries: &[
3482 wgpu::BindGroupEntry {
3483 binding: 0,
3484 resource: wgpu::BindingResource::TextureView(&bloom_view_b),
3485 },
3486 wgpu::BindGroupEntry {
3487 binding: 1,
3488 resource: wgpu::BindingResource::Sampler(&sampler),
3489 },
3490 ],
3491 label: Some("Bloom Env Bind Group B"),
3492 });
3493
3494 let scene_views: Vec<&wgpu::TextureView> = (0..32).map(|_| &scene_texture).collect();
3495 let scene_texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3496 layout: texture_bind_group_layout,
3497 entries: &[
3498 wgpu::BindGroupEntry {
3499 binding: 0,
3500 resource: wgpu::BindingResource::TextureViewArray(&scene_views),
3501 },
3502 wgpu::BindGroupEntry {
3503 binding: 1,
3504 resource: wgpu::BindingResource::Sampler(&sampler),
3505 },
3506 ],
3507 label: Some("Scene Texture Bind Group"),
3508 });
3509
3510 let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
3511 label: Some("Surtr Depth Texture"),
3512 size: wgpu::Extent3d {
3513 width,
3514 height,
3515 depth_or_array_layers: 1,
3516 },
3517 mip_level_count: 1,
3518 sample_count: 4,
3519 dimension: wgpu::TextureDimension::D2,
3520 format: wgpu::TextureFormat::Depth32Float,
3521 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
3522 view_formats: &[],
3523 });
3524 let depth_texture_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
3525
3526 crate::types::SurfaceContext {
3527 surface,
3528 config,
3529 scene_texture,
3530 scene_msaa_texture,
3531 scene_bind_group,
3532 scene_texture_bind_group,
3533 depth_texture_view,
3534 blur_tex_a,
3535 blur_tex_b,
3536 bloom_tex_a,
3537 bloom_tex_b,
3538 blur_env_bind_group_a,
3539 blur_env_bind_group_b,
3540 bloom_env_bind_group_a,
3541 bloom_env_bind_group_b,
3542 scale_factor,
3543 sampler,
3544 }
3545 }
3546
3547 pub fn reset_time(&mut self) {
3548 self.start_time = std::time::Instant::now();
3549 }
3550
3551 pub fn reclaim_vram(&mut self) {
3554 log::warn!("[GPU] Sundr Compaction: Compacting Mega-Heim...");
3555
3556 let new_mega_heim_tex = self.device.create_texture(&wgpu::TextureDescriptor {
3557 label: Some("Sundr Mega-Heim (Compacted)"),
3558 size: wgpu::Extent3d {
3559 width: 4096,
3560 height: 4096,
3561 depth_or_array_layers: 1,
3562 },
3563 mip_level_count: 1,
3564 sample_count: 1,
3565 dimension: wgpu::TextureDimension::D2,
3566 format: wgpu::TextureFormat::Rgba8UnormSrgb,
3567 usage: wgpu::TextureUsages::TEXTURE_BINDING
3568 | wgpu::TextureUsages::COPY_DST
3569 | wgpu::TextureUsages::COPY_SRC,
3570 view_formats: &[],
3571 });
3572
3573 let mut new_packer = SundrPacker::new(4096, 4096);
3574 let mut encoder = self
3575 .device
3576 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
3577 label: Some("Heim Compaction Encoder"),
3578 });
3579
3580 let image_entries: Vec<(String, Rect)> = self
3581 .image_uv_registry
3582 .iter()
3583 .map(|(k, v)| (k.clone(), *v))
3584 .collect();
3585 for (name, old_uv) in image_entries {
3586 if let Some(&tex_idx) = self.texture_registry.get(&name)
3587 && tex_idx == 0
3588 {
3589 let w_px = (old_uv.width * 4096.0).round() as u32;
3590 let h_px = (old_uv.height * 4096.0).round() as u32;
3591 let old_x_px = (old_uv.x * 4096.0).round() as u32;
3592 let old_y_px = (old_uv.y * 4096.0).round() as u32;
3593
3594 if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
3595 encoder.copy_texture_to_texture(
3596 wgpu::TexelCopyTextureInfo {
3597 texture: &self.mega_heim_tex,
3598 mip_level: 0,
3599 origin: wgpu::Origin3d {
3600 x: old_x_px,
3601 y: old_y_px,
3602 z: 0,
3603 },
3604 aspect: wgpu::TextureAspect::All,
3605 },
3606 wgpu::TexelCopyTextureInfo {
3607 texture: &new_mega_heim_tex,
3608 mip_level: 0,
3609 origin: wgpu::Origin3d {
3610 x: new_x,
3611 y: new_y,
3612 z: 0,
3613 },
3614 aspect: wgpu::TextureAspect::All,
3615 },
3616 wgpu::Extent3d {
3617 width: w_px,
3618 height: h_px,
3619 depth_or_array_layers: 1,
3620 },
3621 );
3622
3623 let new_uv = Rect {
3624 x: new_x as f32 / 4096.0,
3625 y: new_y as f32 / 4096.0,
3626 width: old_uv.width,
3627 height: old_uv.height,
3628 };
3629 self.image_uv_registry.put(name.clone(), new_uv);
3630 }
3631 }
3632 }
3633
3634 let text_entries: Vec<(u64, (Rect, f32, f32, f32, f32))> =
3635 self.text.glyph_cache.iter().map(|(k, v)| (*k, *v)).collect();
3636 for (hash, (old_uv, w_f, h_f, x_off, y_off)) in text_entries {
3637 let w_px = (old_uv.width * 4096.0).round() as u32;
3638 let h_px = (old_uv.height * 4096.0).round() as u32;
3639 let old_x_px = (old_uv.x * 4096.0).round() as u32;
3640 let old_y_px = (old_uv.y * 4096.0).round() as u32;
3641
3642 if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
3643 encoder.copy_texture_to_texture(
3644 wgpu::TexelCopyTextureInfo {
3645 texture: &self.mega_heim_tex,
3646 mip_level: 0,
3647 origin: wgpu::Origin3d {
3648 x: old_x_px,
3649 y: old_y_px,
3650 z: 0,
3651 },
3652 aspect: wgpu::TextureAspect::All,
3653 },
3654 wgpu::TexelCopyTextureInfo {
3655 texture: &new_mega_heim_tex,
3656 mip_level: 0,
3657 origin: wgpu::Origin3d {
3658 x: new_x,
3659 y: new_y,
3660 z: 0,
3661 },
3662 aspect: wgpu::TextureAspect::All,
3663 },
3664 wgpu::Extent3d {
3665 width: w_px,
3666 height: h_px,
3667 depth_or_array_layers: 1,
3668 },
3669 );
3670
3671 let new_uv = Rect {
3672 x: new_x as f32 / 4096.0,
3673 y: new_y as f32 / 4096.0,
3674 width: old_uv.width,
3675 height: old_uv.height,
3676 };
3677 self.text.glyph_cache.put(hash, (new_uv, w_f, h_f, x_off, y_off));
3678 }
3679 }
3680
3681 self.queue.submit(std::iter::once(encoder.finish()));
3682
3683 self.mega_heim_tex = new_mega_heim_tex;
3684 let mega_heim_view_obj = self
3685 .mega_heim_tex
3686 .create_view(&wgpu::TextureViewDescriptor::default());
3687 self.texture_views[0] = mega_heim_view_obj.clone();
3688
3689 self.rebuild_texture_array_bind_group();
3690
3691 if !self.texture_bind_groups.is_empty() {
3692 self.texture_bind_groups[0] = self.mega_heim_bind_group.clone();
3693 }
3694
3695 self.heim_packer = new_packer;
3696 self.telemetry.vram_exhausted = false;
3697 }
3698
3699 pub(crate) fn shatter_internal(
3700 &mut self,
3701 rect: Rect,
3702 pieces: u32,
3703 force: f32,
3704 color: [f32; 4],
3705 material_id: u32,
3706 ) {
3707 let count = (pieces as f32).sqrt().ceil() as u32;
3709 let dw = rect.width / count as f32;
3710 let dh = rect.height / count as f32;
3711
3712 let c = self.apply_opacity(color);
3713
3714 let cx = rect.x + rect.width * 0.5;
3715 let cy = rect.y + rect.height * 0.5;
3716
3717 for y in 0..count {
3718 for x in 0..count {
3719 let init_x = rect.x + x as f32 * dw;
3720 let init_y = rect.y + y as f32 * dh;
3721
3722 let dx = (init_x + dw * 0.5) - cx;
3724 let dy = (init_y + dh * 0.5) - cy;
3725 let dist = (dx * dx + dy * dy).sqrt().max(1.0);
3726
3727 let nx = dx / dist;
3729 let ny = dy / dist;
3730
3731 let hash =
3733 ((x as f32 * 12.9898 + y as f32 * 78.233).sin().fract() * 43_758.547).fract();
3734 let hash2 =
3735 ((x as f32 * 37.11 + y as f32 * 149.87).sin().fract() * 23_412.19).fract();
3736
3737 let speed_var = 0.5 + hash * 1.5;
3738 let angle = ny.atan2(nx) + (hash2 - 0.5) * 0.6;
3739 let disp_x = angle.cos() * force * 50.0 * speed_var;
3740 let disp_y = angle.sin() * force * 50.0 * speed_var;
3741
3742 let gravity = force * force * 20.0;
3744
3745 let scale_factor = (1.0 - (force / 6.0).min(1.0)).max(0.0);
3748 let shard_w = dw * scale_factor;
3749 let shard_h = dh * scale_factor;
3750
3751 let displaced_x = init_x + disp_x + (dw - shard_w) * 0.5;
3752 let displaced_y = init_y + disp_y + gravity + (dh - shard_h) * 0.5;
3753
3754 let shard_rect = Rect {
3755 x: displaced_x,
3756 y: displaced_y,
3757 width: shard_w,
3758 height: shard_h,
3759 };
3760
3761 let uv = Rect {
3762 x: x as f32 / count as f32,
3763 y: y as f32 / count as f32,
3764 width: 1.0 / count as f32,
3765 height: 1.0 / count as f32,
3766 };
3767
3768 self.fill_rect_with_full_params(shard_rect, c, material_id, None, force, uv);
3769 }
3770 }
3771 }
3772
3773 pub(crate) fn recursive_bolt(
3774 &mut self,
3775 from: [f32; 2],
3776 to: [f32; 2],
3777 depth: u32,
3778 color: [f32; 4],
3779 ) {
3780 if depth == 0 {
3781 self.draw_lightning_segment(from, to, color);
3782 return;
3783 }
3784
3785 let mid_x = (from[0] + to[0]) * 0.5;
3786 let mid_y = (from[1] + to[1]) * 0.5;
3787
3788 let dx = to[0] - from[0];
3789 let dy = to[1] - from[1];
3790 let len = (dx * dx + dy * dy).sqrt();
3791
3792 if len < 1e-4 {
3793 return;
3794 }
3795
3796 let offset_scale = len * 0.15;
3798 let seed = (from[0] * 12.9898 + from[1] * 78.233 + (depth as f32) * 37.11)
3799 .sin()
3800 .fract();
3801 let offset_x = -dy / len * (seed - 0.5) * offset_scale;
3802 let offset_y = dx / len * (seed - 0.5) * offset_scale;
3803
3804 let mid = [mid_x + offset_x, mid_y + offset_y];
3805
3806 self.recursive_bolt(from, mid, depth - 1, color);
3807 self.recursive_bolt(mid, to, depth - 1, color);
3808
3809 if depth > 2 && seed > 0.8 {
3811 let branch_to = [
3812 mid[0] + offset_x * 2.0 + (seed * 100.0).sin() * 50.0,
3813 mid[1] + offset_y * 2.0 + (seed * 100.0).cos() * 50.0,
3814 ];
3815 self.recursive_bolt(mid, branch_to, depth - 2, color);
3816 }
3817 }
3818
3819 pub(crate) fn draw_lightning_segment(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
3820 let dx = to[0] - from[0];
3821 let dy = to[1] - from[1];
3822 let len = (dx * dx + dy * dy).sqrt();
3823 if len < 0.001 {
3824 return;
3825 }
3826
3827 let glow_width = 32.0;
3828 let core_width = 4.0;
3829 let c = self.apply_opacity(color);
3830
3831 let gnx = -dy / len * glow_width * 0.5;
3833 let gny = dx / len * glow_width * 0.5;
3834 let gp1 = [from[0] + gnx, from[1] + gny];
3835 let gp2 = [to[0] + gnx, to[1] + gny];
3836 let gp3 = [to[0] - gnx, to[1] - gny];
3837 let gp4 = [from[0] - gnx, from[1] - gny];
3838 self.push_oriented_quad(
3839 [gp1, gp2, gp3, gp4],
3840 c,
3841 9,
3842 Rect {
3843 x: 0.0,
3844 y: 0.0,
3845 width: 1.0,
3846 height: 1.0,
3847 },
3848 );
3849
3850 let cnx = -dy / len * core_width * 0.5;
3852 let cny = dx / len * core_width * 0.5;
3853 let cp1 = [from[0] + cnx, from[1] + cny];
3854 let cp2 = [to[0] + cnx, to[1] + cny];
3855 let cp3 = [to[0] - cnx, to[1] - cny];
3856 let cp4 = [from[0] - cnx, from[1] - cny];
3857 self.push_oriented_quad(
3858 [cp1, cp2, cp3, cp4],
3859 [1.0, 1.0, 1.0, c[3]],
3860 0,
3861 Rect {
3862 x: 0.0,
3863 y: 0.0,
3864 width: 1.0,
3865 height: 1.0,
3866 },
3867 );
3868 }
3869
3870 pub(crate) fn push_oriented_quad(
3871 &mut self,
3872 points: [[f32; 2]; 4],
3873 color: [f32; 4],
3874 material_id: u32,
3875 uv_rect: Rect,
3876 ) {
3877 let scissor = self.clip_stack.last().copied();
3878 let texture_id = None; let (translation, scale_transform, rotation, _, _) = self.current_transform();
3881 let current_instance_data = InstanceData {
3882 translation,
3883 scale: scale_transform,
3884 rotation,
3885 blur_radius: 0.0,
3886 ior_override: 0.0,
3887 glass_intensity: 1.0,
3888 };
3889
3890 let last_call = self.draw_calls.last();
3893 let needs_new_call = self.draw_calls.is_empty()
3894 || self.current_texture_id != texture_id
3895 || last_call.unwrap().scissor_rect != scissor
3896 || last_call.unwrap().material != Self::resolve_material_with_context(material_id, &self.current_draw_material)
3897 || {
3898 let last_material = last_call.unwrap().material;
3899 let current_material = Self::resolve_material_with_context(material_id, &self.current_draw_material);
3900 matches!((current_material, last_material),
3901 (cvkg_core::DrawMaterial::Glass { blur_radius: a, ior_override: b, glass_intensity: c },
3902 cvkg_core::DrawMaterial::Glass { blur_radius: d, ior_override: e, glass_intensity: f })
3903 if a != d || b != e || c != f)
3904 };
3905
3906 if needs_new_call {
3907 self.current_texture_id = texture_id;
3908 self.instance_data.push(current_instance_data);
3909 self.draw_calls.push(DrawCall {
3910 target_id: None,
3911 texture_id,
3912 scissor_rect: scissor,
3913 index_start: self.indices.len() as u32,
3914 index_count: 0,
3915 instance_count: 1,
3916 material: Self::resolve_material_with_context(material_id, &self.current_draw_material),
3917 instance_start: (self.instance_data.len() - 1) as u32,
3918 draw_order: 0,
3919 });
3920 } else {
3921 self.instance_data.push(current_instance_data);
3923 if let Some(call) = self.draw_calls.last_mut() {
3924 call.instance_count += 1;
3925 }
3926 }
3927
3928 let uvs = [
3929 [uv_rect.x, uv_rect.y],
3930 [uv_rect.x + uv_rect.width, uv_rect.y],
3931 [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
3932 [uv_rect.x, uv_rect.y + uv_rect.height],
3933 ];
3934
3935 let rect = Rect {
3936 x: points[0][0],
3937 y: points[0][1],
3938 width: 1.0,
3939 height: 1.0,
3940 };
3941
3942 for i in 0..4 {
3943 let px = points[i][0];
3944 let py = points[i][1];
3945
3946 let (translation, scale_transform, rotation, _, _) = self.current_transform();
3947 self.vertices.push(Vertex {
3948 position: [px, py, 0.0],
3949 normal: [0.0, 0.0, 1.0],
3950 uv: uvs[i],
3951 color,
3952 material_id,
3953 radius: 0.0,
3954 slice: [0.0, 0.0, 0.0, 1.0],
3955 logical: [px - rect.x, py - rect.y],
3956 size: [rect.width, rect.height],
3957 clip: [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY],
3958 tex_index: 0,
3959 });
3960 }
3961
3962 let base = self.vertices.len() as u32 - 4;
3964 self.indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
3965
3966 if let Some(call) = self.draw_calls.last_mut() {
3967 call.index_count += 6;
3968 }
3969 }
3970 pub(crate) fn get_texture_id(&mut self, name: &str) -> Option<u32> {
3971 self.texture_registry.get(name).copied()
3972 }
3973
3974 pub fn fill_rect_with_mode(
3976 &mut self,
3977 rect: Rect,
3978 color: [f32; 4],
3979 material_id: u32,
3980 texture_id: Option<u32>,
3981 ) {
3982 self.fill_rect_with_full_params(
3983 rect,
3984 color,
3985 material_id,
3986 texture_id,
3987 0.0,
3988 Rect {
3989 x: 0.0,
3990 y: 0.0,
3991 width: 1.0,
3992 height: 1.0,
3993 },
3994 );
3995 }
3996
3997 pub(crate) fn fill_rect_with_full_params(
3998 &mut self,
3999 rect: Rect,
4000 color: [f32; 4],
4001 material_id: u32,
4002 texture_id: Option<u32>,
4003 radius: f32,
4004 uv_rect: Rect,
4005 ) {
4006 if let Some(shadow) = self.shadow_stack.last().copied()
4008 && shadow.color[3] > 0.001
4009 {
4010 let shadow_rect = Rect {
4011 x: rect.x + shadow._offset[0],
4012 y: rect.y + shadow._offset[1],
4013 width: rect.width,
4014 height: rect.height,
4015 };
4016 Renderer::draw_drop_shadow(
4017 self,
4018 shadow_rect,
4019 radius,
4020 shadow.color,
4021 shadow.radius,
4022 0.0, );
4024 }
4025
4026 let slice = self
4027 .slice_stack
4028 .last()
4029 .copied()
4030 .map(|(a, o)| [a, o, 1.0, 1.0])
4031 .unwrap_or([0.0, 0.0, 0.0, 1.0]);
4032 self.fill_rect_with_full_params_and_slice(
4033 rect,
4034 color,
4035 material_id,
4036 texture_id,
4037 radius,
4038 uv_rect,
4039 slice,
4040 [0.0, 0.0],
4041 );
4042 }
4043
4044 #[allow(clippy::too_many_arguments)]
4045 pub(crate) fn fill_rect_with_full_params_and_slice(
4046 &mut self,
4047 mut rect: Rect,
4048 color: [f32; 4],
4049 material_id: u32,
4050 texture_id: Option<u32>,
4051 radius: f32,
4052 uv_rect: Rect,
4053 slice: [f32; 4],
4054 glyph_time: [f32; 2],
4055 ) {
4056 if material_id != material_id::GLASS {
4059 let scale = self.current_scale_factor();
4060 let snap = |v: f32| (v * scale).round() / scale;
4061 rect.x = snap(rect.x);
4062 rect.y = snap(rect.y);
4063 rect.width = snap(rect.width);
4064 rect.height = snap(rect.height);
4065 }
4066
4067 let scissor = self.clip_stack.last().copied();
4068
4069 let material = Self::resolve_material_with_context(material_id, &self.current_draw_material);
4070
4071 let (translation, scale_transform, rotation, _, _) = self.current_transform();
4072 let (blur_radius, ior_override, glass_intensity) = if let cvkg_core::DrawMaterial::Glass {
4073 blur_radius,
4074 ior_override,
4075 glass_intensity,
4076 } = material
4077 {
4078 (blur_radius, ior_override, glass_intensity)
4079 } else {
4080 (0.0, 0.0, 1.0)
4081 };
4082
4083 let current_instance_data = InstanceData {
4084 translation,
4085 scale: scale_transform,
4086 rotation,
4087 blur_radius,
4088 ior_override,
4089 glass_intensity,
4090 };
4091
4092 let last_call = self.draw_calls.last();
4099 let needs_new_call = self.draw_calls.is_empty()
4100 || last_call.unwrap().scissor_rect != scissor
4101 || last_call.unwrap().material != material
4102 || last_call.unwrap().texture_id != self.current_texture_id
4103 || {
4104 let last_material = last_call.unwrap().material;
4106 matches!((material, last_material),
4107 (cvkg_core::DrawMaterial::Glass { blur_radius: a, ior_override: b, glass_intensity: c },
4108 cvkg_core::DrawMaterial::Glass { blur_radius: d, ior_override: e, glass_intensity: f })
4109 if a != d || b != e || c != f)
4110 };
4111
4112 if needs_new_call {
4113 self.current_texture_id = Some(0); self.instance_data.push(current_instance_data);
4115 self.draw_calls.push(DrawCall {
4116 target_id: None,
4117 texture_id: self.current_texture_id,
4118 scissor_rect: scissor,
4119 index_start: self.indices.len() as u32,
4120 index_count: 0,
4121 instance_count: 1,
4122 material,
4123 instance_start: (self.instance_data.len() - 1) as u32,
4124 draw_order: 0,
4125 });
4126 } else {
4127 self.instance_data.push(current_instance_data);
4129 if let Some(call) = self.draw_calls.last_mut() {
4130 call.instance_count += 1;
4131 }
4132 }
4133
4134 let scale = self.current_scale_factor();
4135 let snap = |v: f32| (v * scale).round() / scale;
4136
4137 let base_idx = self.vertices.len() as u32;
4138 let x1 = snap(rect.x);
4139 let y1 = snap(rect.y);
4140 let x2 = snap(rect.x + rect.width);
4141 let y2 = snap(rect.y + rect.height);
4142 let z = self.current_z;
4143 let normal = [0.0, 0.0, 1.0];
4144 let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
4145 x: -10000.0,
4146 y: -10000.0,
4147 width: 20000.0,
4148 height: 20000.0,
4149 });
4150 let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
4151
4152 let tex_index = texture_id.unwrap_or(0);
4153
4154 self.vertices.push(Vertex {
4155 position: [x1, y1, z],
4156 normal,
4157 uv: [uv_rect.x, uv_rect.y],
4158 color,
4159 material_id,
4160 radius,
4161 slice,
4162 logical: [0.0, 0.0],
4163 size: [rect.width, rect.height],
4164 clip,
4165 tex_index,
4166 });
4167 self.vertices.push(Vertex {
4168 position: [x2, y1, z],
4169 normal,
4170 uv: [uv_rect.x + uv_rect.width, uv_rect.y],
4171 color,
4172 material_id,
4173 radius,
4174 slice,
4175 logical: [rect.width, 0.0],
4176 size: [rect.width, rect.height],
4177 clip,
4178 tex_index,
4179 });
4180 self.vertices.push(Vertex {
4181 position: [x2, y2, z],
4182 normal,
4183 uv: [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
4184 color,
4185 material_id,
4186 radius,
4187 slice,
4188 logical: [rect.width, rect.height],
4189 size: [rect.width, rect.height],
4190 clip,
4191 tex_index,
4192 });
4193 self.vertices.push(Vertex {
4194 position: [x1, y2, z],
4195 normal,
4196 uv: [uv_rect.x, uv_rect.y + uv_rect.height],
4197 color,
4198 material_id,
4199 radius,
4200 slice,
4201 logical: [0.0, rect.height],
4202 size: [rect.width, rect.height],
4203 clip,
4204 tex_index,
4205 });
4206
4207 self.indices.extend_from_slice(&[
4208 base_idx,
4209 base_idx + 1,
4210 base_idx + 2,
4211 base_idx,
4212 base_idx + 2,
4213 base_idx + 3,
4214 ]);
4215
4216 if let Some(call) = self.draw_calls.last_mut() {
4217 call.index_count += 6;
4218 }
4219 }
4220
4221 pub fn end_frame(&mut self, mut encoder: wgpu::CommandEncoder) {
4236 struct ActiveFrameResources {
4237 surface_texture: Option<wgpu::SurfaceTexture>,
4238 target_view: wgpu::TextureView,
4239 scene_texture: wgpu::TextureView,
4240 scene_msaa_texture: wgpu::TextureView,
4241 depth_texture_view: wgpu::TextureView,
4242 blur_env_bind_group_a: wgpu::BindGroup,
4243 blur_env_bind_group_b: wgpu::BindGroup,
4244 bloom_env_bind_group_a: wgpu::BindGroup,
4245 bloom_env_bind_group_b: wgpu::BindGroup,
4246 }
4247
4248 let res = if let Some(window_id) = self.current_window {
4249 let Some(ctx) = self.surfaces.get(&window_id) else {
4250 log::error!("[GPU] Missing surface context for end_frame");
4251 return;
4252 };
4253 let frame = match ctx.surface.get_current_texture() {
4254 wgpu::CurrentSurfaceTexture::Success(t) => t,
4255 wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
4256 ctx.surface.configure(&self.device, &ctx.config);
4257 t
4258 }
4259 other => {
4260 log::warn!(
4261 "[GPU] Surface texture acquisition failed ({:?}), reconfiguring surface",
4262 other
4263 );
4264 ctx.surface.configure(&self.device, &ctx.config);
4265 match ctx.surface.get_current_texture() {
4267 wgpu::CurrentSurfaceTexture::Success(t) => t,
4268 wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
4269 ctx.surface.configure(&self.device, &ctx.config);
4270 t
4271 }
4272 retry_failed => {
4273 log::error!(
4274 "[GPU] Surface texture retry also failed ({:?}), skipping frame",
4275 retry_failed
4276 );
4277 self.queue.submit(std::iter::once(encoder.finish()));
4278 return;
4279 }
4280 }
4281 }
4282 };
4283 let view = frame
4284 .texture
4285 .create_view(&wgpu::TextureViewDescriptor::default());
4286
4287 ActiveFrameResources {
4288 surface_texture: Some(frame),
4289 target_view: view,
4290 scene_texture: ctx.scene_texture.clone(),
4291 scene_msaa_texture: ctx.scene_msaa_texture.clone(),
4292 depth_texture_view: ctx.depth_texture_view.clone(),
4293 blur_env_bind_group_a: ctx.blur_env_bind_group_a.clone(),
4294 blur_env_bind_group_b: ctx.blur_env_bind_group_b.clone(),
4295 bloom_env_bind_group_a: ctx.bloom_env_bind_group_a.clone(),
4296 bloom_env_bind_group_b: ctx.bloom_env_bind_group_b.clone(),
4297 }
4298 } else {
4299 let Some(ctx) = self.headless_context.as_ref() else {
4300 log::error!("[GPU] No headless context for end_frame");
4301 return;
4302 };
4303
4304 ActiveFrameResources {
4305 surface_texture: None,
4306 target_view: ctx.output_view.clone(),
4307 scene_texture: ctx.scene_texture.clone(),
4308 scene_msaa_texture: ctx.scene_msaa_texture.clone(),
4309 depth_texture_view: ctx.depth_texture_view.clone(),
4310 blur_env_bind_group_a: ctx.blur_env_bind_group_a.clone(),
4311 blur_env_bind_group_b: ctx.blur_env_bind_group_b.clone(),
4312 bloom_env_bind_group_a: ctx.bloom_env_bind_group_a.clone(),
4313 bloom_env_bind_group_b: ctx.bloom_env_bind_group_b.clone(),
4314 }
4315 };
4316
4317 if !self.frame_rendered && (!self.vertices.is_empty() || !self.indices.is_empty()) {
4320 log::debug!("[GPU] Auto-flushing staging belt in end_frame (render_frame was not called)");
4321 let mut staging_encoder =
4322 self.device
4323 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
4324 label: Some("Surtr Auto-Flush Staging Encoder"),
4325 });
4326 if !self.vertices.is_empty() {
4327 let v_bytes = bytemuck::cast_slice(&self.vertices);
4328 self.staging_belt
4329 .write_buffer(
4330 &mut staging_encoder,
4331 &self.geometry_buffers.vertex_buffer,
4332 0,
4333 wgpu::BufferSize::new(v_bytes.len() as u64).unwrap(),
4334 )
4335 .copy_from_slice(v_bytes);
4336 }
4337 if !self.indices.is_empty() {
4338 let i_bytes = bytemuck::cast_slice(&self.indices);
4339 self.staging_belt
4340 .write_buffer(
4341 &mut staging_encoder,
4342 &self.geometry_buffers.index_buffer,
4343 0,
4344 wgpu::BufferSize::new(i_bytes.len() as u64).unwrap(),
4345 )
4346 .copy_from_slice(i_bytes);
4347 }
4348 if !self.instance_data.is_empty() {
4349 let inst_bytes = bytemuck::cast_slice(&self.instance_data);
4350 self.staging_belt
4351 .write_buffer(
4352 &mut staging_encoder,
4353 &self.geometry_buffers.instance_buffer,
4354 0,
4355 wgpu::BufferSize::new(inst_bytes.len() as u64).unwrap(),
4356 )
4357 .copy_from_slice(inst_bytes);
4358 }
4359 self.staging_belt.finish();
4360 self.staging_command_buffers.push(staging_encoder.finish());
4361 }
4362
4363 let has_glass = self
4365 .draw_calls
4366 .iter()
4367 .any(|c| matches!(c.material, cvkg_core::DrawMaterial::Glass { .. }));
4368 let has_bloom = self.bloom_enabled;
4369 let has_accessibility =
4370 self.color_blind_mode != crate::color_blindness::ColorBlindMode::Normal;
4371
4372 let (blur_id, bloom_id) = if let Some(window_id) = self.current_window {
4382 let ctx = self.surfaces.get(&window_id).unwrap();
4383 (ctx.blur_tex_a, ctx.bloom_tex_a)
4384 } else {
4385 let ctx = self.headless_context.as_ref().unwrap();
4386 (ctx.blur_tex_a, ctx.bloom_tex_a)
4387 };
4388 self.registry.alias(kvasir::nodes::RES_BLUR_A, blur_id);
4389 self.registry.alias(kvasir::nodes::RES_BLOOM_A, bloom_id);
4390 self.registry
4391 .alias_view(kvasir::nodes::RES_SCENE, res.scene_texture.clone());
4392 self.registry.alias_view(
4393 kvasir::nodes::RES_SCENE_MSAA,
4394 res.scene_msaa_texture.clone(),
4395 );
4396
4397 let scale = self.current_scale_factor();
4398 let scale_bits = scale.to_bits();
4399 let active_offscreens_count = self.active_offscreens.len();
4400 let portal_regions_count = self.portal_regions.len();
4401 let width = self.current_width();
4402 let height = self.current_height();
4403 let has_volumetric = self.volumetric_enabled;
4404
4405 let mut offscreen_hash: u64 = 0;
4407 for offscreen in &self.active_offscreens {
4408 offscreen_hash = offscreen_hash.wrapping_add(
4409 offscreen.target_id.wrapping_mul(31)
4410 ^ (offscreen.blend_mode as u64).wrapping_mul(17)
4411 );
4412 }
4413 let mut portal_hash: u64 = 0;
4414 for region in &self.portal_regions {
4415 portal_hash = portal_hash.wrapping_add(
4416 (region.x.to_bits() as u64).wrapping_mul(7)
4417 .wrapping_add((region.y.to_bits() as u64).wrapping_mul(13))
4418 .wrapping_add((region.width.to_bits() as u64).wrapping_mul(19))
4419 .wrapping_add((region.height.to_bits() as u64).wrapping_mul(23))
4420 );
4421 }
4422
4423 let use_cache = if let Some(ref cached) = self.cached_graph_plan {
4424 cached.matches(
4425 has_glass,
4426 has_bloom,
4427 has_accessibility,
4428 has_volumetric,
4429 active_offscreens_count,
4430 offscreen_hash,
4431 portal_regions_count,
4432 portal_hash,
4433 width,
4434 height,
4435 scale_bits,
4436 self.material_compilation_hash,
4437 )
4438 } else {
4439 false
4440 };
4441
4442 if !use_cache {
4443 let render_graph = kvasir::nodes::build_render_graph(&kvasir::nodes::RenderGraphConfig {
4444 has_glass,
4445 has_bloom,
4446 has_accessibility,
4447 has_volumetric,
4448 active_offscreens: &self.active_offscreens,
4449 portal_regions: &self.portal_regions.iter().cloned().collect::<Vec<_>>(),
4450 width,
4451 height,
4452 scale,
4453 });
4454 let planner = kvasir::planner::ExecutionPlanner::new(&render_graph);
4455 let compiled_plan = match planner.compile() {
4456 Ok(plan) => plan,
4457 Err(e) => {
4458 log::error!(
4459 "[Kvasir] Render graph compilation failed ({}), skipping render passes",
4460 e
4461 );
4462 if let Some(surface_texture) = res.surface_texture {
4464 surface_texture.present();
4465 }
4466 return;
4467 }
4468 };
4469
4470 self.cached_graph_plan = Some(kvasir::graph_cache::CachedGraphPlan {
4472 has_glass,
4473 has_bloom,
4474 has_accessibility,
4475 has_volumetric,
4476 active_offscreens_count,
4477 offscreen_content_hash: offscreen_hash,
4478 portal_regions_count,
4479 portal_content_hash: portal_hash,
4480 width,
4481 height,
4482 scale_bits,
4483 material_compilation_hash: self.material_compilation_hash,
4484 graph: render_graph,
4485 plan: compiled_plan,
4486 });
4487 }
4488
4489 let cached = self.cached_graph_plan.as_ref().unwrap();
4490 let frame_start = self.last_frame_start;
4491 let budget_ms = self.frame_budget.target_ms;
4492 let allow_degradation = self.frame_budget.allow_degradation;
4493
4494 for &node_key in &cached.plan {
4495 if allow_degradation && budget_ms > 0.0 {
4509 let elapsed_ms = frame_start.elapsed().as_secs_f32() * 1000.0;
4510 if elapsed_ms > budget_ms {
4511 if let Some(node) = cached.graph.node(node_key) {
4512 match node.pass_id() {
4513 kvasir::nodes::PassId::BloomExtract
4514 | kvasir::nodes::PassId::BloomBlur
4515 | kvasir::nodes::PassId::Volumetric => {
4516 log::trace!(
4517 "[Kvasir] Skipping {} (over budget: {:.1}ms > {:.1}ms)",
4518 node.label(),
4519 elapsed_ms,
4520 budget_ms
4521 );
4522 continue;
4523 }
4524 _ => {} }
4527 }
4528 }
4529 }
4530 if let Some(node) = cached.graph.node(node_key) {
4531 log::trace!("[Kvasir] Executing node: {}", node.label());
4532 let mut ctx = kvasir::node::ExecutionContext {
4533 device: &self.device,
4534 queue: &self.queue,
4535 encoder: &mut encoder,
4536 registry: &self.registry,
4537 renderer: self,
4538 target_view: &res.target_view,
4539 depth_view: &res.depth_texture_view,
4540 blur_env_bind_group_a: &res.blur_env_bind_group_a,
4541 blur_env_bind_group_b: &res.blur_env_bind_group_b,
4542 bloom_env_bind_group_a: &res.bloom_env_bind_group_a,
4543 bloom_env_bind_group_b: &res.bloom_env_bind_group_b,
4544 scale_factor: scale,
4545 };
4546 node.execute(&mut ctx);
4547 }
4548 }
4549
4550 if !self.particles.staging.is_empty() || self.particles.count > 0 {
4554 if !self.particles.staging.is_empty() {
4556 let write_start = self.particles.write_head as usize;
4557 let write_count = self.particles.staging.len();
4558 let max = MAX_PARTICLES;
4559
4560 let effective_count = write_count.min(max);
4569 let drop_count = write_count - effective_count;
4570
4571 let first_chunk = (max - write_start).min(effective_count);
4573 let bytes = bytemuck::cast_slice(&self.particles.staging[drop_count..drop_count + first_chunk]);
4574 self.queue.write_buffer(
4575 &self.particle_buffer,
4576 (write_start * std::mem::size_of::<crate::types::GpuParticle>()) as u64,
4577 bytes,
4578 );
4579 if first_chunk < effective_count {
4580 let remaining = effective_count - first_chunk;
4581 let bytes2 = bytemuck::cast_slice(&self.particles.staging[drop_count + first_chunk..drop_count + first_chunk + remaining]);
4582 self.queue.write_buffer(
4583 &self.particle_buffer,
4584 0,
4585 bytes2,
4586 );
4587 self.particles.write_head = remaining as u32;
4588 } else {
4589 self.particles.write_head =
4590 ((write_start + effective_count) % max) as u32;
4591 }
4592 self.particles.count = (self.particles.count as usize + effective_count)
4593 .min(max) as u32;
4594 self.particles.staging.clear();
4595
4596 self.particle_render_bind_group = None;
4598 }
4599
4600 let dt = self.current_scene.delta_time;
4602 let uniforms = crate::types::ParticleUniforms { dt, _pad: [0.0; 7] };
4603 self.queue.write_buffer(
4604 &self.particle_uniform_buffer,
4605 0,
4606 bytemuck::bytes_of(&uniforms),
4607 );
4608
4609 let compute_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
4610 label: Some("Particle Compute BG"),
4611 layout: &self.particle_compute_bgl,
4612 entries: &[
4613 wgpu::BindGroupEntry {
4614 binding: 0,
4615 resource: self.particle_buffer.as_entire_binding(),
4616 },
4617 wgpu::BindGroupEntry {
4618 binding: 1,
4619 resource: self.particle_uniform_buffer.as_entire_binding(),
4620 },
4621 ],
4622 });
4623
4624 let mut compute_encoder =
4625 self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
4626 label: Some("Particle Compute Encoder"),
4627 });
4628 {
4629 let mut cpass = compute_encoder.begin_compute_pass(
4630 &wgpu::ComputePassDescriptor {
4631 label: Some("Particle Integration"),
4632 ..Default::default()
4633 },
4634 );
4635 cpass.set_pipeline(&self.particle_compute_pipeline);
4636 cpass.set_bind_group(0, &compute_bind_group, &[]);
4637 let workgroups = ((self.particles.count + 63) / 64).max(1);
4638 cpass.dispatch_workgroups(workgroups, 1, 1);
4639 }
4640 self.staging_command_buffers.push(compute_encoder.finish());
4641 }
4642
4643 if self.particles.count > 0
4645 && self.particles.last_compact.elapsed().as_secs_f32() > 2.0
4646 {
4647 self.particles.last_compact = std::time::Instant::now();
4648 let read_size =
4650 (self.particles.count as usize * std::mem::size_of::<crate::types::GpuParticle>())
4651 as u64;
4652 let staging_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
4653 label: Some("Particle Compact Staging"),
4654 size: read_size,
4655 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
4656 mapped_at_creation: false,
4657 });
4658 let mut compact_encoder =
4659 self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
4660 label: Some("Particle Compact Copy"),
4661 });
4662 compact_encoder.copy_buffer_to_buffer(
4663 &self.particle_buffer,
4664 0,
4665 &staging_buf,
4666 0,
4667 read_size,
4668 );
4669 self.staging_command_buffers.push(compact_encoder.finish());
4670 }
4675
4676 if self.particles.count > 0 {
4680 if self.particle_render_bind_group.is_none() {
4682 self.particle_render_bind_group =
4683 Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
4684 label: Some("Particle Render BG"),
4685 layout: &self.particle_render_bgl,
4686 entries: &[wgpu::BindGroupEntry {
4687 binding: 0,
4688 resource: self.particle_buffer.as_entire_binding(),
4689 }],
4690 }));
4691 }
4692 if let Some(bg) = &self.particle_render_bind_group {
4693 let mut render_encoder =
4694 self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
4695 label: Some("Particle Render Encoder"),
4696 });
4697 {
4698 let mut rpass = render_encoder.begin_render_pass(
4699 &wgpu::RenderPassDescriptor {
4700 label: Some("Particle Render"),
4701 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
4702 view: &res.target_view,
4703 resolve_target: None,
4704 ops: wgpu::Operations {
4705 load: wgpu::LoadOp::Load,
4706 store: wgpu::StoreOp::Store,
4707 },
4708 depth_slice: None,
4709 })],
4710 depth_stencil_attachment: None,
4711 timestamp_writes: None,
4712 occlusion_query_set: None,
4713 multiview_mask: None,
4714 },
4715 );
4716 rpass.set_pipeline(&self.particle_render_pipeline);
4717 rpass.set_bind_group(0, bg, &[]);
4718 rpass.draw(0..self.particles.count, 0..1);
4719 }
4720 self.staging_command_buffers.push(render_encoder.finish());
4721 }
4722 }
4723
4724 self.staging_command_buffers.push(encoder.finish());
4729
4730 if let (Some(q), Some(b), Some(rb)) = (
4732 &self.skuld_queries,
4733 &self.skuld_buffer,
4734 &self.skuld_read_buffer,
4735 ) {
4736 let mut resolve_encoder =
4737 self.device
4738 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
4739 label: Some("Skuld Resolve Encoder"),
4740 });
4741 resolve_encoder.resolve_query_set(q, 0..2, b, 0);
4742 resolve_encoder.copy_buffer_to_buffer(b, 0, rb, 0, 16);
4743 self.staging_command_buffers.push(resolve_encoder.finish());
4744 }
4745
4746 let cmds = std::mem::take(&mut self.staging_command_buffers);
4747 self.queue.submit(cmds);
4748 self.telemetry.frame_time_ms = self.last_frame_start.elapsed().as_secs_f32() * 1000.0;
4749 self.update_vram_telemetry();
4750
4751 self.registry.evict_frame_resources();
4754
4755 if let Some(f) = res.surface_texture {
4756 f.present();
4757 }
4758 }
4759}
4760
4761impl Drop for SurtrRenderer {
4762 fn drop(&mut self) {
4763 let cache_dir = std::env::current_exe()
4767 .ok()
4768 .and_then(|p| p.parent().map(|d| d.join("pipeline_cache")))
4769 .unwrap_or_else(|| std::env::temp_dir().join("cvkg_pipeline_cache"));
4770 let _ = std::fs::create_dir_all(&cache_dir);
4771 let cache_path = cache_dir.join("cvkg_render_gpu.bin");
4772 if let Some(cache) = &self.pipeline_cache {
4773 if let Some(data) = cache.get_data() {
4774 if let Err(e) = std::fs::write(&cache_path, data) {
4775 log::warn!("Failed to persist pipeline cache: {}", e);
4776 }
4777 }
4778 }
4779
4780 let _ = self.device.poll(wgpu::PollType::Wait {
4782 submission_index: None,
4783 timeout: None,
4784 });
4785 }
4786}
4787
4788impl SurtrRenderer {
4789 pub fn submit_buckets(&mut self, buckets: &cvkg_compositor::CommandBuckets) {
4798 let mut active_offscreens = Vec::new();
4800 let mut current_target_id = None;
4801
4802 let mut sorted_scene: Vec<_> = buckets.scene_commands.iter().collect();
4804 sorted_scene.sort_by_key(|cmd| {
4805 match cmd {
4806 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
4807 (routed.z_index as i64, routed.draw_order as i64)
4808 }
4809 _ => (0, 0),
4810 }
4811 });
4812
4813 for cmd in sorted_scene {
4814 match cmd {
4815 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
4816 self.set_material(cvkg_core::DrawMaterial::Opaque);
4817 self.submit_routed(routed, current_target_id);
4818 }
4819 cvkg_compositor::engine::RenderCommand::PushOffscreen {
4820 source_layer,
4821 material,
4822 bounds,
4823 } => {
4824 current_target_id = Some(source_layer.0);
4825
4826 let width = (bounds.width).max(1.0) as u32;
4828 let height = (bounds.height).max(1.0) as u32;
4829 self.registry
4830 .allocate_offscreen(&self.device, source_layer.0, [width, height]);
4831
4832 if let cvkg_compositor::Material::ShaderEffect {
4833 effect_name,
4834 params_json: _,
4835 ..
4836 } = material
4837 {
4838 active_offscreens.push(crate::types::OffscreenEffectConfig {
4839 target_id: source_layer.0,
4840 effect: effect_name.clone(),
4841 blend_mode: 0, effect_args: [0.0; 16], });
4844 }
4845 }
4846 cvkg_compositor::engine::RenderCommand::PopOffscreen => {
4847 current_target_id = None;
4848 }
4849 }
4850 }
4851 self.active_offscreens = active_offscreens;
4852
4853 let mut sorted_glass: Vec<_> = buckets.glass_commands.iter().collect();
4855 sorted_glass.sort_by_key(|cmd| match cmd {
4856 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
4857 (routed.z_index as i64, routed.draw_order as i64)
4858 }
4859 _ => (0, 0),
4860 });
4861 for cmd in sorted_glass {
4862 if let cvkg_compositor::engine::RenderCommand::Draw(routed) = cmd {
4863 self.set_material(Self::convert_compositor_material(&routed.material));
4864 self.submit_routed(routed, None);
4865 }
4866 }
4867
4868 let mut sorted_overlay: Vec<_> = buckets.overlay_commands.iter().collect();
4870 sorted_overlay.sort_by_key(|cmd| match cmd {
4871 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
4872 (routed.z_index as i64, routed.draw_order as i64)
4873 }
4874 _ => (0, 0),
4875 });
4876 for cmd in sorted_overlay {
4877 if let cvkg_compositor::engine::RenderCommand::Draw(routed) = cmd {
4878 self.set_material(cvkg_core::DrawMaterial::TopUI);
4879 self.submit_routed(routed, None);
4880 }
4881 }
4882 }
4883
4884 pub(crate) fn submit_routed(
4886 &mut self,
4887 routed: &cvkg_compositor::RoutedDrawCommand,
4888 target_id: Option<u64>,
4889 ) {
4890 let cmd = &routed.command;
4891 if cmd.index_count == 0 {
4892 return;
4893 }
4894 let material = Self::convert_compositor_material(&routed.material);
4895 self.draw_calls.push(DrawCall {
4896 texture_id: cmd.texture_id,
4897 scissor_rect: cmd.scissor_rect,
4898 index_start: cmd.index_start,
4899 index_count: cmd.index_count,
4900 instance_count: 1,
4901 material,
4902 target_id,
4903 instance_start: cmd.instance_id,
4904 draw_order: 0,
4905 });
4906 }
4907}
4908
4909impl SurtrRenderer {
4910 pub(crate) fn apply_opacity(&self, mut color: [f32; 4]) -> [f32; 4] {
4912 if let Some(&alpha) = self.opacity_stack.last() {
4913 color[3] *= alpha;
4914 }
4915 color
4916 }
4917
4918 pub fn load_svg(&mut self, name: &str, data: &[u8]) {
4920 if self.svg.model_cache.contains(name) {
4921 return;
4922 }
4923
4924 let mut opt = usvg::Options::default();
4925 opt.fontdb_mut().load_system_fonts();
4926 let tree = match usvg::Tree::from_data(data, &opt) {
4927 Ok(t) => t,
4928 Err(e) => {
4929 log::error!("Failed to parse SVG '{}': {:?}, skipping load", name, e);
4930 return;
4931 }
4932 };
4933
4934 let view_box = Rect {
4937 x: 0.0,
4938 y: 0.0,
4939 width: tree.size().width(),
4940 height: tree.size().height(),
4941 };
4942
4943 let parsed_animations = parse_svg_animations(data);
4944
4945 let mut vertices = Vec::new();
4946 let mut indices = Vec::new();
4947 let mut fill_tessellator = FillTessellator::new();
4948 let mut stroke_tessellator = StrokeTessellator::new();
4949 let mut finalized_animations = Vec::new();
4950 let mut paths = Vec::new();
4951
4952 for child in tree.root().children() {
4953 let mut tess_params = TessellateParams {
4954 fill_tessellator: &mut fill_tessellator,
4955 stroke_tessellator: &mut stroke_tessellator,
4956 vertices: &mut vertices,
4957 indices: &mut indices,
4958 parsed_animations: &parsed_animations,
4959 finalized_animations: &mut finalized_animations,
4960 paths: &mut paths,
4961 };
4962 self.tessellate_node(child, &mut tess_params);
4963 }
4964
4965 self.svg.model_cache.put(
4966 name.to_string(),
4967 SvgModel {
4968 vertices,
4969 indices,
4970 view_box,
4971 paths,
4972 animations: finalized_animations,
4973 },
4974 );
4975 self.svg.tree_cache.put(name.to_string(), tree);
4976 }
4977
4978 pub(crate) fn tessellate_node(&self, node: &usvg::Node, params: &mut TessellateParams<'_>) {
4979 let start_idx = params.vertices.len();
4980 let node_id = match node {
4981 usvg::Node::Group(g) => g.id().to_string(),
4982 usvg::Node::Path(p) => p.id().to_string(),
4983 _ => String::new(),
4984 };
4985
4986 if let usvg::Node::Group(ref group) = *node {
4987 for child in group.children() {
4988 let mut child_params = TessellateParams {
4989 fill_tessellator: params.fill_tessellator,
4990 stroke_tessellator: params.stroke_tessellator,
4991 vertices: params.vertices,
4992 indices: params.indices,
4993 parsed_animations: params.parsed_animations,
4994 finalized_animations: params.finalized_animations,
4995 paths: params.paths,
4996 };
4997 self.tessellate_node(child, &mut child_params);
4998 }
4999 } else if let usvg::Node::Path(ref path) = *node {
5000 let has_fill = path.fill().is_some();
5001 let has_stroke = path.stroke().is_some();
5002
5003 if !has_fill && !has_stroke {
5005 log::debug!("SVG path '{}' has no fill or stroke, skipping", node_id);
5006 return;
5007 }
5008
5009 let lyon_path = usvg_to_lyon(path, node.abs_transform());
5010 let clip = [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY]; if has_fill && let Some(fill) = path.fill() {
5014 let paint = fill.paint();
5015 let fill_opacity = fill.opacity().get();
5016 let fill_rule = match fill.rule() {
5018 usvg::FillRule::EvenOdd => lyon::tessellation::FillRule::EvenOdd,
5019 usvg::FillRule::NonZero => lyon::tessellation::FillRule::NonZero,
5020 };
5021
5022 match paint {
5023 usvg::Paint::Color(c) => {
5024 let color = [
5025 c.red as f32 / 255.0,
5026 c.green as f32 / 255.0,
5027 c.blue as f32 / 255.0,
5028 fill_opacity,
5029 ];
5030 Self::tessellate_fill_solid(
5031 &lyon_path, color, &node_id, params, fill_rule,
5032 );
5033 }
5034 usvg::Paint::LinearGradient(g) => {
5035 Self::tessellate_fill_gradient(
5036 &lyon_path, g, fill_opacity, &node_id, params, fill_rule,
5037 );
5038 }
5039 usvg::Paint::RadialGradient(g) => {
5040 Self::tessellate_fill_radial_gradient(
5041 &lyon_path, g, fill_opacity, &node_id, params, fill_rule,
5042 );
5043 }
5044 usvg::Paint::Pattern(_) => {
5045 log::warn!(
5046 "SVG path '{}' uses pattern fill which is not supported, using white fallback",
5047 node_id
5048 );
5049 let color = [1.0, 1.0, 1.0, fill_opacity];
5050 Self::tessellate_fill_solid(
5051 &lyon_path, color, &node_id, params, fill_rule,
5052 );
5053 }
5054 }
5055 }
5056
5057 if has_stroke && let Some(stroke) = path.stroke() {
5059 let base_vertex_idx = params.vertices.len() as u32;
5060 let stroke_width = stroke.width().get(); let color = match stroke.paint() {
5062 usvg::Paint::Color(c) => [
5063 c.red as f32 / 255.0,
5064 c.green as f32 / 255.0,
5065 c.blue as f32 / 255.0,
5066 stroke.opacity().get(),
5067 ],
5068 usvg::Paint::LinearGradient(_)
5069 | usvg::Paint::RadialGradient(_)
5070 | usvg::Paint::Pattern(_) => {
5071 log::warn!(
5072 "SVG path '{}' uses gradient/pattern stroke which is not supported, using white fallback",
5073 node_id
5074 );
5075 [1.0, 1.0, 1.0, 1.0]
5076 }
5077 };
5078
5079 let mut stroke_opts = StrokeOptions::default()
5081 .with_line_width(stroke_width);
5082
5083 stroke_opts = match stroke.linecap() {
5085 usvg::LineCap::Butt => stroke_opts.with_line_cap(lyon::tessellation::LineCap::Butt),
5086 usvg::LineCap::Round => stroke_opts.with_line_cap(lyon::tessellation::LineCap::Round),
5087 usvg::LineCap::Square => stroke_opts.with_line_cap(lyon::tessellation::LineCap::Square),
5088 };
5089
5090 stroke_opts = match stroke.linejoin() {
5092 usvg::LineJoin::Miter => stroke_opts.with_line_join(lyon::tessellation::LineJoin::Miter),
5093 usvg::LineJoin::Round => stroke_opts.with_line_join(lyon::tessellation::LineJoin::Round),
5094 usvg::LineJoin::Bevel => stroke_opts.with_line_join(lyon::tessellation::LineJoin::Bevel),
5095 _ => stroke_opts,
5096 };
5097
5098 stroke_opts = stroke_opts.with_miter_limit(stroke.miterlimit().get());
5100
5101 if let Some(dasharray) = stroke.dasharray() {
5107 let _ = dasharray; }
5109
5110 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
5111 let path_length = lyon::algorithms::length::approximate_length(&lyon_path, 0.1);
5112
5113 if let Err(e) = params.stroke_tessellator.tessellate_path(
5114 &lyon_path,
5115 &stroke_opts,
5116 &mut BuffersBuilder::new(
5117 &mut buffers,
5118 CustomStrokeVertexConstructor { color, clip, path_length },
5119 ),
5120 ) {
5121 log::warn!(
5122 "SVG stroke tessellation failed for path '{}': {:?}, skipping",
5123 node_id,
5124 e
5125 );
5126 return;
5127 }
5128
5129 params.vertices.extend(buffers.vertices);
5130 for idx in buffers.indices {
5131 params.indices.push(base_vertex_idx + idx);
5132 }
5133 }
5134 }
5135
5136 let end_idx = params.vertices.len();
5137 let end_idx_indices = params.indices.len();
5138 if !node_id.is_empty() && start_idx < end_idx {
5139 for anim in params.parsed_animations {
5140 if anim.target_id == node_id {
5141 let mut final_anim = anim.clone();
5142 final_anim.vertex_range = start_idx..end_idx;
5143 params.finalized_animations.push(final_anim);
5144 }
5145 }
5146 params.paths.push(crate::types::SvgPath {
5148 id: node_id,
5149 vertex_range: start_idx..end_idx,
5150 index_range: end_idx_indices..params.indices.len(),
5151 local_transform: Default::default(),
5152 });
5153 }
5154 }
5155
5156 fn tessellate_fill_solid(
5158 lyon_path: &lyon::path::Path,
5159 color: [f32; 4],
5160 node_id: &String,
5161 params: &mut TessellateParams<'_>,
5162 fill_rule: lyon::tessellation::FillRule,
5163 ) {
5164 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
5165 let base_vertex_idx = params.vertices.len() as u32;
5166 if let Err(e) = params.fill_tessellator.tessellate_path(
5167 lyon_path,
5168 &FillOptions::default().with_fill_rule(fill_rule),
5169 &mut BuffersBuilder::new(&mut buffers, SceneVertexConstructor { color }),
5170 ) {
5171 log::warn!(
5172 "SVG fill tessellation failed for path '{}': {:?}, skipping",
5173 node_id,
5174 e
5175 );
5176 return;
5177 }
5178 params.vertices.extend(buffers.vertices);
5179 for idx in buffers.indices {
5180 params.indices.push(base_vertex_idx + idx);
5181 }
5182 }
5183
5184 fn gradient_color_at(
5186 stops: &[usvg::Stop],
5187 pos: f32,
5188 fill_opacity: f32,
5189 ) -> [f32; 4] {
5190 if stops.is_empty() {
5191 return [1.0, 1.0, 1.0, fill_opacity];
5192 }
5193 let pos = pos.clamp(0.0, 1.0);
5194 let mut start = &stops[0];
5195 let mut end = &stops[stops.len() - 1];
5196 for w in stops.windows(2) {
5197 if pos >= w[0].offset().get() && pos <= w[1].offset().get() {
5198 start = &w[0];
5199 end = &w[1];
5200 break;
5201 }
5202 }
5203 let so = start.offset().get();
5204 let eo = end.offset().get();
5205 if pos <= so {
5206 let c = start.color();
5207 return [c.red as f32 / 255.0, c.green as f32 / 255.0, c.blue as f32 / 255.0, start.opacity().get() * fill_opacity];
5208 }
5209 if pos >= eo {
5210 let c = end.color();
5211 return [c.red as f32 / 255.0, c.green as f32 / 255.0, c.blue as f32 / 255.0, end.opacity().get() * fill_opacity];
5212 }
5213 let range = eo - so;
5214 if range < 0.0001 {
5215 let c = start.color();
5216 return [c.red as f32 / 255.0, c.green as f32 / 255.0, c.blue as f32 / 255.0, start.opacity().get() * fill_opacity];
5217 }
5218 let t = (pos - so) / range;
5219 let sc = start.color();
5220 let ec = end.color();
5221 [
5222 (sc.red as f32 + (ec.red as f32 - sc.red as f32) * t) / 255.0,
5223 (sc.green as f32 + (ec.green as f32 - sc.green as f32) * t) / 255.0,
5224 (sc.blue as f32 + (ec.blue as f32 - sc.blue as f32) * t) / 255.0,
5225 (start.opacity().get() + (end.opacity().get() - start.opacity().get()) * t) * fill_opacity,
5226 ]
5227 }
5228
5229 fn tessellate_fill_gradient(
5231 lyon_path: &lyon::path::Path,
5232 gradient: &usvg::LinearGradient,
5233 fill_opacity: f32,
5234 node_id: &String,
5235 params: &mut TessellateParams<'_>,
5236 fill_rule: lyon::tessellation::FillRule,
5237 ) {
5238 let x1 = gradient.x1();
5239 let y1 = gradient.y1();
5240 let x2 = gradient.x2();
5241 let y2 = gradient.y2();
5242 let dx = x2 - x1;
5243 let dy = y2 - y1;
5244 let grad_len_sq = dx * dx + dy * dy;
5245
5246 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
5247 let base_vertex_idx = params.vertices.len() as u32;
5248 if let Err(e) = params.fill_tessellator.tessellate_path(
5249 lyon_path,
5250 &FillOptions::default(),
5251 &mut BuffersBuilder::new(&mut buffers, SceneVertexConstructor { color: [1.0, 1.0, 1.0, 1.0] }),
5252 ) {
5253 log::warn!("SVG gradient fill tessellation failed for path '{}': {:?}, skipping", node_id, e);
5254 return;
5255 }
5256
5257 let stops = gradient.stops();
5258 for mut vertex in buffers.vertices {
5259 let px = vertex.position[0];
5260 let py = vertex.position[1];
5261 let t = if grad_len_sq < 0.0001 { 0.5 } else { ((px - x1) * dx + (py - y1) * dy) / grad_len_sq };
5262 vertex.color = Self::gradient_color_at(stops, t as f32, fill_opacity);
5263 params.vertices.push(vertex);
5264 }
5265 for idx in buffers.indices {
5266 params.indices.push(base_vertex_idx + idx);
5267 }
5268 }
5269
5270 fn tessellate_fill_radial_gradient(
5272 lyon_path: &lyon::path::Path,
5273 gradient: &usvg::RadialGradient,
5274 fill_opacity: f32,
5275 node_id: &String,
5276 params: &mut TessellateParams<'_>,
5277 fill_rule: lyon::tessellation::FillRule,
5278 ) {
5279 let cx = gradient.cx();
5280 let cy = gradient.cy();
5281 let r = gradient.r();
5282 let stops = gradient.stops();
5283
5284 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
5285 let base_vertex_idx = params.vertices.len() as u32;
5286 if let Err(e) = params.fill_tessellator.tessellate_path(
5287 lyon_path,
5288 &FillOptions::default(),
5289 &mut BuffersBuilder::new(&mut buffers, SceneVertexConstructor { color: [1.0, 1.0, 1.0, 1.0] }),
5290 ) {
5291 log::warn!("SVG radial gradient fill tessellation failed for path '{}': {:?}, skipping", node_id, e);
5292 return;
5293 }
5294
5295 for mut vertex in buffers.vertices {
5296 let px = vertex.position[0];
5297 let py = vertex.position[1];
5298 let dist = ((px - cx) * (px - cx) + (py - cy) * (py - cy)).sqrt();
5299 let r_val = r.get();
5300 let t = if r_val < 0.001 { 0.5 } else { (dist / r_val).clamp(0.0, 1.0) };
5301 vertex.color = Self::gradient_color_at(stops, t, fill_opacity);
5302 params.vertices.push(vertex);
5303 }
5304 for idx in buffers.indices {
5305 params.indices.push(base_vertex_idx + idx);
5306 }
5307 }
5308
5309 pub fn draw_svg(&mut self, name: &str, rect: Rect, color: Option<[f32; 4]>, material_id: u32) {
5313 self.draw_svg_with_offset(name, rect, color, material_id, 0.0);
5314 }
5315
5316 pub fn draw_svg_with_offset(&mut self, name: &str, rect: Rect, color: Option<[f32; 4]>, material_id: u32, animation_time_offset: f32) {
5317 self.draw_svg_with_order(name, rect, color, material_id, animation_time_offset, 0);
5318 }
5319
5320 pub fn draw_svg_with_order(&mut self, name: &str, rect: Rect, color: Option<[f32; 4]>, material_id: u32, animation_time_offset: f32, draw_order: i32) {
5321 let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
5322 x: -10000.0,
5323 y: -10000.0,
5324 width: 20000.0,
5325 height: 20000.0,
5326 });
5327 let scale = self.current_scale_factor();
5328 let screen_w = self.current_width() as f32 / scale;
5329 let screen_h = self.current_height() as f32 / scale;
5330
5331 if rect.x > clip_rect.x + clip_rect.width
5332 || rect.x + rect.width < clip_rect.x
5333 || rect.y > clip_rect.y + clip_rect.height
5334 || rect.y + rect.height < clip_rect.y
5335 {
5336 return;
5337 }
5338
5339 log::info!("DRAW_SVG '{}' called with rect: {:?}, model_view_box: {:?}", name, rect, self.svg.model_cache.get(name).map(|m| m.view_box));
5340
5341 if rect.x > screen_w
5342 || rect.x + rect.width < 0.0
5343 || rect.y > screen_h
5344 || rect.y + rect.height < 0.0
5345 {
5346 return;
5347 }
5348
5349 let model = if let Some(m) = self.svg.model_cache.get(name) {
5350 m.clone()
5351 } else {
5352 return;
5353 };
5354
5355 let base_idx = self.vertices.len() as u32;
5356 let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
5357 x: -10000.0,
5358 y: -10000.0,
5359 width: 20000.0,
5360 height: 20000.0,
5361 });
5362 let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
5363 let scale = self.current_scale_factor();
5364 let snap = |v: f32| (v * scale).round() / scale;
5365
5366 if model.paths.is_empty() {
5367 let mut local_vertices = model.vertices.clone();
5369 Self::position_vertices(&mut local_vertices, model.view_box, rect, material_id, clip, snap);
5370 let base_vertex = self.vertices.len() as u32;
5371 self.vertices.extend(local_vertices);
5372 let index_count = model.indices.len();
5373 for idx in &model.indices {
5374 self.indices.push(base_vertex + *idx);
5375 }
5376 let material = Self::resolve_material(material_id);
5377 let tid = self.get_texture_id("__mega_heim");
5378 Self::emit_draw_call(self, material, tid, clip_rect, index_count as u32, base_vertex);
5379 } else {
5380 for path in &model.paths {
5382 let mut path_verts: Vec<Vertex> = model.vertices[path.vertex_range.clone()].to_vec();
5383 if path.local_transform.scale != 1.0 || path.local_transform.rotation != 0.0 || path.local_transform.translate != [0.0, 0.0] {
5385 let s = path.local_transform.scale;
5386 let rad = path.local_transform.rotation.to_radians();
5387 let c = rad.cos();
5388 let sn = rad.sin();
5389 let tx = path.local_transform.translate[0];
5390 let ty = path.local_transform.translate[1];
5391 for v in &mut path_verts {
5392 let px = v.position[0] * s;
5393 let py = v.position[1] * s;
5394 v.position[0] = px * c - py * sn + tx;
5395 v.position[1] = px * sn + py * c + ty;
5396 }
5397 }
5398 for anim in &model.animations {
5400 if anim.target_id == path.id {
5401 let effective_time = self.current_scene.time + animation_time_offset;
5402 let t = (effective_time % anim.duration) / anim.duration;
5403 let val = anim.evaluate(t);
5404 if anim.attribute_name == "transform" {
5405 let mut min_x = f32::MAX; let mut min_y = f32::MAX;
5406 let mut max_x = f32::MIN; let mut max_y = f32::MIN;
5407 for v in &path_verts {
5408 min_x = min_x.min(v.position[0]);
5409 min_y = min_y.min(v.position[1]);
5410 max_x = max_x.max(v.position[0]);
5411 max_y = max_y.max(v.position[1]);
5412 }
5413 let cx = (min_x + max_x) * 0.5;
5414 let cy = (min_y + max_y) * 0.5;
5415 let c = val.to_radians().cos();
5416 let s = val.to_radians().sin();
5417 for v in &mut path_verts {
5418 let dx = v.position[0] - cx;
5419 let dy = v.position[1] - cy;
5420 v.position[0] = cx + dx * c - dy * s;
5421 v.position[1] = cy + dx * s + dy * c;
5422 }
5423 } else if anim.attribute_name == "opacity" {
5424 for v in &mut path_verts { v.color[3] = val; }
5425 } else if anim.attribute_name == "stroke-dashoffset" {
5426 for v in &mut path_verts { v.slice[3] = 1.0 - val; }
5427 }
5428 }
5429 }
5430 Self::position_vertices(&mut path_verts, model.view_box, rect, material_id, clip, snap);
5432 let base_vertex = self.vertices.len() as u32;
5433 let index_start = self.indices.len();
5434 self.vertices.extend(path_verts);
5435 let path_index_start = path.index_range.start;
5437 for idx in &model.indices[path.index_range.clone()] {
5438 self.indices.push(base_vertex + *idx - path_index_start as u32);
5439 }
5440 let index_count = path.index_range.len() as u32;
5441 let material = Self::resolve_material(material_id);
5442 let tid = self.get_texture_id("__mega_heim");
5443 Self::emit_draw_call(self, material, tid, clip_rect, index_count, base_vertex);
5444 }
5445 }
5446 }
5447
5448 fn resolve_material(material_id: u32) -> cvkg_core::DrawMaterial {
5451 Self::resolve_material_with_context(material_id, &cvkg_core::DrawMaterial::Opaque)
5452 }
5453
5454 fn resolve_material_with_context(
5458 material_id: u32,
5459 current: &cvkg_core::DrawMaterial,
5460 ) -> cvkg_core::DrawMaterial {
5461 use material_id::*;
5462
5463 if matches!(current, cvkg_core::DrawMaterial::TopUI) && material_id != GLASS {
5466 return cvkg_core::DrawMaterial::TopUI;
5467 }
5468
5469 match material_id {
5470 GLASS => {
5471 if let cvkg_core::DrawMaterial::Glass {
5472 blur_radius,
5473 ior_override,
5474 glass_intensity,
5475 } = current
5476 {
5477 cvkg_core::DrawMaterial::Glass {
5478 blur_radius: *blur_radius,
5479 ior_override: *ior_override,
5480 glass_intensity: *glass_intensity,
5481 }
5482 } else {
5483 cvkg_core::DrawMaterial::Glass {
5484 blur_radius: 20.0,
5485 ior_override: 0.0,
5486 glass_intensity: 1.0,
5487 }
5488 }
5489 }
5490 TOP_UI => cvkg_core::DrawMaterial::TopUI,
5491 BLEND_START..=BLEND_END => cvkg_core::DrawMaterial::Blend {
5492 mode: (material_id - 7) as u32,
5493 },
5494 _ => cvkg_core::DrawMaterial::Opaque,
5495 }
5496 }
5497
5498 fn convert_compositor_material(mat: &cvkg_compositor::Material) -> cvkg_core::DrawMaterial {
5501 match mat {
5502 cvkg_compositor::Material::Glass { blur_radius, .. } => {
5503 cvkg_core::DrawMaterial::Glass {
5504 blur_radius: *blur_radius,
5505 ior_override: 0.0,
5506 glass_intensity: 1.0,
5507 }
5508 }
5509 cvkg_compositor::Material::Overlay => cvkg_core::DrawMaterial::TopUI,
5510 cvkg_compositor::Material::Multiply => cvkg_core::DrawMaterial::Blend { mode: 1 },
5511 cvkg_compositor::Material::Screen => cvkg_core::DrawMaterial::Blend { mode: 2 },
5512 cvkg_compositor::Material::BlendOverlay => cvkg_core::DrawMaterial::Blend { mode: 3 },
5513 cvkg_compositor::Material::Darken => cvkg_core::DrawMaterial::Blend { mode: 4 },
5514 cvkg_compositor::Material::Lighten => cvkg_core::DrawMaterial::Blend { mode: 5 },
5515 cvkg_compositor::Material::ColorDodge => cvkg_core::DrawMaterial::Blend { mode: 6 },
5516 cvkg_compositor::Material::ColorBurn => cvkg_core::DrawMaterial::Blend { mode: 7 },
5517 cvkg_compositor::Material::HardLight => cvkg_core::DrawMaterial::Blend { mode: 8 },
5518 cvkg_compositor::Material::SoftLight => cvkg_core::DrawMaterial::Blend { mode: 9 },
5519 cvkg_compositor::Material::Difference => cvkg_core::DrawMaterial::Blend { mode: 10 },
5520 cvkg_compositor::Material::Exclusion => cvkg_core::DrawMaterial::Blend { mode: 11 },
5521 cvkg_compositor::Material::Hue => cvkg_core::DrawMaterial::Blend { mode: 12 },
5522 cvkg_compositor::Material::Saturation => cvkg_core::DrawMaterial::Blend { mode: 13 },
5523 cvkg_compositor::Material::Color => cvkg_core::DrawMaterial::Blend { mode: 14 },
5524 cvkg_compositor::Material::Luminosity => cvkg_core::DrawMaterial::Blend { mode: 15 },
5525 cvkg_compositor::Material::Opaque => cvkg_core::DrawMaterial::Opaque,
5526 _ => cvkg_core::DrawMaterial::Opaque,
5527 }
5528 }
5529
5530 fn position_vertices(
5532 vertices: &mut [Vertex],
5533 view_box: Rect,
5534 rect: Rect,
5535 material_id: u32,
5536 clip: [f32; 4],
5537 snap: impl Fn(f32) -> f32,
5538 ) {
5539 for v in vertices.iter_mut() {
5540 let rel_x = (v.position[0] - view_box.x) / view_box.width;
5541 let rel_y = (v.position[1] - view_box.y) / view_box.height;
5542 v.position[0] = snap(rect.x + rel_x * rect.width);
5543 v.position[1] = snap(rect.y + rel_y * rect.height);
5544 v.position[2] = 0.0; v.logical = [v.position[0], v.position[1]];
5546 v.clip = clip;
5547 v.material_id = material_id;
5548 }
5549 }
5550
5551 fn emit_draw_call(
5553 renderer: &mut SurtrRenderer,
5554 material: cvkg_core::DrawMaterial,
5555 texture_id: Option<u32>,
5556 scissor_rect: Rect,
5557 index_count: u32,
5558 base_vertex: u32,
5559 ) {
5560 let draw_order = renderer.current_draw_order;
5561 let (translation, scale_transform, rotation, _, _) = renderer.current_transform();
5562 let current_instance_data = InstanceData {
5563 translation,
5564 scale: scale_transform,
5565 rotation,
5566 blur_radius: 0.0,
5567 ior_override: 0.0,
5568 glass_intensity: 1.0,
5569 };
5570 let last_call = renderer.draw_calls.last();
5573 let needs_new_call = renderer.draw_calls.is_empty()
5574 || renderer.current_texture_id != texture_id
5575 || last_call.unwrap().scissor_rect != renderer.clip_stack.last().copied()
5576 || last_call.unwrap().material != material
5577 || {
5578 let last_material = last_call.unwrap().material;
5579 matches!((material, last_material),
5580 (cvkg_core::DrawMaterial::Glass { blur_radius: a, ior_override: b, glass_intensity: c },
5581 cvkg_core::DrawMaterial::Glass { blur_radius: d, ior_override: e, glass_intensity: f })
5582 if a != d || b != e || c != f)
5583 };
5584
5585 if needs_new_call {
5586 renderer.current_texture_id = texture_id;
5587 renderer.instance_data.push(current_instance_data);
5588 renderer.draw_calls.push(DrawCall {
5589 target_id: None,
5590 texture_id,
5591 scissor_rect: renderer.clip_stack.last().copied(),
5592 index_start: (renderer.indices.len() - index_count as usize) as u32,
5593 index_count,
5594 instance_count: 1,
5595 material,
5596 instance_start: (renderer.instance_data.len() - 1) as u32,
5597 draw_order: 0,
5598 });
5599 } else {
5600 renderer.instance_data.push(current_instance_data);
5602 if let Some(call) = renderer.draw_calls.last_mut() {
5603 call.instance_count += 1;
5604 }
5605 }
5606 }
5607
5608 pub async fn forge_headless(width: u32, height: u32) -> Self {
5610 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
5611 backends: wgpu::Backends::all(),
5612 flags: wgpu::InstanceFlags::default(),
5613 backend_options: wgpu::BackendOptions::default(),
5614 display: None,
5615 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
5616 });
5617
5618 log::info!("[GPU] Requesting HighPerformance adapter (headless)...");
5620 let mut adapter = instance
5621 .request_adapter(&wgpu::RequestAdapterOptions {
5622 power_preference: wgpu::PowerPreference::HighPerformance,
5623 compatible_surface: None,
5624 force_fallback_adapter: false,
5625 })
5626 .await
5627 .ok();
5628
5629 if adapter.is_none() {
5630 log::warn!(
5631 "[GPU] HighPerformance adapter failed (possible Bumblebee/Optimus), trying LowPower..."
5632 );
5633 adapter = instance
5634 .request_adapter(&wgpu::RequestAdapterOptions {
5635 power_preference: wgpu::PowerPreference::LowPower,
5636 compatible_surface: None,
5637 force_fallback_adapter: false,
5638 })
5639 .await
5640 .ok();
5641 }
5642
5643 if adapter.is_none() {
5644 log::warn!("[GPU] Hardware adapters failed, trying Software fallback...");
5645 adapter = instance
5646 .request_adapter(&wgpu::RequestAdapterOptions {
5647 power_preference: wgpu::PowerPreference::LowPower,
5648 compatible_surface: None,
5649 force_fallback_adapter: true,
5650 })
5651 .await
5652 .ok();
5653 }
5654
5655 let adapter = adapter.expect("Failed to find a suitable GPU for Surtr");
5656 let info = adapter.get_info();
5657 let caps = crate::subsystems::GpuCapabilities::detect(
5660 &info.name,
5661 format!("{:?}", info.backend),
5662 );
5663 log::info!(
5664 "[GPU] Selected adapter: {} ({:?}) on backend: {:?} -- detected as {}",
5665 info.name,
5666 info.device_type,
5667 info.backend,
5668 caps.vendor
5669 );
5670 log::info!("[GPU] Driver info: {} - {}", info.driver, info.driver_info);
5671 let required_features = adapter.features()
5672 & (wgpu::Features::TIMESTAMP_QUERY
5673 | wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
5674 | wgpu::Features::TEXTURE_BINDING_ARRAY);
5675
5676 let (device, queue) = adapter
5677 .request_device(&wgpu::DeviceDescriptor {
5678 label: Some("Surtr Headless Forge"),
5679 required_features,
5680 required_limits: wgpu::Limits {
5681 max_bindings_per_bind_group: adapter
5682 .limits()
5683 .max_bindings_per_bind_group
5684 .min(256),
5685 max_binding_array_elements_per_shader_stage: adapter
5686 .limits()
5687 .max_binding_array_elements_per_shader_stage
5688 .min(256),
5689 ..wgpu::Limits::default()
5690 },
5691 memory_hints: wgpu::MemoryHints::default(),
5692 experimental_features: wgpu::ExperimentalFeatures::disabled(),
5693 trace: wgpu::Trace::Off,
5694 })
5695 .await
5696 .expect("Failed to create Surtr device");
5697
5698 let instance = Arc::new(instance);
5699 let adapter = Arc::new(adapter);
5700
5701 device.on_uncaptured_error(Arc::new(|error| {
5702 log::error!(
5703 "[GPU] Uncaptured device error (Device Lost or Panic): {:?}",
5704 error
5705 );
5706 }));
5707
5708 let device = Arc::new(device);
5709 let queue = Arc::new(queue);
5710
5711 Self::forge_internal(
5712 instance,
5713 adapter,
5714 device,
5715 queue,
5716 None,
5717 Some((width, height, wgpu::TextureFormat::Rgba8UnormSrgb)),
5718 )
5719 .await
5720 }
5721
5722 pub async fn capture_frame(&self) -> Result<Vec<u8>, String> {
5724 let ctx = self
5725 .headless_context
5726 .as_ref()
5727 .ok_or("Headless context required for capture")?;
5728 let current_width = self.current_width();
5729 let current_height = self.current_height();
5730
5731 let u32_size = std::mem::size_of::<u32>() as u32;
5732 let width = ctx.width;
5733 let height = ctx.height;
5734 let bytes_per_row = width * u32_size;
5735 let padding = (256 - (bytes_per_row % 256)) % 256;
5736 let padded_bytes_per_row = bytes_per_row + padding;
5737
5738 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
5739 label: Some("Capture Buffer"),
5740 size: (padded_bytes_per_row as u64 * height as u64),
5741 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
5742 mapped_at_creation: false,
5743 });
5744
5745 let mut encoder = self
5746 .device
5747 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
5748 label: Some("Capture Encoder"),
5749 });
5750
5751 encoder.copy_texture_to_buffer(
5752 wgpu::TexelCopyTextureInfo {
5753 texture: &ctx.output_texture,
5754 mip_level: 0,
5755 origin: wgpu::Origin3d::ZERO,
5756 aspect: wgpu::TextureAspect::All,
5757 },
5758 wgpu::TexelCopyBufferInfo {
5759 buffer: &output_buffer,
5760 layout: wgpu::TexelCopyBufferLayout {
5761 offset: 0,
5762 bytes_per_row: Some(padded_bytes_per_row),
5763 rows_per_image: Some(height),
5764 },
5765 },
5766 wgpu::Extent3d {
5767 width,
5768 height,
5769 depth_or_array_layers: 1,
5770 },
5771 );
5772
5773 self.queue.submit(Some(encoder.finish()));
5774
5775 let buffer_slice = output_buffer.slice(..);
5776 let (sender, receiver) = futures::channel::oneshot::channel();
5777 buffer_slice.map_async(wgpu::MapMode::Read, move |v| {
5778 let _ = sender.send(v);
5779 });
5780
5781 let _ = self.device.poll(wgpu::PollType::Wait {
5782 submission_index: None,
5783 timeout: None,
5784 });
5785
5786 if let Ok(Ok(_)) = receiver.await {
5787 let data = buffer_slice.get_mapped_range();
5788 let mut result = Vec::with_capacity((width * height * 4) as usize);
5789
5790 for y in 0..height {
5791 let start = (y * padded_bytes_per_row) as usize;
5792 let end = start + bytes_per_row as usize;
5793 result.extend_from_slice(&data[start..end]);
5794 }
5795
5796 log::trace!(
5797 "[GPU] capture_frame: data len={}, first 4 bytes={:?}",
5798 data.len(),
5799 &data[0..4.min(data.len())]
5800 );
5801
5802 drop(data);
5803 output_buffer.unmap();
5804 Ok(result)
5805 } else {
5806 Err("Failed to capture frame".to_string())
5807 }
5808 }
5809
5810 pub(crate) fn current_width(&self) -> u32 {
5811 if let Some(id) = self.current_window {
5812 self.surfaces.get(&id).map(|s| s.config.width).unwrap_or(1)
5813 } else {
5814 self.headless_context.as_ref().map(|h| h.width).unwrap_or(1)
5815 }
5816 }
5817
5818 pub(crate) fn current_height(&self) -> u32 {
5819 if let Some(id) = self.current_window {
5820 self.surfaces.get(&id).map(|s| s.config.height).unwrap_or(1)
5821 } else {
5822 self.headless_context
5823 .as_ref()
5824 .map(|h| h.height)
5825 .unwrap_or(1)
5826 }
5827 }
5828
5829 pub(crate) fn current_scale_factor(&self) -> f32 {
5830 if let Some(id) = self.current_window {
5831 self.surfaces
5832 .get(&id)
5833 .map(|s| s.scale_factor)
5834 .unwrap_or(1.0)
5835 } else {
5836 self.headless_context
5837 .as_ref()
5838 .map(|h| h.scale_factor)
5839 .unwrap_or(1.0)
5840 }
5841 }
5842
5843 pub(crate) fn current_time(&self) -> f32 {
5846 self.start_time.elapsed().as_secs_f32()
5847 }
5848
5849 pub(crate) fn find_filter<'a>(
5851 tree: &'a usvg::Tree,
5852 filter_id: &str,
5853 ) -> Option<&'a usvg::filter::Filter> {
5854 tree.filters()
5855 .iter()
5856 .find(|f| f.id() == filter_id)
5857 .map(|arc| arc.as_ref())
5858 }
5859}
5860
5861#[cfg(test)]
5862mod lock_or_clear_cache_tests {
5863 use crate::renderer::SurtrRenderer;
5864 use std::collections::HashMap;
5865 use std::sync::Mutex;
5866
5867 #[test]
5868 fn returns_lock_when_not_poisoned() {
5869 let mutex = Mutex::new(HashMap::<u64, u32>::new());
5870 let guard = SurtrRenderer::lock_or_clear_cache(&mutex);
5871 assert!(guard.is_empty());
5872 }
5873
5874 #[test]
5875 fn clears_cache_when_poisoned() {
5876 let mutex = Mutex::new(HashMap::<u64, u32>::new());
5877 {
5878 let mut guard = mutex.lock().unwrap();
5879 guard.insert(1, 100);
5880 guard.insert(2, 200);
5881 }
5882 let result = std::panic::catch_unwind(|| {
5884 let mutex = std::panic::AssertUnwindSafe(&mutex);
5885 let _guard = mutex.lock().unwrap();
5886 panic!("intentional panic to poison the mutex");
5887 });
5888 assert!(result.is_err(), "the inner panic should propagate");
5889
5890 let guard = SurtrRenderer::lock_or_clear_cache(&mutex);
5893 assert!(
5894 guard.is_empty(),
5895 "cache must be cleared after poison recovery, got {:?}",
5896 *guard
5897 );
5898 }
5899
5900 #[test]
5901 fn works_with_vec_cache() {
5902 let mutex = Mutex::new(Vec::<u32>::new());
5903 {
5904 let mut guard = mutex.lock().unwrap();
5905 guard.push(1);
5906 guard.push(2);
5907 guard.push(3);
5908 }
5909 let _ = std::panic::catch_unwind(|| {
5911 let mutex = std::panic::AssertUnwindSafe(&mutex);
5912 let _guard = mutex.lock().unwrap();
5913 panic!("poison");
5914 });
5915
5916 let guard = SurtrRenderer::lock_or_clear_cache(&mutex);
5918 assert!(guard.is_empty(), "Vec cache should be cleared on poison");
5919 }
5920}
5921
5922#[cfg(test)]
5923mod wgsl_tests {
5924 #[test]
5925 fn test_wgsl() {
5926 let source = include_str!("shaders/effects.wgsl");
5927 let mut frontend = naga::front::wgsl::Frontend::new();
5928 match frontend.parse(source) {
5929 Ok(_) => println!("WGSL parsed successfully!"),
5930 Err(e) => {
5931 panic!("WGSL parsing failed: \n{}", e.emit_to_string(source));
5932 }
5933 }
5934 }
5935
5936 #[test]
5942 fn test_wgsl_common_uses_binding_array_on_native() {
5943 let source = include_str!("shaders/common.wgsl");
5944 assert!(
5945 source.contains("binding_array<texture_2d<f32>, 32>"),
5946 "native common.wgsl must declare a 32-element texture binding_array"
5947 );
5948 assert!(
5949 source.contains("t_diffuse:"),
5950 "native common.wgsl must declare t_diffuse"
5951 );
5952 }
5953
5954 #[test]
5957 fn test_wgsl_native_indexed_access() {
5958 let bloom = include_str!("shaders/bloom.wgsl");
5959 let material = include_str!("shaders/material_opaque.wgsl");
5960 assert!(
5961 bloom.contains("t_diffuse["),
5962 "native bloom.wgsl must index t_diffuse as an array"
5963 );
5964 assert!(
5965 material.contains("t_diffuse["),
5966 "native material_opaque.wgsl must index t_diffuse as an array"
5967 );
5968 }
5969}
5970
5971#[derive(Clone)]
5978struct Sha256 {
5979 state: [u32; 8],
5980 buffer: [u8; 64],
5981 buffer_len: usize,
5982 total_len: u64,
5983}
5984
5985impl Sha256 {
5986 const K: [u32; 64] = [
5987 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
5988 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
5989 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
5990 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
5991 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
5992 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
5993 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
5994 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
5995 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
5996 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
5997 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
5998 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
5999 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
6000 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
6001 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
6002 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
6003 ];
6004
6005 fn new() -> Self {
6006 Self {
6007 state: [
6008 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
6009 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
6010 ],
6011 buffer: [0; 64],
6012 buffer_len: 0,
6013 total_len: 0,
6014 }
6015 }
6016
6017 fn update(&mut self, data: &[u8]) {
6018 self.total_len = self.total_len.wrapping_add(data.len() as u64);
6019 for &b in data {
6020 self.buffer[self.buffer_len] = b;
6021 self.buffer_len += 1;
6022 if self.buffer_len == 64 {
6023 let block = self.buffer;
6024 self.compress(&block);
6025 self.buffer_len = 0;
6026 }
6027 }
6028 }
6029
6030 fn finalize(mut self) -> [u8; 32] {
6031 self.buffer[self.buffer_len] = 0x80;
6033 self.buffer_len += 1;
6034 if self.buffer_len > 56 {
6035 for b in &mut self.buffer[self.buffer_len..] { *b = 0; }
6036 let block = self.buffer;
6037 self.compress(&block);
6038 self.buffer_len = 0;
6039 }
6040 for b in &mut self.buffer[self.buffer_len..56] { *b = 0; }
6041 let bit_len = self.total_len.wrapping_mul(8);
6042 self.buffer[56..64].copy_from_slice(&bit_len.to_be_bytes());
6043 let block = self.buffer;
6044 self.compress(&block);
6045
6046 let mut out = [0u8; 32];
6047 for (i, &s) in self.state.iter().enumerate() {
6048 out[i*4..(i+1)*4].copy_from_slice(&s.to_be_bytes());
6049 }
6050 out
6051 }
6052
6053 fn compress(&mut self, block: &[u8]) {
6054 let mut w = [0u32; 64];
6055 for i in 0..16 {
6056 w[i] = u32::from_be_bytes([
6057 block[i*4], block[i*4+1], block[i*4+2], block[i*4+3]
6058 ]);
6059 }
6060 for i in 16..64 {
6061 let s0 = w[i-15].rotate_right(7) ^ w[i-15].rotate_right(18) ^ (w[i-15] >> 3);
6062 let s1 = w[i-2].rotate_right(17) ^ w[i-2].rotate_right(19) ^ (w[i-2] >> 10);
6063 w[i] = w[i-16].wrapping_add(s0).wrapping_add(w[i-7]).wrapping_add(s1);
6064 }
6065 let mut a = self.state[0];
6066 let mut b = self.state[1];
6067 let mut c = self.state[2];
6068 let mut d = self.state[3];
6069 let mut e = self.state[4];
6070 let mut f = self.state[5];
6071 let mut g = self.state[6];
6072 let mut h = self.state[7];
6073 for i in 0..64 {
6074 let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
6075 let ch = (e & f) ^ ((!e) & g);
6076 let t1 = h.wrapping_add(s1).wrapping_add(ch).wrapping_add(Self::K[i]).wrapping_add(w[i]);
6077 let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
6078 let mj = (a & b) ^ (a & c) ^ (b & c);
6079 let t2 = s0.wrapping_add(mj);
6080 h = g; g = f; f = e;
6081 e = d.wrapping_add(t1);
6082 d = c; c = b; b = a;
6083 a = t1.wrapping_add(t2);
6084 }
6085 self.state[0] = self.state[0].wrapping_add(a);
6086 self.state[1] = self.state[1].wrapping_add(b);
6087 self.state[2] = self.state[2].wrapping_add(c);
6088 self.state[3] = self.state[3].wrapping_add(d);
6089 self.state[4] = self.state[4].wrapping_add(e);
6090 self.state[5] = self.state[5].wrapping_add(f);
6091 self.state[6] = self.state[6].wrapping_add(g);
6092 self.state[7] = self.state[7].wrapping_add(h);
6093 }
6094}
6095
6096#[cfg(test)]
6097mod p1_11_pipeline_cache_tests {
6098 use super::*;
6099
6100 fn write_cache(cache_path: &std::path::Path, data: &[u8]) -> std::io::Result<()> {
6103 use std::io::Write;
6104 let mut hasher = Sha256::new();
6105 hasher.update(data);
6106 let hash = hasher.finalize();
6107 let hash_hex = format!(
6108 "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
6109 hash[0], hash[1], hash[2], hash[3],
6110 hash[4], hash[5], hash[6], hash[7]
6111 );
6112 std::fs::write(cache_path, data)?;
6113 let hash_path = cache_path.with_extension("bin.sha256");
6114 let mut f = std::fs::File::create(hash_path)?;
6115 f.write_all(hash_hex.as_bytes())?;
6116 Ok(())
6117 }
6118
6119 #[test]
6120 fn returns_none_when_cache_does_not_exist() {
6121 let tmp = std::env::temp_dir().join("cvkg_test_no_cache.bin");
6122 let _ = std::fs::remove_file(&tmp);
6123 let result = load_pipeline_cache_with_integrity_check(&tmp);
6124 assert!(matches!(result, Ok(None)), "missing cache should yield Ok(None), got {result:?}");
6125 }
6126
6127 #[test]
6128 fn returns_data_when_sidecar_matches() {
6129 let tmp = std::env::temp_dir().join("cvkg_test_good_cache.bin");
6130 let data = b"pipeline cache blob with some bytes";
6131 write_cache(&tmp, data).expect("failed to write test cache");
6132 let result = load_pipeline_cache_with_integrity_check(&tmp);
6133 match result {
6134 Ok(Some(d)) => assert_eq!(d, data),
6135 other => panic!("expected Ok(Some(data)), got {other:?}"),
6136 }
6137 let _ = std::fs::remove_file(&tmp);
6138 let _ = std::fs::remove_file(tmp.with_extension("bin.sha256"));
6139 }
6140
6141 #[test]
6142 fn returns_err_when_sidecar_missing() {
6143 let tmp = std::env::temp_dir().join("cvkg_test_no_sidecar.bin");
6144 std::fs::write(&tmp, b"data without sidecar").expect("failed to write test file");
6145 let result = load_pipeline_cache_with_integrity_check(&tmp);
6146 assert!(result.is_err(), "missing sidecar must yield Err");
6147 let msg = result.unwrap_err();
6148 assert!(msg.contains("sidecar hash file missing"), "got: {msg}");
6149 let _ = std::fs::remove_file(&tmp);
6150 }
6151
6152 #[test]
6153 fn returns_err_when_sidecar_hash_mismatches() {
6154 let tmp = std::env::temp_dir().join("cvkg_test_bad_hash.bin");
6158 std::fs::write(&tmp, b"original data").expect("failed to write test file");
6159 let hash_path = tmp.with_extension("bin.sha256");
6160 std::fs::write(&hash_path, b"0000000000000000000000000000000000000000000000000000000000000000")
6161 .expect("failed to write hash sidecar");
6162 std::fs::write(&tmp, b"tampered data with extra bytes").expect("failed to write test file");
6164 let result = load_pipeline_cache_with_integrity_check(&tmp);
6165 assert!(result.is_err(), "tampered cache must yield Err");
6166 let msg = result.unwrap_err();
6167 assert!(msg.contains("hash mismatch"), "got: {msg}");
6168 let _ = std::fs::remove_file(&tmp);
6169 let _ = std::fs::remove_file(&hash_path);
6170 }
6171
6172 #[test]
6173 fn sha256_of_known_input() {
6174 let result = compute_sha256(b"abc");
6177 let hex = format!(
6178 "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}\
6179 {:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}\
6180 {:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}\
6181 {:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
6182 result[0], result[1], result[2], result[3],
6183 result[4], result[5], result[6], result[7],
6184 result[8], result[9], result[10], result[11],
6185 result[12], result[13], result[14], result[15],
6186 result[16], result[17], result[18], result[19],
6187 result[20], result[21], result[22], result[23],
6188 result[24], result[25], result[26], result[27],
6189 result[28], result[29], result[30], result[31],
6190 );
6191 assert_eq!(
6192 hex,
6193 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
6194 );
6195 }
6196}
6197
6198#[cfg(test)]
6208mod p1_5_cache_size_tests {
6209 const MIN_SVG_CAPACITY: usize = 512;
6211 const MIN_SVG_TREES_CAPACITY: usize = 512;
6213 const MIN_TEXT_CAPACITY: usize = 8192;
6215
6216 #[test]
6217 fn svg_cache_capacity_meets_benchmark() {
6218 assert!(
6219 MIN_SVG_CAPACITY >= 512,
6220 "SVG cache must be >= 512 to cover 200+ brush strokes"
6221 );
6222 }
6223
6224 #[test]
6225 fn svg_trees_capacity_meets_benchmark() {
6226 assert!(
6227 MIN_SVG_TREES_CAPACITY >= 512,
6228 "SVG trees cache must be >= 512 to cover 150+ unique sprites"
6229 );
6230 }
6231
6232 #[test]
6233 fn text_cache_capacity_meets_benchmark() {
6234 assert!(
6235 MIN_TEXT_CAPACITY >= 8192,
6236 "Text cache must be >= 8192 for typical text-heavy UIs"
6237 );
6238 }
6239}
6240
6241#[cfg(test)]
6246mod p1_10_quality_level_tests {
6247 use super::QualityLevel;
6248
6249 #[test]
6250 fn high_quality_uses_msaa_4x() {
6251 assert_eq!(QualityLevel::High.msaa_sample_count(), 4);
6252 }
6253
6254 #[test]
6255 fn medium_quality_uses_msaa_2x() {
6256 assert_eq!(QualityLevel::Medium.msaa_sample_count(), 2);
6257 }
6258
6259 #[test]
6260 fn low_quality_disables_msaa() {
6261 assert_eq!(QualityLevel::Low.msaa_sample_count(), 1);
6262 }
6263
6264 #[test]
6265 fn default_is_high() {
6266 assert_eq!(QualityLevel::default(), QualityLevel::High);
6267 }
6268
6269 #[test]
6270 fn all_levels_produce_valid_sample_counts() {
6271 for level in [QualityLevel::High, QualityLevel::Medium, QualityLevel::Low] {
6273 let n = level.msaa_sample_count();
6274 assert!(
6275 [1, 2, 4, 8, 16].contains(&n),
6276 "QualityLevel {level:?} produced invalid sample count {n}"
6277 );
6278 }
6279 }
6280}
6281
6282#[cfg(test)]
6287mod p1_7_surface_format_tests {
6288 use super::SurtrRenderer;
6289 use wgpu::TextureFormat;
6290
6291 #[test]
6292 fn empty_list_returns_safe_format() {
6293 let result = SurtrRenderer::select_best_surface_format(&[]);
6296 assert!(
6298 matches!(result, TextureFormat::Rgba8Unorm | TextureFormat::Bgra8Unorm
6299 | TextureFormat::Rgba8UnormSrgb | TextureFormat::Bgra8UnormSrgb),
6300 "empty list should return a known-safe format, got {result:?}"
6301 );
6302 }
6303
6304 #[test]
6305 fn prefers_hdr_format_when_available() {
6306 let formats = [
6308 TextureFormat::Rgba8UnormSrgb,
6309 TextureFormat::Rgba16Float,
6310 TextureFormat::Bgra8UnormSrgb,
6311 ];
6312 let result = SurtrRenderer::select_best_surface_format(&formats);
6313 assert_eq!(result, TextureFormat::Rgba16Float);
6314 }
6315
6316 #[test]
6317 fn prefers_srgb_when_no_hdr() {
6318 let formats = [
6320 TextureFormat::Rgba8Unorm,
6321 TextureFormat::Rgba8UnormSrgb,
6322 TextureFormat::Bgra8UnormSrgb,
6323 ];
6324 let result = SurtrRenderer::select_best_surface_format(&formats);
6325 assert!(
6329 matches!(result, TextureFormat::Rgba8Unorm
6330 | TextureFormat::Rgba8UnormSrgb
6331 | TextureFormat::Bgra8UnormSrgb),
6332 "expected a sRGB or linear format, got {result:?}"
6333 );
6334 }
6335
6336 #[test]
6337 fn falls_back_to_linear_for_mobile_gpu() {
6338 let formats = [
6342 TextureFormat::Rgba8Unorm,
6343 TextureFormat::Bgra8Unorm,
6344 ];
6345 let result = SurtrRenderer::select_best_surface_format(&formats);
6346 assert!(
6348 formats.contains(&result),
6349 "mobile GPU should get a linear format from the list, got {result:?}"
6350 );
6351 }
6352
6353 #[test]
6354 fn exotic_formats_fall_back_safely() {
6355 let formats = [TextureFormat::Rgb9e5Ufloat];
6358 let result = SurtrRenderer::select_best_surface_format(&formats);
6359 assert_eq!(result, TextureFormat::Rgb9e5Ufloat);
6362 }
6363}
6364
6365#[cfg(test)]
6375mod p1_6_particle_ring_buffer_tests {
6376 fn compute_ring_buffer_write(
6379 write_start: usize,
6380 write_count: usize,
6381 max: usize,
6382 ) -> (usize, usize, usize) {
6383 let effective_count = write_count.min(max);
6385 let drop_count = write_count - effective_count;
6386 let first_chunk = (max - write_start).min(effective_count);
6387 if first_chunk < effective_count {
6388 let remaining = effective_count - first_chunk;
6389 (first_chunk, remaining, remaining)
6390 } else {
6391 (first_chunk, 0, (write_start + effective_count) % max)
6392 }
6393 }
6394
6395 #[test]
6396 fn no_wrap_no_overflow() {
6397 let (first, second, head) = compute_ring_buffer_write(0, 10, 100);
6399 assert_eq!(first, 10);
6400 assert_eq!(second, 0);
6401 assert_eq!(head, 10);
6402 }
6403
6404 #[test]
6405 fn wrap_without_overflow() {
6406 let (first, second, head) = compute_ring_buffer_write(80, 50, 100);
6408 assert_eq!(first, 20); assert_eq!(second, 30); assert_eq!(head, 30);
6411 }
6412
6413 #[test]
6414 fn overflow_caps_to_max() {
6415 let (first, second, head) = compute_ring_buffer_write(80, 200, 100);
6417 assert_eq!(first, 20);
6421 assert_eq!(second, 80);
6422 assert_eq!(head, 80);
6423 }
6424
6425 #[test]
6426 fn overflow_at_offset_zero() {
6427 let (first, second, head) = compute_ring_buffer_write(0, 150, 100);
6429 assert_eq!(first, 100);
6433 assert_eq!(second, 0);
6434 assert_eq!(head, 0); }
6436
6437 #[test]
6438 fn empty_write() {
6439 let (first, second, head) = compute_ring_buffer_write(50, 0, 100);
6440 assert_eq!(first, 0);
6441 assert_eq!(second, 0);
6442 assert_eq!(head, 50);
6443 }
6444}
6445
6446
6447#[cfg(test)]
6452mod p1_1_surtr_config_tests {
6453 use crate::subsystems::SurtrConfig;
6454
6455 #[test]
6456 fn default_has_p1_5_cache_sizes() {
6457 let cfg = SurtrConfig::default();
6460 assert_eq!(cfg.text_cache_capacity.get(), 8192);
6461 assert_eq!(cfg.svg_cache_capacity.get(), 512);
6462 assert_eq!(cfg.svg_trees_capacity.get(), 512);
6463 assert_eq!(cfg.shared_elements_capacity.get(), 1024);
6464 assert_eq!(cfg.image_uv_capacity.get(), 256);
6465 assert_eq!(cfg.texture_registry_capacity.get(), 31);
6466 assert_eq!(cfg.mega_heim_width, 4096);
6467 assert_eq!(cfg.mega_heim_height, 4096);
6468 }
6469
6470 #[test]
6471 fn low_vram_uses_smaller_atlas() {
6472 let cfg = SurtrConfig::low_vram();
6475 assert_eq!(cfg.mega_heim_width, 2048);
6476 assert_eq!(cfg.mega_heim_height, 2048);
6477 assert!(
6478 cfg.mega_heim_vram_bytes() < 32 * 1024 * 1024,
6479 "low_vram atlas should fit in 32MB, got {} bytes",
6480 cfg.mega_heim_vram_bytes()
6481 );
6482 }
6483
6484 #[test]
6485 fn high_end_uses_larger_atlas() {
6486 let cfg = SurtrConfig::high_end();
6489 assert_eq!(cfg.mega_heim_width, 8192);
6490 assert_eq!(cfg.mega_heim_height, 8192);
6491 assert!(cfg.mega_heim_vram_bytes() >= 256 * 1024 * 1024);
6492 }
6493
6494 #[test]
6495 fn mega_heim_vram_is_4_bytes_per_pixel() {
6496 let cfg = SurtrConfig::default();
6498 let expected = 4096u64 * 4096 * 4;
6499 assert_eq!(cfg.mega_heim_vram_bytes(), expected);
6500 }
6501
6502 #[test]
6503 fn all_presets_have_nonzero_capacities() {
6504 for (name, cfg) in [
6507 ("default", SurtrConfig::default()),
6508 ("low_vram", SurtrConfig::low_vram()),
6509 ("high_end", SurtrConfig::high_end()),
6510 ] {
6511 assert!(cfg.text_cache_capacity.get() > 0, "{name} text_cache");
6512 assert!(cfg.svg_cache_capacity.get() > 0, "{name} svg_cache");
6513 assert!(cfg.svg_trees_capacity.get() > 0, "{name} svg_trees");
6514 assert!(
6515 cfg.shared_elements_capacity.get() > 0,
6516 "{name} shared_elements"
6517 );
6518 assert!(cfg.image_uv_capacity.get() > 0, "{name} image_uv");
6519 assert!(
6520 cfg.texture_registry_capacity.get() > 0,
6521 "{name} texture_registry"
6522 );
6523 assert!(cfg.mega_heim_width > 0, "{name} mega_heim_width");
6524 assert!(cfg.mega_heim_height > 0, "{name} mega_heim_height");
6525 }
6526 }
6527
6528 #[test]
6529 fn config_is_cloneable_and_debug() {
6530 let cfg = SurtrConfig::default();
6532 let _cloned = cfg.clone();
6533 let _formatted = format!("{cfg:?}");
6534 }
6535
6536 #[test]
6546 fn p1_19_text_subsystem_shaped_cache_clearable() {
6547 let mut subsystem = crate::types::TextSubsystem::forge(
6550 std::num::NonZeroUsize::new(100).unwrap(),
6551 );
6552 assert!(subsystem.shaped_cache.is_empty());
6554 subsystem.shaped_cache.clear();
6555 assert!(subsystem.shaped_cache.is_empty());
6557 }
6558
6559 #[test]
6560 fn p1_19_svg_subsystem_filter_batches_clearable() {
6561 fn _has_clear_method(s: &mut crate::types::SvgSubsystem) {
6570 s.clear_filter_batches();
6571 }
6572 }
6574}
6575
6576#[cfg(test)]
6581mod volumetric_depth_tests {
6582 #[test]
6584 fn volumetric_wgsl_has_depth_bindings() {
6585 let source = include_str!("shaders/volumetric.wgsl");
6586 assert!(
6587 source.contains("depth_texture: texture_depth_2d"),
6588 "volumetric.wgsl must declare single-sample depth texture binding"
6589 );
6590 assert!(
6591 source.contains("depth_texture_msaa: texture_depth_multisampled_2d"),
6592 "volumetric.wgsl must declare multisampled depth texture binding"
6593 );
6594 assert!(
6595 source.contains("depth_sampler: sampler_comparison"),
6596 "volumetric.wgsl must declare comparison sampler binding"
6597 );
6598 }
6599
6600 #[test]
6602 fn volumetric_wgsl_reads_depth_for_occlusion() {
6603 let source = include_str!("shaders/volumetric.wgsl");
6604 assert!(
6605 source.contains("scene_depth"),
6606 "volumetric.wgsl must read scene depth for occlusion"
6607 );
6608 assert!(
6609 source.contains("msaa_count"),
6610 "volumetric.wgsl must use msaa_count to select depth texture"
6611 );
6612 }
6613
6614 #[test]
6616 fn volumetric_uniforms_has_msaa_count() {
6617 let source = include_str!("shaders/volumetric.wgsl");
6618 assert!(
6619 source.contains("msaa_count: f32"),
6620 "VolumetricUniforms must have msaa_count field"
6621 );
6622 }
6623
6624 #[test]
6628 fn depth_texture_usage_includes_texture_binding() {
6629 let usage = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING;
6633 assert!(usage.contains(wgpu::TextureUsages::RENDER_ATTACHMENT));
6634 assert!(usage.contains(wgpu::TextureUsages::TEXTURE_BINDING));
6635 }
6636}