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_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;
21use std::sync::Arc;
22
23pub struct SurtrRenderer {
25 pub(crate) instance: Arc<wgpu::Instance>,
26 pub(crate) adapter: Arc<wgpu::Adapter>,
27 pub(crate) device: Arc<wgpu::Device>,
28 pub(crate) queue: Arc<wgpu::Queue>,
29
30 pub(crate) registry: crate::kvasir::registry::ResourceRegistry,
32
33 pub(crate) active_offscreens: Vec<crate::types::OffscreenEffectConfig>,
34 pub(crate) effect_pipelines: std::collections::HashMap<String, wgpu::RenderPipeline>,
35 pub(crate) effect_params_buffer: wgpu::Buffer,
36 pub(crate) effect_params_bind_group: wgpu::BindGroup,
37 pub(crate) linear_sampler: wgpu::Sampler,
38 pub ai_material_rx: Option<
40 std::sync::mpsc::Receiver<
41 Result<crate::material::CompiledMaterial, crate::ai::GeneratorError>,
42 >,
43 >,
44
45 pub(crate) surfaces: std::collections::HashMap<winit::window::WindowId, SurfaceContext>,
47 pub(crate) current_window: Option<winit::window::WindowId>,
48 pub headless_context: Option<HeadlessContext>,
49
50 pub(crate) text_engine: cvkg_runic_text::RunicTextEngine,
52 pub(crate) mega_heim_tex: wgpu::Texture,
53 pub(crate) mega_heim_bind_group: wgpu::BindGroup,
54 pub(crate) text_cache: LruCache<u64, (Rect, f32, f32, f32, f32)>,
55 pub(crate) shaped_text_cache:
56 std::collections::HashMap<(String, u32), cvkg_runic_text::ShapedText>,
57 pub(crate) heim_packer: SundrPacker,
58 pub(crate) image_uv_registry: LruCache<String, Rect>,
59 pub(crate) texture_registry: LruCache<String, u32>,
60 pub(crate) texture_views: Vec<wgpu::TextureView>,
61 pub(crate) dummy_sampler: wgpu::Sampler,
62 pub(crate) svg_cache: LruCache<String, SvgModel>,
63 pub(crate) svg_trees: LruCache<String, usvg::Tree>,
65 pub(crate) filter_engine: Option<cvkg_svg_filters::FilterEngine>,
67 pub(crate) filter_batches: Vec<cvkg_svg_filters::FilterNode>,
69
70 pub(crate) dummy_texture_bind_group: wgpu::BindGroup,
72 pub(crate) dummy_env_bind_group: wgpu::BindGroup,
73 pub(crate) texture_bind_group_layout: wgpu::BindGroupLayout,
74 pub(crate) texture_bind_groups: Vec<wgpu::BindGroup>,
75 pub(crate) shared_elements: LruCache<String, cvkg_core::Rect>,
76
77 pub(crate) vertex_buffer: wgpu::Buffer,
79 pub(crate) index_buffer: wgpu::Buffer,
80 pub(crate) instance_buffer: wgpu::Buffer,
81 pub(crate) vertices: Vec<Vertex>,
82 pub(crate) indices: Vec<u32>,
83 pub(crate) instance_data: Vec<InstanceData>,
84 pub(crate) staging_belt: wgpu::util::StagingBelt,
85 pub(crate) staging_command_buffers: Vec<wgpu::CommandBuffer>,
86 pub(crate) draw_calls: Vec<DrawCall>,
87 pub(crate) current_texture_id: Option<u32>,
88
89 pub(crate) opacity_stack: Vec<f32>,
91 pub(crate) clip_stack: Vec<Rect>,
92 pub(crate) slice_stack: Vec<(f32, f32)>,
93 pub(crate) shadow_stack: Vec<ShadowState>,
94
95 pub(crate) theme_buffer: wgpu::Buffer,
97 pub(crate) scene_buffer: wgpu::Buffer,
98 pub(crate) berserker_bind_group: wgpu::BindGroup,
99 pub(crate) berserker_bind_group_layout: wgpu::BindGroupLayout,
100 pub(crate) start_time: std::time::Instant,
101 pub(crate) current_theme: ColorTheme,
102 pub(crate) current_scene: SceneUniforms,
103 pub(crate) current_z: f32,
104
105 pub(crate) pipeline: wgpu::RenderPipeline,
107 pub(crate) opaque_pipeline: wgpu::RenderPipeline,
109 pub(crate) ui_pipeline: wgpu::RenderPipeline,
112 pub(crate) glass_pipeline: wgpu::RenderPipeline,
114 pub(crate) background_pipeline: wgpu::RenderPipeline,
115 pub(crate) bloom_extract_pipeline: wgpu::RenderPipeline,
116 pub(crate) copy_pipeline: wgpu::RenderPipeline,
118 pub(crate) composite_pipeline: wgpu::RenderPipeline,
119 pub(crate) color_blind_pipeline: wgpu::RenderPipeline,
121 pub(crate) volumetric_pipeline: wgpu::RenderPipeline,
123 pub(crate) volumetric_bind_group_layout: wgpu::BindGroupLayout,
125 pub(crate) volumetric_uniform_buffer: wgpu::Buffer,
127 pub(crate) kawase_down_pipeline: wgpu::RenderPipeline,
129 pub(crate) kawase_up_pipeline: wgpu::RenderPipeline,
131 pub(crate) kawase_bind_group_layout: wgpu::BindGroupLayout,
133 pub(crate) kawase_uniform: wgpu::Buffer,
135 pub(crate) env_bind_group_layout: wgpu::BindGroupLayout,
137
138 pub telemetry: cvkg_core::TelemetryData,
140
141 pub frame_budget: cvkg_core::FrameBudget,
143 pub(crate) capture_staging_buffer: Option<wgpu::Buffer>,
145 pub last_redraw_start: std::time::Instant,
147 pub last_frame_start: std::time::Instant,
149
150 pub(crate) vram_buffers_bytes: u64,
152 pub(crate) vram_textures_bytes: u64,
153
154 pub(crate) _debug_layout: bool,
156
157 pub(crate) transform_stack: Vec<glam::Mat3>,
159 pub redraw_requested: bool,
161 pub(crate) compositor_index_cursor: u32,
163
164 pub bloom_enabled: bool,
166 pub volumetric_enabled: bool,
168 pub(crate) color_blind_bind_group_layout: wgpu::BindGroupLayout,
170 pub(crate) color_blind_uniform_buffer: wgpu::Buffer,
172 pub color_blind_mode: crate::color_blindness::ColorBlindMode,
174 pub color_blind_intensity: f32,
176 pub(crate) sampler: wgpu::Sampler,
178
179 pub(crate) skuld_queries: Option<wgpu::QuerySet>,
181 pub(crate) skuld_buffer: Option<wgpu::Buffer>,
182 pub(crate) skuld_read_buffer: Option<wgpu::Buffer>,
183 pub(crate) skuld_period: f32,
184 pub last_gpu_time_ns: u64,
185
186 pub(crate) vnode_stack: Vec<(Rect, &'static str)>,
188
189 pub(crate) event_handlers: std::collections::HashMap<
192 String,
193 Vec<std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>>,
194 >,
195
196 pub(crate) glass_output_bind_group_layout: wgpu::BindGroupLayout,
198 pub(crate) current_draw_material: cvkg_core::DrawMaterial,
200
201 pub(crate) portal_regions: std::collections::VecDeque<cvkg_core::Rect>,
204
205 pub(crate) cached_graph_plan: Option<kvasir::graph_cache::CachedGraphPlan>,
208 pub(crate) memo_cache: std::collections::HashMap<u64, u64>,
211 pub(crate) bind_group_cache: std::sync::Mutex<
214 std::collections::HashMap<
215 (crate::kvasir::resource::ResourceId, u32, bool),
216 wgpu::BindGroup,
217 >,
218 >,
219 pub(crate) texture_view_cache: std::sync::Mutex<
222 std::collections::HashMap<(crate::kvasir::resource::ResourceId, u32), wgpu::TextureView>,
223 >,
224}
225
226#[cfg(target_arch = "wasm32")]
227unsafe impl Send for SurtrRenderer {}
228#[cfg(target_arch = "wasm32")]
229unsafe impl Sync for SurtrRenderer {}
230
231pub(crate) struct TessellateParams<'a> {
233 fill_tessellator: &'a mut FillTessellator,
234 stroke_tessellator: &'a mut StrokeTessellator,
235 vertices: &'a mut Vec<Vertex>,
236 indices: &'a mut Vec<u32>,
237 parsed_animations: &'a [SvgAnimation],
238 finalized_animations: &'a mut Vec<SvgAnimation>,
239}
240
241impl SurtrRenderer {
242 pub fn update_mouse(&mut self, mouse: [f32; 2], velocity: [f32; 2]) {
248 self.current_scene.mouse = mouse;
249 self.current_scene.mouse_velocity = velocity;
250 }
251
252 pub(crate) fn select_best_surface_format(
256 formats: &[wgpu::TextureFormat],
257 ) -> wgpu::TextureFormat {
258 let preferred_formats = [
259 wgpu::TextureFormat::Rgba16Float, wgpu::TextureFormat::Rgba8Unorm, wgpu::TextureFormat::Bgra8UnormSrgb,
262 wgpu::TextureFormat::Rgba8UnormSrgb,
263 ];
264 for preferred in &preferred_formats {
265 if formats.contains(preferred) {
266 return *preferred;
267 }
268 }
269 formats[0]
270 }
271
272 pub async fn forge(window: Arc<winit::window::Window>) -> Self {
279 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
280 backends: wgpu::Backends::all(),
281 flags: wgpu::InstanceFlags::default(),
282 backend_options: wgpu::BackendOptions::default(),
283 display: None,
284 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
285 });
286
287 let surface = instance
288 .create_surface(window.clone())
289 .expect("Failed to create surface");
290
291 log::info!("[GPU] Requesting HighPerformance adapter...");
293
294 let mut adapter = None;
295
296 #[cfg(not(target_arch = "wasm32"))]
297 if let Ok(filter) = std::env::var("WGPU_ADAPTER_NAME") {
298 let adapters = instance.enumerate_adapters(wgpu::Backends::all()).await;
299 log::info!("[GPU] Available adapters:");
300 for a in &adapters {
301 let info = a.get_info();
302 log::info!(
303 " - Name: '{}' | Driver: '{}' | Backend: {:?}",
304 info.name,
305 info.driver,
306 info.backend
307 );
308 }
309
310 adapter = adapters.into_iter().find(|a| {
311 let info = a.get_info();
312 let match_found = info.name.to_lowercase().contains(&filter.to_lowercase())
313 || info.driver.to_lowercase().contains(&filter.to_lowercase());
314 if match_found {
315 log::info!(
316 "[GPU] Manual selection match: {} | Driver: {}",
317 info.name,
318 info.driver
319 );
320 }
321 match_found
322 });
323
324 if adapter.is_some() {
325 log::info!(
326 "[GPU] Forced adapter selection via WGPU_ADAPTER_NAME='{}'",
327 filter
328 );
329 } else {
330 log::warn!(
331 "[GPU] WGPU_ADAPTER_NAME='{}' provided but no matching adapter found. Falling back...",
332 filter
333 );
334 }
335 }
336
337 if adapter.is_none() {
338 adapter = instance
339 .request_adapter(&wgpu::RequestAdapterOptions {
340 power_preference: wgpu::PowerPreference::HighPerformance,
341 compatible_surface: Some(&surface),
342 force_fallback_adapter: false,
343 })
344 .await
345 .ok();
346 }
347
348 if adapter.is_none() {
349 log::warn!(
350 "[GPU] HighPerformance adapter failed (possible Bumblebee/Optimus), trying LowPower..."
351 );
352 adapter = instance
353 .request_adapter(&wgpu::RequestAdapterOptions {
354 power_preference: wgpu::PowerPreference::LowPower,
355 compatible_surface: Some(&surface),
356 force_fallback_adapter: false,
357 })
358 .await
359 .ok();
360 }
361
362 if adapter.is_none() {
363 log::warn!("[GPU] Hardware adapters failed, trying Software fallback...");
364 adapter = instance
365 .request_adapter(&wgpu::RequestAdapterOptions {
366 power_preference: wgpu::PowerPreference::LowPower,
367 compatible_surface: Some(&surface),
368 force_fallback_adapter: true,
369 })
370 .await
371 .ok();
372 }
373
374 let adapter = adapter.expect("Failed to find a suitable GPU for Surtr");
375 let info = adapter.get_info();
376 log::info!(
377 "[GPU] Selected adapter: {} ({:?}) on backend: {:?}",
378 info.name,
379 info.device_type,
380 info.backend
381 );
382 log::info!("[GPU] Driver info: {} - {}", info.driver, info.driver_info);
383 let supports_timestamps = adapter.features().contains(wgpu::Features::TIMESTAMP_QUERY);
384 #[cfg(not(target_arch = "wasm32"))]
385 let mut required_features =
386 wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
387 | wgpu::Features::TEXTURE_BINDING_ARRAY;
388
389 #[cfg(target_arch = "wasm32")]
390 let mut required_features = wgpu::Features::empty(); if supports_timestamps {
392 required_features |= wgpu::Features::TIMESTAMP_QUERY;
393 }
394 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
396 {
397 log::info!("[GPU] Validation layer enabled (debug build)");
398 }
399
400 let (device, queue) = adapter
401 .request_device(&wgpu::DeviceDescriptor {
402 label: Some("Surtr Forge"),
403 required_features,
404 required_limits: wgpu::Limits {
405 max_bindings_per_bind_group: 256,
406 max_binding_array_elements_per_shader_stage: 256,
407 ..wgpu::Limits::default()
408 },
409 memory_hints: wgpu::MemoryHints::default(),
410 experimental_features: wgpu::ExperimentalFeatures::disabled(),
411 trace: wgpu::Trace::Off,
412 })
413 .await
414 .expect("Failed to create Surtr device");
415
416 let instance = Arc::new(instance);
417 let adapter = Arc::new(adapter);
418
419 device.on_uncaptured_error(Arc::new(|error| {
420 log::error!(
421 "[GPU] Uncaptured device error (Device Lost or Panic): {:?}",
422 error
423 );
424 }));
426
427 let device = Arc::new(device);
428 let queue = Arc::new(queue);
429
430 let size = window.inner_size();
431 let width = if size.width > 0 { size.width } else { 1280 };
433 let height = if size.height > 0 { size.height } else { 720 };
434 let surface_caps = surface.get_capabilities(&adapter);
435 let surface_format = if surface_caps.formats.is_empty() {
436 log::error!("[GPU] CRITICAL: No compatible surface formats found for this adapter!");
437 log::error!(
438 "[GPU] Adapter: {} | Backend: {:?}",
439 adapter.get_info().name,
440 adapter.get_info().backend
441 );
442 wgpu::TextureFormat::Rgba8UnormSrgb
444 } else {
445 surface_caps
446 .formats
447 .iter()
448 .find(|f| f.is_srgb())
449 .copied()
450 .unwrap_or(surface_caps.formats[0])
451 };
452
453 let present_mode = if surface_caps
455 .present_modes
456 .contains(&wgpu::PresentMode::Mailbox)
457 {
458 wgpu::PresentMode::Mailbox
459 } else {
460 log::warn!("[GPU] Mailbox not supported, falling back to Fifo (V-Sync)");
461 wgpu::PresentMode::Fifo
462 };
463
464 let alpha_mode = if surface_caps
465 .alpha_modes
466 .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
467 {
468 wgpu::CompositeAlphaMode::PostMultiplied
469 } else if surface_caps
470 .alpha_modes
471 .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
472 {
473 wgpu::CompositeAlphaMode::PreMultiplied
474 } else {
475 surface_caps.alpha_modes[0]
476 };
477
478 log::info!(
479 "[GPU] Configuring surface: {}x{} | {:?} | {:?}",
480 width,
481 height,
482 present_mode,
483 alpha_mode
484 );
485
486 let config = wgpu::SurfaceConfiguration {
487 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
488 format: surface_format,
489 width,
490 height,
491 present_mode,
492 alpha_mode,
493 view_formats: vec![],
494 desired_maximum_frame_latency: 2,
495 };
496 surface.configure(&device, &config);
497 log::info!("[GPU] Surface configuration successful.");
498
499 let renderer = Self::forge_internal(
500 instance,
501 adapter,
502 device,
503 queue,
504 Some((window, surface, config)),
505 None,
506 )
507 .await;
508 log::info!("[GPU] Forge internal complete.");
509 renderer
510 }
511
512 pub(crate) async fn forge_internal(
522 instance: Arc<wgpu::Instance>,
523 adapter: Arc<wgpu::Adapter>,
524 device: Arc<wgpu::Device>,
525 queue: Arc<wgpu::Queue>,
526 surface_info: Option<(
527 Arc<winit::window::Window>,
528 wgpu::Surface<'static>,
529 wgpu::SurfaceConfiguration,
530 )>,
531 headless_info: Option<(u32, u32, wgpu::TextureFormat)>,
532 ) -> Self {
533 let format = if let Some((_, _, ref config)) = surface_info {
534 config.format
535 } else if let Some((_, _, f)) = headless_info {
536 f
537 } else {
538 wgpu::TextureFormat::Rgba8UnormSrgb
539 };
540
541 let supports_timestamps = adapter.features().contains(wgpu::Features::TIMESTAMP_QUERY);
542 let skuld_period = queue.get_timestamp_period();
543 let (skuld_queries, skuld_buffer, skuld_read_buffer) = if supports_timestamps {
544 let q = device.create_query_set(&wgpu::QuerySetDescriptor {
545 label: Some("Skuld Timestamp Queries"),
546 count: 2,
547 ty: wgpu::QueryType::Timestamp,
548 });
549 let b = device.create_buffer(&wgpu::BufferDescriptor {
550 label: Some("Skuld Query Buffer"),
551 size: 16,
552 usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
553 mapped_at_creation: false,
554 });
555 let rb = device.create_buffer(&wgpu::BufferDescriptor {
556 label: Some("Skuld Read Buffer"),
557 size: 16,
558 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
559 mapped_at_creation: false,
560 });
561 (Some(q), Some(b), Some(rb))
562 } else {
563 (None, None, None)
564 };
565
566 let materials_generated = crate::material::generate_builtins_wgsl();
568
569 let wgsl_src = format!(
570 "{}{}{}{}{}{}",
571 WGSL_COMMON,
572 WGSL_SHAPES,
573 WGSL_BIFROST,
574 WGSL_BLOOM,
575 WGSL_COLOR_BLIND,
576 materials_generated
577 );
578 let wgsl_opaque = format!(
579 "{}{}{}{}{}{}",
580 WGSL_COMMON,
581 WGSL_MATERIAL_OPAQUE,
582 WGSL_BIFROST,
583 WGSL_BLOOM,
584 WGSL_COLOR_BLIND,
585 materials_generated
586 );
587 let wgsl_glass = format!(
588 "{}{}{}{}{}{}",
589 WGSL_COMMON,
590 WGSL_MATERIAL_GLASS,
591 WGSL_BIFROST,
592 WGSL_BLOOM,
593 WGSL_COLOR_BLIND,
594 materials_generated
595 );
596
597 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
598 label: Some("Surtr Main Shader"),
599 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(wgsl_src)),
600 });
601
602 let texture_bind_group_layout =
604 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
605 entries: &[
606 wgpu::BindGroupLayoutEntry {
607 binding: 0,
608 visibility: wgpu::ShaderStages::FRAGMENT,
609 ty: wgpu::BindingType::Texture {
610 multisampled: false,
611 view_dimension: wgpu::TextureViewDimension::D2,
612 sample_type: wgpu::TextureSampleType::Float { filterable: true },
613 },
614 count: std::num::NonZeroU32::new(256),
615 },
616 wgpu::BindGroupLayoutEntry {
617 binding: 1,
618 visibility: wgpu::ShaderStages::FRAGMENT,
619 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
620 count: None,
621 },
622 ],
623 label: Some("Niflheim Texture Bind Group Layout"),
624 });
625
626 let env_bind_group_layout =
629 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
630 entries: &[
631 wgpu::BindGroupLayoutEntry {
632 binding: 0,
633 visibility: wgpu::ShaderStages::FRAGMENT,
634 ty: wgpu::BindingType::Texture {
635 multisampled: false,
636 view_dimension: wgpu::TextureViewDimension::D2,
637 sample_type: wgpu::TextureSampleType::Float { filterable: true },
638 },
639 count: None,
640 },
641 wgpu::BindGroupLayoutEntry {
642 binding: 1,
643 visibility: wgpu::ShaderStages::FRAGMENT,
644 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
645 count: None,
646 },
647 ],
648 label: Some("Surtr Environment Bind Group Layout"),
649 });
650
651 let berserker_bind_group_layout =
652 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
653 entries: &[
654 wgpu::BindGroupLayoutEntry {
655 binding: 0,
656 visibility: wgpu::ShaderStages::FRAGMENT,
657 ty: wgpu::BindingType::Buffer {
658 ty: wgpu::BufferBindingType::Uniform,
659 has_dynamic_offset: false,
660 min_binding_size: None,
661 },
662 count: None,
663 },
664 wgpu::BindGroupLayoutEntry {
665 binding: 1,
666 visibility: wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::VERTEX,
667 ty: wgpu::BindingType::Buffer {
668 ty: wgpu::BufferBindingType::Uniform,
669 has_dynamic_offset: false,
670 min_binding_size: None,
671 },
672 count: None,
673 },
674 ],
675 label: Some("Surtr Berserker Bind Group Layout"),
676 });
677
678 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
680 label: Some("Surtr Main Pipeline Layout"),
681 bind_group_layouts: &[
682 Some(&texture_bind_group_layout),
683 Some(&env_bind_group_layout),
684 Some(&berserker_bind_group_layout),
685 ],
686 immediate_size: 0,
687 });
688
689 let post_process_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
691 label: Some("Muspelheim Post Process Layout"),
692 bind_group_layouts: &[
693 Some(&texture_bind_group_layout),
694 Some(&env_bind_group_layout),
695 Some(&berserker_bind_group_layout),
696 ],
697 immediate_size: 0,
698 });
699
700 let composite_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
702 label: Some("Muspelheim Composite Layout"),
703 bind_group_layouts: &[
704 Some(&texture_bind_group_layout),
705 Some(&env_bind_group_layout),
706 Some(&berserker_bind_group_layout),
707 ],
708 immediate_size: 0,
709 });
710
711 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
712 label: Some("Surtr Main Pipeline"),
713 layout: Some(&pipeline_layout),
714 vertex: wgpu::VertexState {
715 module: &shader,
716 entry_point: Some("vs_main"),
717 buffers: &[Vertex::desc(), InstanceData::desc()],
718 compilation_options: wgpu::PipelineCompilationOptions::default(),
719 },
720 fragment: Some(wgpu::FragmentState {
721 module: &shader,
722 entry_point: Some("fs_main"),
723 targets: &[Some(wgpu::ColorTargetState {
724 format: wgpu::TextureFormat::Rgba16Float,
725 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
726 write_mask: wgpu::ColorWrites::ALL,
727 })],
728 compilation_options: wgpu::PipelineCompilationOptions::default(),
729 }),
730 primitive: wgpu::PrimitiveState::default(),
731 depth_stencil: Some(wgpu::DepthStencilState {
732 format: wgpu::TextureFormat::Depth32Float,
733 depth_write_enabled: Some(true),
734 depth_compare: Some(wgpu::CompareFunction::LessEqual),
735 stencil: wgpu::StencilState::default(),
736 bias: wgpu::DepthBiasState::default(),
737 }),
738 multisample: wgpu::MultisampleState {
739 count: 4,
740 mask: !0,
741 alpha_to_coverage_enabled: false,
742 },
743 multiview_mask: None,
744 cache: None,
745 });
746
747 let background_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
748 label: Some("Surtr Background Pipeline"),
749 layout: Some(&pipeline_layout),
750 vertex: wgpu::VertexState {
751 module: &shader,
752 entry_point: Some("vs_fullscreen"),
753 buffers: &[],
754 compilation_options: wgpu::PipelineCompilationOptions::default(),
755 },
756 fragment: Some(wgpu::FragmentState {
757 module: &shader,
758 entry_point: Some("fs_background"),
759 targets: &[Some(wgpu::ColorTargetState {
760 format: wgpu::TextureFormat::Rgba16Float,
761 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
762 write_mask: wgpu::ColorWrites::ALL,
763 })],
764 compilation_options: wgpu::PipelineCompilationOptions::default(),
765 }),
766 primitive: wgpu::PrimitiveState::default(),
767 depth_stencil: Some(wgpu::DepthStencilState {
768 format: wgpu::TextureFormat::Depth32Float,
769 depth_write_enabled: Some(false),
770 depth_compare: Some(wgpu::CompareFunction::Always),
771 stencil: wgpu::StencilState::default(),
772 bias: wgpu::DepthBiasState::default(),
773 }),
774 multisample: wgpu::MultisampleState {
775 count: 4,
776 mask: !0,
777 alpha_to_coverage_enabled: false,
778 },
779 multiview_mask: None,
780 cache: None,
781 });
782
783 let opaque_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
785 label: Some("Muspelheim Opaque"),
786 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(wgsl_opaque)),
787 });
788 let glass_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
789 label: Some("Muspelheim Glass"),
790 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(wgsl_glass)),
791 });
792
793 let opaque_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
794 label: Some("Muspelheim Opaque"),
795 layout: Some(&pipeline_layout),
796 vertex: wgpu::VertexState {
797 module: &opaque_shader,
798 entry_point: Some("vs_main"),
799 buffers: &[Vertex::desc(), InstanceData::desc()],
800 compilation_options: wgpu::PipelineCompilationOptions::default(),
801 },
802 fragment: Some(wgpu::FragmentState {
803 module: &opaque_shader,
804 entry_point: Some("fs_main"),
805 targets: &[Some(wgpu::ColorTargetState {
806 format: wgpu::TextureFormat::Rgba16Float,
807 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
808 write_mask: wgpu::ColorWrites::ALL,
809 })],
810 compilation_options: wgpu::PipelineCompilationOptions::default(),
811 }),
812 primitive: wgpu::PrimitiveState::default(),
813 depth_stencil: Some(wgpu::DepthStencilState {
814 format: wgpu::TextureFormat::Depth32Float,
815 depth_write_enabled: Some(true),
816 depth_compare: Some(wgpu::CompareFunction::LessEqual),
817 stencil: wgpu::StencilState::default(),
818 bias: wgpu::DepthBiasState::default(),
819 }),
820 multisample: wgpu::MultisampleState {
821 count: 4,
822 mask: !0,
823 alpha_to_coverage_enabled: false,
824 },
825 multiview_mask: None,
826 cache: None,
827 });
828 let ui_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
829 label: Some("Muspelheim UI"),
830 layout: Some(&pipeline_layout),
831 vertex: wgpu::VertexState {
832 module: &opaque_shader,
833 entry_point: Some("vs_main"),
834 buffers: &[Vertex::desc(), InstanceData::desc()],
835 compilation_options: wgpu::PipelineCompilationOptions::default(),
836 },
837 fragment: Some(wgpu::FragmentState {
838 module: &opaque_shader,
839 entry_point: Some("fs_main"),
840 targets: &[Some(wgpu::ColorTargetState {
841 format: wgpu::TextureFormat::Rgba16Float,
842 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
843 write_mask: wgpu::ColorWrites::ALL,
844 })],
845 compilation_options: wgpu::PipelineCompilationOptions::default(),
846 }),
847 primitive: wgpu::PrimitiveState::default(),
848 depth_stencil: None,
849 multisample: wgpu::MultisampleState {
850 count: 1,
851 mask: !0,
852 alpha_to_coverage_enabled: false,
853 },
854 multiview_mask: None,
855 cache: None,
856 });
857 let glass_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
858 label: Some("Muspelheim Glass"),
859 layout: Some(&pipeline_layout),
860 vertex: wgpu::VertexState {
861 module: &opaque_shader,
862 entry_point: Some("vs_main"),
863 buffers: &[Vertex::desc(), InstanceData::desc()],
864 compilation_options: wgpu::PipelineCompilationOptions::default(),
865 },
866 fragment: Some(wgpu::FragmentState {
867 module: &glass_shader,
868 entry_point: Some("fs_main"),
869 targets: &[Some(wgpu::ColorTargetState {
870 format: wgpu::TextureFormat::Rgba16Float,
871 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
872 write_mask: wgpu::ColorWrites::ALL,
873 })],
874 compilation_options: wgpu::PipelineCompilationOptions::default(),
875 }),
876 primitive: wgpu::PrimitiveState::default(),
877 depth_stencil: None,
878 multisample: wgpu::MultisampleState {
879 count: 1,
880 mask: !0,
881 alpha_to_coverage_enabled: false,
882 },
883 multiview_mask: None,
884 cache: None,
885 });
886
887 let bloom_extract_pipeline =
889 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
890 label: Some("Muspelheim Bloom Extract"),
891 layout: Some(&post_process_layout),
892 vertex: wgpu::VertexState {
893 module: &shader,
894 entry_point: Some("vs_fullscreen"),
895 buffers: &[],
896 compilation_options: wgpu::PipelineCompilationOptions::default(),
897 },
898 fragment: Some(wgpu::FragmentState {
899 module: &shader,
900 entry_point: Some("fs_bloom_extract"),
901 targets: &[Some(wgpu::ColorTargetState {
902 format,
903 blend: None,
904 write_mask: wgpu::ColorWrites::ALL,
905 })],
906 compilation_options: wgpu::PipelineCompilationOptions::default(),
907 }),
908 primitive: wgpu::PrimitiveState::default(),
909 depth_stencil: None,
910 multisample: wgpu::MultisampleState::default(),
911 multiview_mask: None,
912 cache: None,
913 });
914
915 let copy_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
917 label: Some("Muspelheim Copy"),
918 layout: Some(&post_process_layout),
919 vertex: wgpu::VertexState {
920 module: &shader,
921 entry_point: Some("vs_fullscreen"),
922 buffers: &[],
923 compilation_options: wgpu::PipelineCompilationOptions::default(),
924 },
925 fragment: Some(wgpu::FragmentState {
926 module: &shader,
927 entry_point: Some("fs_copy"),
928 targets: &[Some(wgpu::ColorTargetState {
929 format,
930 blend: None,
931 write_mask: wgpu::ColorWrites::ALL,
932 })],
933 compilation_options: wgpu::PipelineCompilationOptions::default(),
934 }),
935 primitive: wgpu::PrimitiveState::default(),
936 depth_stencil: None,
937 multisample: wgpu::MultisampleState::default(),
938 multiview_mask: None,
939 cache: None,
940 });
941
942 let kawase_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
947 label: Some("Kawase Blur Pyramid"),
948 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
949 "shaders/blur_pyramid.wgsl"
950 ))),
951 });
952 let kawase_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
953 label: Some("Kawase Blur BGL"),
954 entries: &[
955 wgpu::BindGroupLayoutEntry {
956 binding: 0,
957 visibility: wgpu::ShaderStages::FRAGMENT,
958 ty: wgpu::BindingType::Buffer {
959 ty: wgpu::BufferBindingType::Uniform,
960 has_dynamic_offset: false,
961 min_binding_size: wgpu::BufferSize::new(32),
962 },
963 count: None,
964 },
965 wgpu::BindGroupLayoutEntry {
966 binding: 1,
967 visibility: wgpu::ShaderStages::FRAGMENT,
968 ty: wgpu::BindingType::Texture {
969 sample_type: wgpu::TextureSampleType::Float { filterable: true },
970 view_dimension: wgpu::TextureViewDimension::D2,
971 multisampled: false,
972 },
973 count: None,
974 },
975 wgpu::BindGroupLayoutEntry {
976 binding: 2,
977 visibility: wgpu::ShaderStages::FRAGMENT,
978 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
979 count: None,
980 },
981 ],
982 });
983 let kawase_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
984 label: Some("Kawase Pipeline Layout"),
985 bind_group_layouts: &[Some(&kawase_bgl)],
986 immediate_size: 0,
987 });
988 let kawase_down_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
989 label: Some("Kawase Downsample"),
990 layout: Some(&kawase_layout),
991 vertex: wgpu::VertexState {
992 module: &kawase_shader,
993 entry_point: Some("vs_blur"),
994 buffers: &[],
995 compilation_options: wgpu::PipelineCompilationOptions::default(),
996 },
997 fragment: Some(wgpu::FragmentState {
998 module: &kawase_shader,
999 entry_point: Some("fs_kawase_down"),
1000 targets: &[Some(wgpu::ColorTargetState {
1001 format,
1002 blend: None,
1003 write_mask: wgpu::ColorWrites::ALL,
1004 })],
1005 compilation_options: wgpu::PipelineCompilationOptions::default(),
1006 }),
1007 primitive: wgpu::PrimitiveState::default(),
1008 depth_stencil: None,
1009 multisample: wgpu::MultisampleState::default(),
1010 multiview_mask: None,
1011 cache: None,
1012 });
1013 let kawase_up_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1014 label: Some("Kawase Upsample"),
1015 layout: Some(&kawase_layout),
1016 vertex: wgpu::VertexState {
1017 module: &kawase_shader,
1018 entry_point: Some("vs_blur"),
1019 buffers: &[],
1020 compilation_options: wgpu::PipelineCompilationOptions::default(),
1021 },
1022 fragment: Some(wgpu::FragmentState {
1023 module: &kawase_shader,
1024 entry_point: Some("fs_kawase_up"),
1025 targets: &[Some(wgpu::ColorTargetState {
1026 format,
1027 blend: None,
1028 write_mask: wgpu::ColorWrites::ALL,
1029 })],
1030 compilation_options: wgpu::PipelineCompilationOptions::default(),
1031 }),
1032 primitive: wgpu::PrimitiveState::default(),
1033 depth_stencil: None,
1034 multisample: wgpu::MultisampleState::default(),
1035 multiview_mask: None,
1036 cache: None,
1037 });
1038
1039 let composite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1041 label: Some("Muspelheim Composite"),
1042 layout: Some(&composite_layout),
1043 vertex: wgpu::VertexState {
1044 module: &shader,
1045 entry_point: Some("vs_fullscreen"),
1046 buffers: &[],
1047 compilation_options: wgpu::PipelineCompilationOptions::default(),
1048 },
1049 fragment: Some(wgpu::FragmentState {
1050 module: &shader,
1051 entry_point: Some("fs_composite"),
1052 targets: &[Some(wgpu::ColorTargetState {
1053 format,
1054 blend: Some(wgpu::BlendState {
1056 color: wgpu::BlendComponent {
1057 src_factor: wgpu::BlendFactor::One,
1058 dst_factor: wgpu::BlendFactor::One,
1059 operation: wgpu::BlendOperation::Add,
1060 },
1061 alpha: wgpu::BlendComponent {
1062 src_factor: wgpu::BlendFactor::SrcAlpha,
1063 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
1064 operation: wgpu::BlendOperation::Add,
1065 },
1066 }),
1067 write_mask: wgpu::ColorWrites::ALL,
1068 })],
1069 compilation_options: wgpu::PipelineCompilationOptions::default(),
1070 }),
1071 primitive: wgpu::PrimitiveState::default(),
1072 depth_stencil: None,
1073 multisample: wgpu::MultisampleState::default(),
1074 multiview_mask: None,
1075 cache: None,
1076 });
1077
1078 let mega_heim_tex = device.create_texture(&wgpu::TextureDescriptor {
1080 label: Some("Surtr Mega-Heim"),
1081 size: wgpu::Extent3d {
1082 width: 4096,
1083 height: 4096,
1084 depth_or_array_layers: 1,
1085 },
1086 mip_level_count: 1,
1087 sample_count: 1,
1088 dimension: wgpu::TextureDimension::D2,
1089 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1090 usage: wgpu::TextureUsages::TEXTURE_BINDING
1091 | wgpu::TextureUsages::COPY_DST
1092 | wgpu::TextureUsages::COPY_SRC,
1093 view_formats: &[],
1094 });
1095 let mega_heim_view_obj = mega_heim_tex.create_view(&wgpu::TextureViewDescriptor::default());
1096 let text_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1097 address_mode_u: wgpu::AddressMode::ClampToEdge,
1098 address_mode_v: wgpu::AddressMode::ClampToEdge,
1099 mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear,
1101 ..Default::default()
1102 });
1103
1104 let dummy_size = wgpu::Extent3d {
1106 width: 1,
1107 height: 1,
1108 depth_or_array_layers: 1,
1109 };
1110 let dummy_texture = device.create_texture(&wgpu::TextureDescriptor {
1111 label: Some("Niflheim Dummy Texture"),
1112 size: dummy_size,
1113 mip_level_count: 1,
1114 sample_count: 1,
1115 dimension: wgpu::TextureDimension::D2,
1116 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1117 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1118 view_formats: &[],
1119 });
1120 queue.write_texture(
1121 wgpu::TexelCopyTextureInfo {
1122 texture: &dummy_texture,
1123 mip_level: 0,
1124 origin: wgpu::Origin3d::ZERO,
1125 aspect: wgpu::TextureAspect::All,
1126 },
1127 &[255, 255, 255, 255],
1128 wgpu::TexelCopyBufferLayout {
1129 offset: 0,
1130 bytes_per_row: Some(4),
1131 rows_per_image: Some(1),
1132 },
1133 dummy_size,
1134 );
1135
1136 let dummy_view = dummy_texture.create_view(&wgpu::TextureViewDescriptor::default());
1137 let dummy_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1138 address_mode_u: wgpu::AddressMode::ClampToEdge,
1139 address_mode_v: wgpu::AddressMode::ClampToEdge,
1140 address_mode_w: wgpu::AddressMode::ClampToEdge,
1141 mag_filter: wgpu::FilterMode::Linear,
1142 min_filter: wgpu::FilterMode::Nearest,
1143 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
1144 ..Default::default()
1145 });
1146
1147 let mut texture_views_list: Vec<wgpu::TextureView> =
1148 (0..256).map(|_| dummy_view.clone()).collect();
1149 texture_views_list[0] = mega_heim_view_obj.clone();
1150
1151 let views_refs: Vec<&wgpu::TextureView> = texture_views_list.iter().collect();
1152 let mega_heim_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1153 layout: &texture_bind_group_layout,
1154 entries: &[
1155 wgpu::BindGroupEntry {
1156 binding: 0,
1157 resource: wgpu::BindingResource::TextureViewArray(&views_refs),
1158 },
1159 wgpu::BindGroupEntry {
1160 binding: 1,
1161 resource: wgpu::BindingResource::Sampler(&text_sampler),
1162 },
1163 ],
1164 label: Some("Mega-Heim Bind Group"),
1165 });
1166
1167 let dummy_views_refs: Vec<&wgpu::TextureView> = (0..256).map(|_| &dummy_view).collect();
1168 let dummy_texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1169 layout: &texture_bind_group_layout,
1170 entries: &[
1171 wgpu::BindGroupEntry {
1172 binding: 0,
1173 resource: wgpu::BindingResource::TextureViewArray(&dummy_views_refs),
1174 },
1175 wgpu::BindGroupEntry {
1176 binding: 1,
1177 resource: wgpu::BindingResource::Sampler(&dummy_sampler),
1178 },
1179 ],
1180 label: Some("Dummy Texture Bind Group"),
1181 });
1182
1183 let dummy_env_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1184 layout: &env_bind_group_layout,
1185 entries: &[
1186 wgpu::BindGroupEntry {
1187 binding: 0,
1188 resource: wgpu::BindingResource::TextureView(&dummy_view),
1189 },
1190 wgpu::BindGroupEntry {
1191 binding: 1,
1192 resource: wgpu::BindingResource::Sampler(&dummy_sampler),
1193 },
1194 ],
1195 label: Some("Dummy Env Bind Group"),
1196 });
1197
1198 let mut texture_registry = LruCache::new(NonZeroUsize::new(255).unwrap());
1199 let mut texture_bind_groups = Vec::new();
1200
1201 texture_registry.put("__mega_heim".to_string(), 0);
1202 texture_bind_groups.push(mega_heim_bind_group.clone());
1203
1204 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1206 label: Some("Surtr Vertex Anvil"),
1207 size: (MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64,
1208 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1209 mapped_at_creation: false,
1210 });
1211
1212 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1213 label: Some("Surtr Index Anvil"),
1214 size: (MAX_INDICES * std::mem::size_of::<u32>()) as u64,
1215 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1216 mapped_at_creation: false,
1217 });
1218 let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1219 label: Some("Surtr Instance Anvil"),
1220 size: (MAX_VERTICES / 4 * std::mem::size_of::<InstanceData>()) as u64,
1221 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1222 mapped_at_creation: false,
1223 });
1224
1225 let current_theme = ColorTheme::default();
1227 use wgpu::util::DeviceExt;
1228 let theme_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1229 label: Some("Surtr Theme Buffer"),
1230 contents: bytemuck::bytes_of(¤t_theme),
1231 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1232 });
1233
1234 let (width, height, scale_factor) = if let Some((ref window, _, ref config)) = surface_info
1235 {
1236 (config.width, config.height, window.scale_factor() as f32)
1237 } else if let Some((w, h, _)) = headless_info {
1238 (w, h, 1.0)
1239 } else {
1240 (1280, 720, 1.0)
1241 };
1242
1243 let mut current_scene =
1244 SceneUniforms::new(width as f32 / scale_factor, height as f32 / scale_factor);
1245 current_scene.scale_factor = scale_factor;
1246 let scene_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1247 label: Some("Surtr Scene Buffer"),
1248 contents: bytemuck::bytes_of(¤t_scene),
1249 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1250 });
1251
1252 let berserker_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1253 layout: &berserker_bind_group_layout,
1254 entries: &[
1255 wgpu::BindGroupEntry {
1256 binding: 0,
1257 resource: theme_buffer.as_entire_binding(),
1258 },
1259 wgpu::BindGroupEntry {
1260 binding: 1,
1261 resource: scene_buffer.as_entire_binding(),
1262 },
1263 ],
1264 label: Some("Surtr Berserker Bind Group"),
1265 });
1266
1267 let mut registry = crate::kvasir::registry::ResourceRegistry::new();
1268 let mut surfaces = std::collections::HashMap::new();
1269 let mut current_window = None;
1270 let mut headless_context = None;
1271
1272 if let Some((window, surface, config)) = surface_info {
1273 let window_id = window.id();
1274 let ctx = Self::create_surface_context(
1275 &device,
1276 surface,
1277 config,
1278 &env_bind_group_layout,
1279 &texture_bind_group_layout,
1280 scale_factor,
1281 &mut registry,
1282 );
1283 surfaces.insert(window_id, ctx);
1284 current_window = Some(window_id);
1285 } else if let Some((w, h, f)) = headless_info {
1286 headless_context = Some(Self::create_headless_context(
1287 &device,
1288 w,
1289 h,
1290 f,
1291 &env_bind_group_layout,
1292 &texture_bind_group_layout,
1293 &mut registry,
1294 ));
1295 }
1296
1297 let staging_belt = wgpu::util::StagingBelt::new((*device).clone(), 1024 * 1024);
1298
1299 let glass_output_bind_group_layout = env_bind_group_layout.clone();
1300
1301 let color_blind_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1303 label: Some("Color Blind Bind Group Layout"),
1304 entries: &[
1305 wgpu::BindGroupLayoutEntry {
1306 binding: 0,
1307 visibility: wgpu::ShaderStages::FRAGMENT,
1308 ty: wgpu::BindingType::Texture {
1309 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1310 view_dimension: wgpu::TextureViewDimension::D2,
1311 multisampled: false,
1312 },
1313 count: None,
1314 },
1315 wgpu::BindGroupLayoutEntry {
1316 binding: 1,
1317 visibility: wgpu::ShaderStages::FRAGMENT,
1318 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1319 count: None,
1320 },
1321 wgpu::BindGroupLayoutEntry {
1322 binding: 2,
1323 visibility: wgpu::ShaderStages::FRAGMENT,
1324 ty: wgpu::BindingType::Buffer {
1325 ty: wgpu::BufferBindingType::Uniform,
1326 has_dynamic_offset: false,
1327 min_binding_size: wgpu::BufferSize::new(std::mem::size_of::<
1328 crate::color_blindness::ColorBlindUniforms,
1329 >() as u64),
1330 },
1331 count: None,
1332 },
1333 ],
1334 });
1335 let color_blind_pipeline_layout =
1336 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1337 label: Some("Color Blind Pipeline Layout"),
1338 bind_group_layouts: &[Some(&color_blind_bgl)],
1339 immediate_size: 0,
1340 });
1341
1342 let color_blind_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1344 label: Some("Surtr Color Blind Shader"),
1345 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
1346 crate::color_blindness::shader_source(),
1347 )),
1348 });
1349 let color_blind_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1350 label: Some("Surtr Color Blindness"),
1351 layout: Some(&color_blind_pipeline_layout),
1352 vertex: wgpu::VertexState {
1353 module: &color_blind_shader,
1354 entry_point: Some("fs_main_vs"),
1355 buffers: &[],
1356 compilation_options: wgpu::PipelineCompilationOptions::default(),
1357 },
1358 fragment: Some(wgpu::FragmentState {
1359 module: &color_blind_shader,
1360 entry_point: Some("fs_color_blind"),
1361 targets: &[Some(wgpu::ColorTargetState {
1362 format,
1363 blend: None,
1364 write_mask: wgpu::ColorWrites::ALL,
1365 })],
1366 compilation_options: wgpu::PipelineCompilationOptions::default(),
1367 }),
1368 primitive: wgpu::PrimitiveState::default(),
1369 depth_stencil: None,
1370 multisample: wgpu::MultisampleState::default(),
1371 multiview_mask: None,
1372 cache: None,
1373 });
1374
1375 let volumetric_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1379 label: Some("Surtr Volumetric Shader"),
1380 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
1381 "shaders/volumetric.wgsl"
1382 ))),
1383 });
1384 let volumetric_bgl =
1386 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1387 label: Some("Volumetric Bind Group Layout"),
1388 entries: &[wgpu::BindGroupLayoutEntry {
1389 binding: 0,
1390 visibility: wgpu::ShaderStages::FRAGMENT,
1391 ty: wgpu::BindingType::Buffer {
1392 ty: wgpu::BufferBindingType::Uniform,
1393 has_dynamic_offset: false,
1394 min_binding_size: wgpu::BufferSize::new(
1395 std::mem::size_of::<[f32; 16]>() as u64
1396 ),
1397 },
1398 count: None,
1399 }],
1400 });
1401 let volumetric_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1402 label: Some("Surtr Volumetric Layout"),
1403 bind_group_layouts: &[Some(&volumetric_bgl)],
1404 immediate_size: 0,
1405 });
1406
1407 let volumetric_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1408 label: Some("Surtr Volumetric Raymarching"),
1409 layout: Some(&volumetric_layout),
1410 vertex: wgpu::VertexState {
1411 module: &volumetric_shader,
1412 entry_point: Some("vs_fullscreen"),
1413 buffers: &[],
1414 compilation_options: wgpu::PipelineCompilationOptions::default(),
1415 },
1416 fragment: Some(wgpu::FragmentState {
1417 module: &volumetric_shader,
1418 entry_point: Some("fs_main"),
1419 targets: &[Some(wgpu::ColorTargetState {
1420 format: wgpu::TextureFormat::Rgba16Float,
1421 blend: Some(wgpu::BlendState {
1422 color: wgpu::BlendComponent {
1423 src_factor: wgpu::BlendFactor::One,
1424 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
1425 operation: wgpu::BlendOperation::Add,
1426 },
1427 alpha: wgpu::BlendComponent {
1428 src_factor: wgpu::BlendFactor::One,
1429 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
1430 operation: wgpu::BlendOperation::Add,
1431 },
1432 }),
1433 write_mask: wgpu::ColorWrites::ALL,
1434 })],
1435 compilation_options: wgpu::PipelineCompilationOptions::default(),
1436 }),
1437 primitive: wgpu::PrimitiveState::default(),
1438 depth_stencil: None,
1439 multisample: wgpu::MultisampleState::default(),
1440 multiview_mask: None,
1441 cache: None,
1442 });
1443
1444 let tonemap_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1447 label: Some("Surtr ToneMap Shader"),
1448 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(WGSL_TONEMAP)),
1449 });
1450 let tonemap_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1451 label: Some("ToneMap Bind Group Layout"),
1452 entries: &[
1453 wgpu::BindGroupLayoutEntry {
1454 binding: 0,
1455 visibility: wgpu::ShaderStages::FRAGMENT,
1456 ty: wgpu::BindingType::Buffer {
1457 ty: wgpu::BufferBindingType::Uniform,
1458 has_dynamic_offset: false,
1459 min_binding_size: wgpu::BufferSize::new(
1460 std::mem::size_of::<[f32; 4]>() as u64
1461 ),
1462 },
1463 count: None,
1464 },
1465 wgpu::BindGroupLayoutEntry {
1466 binding: 1,
1467 visibility: wgpu::ShaderStages::FRAGMENT,
1468 ty: wgpu::BindingType::Texture {
1469 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1470 view_dimension: wgpu::TextureViewDimension::D2,
1471 multisampled: false,
1472 },
1473 count: None,
1474 },
1475 wgpu::BindGroupLayoutEntry {
1476 binding: 2,
1477 visibility: wgpu::ShaderStages::FRAGMENT,
1478 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1479 count: None,
1480 },
1481 ],
1482 });
1483 let tonemap_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1484 label: Some("Surtr ToneMap Layout"),
1485 bind_group_layouts: &[Some(&tonemap_bgl)],
1486 immediate_size: 0,
1487 });
1488 let tonemap_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1489 label: Some("Surtr ToneMapping"),
1490 layout: Some(&tonemap_layout),
1491 vertex: wgpu::VertexState {
1492 module: &tonemap_shader,
1493 entry_point: Some("vs_fullscreen"),
1494 buffers: &[],
1495 compilation_options: wgpu::PipelineCompilationOptions::default(),
1496 },
1497 fragment: Some(wgpu::FragmentState {
1498 module: &tonemap_shader,
1499 entry_point: Some("fs_main"),
1500 targets: &[Some(wgpu::ColorTargetState {
1501 format,
1502 blend: None,
1503 write_mask: wgpu::ColorWrites::ALL,
1504 })],
1505 compilation_options: wgpu::PipelineCompilationOptions::default(),
1506 }),
1507 primitive: wgpu::PrimitiveState::default(),
1508 depth_stencil: None,
1509 multisample: wgpu::MultisampleState::default(),
1510 multiview_mask: None,
1511 cache: None,
1512 });
1513
1514 let color_blind_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1516 label: Some("Color Blind Uniforms"),
1517 size: std::mem::size_of::<crate::color_blindness::ColorBlindUniforms>() as u64,
1518 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1519 mapped_at_creation: false,
1520 });
1521
1522 let volumetric_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1524 label: Some("Volumetric Uniforms"),
1525 size: std::mem::size_of::<[f32; 16]>() as u64,
1526 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1527 mapped_at_creation: false,
1528 });
1529
1530 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1532 address_mode_u: wgpu::AddressMode::ClampToEdge,
1533 address_mode_v: wgpu::AddressMode::ClampToEdge,
1534 mag_filter: wgpu::FilterMode::Linear,
1535 min_filter: wgpu::FilterMode::Linear,
1536 ..Default::default()
1537 });
1538
1539 Self {
1540 registry,
1541 ai_material_rx: None,
1542 active_offscreens: Vec::new(),
1543 effect_pipelines: std::collections::HashMap::new(),
1544 effect_params_buffer: device.create_buffer(&wgpu::BufferDescriptor {
1545 label: Some("Dummy Effect Buffer"),
1546 size: 256,
1547 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1548 mapped_at_creation: false,
1549 }),
1550 effect_params_bind_group: device.create_bind_group(&wgpu::BindGroupDescriptor {
1551 label: Some("Dummy Effect Bind Group"),
1552 layout: &device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1553 label: None,
1554 entries: &[],
1555 }),
1556 entries: &[],
1557 }),
1558 linear_sampler: device.create_sampler(&wgpu::SamplerDescriptor {
1559 label: Some("Linear Sampler"),
1560 address_mode_u: wgpu::AddressMode::ClampToEdge,
1561 address_mode_v: wgpu::AddressMode::ClampToEdge,
1562 address_mode_w: wgpu::AddressMode::ClampToEdge,
1563 mag_filter: wgpu::FilterMode::Linear,
1564 min_filter: wgpu::FilterMode::Linear,
1565 mipmap_filter: wgpu::MipmapFilterMode::Linear,
1566 ..Default::default()
1567 }),
1568 instance,
1569 adapter,
1570 device: device.clone(),
1571 queue: queue.clone(),
1572
1573 surfaces,
1574 current_window,
1575 headless_context,
1576 pipeline,
1577 opaque_pipeline,
1578 ui_pipeline,
1579 glass_pipeline,
1580 bloom_extract_pipeline,
1581 copy_pipeline,
1582 composite_pipeline,
1583 env_bind_group_layout,
1584 text_engine: cvkg_runic_text::RunicTextEngine::default(),
1585 mega_heim_tex,
1586 mega_heim_bind_group,
1587 text_cache: LruCache::new(NonZeroUsize::new(2048).unwrap()),
1588 shaped_text_cache: std::collections::HashMap::new(),
1589 heim_packer: SundrPacker::new(4096, 4096),
1590 image_uv_registry: {
1591 let mut cache = LruCache::new(NonZeroUsize::new(256).unwrap());
1592 cache.put("__mega_heim".to_string(), cvkg_core::Rect { x: 0.0, y: 0.0, width: 1.0, height: 1.0 });
1593 cache
1594 },
1595 texture_registry,
1596 texture_views: texture_views_list,
1597 dummy_sampler,
1598 svg_cache: LruCache::new(NonZeroUsize::new(128).unwrap()),
1599 svg_trees: LruCache::new(NonZeroUsize::new(128).unwrap()),
1600 filter_engine: Some(
1601 cvkg_svg_filters::FilterEngine::new(cvkg_svg_filters::GpuContext {
1602 device: device.clone(),
1603 queue: queue.clone(),
1604 })
1605 .expect("Failed to create SVG filter engine"),
1606 ),
1607 filter_batches: Vec::new(),
1608 dummy_texture_bind_group,
1609 dummy_env_bind_group,
1610 texture_bind_group_layout,
1611 texture_bind_groups,
1612 shared_elements: LruCache::new(NonZeroUsize::new(1024).unwrap()),
1613 vertex_buffer,
1614 index_buffer,
1615 instance_buffer,
1616 vertices: Vec::with_capacity(MAX_VERTICES),
1617 indices: Vec::with_capacity(MAX_INDICES),
1618 instance_data: Vec::with_capacity(MAX_VERTICES / 4),
1619 draw_calls: Vec::new(),
1620 current_texture_id: None,
1621 opacity_stack: vec![1.0],
1622 clip_stack: Vec::new(),
1623 slice_stack: Vec::new(),
1624 shadow_stack: Vec::new(),
1625 theme_buffer,
1626 scene_buffer,
1627 berserker_bind_group,
1628 berserker_bind_group_layout,
1629 start_time: std::time::Instant::now(),
1630 current_theme,
1631 current_scene,
1632 background_pipeline,
1633 current_z: 0.0,
1634 telemetry: cvkg_core::TelemetryData::default(),
1635 last_frame_start: std::time::Instant::now(),
1636 last_redraw_start: std::time::Instant::now(),
1637 frame_budget: cvkg_core::FrameBudget::default(),
1638 capture_staging_buffer: None,
1639 compositor_index_cursor: 0,
1640 vram_buffers_bytes: 0,
1641 vram_textures_bytes: 0,
1642 _debug_layout: false,
1643 transform_stack: Vec::new(),
1644 redraw_requested: false,
1645 skuld_queries,
1646 skuld_buffer,
1647 skuld_read_buffer,
1648 skuld_period,
1649 last_gpu_time_ns: 0,
1650 vnode_stack: Vec::new(),
1651 event_handlers: std::collections::HashMap::new(),
1652 staging_belt,
1653 staging_command_buffers: Vec::new(),
1654 glass_output_bind_group_layout,
1655 current_draw_material: cvkg_core::DrawMaterial::Opaque,
1656 portal_regions: VecDeque::new(),
1657 cached_graph_plan: None,
1658 memo_cache: std::collections::HashMap::new(),
1659 bloom_enabled: true,
1660 volumetric_enabled: false,
1661 color_blind_mode: crate::color_blindness::ColorBlindMode::Normal,
1662 color_blind_intensity: 1.0,
1663 color_blind_pipeline,
1664 volumetric_pipeline,
1665 volumetric_bind_group_layout: volumetric_bgl,
1666 volumetric_uniform_buffer,
1667 color_blind_bind_group_layout: color_blind_bgl,
1668 color_blind_uniform_buffer,
1669 sampler,
1670 kawase_down_pipeline,
1671 kawase_up_pipeline,
1672 kawase_bind_group_layout: kawase_bgl,
1673 kawase_uniform: device.create_buffer(&wgpu::BufferDescriptor {
1674 label: Some("Kawase Persistent Uniform"),
1675 size: 32,
1676 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1677 mapped_at_creation: false,
1678 }),
1679 bind_group_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
1680 texture_view_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
1681 }
1682 }
1683
1684 pub(crate) fn rebuild_texture_array_bind_group(&mut self) {
1685 let views: Vec<&wgpu::TextureView> = self.texture_views.iter().collect();
1686 self.mega_heim_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1687 layout: &self.texture_bind_group_layout,
1688 entries: &[
1689 wgpu::BindGroupEntry {
1690 binding: 0,
1691 resource: wgpu::BindingResource::TextureViewArray(&views),
1692 },
1693 wgpu::BindGroupEntry {
1694 binding: 1,
1695 resource: wgpu::BindingResource::Sampler(&self.dummy_sampler),
1696 },
1697 ],
1698 label: Some("Surtr Texture Array Bind Group"),
1699 });
1700 }
1701
1702 pub(crate) fn update_vram_telemetry(&mut self) {
1704 let mut buffer_bytes = 0;
1706 buffer_bytes += (MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64;
1707 buffer_bytes += (MAX_INDICES * std::mem::size_of::<u32>()) as u64;
1708 buffer_bytes += std::mem::size_of::<cvkg_core::ColorTheme>() as u64;
1709 buffer_bytes += std::mem::size_of::<cvkg_core::SceneUniforms>() as u64;
1710 self.vram_buffers_bytes = buffer_bytes;
1711
1712 let mut texture_bytes = 0;
1714 texture_bytes += 4096 * 4096 * 4; texture_bytes += 4; for ctx in self.surfaces.values() {
1718 let bpp = 4;
1719 let surface_bytes = (ctx.config.width * ctx.config.height * bpp) as u64;
1720 texture_bytes += surface_bytes * 3; }
1722
1723 self.vram_textures_bytes = texture_bytes;
1724
1725 self.telemetry.vram_buffers_mb = buffer_bytes as f32 / 1_048_576.0;
1726 self.telemetry.vram_textures_mb = texture_bytes as f32 / 1_048_576.0;
1727 self.telemetry.vram_pipelines_mb = 0.0;
1728 self.telemetry.vram_usage_mb =
1729 self.telemetry.vram_buffers_mb + self.telemetry.vram_textures_mb;
1730 }
1731
1732 pub fn get_telemetry(&self) -> cvkg_core::TelemetryData {
1734 self.telemetry.clone()
1735 }
1736
1737 pub fn resize(
1739 &mut self,
1740 window_id: winit::window::WindowId,
1741 width: u32,
1742 height: u32,
1743 scale_factor: f32,
1744 ) {
1745 if width > 0
1746 && height > 0
1747 && let Some(ctx) = self.surfaces.get_mut(&window_id)
1748 {
1749 if ctx.config.width == width && ctx.config.height == height {
1750 return;
1752 }
1753
1754 log::info!("[GPU] Reconfiguring surface: {}x{}", width, height);
1755 self.bind_group_cache.lock().unwrap().clear();
1756 self.texture_view_cache.lock().unwrap().clear();
1757 self.shaped_text_cache.clear();
1758 ctx.config.width = width;
1759 ctx.config.height = height;
1760 ctx.scale_factor = scale_factor;
1761 ctx.surface.configure(&self.device, &ctx.config);
1762
1763 let texture_desc = wgpu::TextureDescriptor {
1765 label: Some("Surtr Scene Texture"),
1766 size: wgpu::Extent3d {
1767 width,
1768 height,
1769 depth_or_array_layers: 1,
1770 },
1771 mip_level_count: 1,
1772 sample_count: 1,
1773 dimension: wgpu::TextureDimension::D2,
1774 format: wgpu::TextureFormat::Rgba16Float,
1775 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1776 | wgpu::TextureUsages::TEXTURE_BINDING,
1777 view_formats: &[],
1778 };
1779
1780 let scene_tex = self.device.create_texture(&texture_desc);
1781
1782 let msaa_desc = wgpu::TextureDescriptor {
1783 label: Some("Scene MSAA"),
1784 size: texture_desc.size,
1785 mip_level_count: 1,
1786 sample_count: 4,
1787 dimension: wgpu::TextureDimension::D2,
1788 format: wgpu::TextureFormat::Rgba16Float,
1789 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1790 view_formats: &[],
1791 };
1792 let scene_msaa_tex = self.device.create_texture(&msaa_desc);
1793 ctx.scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
1794 ctx.scene_msaa_texture =
1795 scene_msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
1796
1797 self.registry.remove_image(ctx.blur_tex_a);
1798 self.registry.remove_image(ctx.blur_tex_b);
1799 self.registry.remove_image(ctx.bloom_tex_a);
1800 self.registry.remove_image(ctx.bloom_tex_b);
1801
1802 let blur_width = (width / 2).max(1);
1803 let blur_height = (height / 2).max(1);
1804
1805 let blur_desc_a = crate::kvasir::resource::ResourceDescriptor {
1806 label: Some("Surtr Blur Texture A".into()),
1807 kind: crate::kvasir::resource::ResourceKind::Image {
1808 format: ctx.config.format,
1809 width: blur_width,
1810 height: blur_height,
1811 mip_level_count: 6,
1812 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1813 | wgpu::TextureUsages::TEXTURE_BINDING
1814 | wgpu::TextureUsages::COPY_SRC,
1815 },
1816 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
1817 };
1818 ctx.blur_tex_a = self.registry.allocate_image(&self.device, &blur_desc_a);
1819
1820 let blur_desc_b = crate::kvasir::resource::ResourceDescriptor {
1821 label: Some("Surtr Blur Texture B".into()),
1822 kind: crate::kvasir::resource::ResourceKind::Image {
1823 format: ctx.config.format,
1824 width: blur_width,
1825 height: blur_height,
1826 mip_level_count: 6,
1827 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1828 | wgpu::TextureUsages::TEXTURE_BINDING
1829 | wgpu::TextureUsages::COPY_SRC,
1830 },
1831 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
1832 };
1833 ctx.blur_tex_b = self.registry.allocate_image(&self.device, &blur_desc_b);
1834
1835 let bloom_desc_a = crate::kvasir::resource::ResourceDescriptor {
1836 label: Some("Surtr Bloom Texture A".into()),
1837 kind: crate::kvasir::resource::ResourceKind::Image {
1838 format: ctx.config.format,
1839 width: blur_width,
1840 height: blur_height,
1841 mip_level_count: 6,
1842 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1843 | wgpu::TextureUsages::TEXTURE_BINDING
1844 | wgpu::TextureUsages::COPY_SRC,
1845 },
1846 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
1847 };
1848 ctx.bloom_tex_a = self.registry.allocate_image(&self.device, &bloom_desc_a);
1849
1850 let bloom_desc_b = crate::kvasir::resource::ResourceDescriptor {
1851 label: Some("Surtr Bloom Texture B".into()),
1852 kind: crate::kvasir::resource::ResourceKind::Image {
1853 format: ctx.config.format,
1854 width: blur_width,
1855 height: blur_height,
1856 mip_level_count: 6,
1857 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1858 | wgpu::TextureUsages::TEXTURE_BINDING
1859 | wgpu::TextureUsages::COPY_SRC,
1860 },
1861 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
1862 };
1863 ctx.bloom_tex_b = self.registry.allocate_image(&self.device, &bloom_desc_b);
1864
1865 ctx.scene_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1867 layout: &self.env_bind_group_layout,
1868 entries: &[
1869 wgpu::BindGroupEntry {
1870 binding: 0,
1871 resource: wgpu::BindingResource::TextureView(&ctx.scene_texture),
1872 },
1873 wgpu::BindGroupEntry {
1874 binding: 1,
1875 resource: wgpu::BindingResource::Sampler(&ctx.sampler),
1876 },
1877 ],
1878 label: Some("Scene Bind Group Resize"),
1879 });
1880
1881 let scene_views: Vec<&wgpu::TextureView> =
1882 (0..256).map(|_| &ctx.scene_texture).collect();
1883 ctx.scene_texture_bind_group =
1884 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1885 layout: &self.texture_bind_group_layout,
1886 entries: &[
1887 wgpu::BindGroupEntry {
1888 binding: 0,
1889 resource: wgpu::BindingResource::TextureViewArray(&scene_views),
1890 },
1891 wgpu::BindGroupEntry {
1892 binding: 1,
1893 resource: wgpu::BindingResource::Sampler(&ctx.sampler),
1894 },
1895 ],
1896 label: Some("Scene Texture Bind Group Resize"),
1897 });
1898
1899 let depth_texture = self.device.create_texture(&wgpu::TextureDescriptor {
1900 label: Some("Surtr Depth Texture"),
1901 size: wgpu::Extent3d {
1902 width,
1903 height,
1904 depth_or_array_layers: 1,
1905 },
1906 mip_level_count: 1,
1907 sample_count: 1,
1908 dimension: wgpu::TextureDimension::D2,
1909 format: wgpu::TextureFormat::Depth32Float,
1910 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1911 view_formats: &[],
1912 });
1913 ctx.depth_texture_view =
1914 depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
1915 }
1916 }
1917
1918 pub fn begin_frame_headless(&mut self) -> wgpu::CommandEncoder {
1920 self.current_window = None;
1921 self.vertices.clear();
1922 self.indices.clear();
1923 self.instance_data.clear();
1924 self.draw_calls.clear();
1925 self.filter_batches.clear();
1926 self.shared_elements.clear();
1927 self.current_texture_id = None;
1928 self.opacity_stack = vec![1.0];
1929 self.clip_stack.clear();
1930 self.slice_stack.clear();
1931 self.transform_stack.clear();
1932 self.portal_regions.clear(); self.current_z = 0.0;
1934 self.compositor_index_cursor = self.indices.len() as u32;
1935 self.vnode_stack.clear();
1936 self.event_handlers.clear();
1937
1938 self.memo_cache.clear();
1940
1941 self.last_frame_start = std::time::Instant::now();
1942 self.telemetry.draw_calls = 0;
1943 self.telemetry.vertices = 0;
1944
1945 self.staging_belt.recall();
1947
1948 let ctx = self
1949 .headless_context
1950 .as_ref()
1951 .expect("Headless context not initialized");
1952 let time = self.start_time.elapsed().as_secs_f32();
1953 let logical_w = ctx.width as f32 / ctx.scale_factor;
1954 let logical_h = ctx.height as f32 / ctx.scale_factor;
1955 let dt = time - self.current_scene.time;
1956 self.current_scene.time = time;
1957 self.current_scene.delta_time = dt;
1958 self.current_scene.resolution = [logical_w, logical_h];
1959 self.current_scene.scale_factor = ctx.scale_factor;
1960 self.current_scene.proj =
1961 glam::Mat4::orthographic_lh(0.0, logical_w, logical_h, 0.0, -1000.0, 1000.0);
1962
1963 self.queue.write_buffer(
1964 &self.scene_buffer,
1965 0,
1966 bytemuck::bytes_of(&self.current_scene),
1967 );
1968
1969 self.device
1970 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1971 label: Some("Surtr Headless Command Encoder"),
1972 })
1973 }
1974
1975 pub fn begin_frame(&mut self, window_id: winit::window::WindowId) -> wgpu::CommandEncoder {
1977 if let Some(rx) = &self.ai_material_rx {
1979 while let Ok(res) = rx.try_recv() {
1980 match res {
1981 Ok(_) => log::info!("[Surtr] Received AI generated material"),
1982 Err(e) => log::warn!("[Surtr] AI material generation error: {:?}", e),
1983 }
1984 }
1985 }
1986
1987 if let Some(rb) = &self.skuld_read_buffer {
1989 let slice = rb.slice(..);
1990 let (tx, rx) = std::sync::mpsc::channel();
1991 slice.map_async(wgpu::MapMode::Read, move |r| {
1992 let _ = tx.send(r);
1993 });
1994
1995 self.device
1997 .poll(wgpu::PollType::Wait {
1998 submission_index: None,
1999 timeout: None,
2000 })
2001 .unwrap();
2002
2003 if rx.recv().is_ok() {
2004 let data = slice.get_mapped_range();
2005 let timestamps: [u64; 2] = bytemuck::cast_slice(&data).try_into().unwrap_or([0, 0]);
2006 drop(data);
2007 rb.unmap();
2008
2009 if timestamps[1] > timestamps[0] {
2010 let diff_ticks = timestamps[1] - timestamps[0];
2011 self.last_gpu_time_ns = (diff_ticks as f64 * self.skuld_period as f64) as u64;
2012 log::trace!(
2013 "[Skuld] GPU time: {} ms",
2014 self.last_gpu_time_ns as f64 / 1_000_000.0
2015 );
2016 }
2017 }
2018 }
2019
2020 self.staging_belt.recall();
2021 self.current_window = Some(window_id);
2022 self.vertices.clear();
2023 self.indices.clear();
2024 self.instance_data.clear();
2025 self.draw_calls.clear();
2026 self.filter_batches.clear();
2027 self.shared_elements.clear();
2028 self.current_texture_id = None;
2029 self.opacity_stack = vec![1.0];
2030 self.clip_stack.clear();
2031 self.slice_stack.clear();
2032 self.transform_stack.clear();
2033 self.portal_regions.clear(); self.current_z = 0.0;
2035 self.vnode_stack.clear();
2036 self.event_handlers.clear();
2037
2038 self.memo_cache.clear();
2040
2041 self.last_frame_start = std::time::Instant::now();
2042 self.telemetry.draw_calls = 0;
2043 self.telemetry.vertices = 0;
2044
2045 let ctx = self
2046 .surfaces
2047 .get(&window_id)
2048 .expect("Window not registered");
2049 let time = self.start_time.elapsed().as_secs_f32();
2050 let logical_w = ctx.config.width as f32 / ctx.scale_factor;
2051 let logical_h = ctx.config.height as f32 / ctx.scale_factor;
2052 let dt = time - self.current_scene.time;
2053 self.current_scene.time = time;
2054 self.current_scene.delta_time = dt;
2055 self.current_scene.resolution = [logical_w, logical_h];
2056 self.current_scene.scale_factor = ctx.scale_factor;
2057 self.current_scene.proj =
2058 glam::Mat4::orthographic_lh(0.0, logical_w, logical_h, 0.0, -1000.0, 1000.0);
2059
2060 self.queue.write_buffer(
2061 &self.scene_buffer,
2062 0,
2063 bytemuck::bytes_of(&self.current_scene),
2064 );
2065
2066 self.device
2067 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2068 label: Some("Surtr Command Encoder"),
2069 })
2070 }
2071
2072 pub fn register_window(&mut self, window: Arc<winit::window::Window>) {
2074 let size = window.inner_size();
2075 let surface = self
2076 .instance
2077 .create_surface(window.clone())
2078 .expect("Failed to create surface");
2079 let caps = surface.get_capabilities(&self.adapter);
2080 let format = caps.formats[0];
2081
2082 let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Mailbox) {
2084 wgpu::PresentMode::Mailbox
2085 } else {
2086 log::warn!("[GPU] Mailbox not supported, falling back to Fifo (V-Sync)");
2087 wgpu::PresentMode::Fifo
2088 };
2089
2090 let alpha_mode = if caps
2091 .alpha_modes
2092 .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
2093 {
2094 wgpu::CompositeAlphaMode::PostMultiplied
2095 } else if caps
2096 .alpha_modes
2097 .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
2098 {
2099 wgpu::CompositeAlphaMode::PreMultiplied
2100 } else {
2101 caps.alpha_modes[0]
2102 };
2103
2104 log::info!(
2105 "[GPU] Configuring surface: {}x{} | {:?} | {:?}",
2106 size.width,
2107 size.height,
2108 present_mode,
2109 alpha_mode
2110 );
2111
2112 let config = wgpu::SurfaceConfiguration {
2113 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2114 format,
2115 width: size.width,
2116 height: size.height,
2117 present_mode,
2118 alpha_mode,
2119 view_formats: vec![],
2120 desired_maximum_frame_latency: 1,
2121 };
2122 surface.configure(&self.device, &config);
2123
2124 let ctx = Self::create_surface_context(
2125 &self.device,
2126 surface,
2127 config,
2128 &self.env_bind_group_layout,
2129 &self.texture_bind_group_layout,
2130 window.scale_factor() as f32,
2131 &mut self.registry,
2132 );
2133
2134 self.surfaces.insert(window.id(), ctx);
2135 }
2136
2137 pub(crate) fn create_headless_context(
2138 device: &wgpu::Device,
2139 width: u32,
2140 height: u32,
2141 format: wgpu::TextureFormat,
2142 env_bind_group_layout: &wgpu::BindGroupLayout,
2143 texture_bind_group_layout: &wgpu::BindGroupLayout,
2144 registry: &mut crate::kvasir::registry::ResourceRegistry,
2145 ) -> HeadlessContext {
2146 let texture_desc = wgpu::TextureDescriptor {
2147 label: Some("Surtr Headless Scene Texture"),
2148 size: wgpu::Extent3d {
2149 width,
2150 height,
2151 depth_or_array_layers: 1,
2152 },
2153 mip_level_count: 1,
2154 sample_count: 1,
2155 dimension: wgpu::TextureDimension::D2,
2156 format: wgpu::TextureFormat::Rgba16Float,
2157 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2158 | wgpu::TextureUsages::TEXTURE_BINDING
2159 | wgpu::TextureUsages::COPY_SRC,
2160 view_formats: &[],
2161 };
2162
2163 let scene_tex = device.create_texture(&texture_desc);
2164
2165 let msaa_desc = wgpu::TextureDescriptor {
2166 label: Some("Scene MSAA"),
2167 size: texture_desc.size,
2168 mip_level_count: 1,
2169 sample_count: 4,
2170 dimension: wgpu::TextureDimension::D2,
2171 format: wgpu::TextureFormat::Rgba16Float,
2172 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2173 view_formats: &[],
2174 };
2175 let scene_msaa_tex = device.create_texture(&msaa_desc);
2176 let scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
2177 let scene_msaa_texture =
2178 scene_msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
2179
2180 let blur_width = (width / 2).max(1);
2181 let blur_height = (height / 2).max(1);
2182 let blur_desc_a = crate::kvasir::resource::ResourceDescriptor {
2183 label: Some("Headless Blur Texture A".into()),
2184 kind: crate::kvasir::resource::ResourceKind::Image {
2185 format,
2186 width: blur_width,
2187 height: blur_height,
2188 mip_level_count: 6,
2189 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2190 | wgpu::TextureUsages::TEXTURE_BINDING
2191 | wgpu::TextureUsages::COPY_SRC,
2192 },
2193 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2194 };
2195 let blur_tex_a = registry.allocate_image(device, &blur_desc_a);
2196
2197 let blur_desc_b = crate::kvasir::resource::ResourceDescriptor {
2198 label: Some("Headless Blur Texture B".into()),
2199 kind: crate::kvasir::resource::ResourceKind::Image {
2200 format,
2201 width: blur_width,
2202 height: blur_height,
2203 mip_level_count: 6,
2204 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2205 | wgpu::TextureUsages::TEXTURE_BINDING
2206 | wgpu::TextureUsages::COPY_SRC,
2207 },
2208 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2209 };
2210 let blur_tex_b = registry.allocate_image(device, &blur_desc_b);
2211
2212 let bloom_desc_a = crate::kvasir::resource::ResourceDescriptor {
2213 label: Some("Headless Bloom Texture A".into()),
2214 kind: crate::kvasir::resource::ResourceKind::Image {
2215 format,
2216 width: blur_width,
2217 height: blur_height,
2218 mip_level_count: 6,
2219 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2220 | wgpu::TextureUsages::TEXTURE_BINDING
2221 | wgpu::TextureUsages::COPY_SRC,
2222 },
2223 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2224 };
2225 let bloom_tex_a = registry.allocate_image(device, &bloom_desc_a);
2226
2227 let bloom_desc_b = crate::kvasir::resource::ResourceDescriptor {
2228 label: Some("Headless Bloom Texture B".into()),
2229 kind: crate::kvasir::resource::ResourceKind::Image {
2230 format,
2231 width: blur_width,
2232 height: blur_height,
2233 mip_level_count: 6,
2234 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2235 | wgpu::TextureUsages::TEXTURE_BINDING
2236 | wgpu::TextureUsages::COPY_SRC,
2237 },
2238 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2239 };
2240 let bloom_tex_b = registry.allocate_image(device, &bloom_desc_b);
2241
2242 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
2243 address_mode_u: wgpu::AddressMode::ClampToEdge,
2244 address_mode_v: wgpu::AddressMode::ClampToEdge,
2245 mag_filter: wgpu::FilterMode::Linear,
2246 min_filter: wgpu::FilterMode::Linear,
2247 ..Default::default()
2248 });
2249
2250 let scene_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2251 layout: env_bind_group_layout,
2252 entries: &[
2253 wgpu::BindGroupEntry {
2254 binding: 0,
2255 resource: wgpu::BindingResource::TextureView(&scene_texture),
2256 },
2257 wgpu::BindGroupEntry {
2258 binding: 1,
2259 resource: wgpu::BindingResource::Sampler(&sampler),
2260 },
2261 ],
2262 label: Some("Headless Scene Bind Group"),
2263 });
2264
2265 let blur_view_a = registry.get_texture_view(blur_tex_a).unwrap();
2266 let blur_view_b = registry.get_texture_view(blur_tex_b).unwrap();
2267 let bloom_view_a = registry.get_texture_view(bloom_tex_a).unwrap();
2268 let bloom_view_b = registry.get_texture_view(bloom_tex_b).unwrap();
2269
2270 let blur_env_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
2271 layout: env_bind_group_layout,
2272 entries: &[
2273 wgpu::BindGroupEntry {
2274 binding: 0,
2275 resource: wgpu::BindingResource::TextureView(&blur_view_a),
2276 },
2277 wgpu::BindGroupEntry {
2278 binding: 1,
2279 resource: wgpu::BindingResource::Sampler(&sampler),
2280 },
2281 ],
2282 label: Some("Headless Blur Env Bind Group A"),
2283 });
2284 let blur_env_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
2285 layout: env_bind_group_layout,
2286 entries: &[
2287 wgpu::BindGroupEntry {
2288 binding: 0,
2289 resource: wgpu::BindingResource::TextureView(&blur_view_b),
2290 },
2291 wgpu::BindGroupEntry {
2292 binding: 1,
2293 resource: wgpu::BindingResource::Sampler(&sampler),
2294 },
2295 ],
2296 label: Some("Headless Blur Env Bind Group B"),
2297 });
2298 let bloom_env_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
2299 layout: env_bind_group_layout,
2300 entries: &[
2301 wgpu::BindGroupEntry {
2302 binding: 0,
2303 resource: wgpu::BindingResource::TextureView(&bloom_view_a),
2304 },
2305 wgpu::BindGroupEntry {
2306 binding: 1,
2307 resource: wgpu::BindingResource::Sampler(&sampler),
2308 },
2309 ],
2310 label: Some("Headless Bloom Env Bind Group A"),
2311 });
2312 let bloom_env_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
2313 layout: env_bind_group_layout,
2314 entries: &[
2315 wgpu::BindGroupEntry {
2316 binding: 0,
2317 resource: wgpu::BindingResource::TextureView(&bloom_view_b),
2318 },
2319 wgpu::BindGroupEntry {
2320 binding: 1,
2321 resource: wgpu::BindingResource::Sampler(&sampler),
2322 },
2323 ],
2324 label: Some("Headless Bloom Env Bind Group B"),
2325 });
2326
2327 let scene_views: Vec<&wgpu::TextureView> = (0..256).map(|_| &scene_texture).collect();
2328 let scene_texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2329 layout: texture_bind_group_layout,
2330 entries: &[
2331 wgpu::BindGroupEntry {
2332 binding: 0,
2333 resource: wgpu::BindingResource::TextureViewArray(&scene_views),
2334 },
2335 wgpu::BindGroupEntry {
2336 binding: 1,
2337 resource: wgpu::BindingResource::Sampler(&sampler),
2338 },
2339 ],
2340 label: Some("Headless Scene Texture Bind Group"),
2341 });
2342
2343 let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
2344 label: Some("Headless Depth Texture"),
2345 size: wgpu::Extent3d {
2346 width,
2347 height,
2348 depth_or_array_layers: 1,
2349 },
2350 mip_level_count: 1,
2351 sample_count: 4,
2352 dimension: wgpu::TextureDimension::D2,
2353 format: wgpu::TextureFormat::Depth32Float,
2354 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2355 view_formats: &[],
2356 });
2357 let depth_texture_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
2358
2359 let output_texture = device.create_texture(&wgpu::TextureDescriptor {
2360 label: Some("Headless Output Texture"),
2361 size: wgpu::Extent3d {
2362 width,
2363 height,
2364 depth_or_array_layers: 1,
2365 },
2366 mip_level_count: 1,
2367 sample_count: 1,
2368 dimension: wgpu::TextureDimension::D2,
2369 format,
2370 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2371 | wgpu::TextureUsages::COPY_DST
2372 | wgpu::TextureUsages::COPY_SRC,
2373 view_formats: &[],
2374 });
2375 let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
2376
2377 crate::types::HeadlessContext {
2378 scene_texture,
2379 scene_msaa_texture,
2380 scene_bind_group,
2381 scene_texture_bind_group,
2382 depth_texture_view,
2383 blur_tex_a,
2384 blur_tex_b,
2385 bloom_tex_a,
2386 bloom_tex_b,
2387 blur_env_bind_group_a,
2388 blur_env_bind_group_b,
2389 bloom_env_bind_group_a,
2390 bloom_env_bind_group_b,
2391 scale_factor: 1.0,
2392 sampler,
2393 width,
2394 height,
2395 output_texture,
2396 output_view,
2397 }
2398 }
2399
2400 pub(crate) fn create_surface_context(
2401 device: &wgpu::Device,
2402 surface: wgpu::Surface<'static>,
2403 config: wgpu::SurfaceConfiguration,
2404 env_bind_group_layout: &wgpu::BindGroupLayout,
2405 texture_bind_group_layout: &wgpu::BindGroupLayout,
2406 scale_factor: f32,
2407 registry: &mut crate::kvasir::registry::ResourceRegistry,
2408 ) -> SurfaceContext {
2409 let width = config.width;
2410 let height = config.height;
2411
2412 let texture_desc = wgpu::TextureDescriptor {
2413 label: Some("Surtr Scene Texture"),
2414 size: wgpu::Extent3d {
2415 width,
2416 height,
2417 depth_or_array_layers: 1,
2418 },
2419 mip_level_count: 1,
2420 sample_count: 1,
2421 dimension: wgpu::TextureDimension::D2,
2422 format: wgpu::TextureFormat::Rgba16Float,
2423 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
2424 view_formats: &[],
2425 };
2426
2427 let scene_tex = device.create_texture(&texture_desc);
2428
2429 let msaa_desc = wgpu::TextureDescriptor {
2430 label: Some("Scene MSAA"),
2431 size: texture_desc.size,
2432 mip_level_count: 1,
2433 sample_count: 4,
2434 dimension: wgpu::TextureDimension::D2,
2435 format: wgpu::TextureFormat::Rgba16Float,
2436 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2437 view_formats: &[],
2438 };
2439 let scene_msaa_tex = device.create_texture(&msaa_desc);
2440 let scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
2441 let scene_msaa_texture =
2442 scene_msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
2443
2444 let blur_width = (config.width / 2).max(1);
2445 let blur_height = (config.height / 2).max(1);
2446 let blur_desc_a = crate::kvasir::resource::ResourceDescriptor {
2447 label: Some("Surface Blur Texture A".into()),
2448 kind: crate::kvasir::resource::ResourceKind::Image {
2449 format: config.format,
2450 width: blur_width,
2451 height: blur_height,
2452 mip_level_count: 6,
2453 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2454 | wgpu::TextureUsages::TEXTURE_BINDING
2455 | wgpu::TextureUsages::COPY_SRC,
2456 },
2457 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2458 };
2459 let blur_tex_a = registry.allocate_image(device, &blur_desc_a);
2460
2461 let blur_desc_b = crate::kvasir::resource::ResourceDescriptor {
2462 label: Some("Surface Blur Texture B".into()),
2463 kind: crate::kvasir::resource::ResourceKind::Image {
2464 format: config.format,
2465 width: blur_width,
2466 height: blur_height,
2467 mip_level_count: 6,
2468 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2469 | wgpu::TextureUsages::TEXTURE_BINDING
2470 | wgpu::TextureUsages::COPY_SRC,
2471 },
2472 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2473 };
2474 let blur_tex_b = registry.allocate_image(device, &blur_desc_b);
2475
2476 let bloom_desc_a = crate::kvasir::resource::ResourceDescriptor {
2477 label: Some("Surface Bloom Texture A".into()),
2478 kind: crate::kvasir::resource::ResourceKind::Image {
2479 format: config.format,
2480 width: blur_width,
2481 height: blur_height,
2482 mip_level_count: 6,
2483 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2484 | wgpu::TextureUsages::TEXTURE_BINDING
2485 | wgpu::TextureUsages::COPY_SRC,
2486 },
2487 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2488 };
2489 let bloom_tex_a = registry.allocate_image(device, &bloom_desc_a);
2490
2491 let bloom_desc_b = crate::kvasir::resource::ResourceDescriptor {
2492 label: Some("Surface Bloom Texture B".into()),
2493 kind: crate::kvasir::resource::ResourceKind::Image {
2494 format: config.format,
2495 width: blur_width,
2496 height: blur_height,
2497 mip_level_count: 6,
2498 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2499 | wgpu::TextureUsages::TEXTURE_BINDING
2500 | wgpu::TextureUsages::COPY_SRC,
2501 },
2502 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2503 };
2504 let bloom_tex_b = registry.allocate_image(device, &bloom_desc_b);
2505
2506 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
2507 address_mode_u: wgpu::AddressMode::ClampToEdge,
2508 address_mode_v: wgpu::AddressMode::ClampToEdge,
2509 mag_filter: wgpu::FilterMode::Linear,
2510 min_filter: wgpu::FilterMode::Linear,
2511 ..Default::default()
2512 });
2513
2514 let scene_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2515 layout: env_bind_group_layout,
2516 entries: &[
2517 wgpu::BindGroupEntry {
2518 binding: 0,
2519 resource: wgpu::BindingResource::TextureView(&scene_texture),
2520 },
2521 wgpu::BindGroupEntry {
2522 binding: 1,
2523 resource: wgpu::BindingResource::Sampler(&sampler),
2524 },
2525 ],
2526 label: Some("Scene Bind Group"),
2527 });
2528
2529 let blur_view_a = registry.get_texture_view(blur_tex_a).unwrap();
2530 let blur_view_b = registry.get_texture_view(blur_tex_b).unwrap();
2531 let bloom_view_a = registry.get_texture_view(bloom_tex_a).unwrap();
2532 let bloom_view_b = registry.get_texture_view(bloom_tex_b).unwrap();
2533
2534 let blur_env_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
2535 layout: env_bind_group_layout,
2536 entries: &[
2537 wgpu::BindGroupEntry {
2538 binding: 0,
2539 resource: wgpu::BindingResource::TextureView(&blur_view_a),
2540 },
2541 wgpu::BindGroupEntry {
2542 binding: 1,
2543 resource: wgpu::BindingResource::Sampler(&sampler),
2544 },
2545 ],
2546 label: Some("Blur Env Bind Group A"),
2547 });
2548 let blur_env_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
2549 layout: env_bind_group_layout,
2550 entries: &[
2551 wgpu::BindGroupEntry {
2552 binding: 0,
2553 resource: wgpu::BindingResource::TextureView(&blur_view_b),
2554 },
2555 wgpu::BindGroupEntry {
2556 binding: 1,
2557 resource: wgpu::BindingResource::Sampler(&sampler),
2558 },
2559 ],
2560 label: Some("Blur Env Bind Group B"),
2561 });
2562 let bloom_env_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
2563 layout: env_bind_group_layout,
2564 entries: &[
2565 wgpu::BindGroupEntry {
2566 binding: 0,
2567 resource: wgpu::BindingResource::TextureView(&bloom_view_a),
2568 },
2569 wgpu::BindGroupEntry {
2570 binding: 1,
2571 resource: wgpu::BindingResource::Sampler(&sampler),
2572 },
2573 ],
2574 label: Some("Bloom Env Bind Group A"),
2575 });
2576 let bloom_env_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
2577 layout: env_bind_group_layout,
2578 entries: &[
2579 wgpu::BindGroupEntry {
2580 binding: 0,
2581 resource: wgpu::BindingResource::TextureView(&bloom_view_b),
2582 },
2583 wgpu::BindGroupEntry {
2584 binding: 1,
2585 resource: wgpu::BindingResource::Sampler(&sampler),
2586 },
2587 ],
2588 label: Some("Bloom Env Bind Group B"),
2589 });
2590
2591 let scene_views: Vec<&wgpu::TextureView> = (0..256).map(|_| &scene_texture).collect();
2592 let scene_texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2593 layout: texture_bind_group_layout,
2594 entries: &[
2595 wgpu::BindGroupEntry {
2596 binding: 0,
2597 resource: wgpu::BindingResource::TextureViewArray(&scene_views),
2598 },
2599 wgpu::BindGroupEntry {
2600 binding: 1,
2601 resource: wgpu::BindingResource::Sampler(&sampler),
2602 },
2603 ],
2604 label: Some("Scene Texture Bind Group"),
2605 });
2606
2607 let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
2608 label: Some("Surtr Depth Texture"),
2609 size: wgpu::Extent3d {
2610 width,
2611 height,
2612 depth_or_array_layers: 1,
2613 },
2614 mip_level_count: 1,
2615 sample_count: 4,
2616 dimension: wgpu::TextureDimension::D2,
2617 format: wgpu::TextureFormat::Depth32Float,
2618 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
2619 view_formats: &[],
2620 });
2621 let depth_texture_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
2622
2623 crate::types::SurfaceContext {
2624 surface,
2625 config,
2626 scene_texture,
2627 scene_msaa_texture,
2628 scene_bind_group,
2629 scene_texture_bind_group,
2630 depth_texture_view,
2631 blur_tex_a,
2632 blur_tex_b,
2633 bloom_tex_a,
2634 bloom_tex_b,
2635 blur_env_bind_group_a,
2636 blur_env_bind_group_b,
2637 bloom_env_bind_group_a,
2638 bloom_env_bind_group_b,
2639 scale_factor,
2640 sampler,
2641 }
2642 }
2643
2644 pub fn reset_time(&mut self) {
2645 self.start_time = std::time::Instant::now();
2646 }
2647
2648 pub fn reclaim_vram(&mut self) {
2651 log::warn!("[GPU] Sundr Compaction: Compacting Mega-Heim...");
2652
2653 let new_mega_heim_tex = self.device.create_texture(&wgpu::TextureDescriptor {
2654 label: Some("Sundr Mega-Heim (Compacted)"),
2655 size: wgpu::Extent3d {
2656 width: 4096,
2657 height: 4096,
2658 depth_or_array_layers: 1,
2659 },
2660 mip_level_count: 1,
2661 sample_count: 1,
2662 dimension: wgpu::TextureDimension::D2,
2663 format: wgpu::TextureFormat::Rgba8UnormSrgb,
2664 usage: wgpu::TextureUsages::TEXTURE_BINDING
2665 | wgpu::TextureUsages::COPY_DST
2666 | wgpu::TextureUsages::COPY_SRC,
2667 view_formats: &[],
2668 });
2669
2670 let mut new_packer = SundrPacker::new(4096, 4096);
2671 let mut encoder = self
2672 .device
2673 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2674 label: Some("Heim Compaction Encoder"),
2675 });
2676
2677 let image_entries: Vec<(String, Rect)> = self
2678 .image_uv_registry
2679 .iter()
2680 .map(|(k, v)| (k.clone(), *v))
2681 .collect();
2682 for (name, old_uv) in image_entries {
2683 if let Some(&tex_idx) = self.texture_registry.get(&name)
2684 && tex_idx == 0
2685 {
2686 let w_px = (old_uv.width * 4096.0).round() as u32;
2687 let h_px = (old_uv.height * 4096.0).round() as u32;
2688 let old_x_px = (old_uv.x * 4096.0).round() as u32;
2689 let old_y_px = (old_uv.y * 4096.0).round() as u32;
2690
2691 if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
2692 encoder.copy_texture_to_texture(
2693 wgpu::TexelCopyTextureInfo {
2694 texture: &self.mega_heim_tex,
2695 mip_level: 0,
2696 origin: wgpu::Origin3d {
2697 x: old_x_px,
2698 y: old_y_px,
2699 z: 0,
2700 },
2701 aspect: wgpu::TextureAspect::All,
2702 },
2703 wgpu::TexelCopyTextureInfo {
2704 texture: &new_mega_heim_tex,
2705 mip_level: 0,
2706 origin: wgpu::Origin3d {
2707 x: new_x,
2708 y: new_y,
2709 z: 0,
2710 },
2711 aspect: wgpu::TextureAspect::All,
2712 },
2713 wgpu::Extent3d {
2714 width: w_px,
2715 height: h_px,
2716 depth_or_array_layers: 1,
2717 },
2718 );
2719
2720 let new_uv = Rect {
2721 x: new_x as f32 / 4096.0,
2722 y: new_y as f32 / 4096.0,
2723 width: old_uv.width,
2724 height: old_uv.height,
2725 };
2726 self.image_uv_registry.put(name.clone(), new_uv);
2727 }
2728 }
2729 }
2730
2731 let text_entries: Vec<(u64, (Rect, f32, f32, f32, f32))> =
2732 self.text_cache.iter().map(|(k, v)| (*k, *v)).collect();
2733 for (hash, (old_uv, w_f, h_f, x_off, y_off)) in text_entries {
2734 let w_px = (old_uv.width * 4096.0).round() as u32;
2735 let h_px = (old_uv.height * 4096.0).round() as u32;
2736 let old_x_px = (old_uv.x * 4096.0).round() as u32;
2737 let old_y_px = (old_uv.y * 4096.0).round() as u32;
2738
2739 if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
2740 encoder.copy_texture_to_texture(
2741 wgpu::TexelCopyTextureInfo {
2742 texture: &self.mega_heim_tex,
2743 mip_level: 0,
2744 origin: wgpu::Origin3d {
2745 x: old_x_px,
2746 y: old_y_px,
2747 z: 0,
2748 },
2749 aspect: wgpu::TextureAspect::All,
2750 },
2751 wgpu::TexelCopyTextureInfo {
2752 texture: &new_mega_heim_tex,
2753 mip_level: 0,
2754 origin: wgpu::Origin3d {
2755 x: new_x,
2756 y: new_y,
2757 z: 0,
2758 },
2759 aspect: wgpu::TextureAspect::All,
2760 },
2761 wgpu::Extent3d {
2762 width: w_px,
2763 height: h_px,
2764 depth_or_array_layers: 1,
2765 },
2766 );
2767
2768 let new_uv = Rect {
2769 x: new_x as f32 / 4096.0,
2770 y: new_y as f32 / 4096.0,
2771 width: old_uv.width,
2772 height: old_uv.height,
2773 };
2774 self.text_cache.put(hash, (new_uv, w_f, h_f, x_off, y_off));
2775 }
2776 }
2777
2778 self.queue.submit(std::iter::once(encoder.finish()));
2779
2780 self.mega_heim_tex = new_mega_heim_tex;
2781 let mega_heim_view_obj = self
2782 .mega_heim_tex
2783 .create_view(&wgpu::TextureViewDescriptor::default());
2784 self.texture_views[0] = mega_heim_view_obj.clone();
2785
2786 self.rebuild_texture_array_bind_group();
2787
2788 if !self.texture_bind_groups.is_empty() {
2789 self.texture_bind_groups[0] = self.mega_heim_bind_group.clone();
2790 }
2791
2792 self.heim_packer = new_packer;
2793 self.telemetry.vram_exhausted = false;
2794 }
2795
2796 pub(crate) fn shatter_internal(
2797 &mut self,
2798 rect: Rect,
2799 pieces: u32,
2800 force: f32,
2801 color: [f32; 4],
2802 material_id: u32,
2803 ) {
2804 let count = (pieces as f32).sqrt().ceil() as u32;
2806 let dw = rect.width / count as f32;
2807 let dh = rect.height / count as f32;
2808
2809 let c = self.apply_opacity(color);
2810
2811 let cx = rect.x + rect.width * 0.5;
2812 let cy = rect.y + rect.height * 0.5;
2813
2814 for y in 0..count {
2815 for x in 0..count {
2816 let init_x = rect.x + x as f32 * dw;
2817 let init_y = rect.y + y as f32 * dh;
2818
2819 let dx = (init_x + dw * 0.5) - cx;
2821 let dy = (init_y + dh * 0.5) - cy;
2822 let dist = (dx * dx + dy * dy).sqrt().max(1.0);
2823
2824 let nx = dx / dist;
2826 let ny = dy / dist;
2827
2828 let hash =
2830 ((x as f32 * 12.9898 + y as f32 * 78.233).sin().fract() * 43_758.547).fract();
2831 let hash2 =
2832 ((x as f32 * 37.11 + y as f32 * 149.87).sin().fract() * 23_412.19).fract();
2833
2834 let speed_var = 0.5 + hash * 1.5;
2835 let angle = ny.atan2(nx) + (hash2 - 0.5) * 0.6;
2836 let disp_x = angle.cos() * force * 50.0 * speed_var;
2837 let disp_y = angle.sin() * force * 50.0 * speed_var;
2838
2839 let gravity = force * force * 20.0;
2841
2842 let scale_factor = (1.0 - (force / 6.0).min(1.0)).max(0.0);
2845 let shard_w = dw * scale_factor;
2846 let shard_h = dh * scale_factor;
2847
2848 let displaced_x = init_x + disp_x + (dw - shard_w) * 0.5;
2849 let displaced_y = init_y + disp_y + gravity + (dh - shard_h) * 0.5;
2850
2851 let shard_rect = Rect {
2852 x: displaced_x,
2853 y: displaced_y,
2854 width: shard_w,
2855 height: shard_h,
2856 };
2857
2858 let uv = Rect {
2859 x: x as f32 / count as f32,
2860 y: y as f32 / count as f32,
2861 width: 1.0 / count as f32,
2862 height: 1.0 / count as f32,
2863 };
2864
2865 self.fill_rect_with_full_params(shard_rect, c, material_id, None, force, uv);
2866 }
2867 }
2868 }
2869
2870 pub(crate) fn recursive_bolt(
2871 &mut self,
2872 from: [f32; 2],
2873 to: [f32; 2],
2874 depth: u32,
2875 color: [f32; 4],
2876 ) {
2877 if depth == 0 {
2878 self.draw_lightning_segment(from, to, color);
2879 return;
2880 }
2881
2882 let mid_x = (from[0] + to[0]) * 0.5;
2883 let mid_y = (from[1] + to[1]) * 0.5;
2884
2885 let dx = to[0] - from[0];
2886 let dy = to[1] - from[1];
2887 let len = (dx * dx + dy * dy).sqrt();
2888
2889 if len < 1e-4 {
2890 return;
2891 }
2892
2893 let offset_scale = len * 0.15;
2895 let seed = (from[0] * 12.9898 + from[1] * 78.233 + (depth as f32) * 37.11)
2896 .sin()
2897 .fract();
2898 let offset_x = -dy / len * (seed - 0.5) * offset_scale;
2899 let offset_y = dx / len * (seed - 0.5) * offset_scale;
2900
2901 let mid = [mid_x + offset_x, mid_y + offset_y];
2902
2903 self.recursive_bolt(from, mid, depth - 1, color);
2904 self.recursive_bolt(mid, to, depth - 1, color);
2905
2906 if depth > 2 && seed > 0.8 {
2908 let branch_to = [
2909 mid[0] + offset_x * 2.0 + (seed * 100.0).sin() * 50.0,
2910 mid[1] + offset_y * 2.0 + (seed * 100.0).cos() * 50.0,
2911 ];
2912 self.recursive_bolt(mid, branch_to, depth - 2, color);
2913 }
2914 }
2915
2916 pub(crate) fn draw_lightning_segment(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
2917 let dx = to[0] - from[0];
2918 let dy = to[1] - from[1];
2919 let len = (dx * dx + dy * dy).sqrt();
2920 if len < 0.001 {
2921 return;
2922 }
2923
2924 let glow_width = 32.0;
2925 let core_width = 4.0;
2926 let c = self.apply_opacity(color);
2927
2928 let gnx = -dy / len * glow_width * 0.5;
2930 let gny = dx / len * glow_width * 0.5;
2931 let gp1 = [from[0] + gnx, from[1] + gny];
2932 let gp2 = [to[0] + gnx, to[1] + gny];
2933 let gp3 = [to[0] - gnx, to[1] - gny];
2934 let gp4 = [from[0] - gnx, from[1] - gny];
2935 self.push_oriented_quad(
2936 [gp1, gp2, gp3, gp4],
2937 c,
2938 9,
2939 Rect {
2940 x: 0.0,
2941 y: 0.0,
2942 width: 1.0,
2943 height: 1.0,
2944 },
2945 );
2946
2947 let cnx = -dy / len * core_width * 0.5;
2949 let cny = dx / len * core_width * 0.5;
2950 let cp1 = [from[0] + cnx, from[1] + cny];
2951 let cp2 = [to[0] + cnx, to[1] + cny];
2952 let cp3 = [to[0] - cnx, to[1] - cny];
2953 let cp4 = [from[0] - cnx, from[1] - cny];
2954 self.push_oriented_quad(
2955 [cp1, cp2, cp3, cp4],
2956 [1.0, 1.0, 1.0, c[3]],
2957 0,
2958 Rect {
2959 x: 0.0,
2960 y: 0.0,
2961 width: 1.0,
2962 height: 1.0,
2963 },
2964 );
2965 }
2966
2967 pub(crate) fn push_oriented_quad(
2968 &mut self,
2969 points: [[f32; 2]; 4],
2970 color: [f32; 4],
2971 material_id: u32,
2972 uv_rect: Rect,
2973 ) {
2974 let scissor = self.clip_stack.last().copied();
2975 let texture_id = None; let (translation, scale_transform, rotation, _, _) = self.current_transform();
2978 let current_instance_data = InstanceData {
2979 translation,
2980 scale: scale_transform,
2981 rotation,
2982 blur_radius: 0.0,
2983 ior_override: 0.0,
2984 };
2985
2986 if self.draw_calls.is_empty()
2987 || self.current_texture_id != texture_id
2988 || self.draw_calls.last().unwrap().scissor_rect != scissor
2989 || self.instance_data.last() != Some(¤t_instance_data)
2990 {
2991 self.current_texture_id = texture_id;
2992 self.instance_data.push(current_instance_data);
2993 self.draw_calls.push(DrawCall {
2994 target_id: None,
2995 texture_id,
2996 scissor_rect: scissor,
2997 index_start: self.indices.len() as u32,
2998 index_count: 0,
2999 material: if material_id == 7 {
3000 if let cvkg_core::DrawMaterial::Glass {
3001 blur_radius,
3002 ior_override,
3003 } = self.current_draw_material
3004 {
3005 cvkg_core::DrawMaterial::Glass {
3006 blur_radius,
3007 ior_override,
3008 }
3009 } else {
3010 cvkg_core::DrawMaterial::Glass {
3011 blur_radius: 20.0,
3012 ior_override: 0.0,
3013 }
3014 }
3015 } else if material_id == 6 {
3016 cvkg_core::DrawMaterial::TopUI
3017 } else {
3018 cvkg_core::DrawMaterial::Opaque
3019 },
3020 instance_start: (self.instance_data.len() - 1) as u32,
3021 });
3022 }
3023
3024 let uvs = [
3025 [uv_rect.x, uv_rect.y],
3026 [uv_rect.x + uv_rect.width, uv_rect.y],
3027 [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
3028 [uv_rect.x, uv_rect.y + uv_rect.height],
3029 ];
3030
3031 let screen = [self.current_width() as f32, self.current_height() as f32];
3032 let rect = Rect {
3033 x: points[0][0],
3034 y: points[0][1],
3035 width: 1.0,
3036 height: 1.0,
3037 };
3038
3039 for i in 0..4 {
3040 let px = points[i][0];
3041 let py = points[i][1];
3042
3043 let (translation, scale_transform, rotation, _, _) = self.current_transform();
3044 self.vertices.push(Vertex {
3045 position: [px, py, 0.0],
3046 normal: [0.0, 0.0, 1.0],
3047 uv: uvs[i],
3048 color,
3049 material_id,
3050 radius: 0.0,
3051 slice: [0.0, 0.0, 0.0, 1.0],
3052 logical: [px - rect.x, py - rect.y],
3053 size: [rect.width, rect.height],
3054 clip: [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY],
3055 tex_index: 0,
3056 });
3057 }
3058
3059 if let Some(call) = self.draw_calls.last_mut() {
3060 call.index_count += 6;
3061 }
3062 }
3063 pub(crate) fn get_texture_id(&mut self, name: &str) -> Option<u32> {
3064 self.texture_registry.get(name).copied()
3065 }
3066
3067 pub fn fill_rect_with_mode(
3069 &mut self,
3070 rect: Rect,
3071 color: [f32; 4],
3072 material_id: u32,
3073 texture_id: Option<u32>,
3074 ) {
3075 self.fill_rect_with_full_params(
3076 rect,
3077 color,
3078 material_id,
3079 texture_id,
3080 0.0,
3081 Rect {
3082 x: 0.0,
3083 y: 0.0,
3084 width: 1.0,
3085 height: 1.0,
3086 },
3087 );
3088 }
3089
3090 pub(crate) fn fill_rect_with_full_params(
3091 &mut self,
3092 rect: Rect,
3093 color: [f32; 4],
3094 material_id: u32,
3095 texture_id: Option<u32>,
3096 radius: f32,
3097 uv_rect: Rect,
3098 ) {
3099 if let Some(shadow) = self.shadow_stack.last().copied()
3101 && shadow.color[3] > 0.001
3102 {
3103 Renderer::draw_drop_shadow(
3104 self,
3105 rect,
3106 radius,
3107 shadow.color,
3108 shadow.radius,
3109 0.0, );
3111 }
3112
3113 let slice = self
3114 .slice_stack
3115 .last()
3116 .copied()
3117 .map(|(a, o)| [a, o, 1.0, 1.0])
3118 .unwrap_or([0.0, 0.0, 0.0, 1.0]);
3119 self.fill_rect_with_full_params_and_slice(
3120 rect,
3121 color,
3122 material_id,
3123 texture_id,
3124 radius,
3125 uv_rect,
3126 slice,
3127 [0.0, 0.0],
3128 );
3129 }
3130
3131 #[allow(clippy::too_many_arguments)]
3132 pub(crate) fn fill_rect_with_full_params_and_slice(
3133 &mut self,
3134 rect: Rect,
3135 color: [f32; 4],
3136 material_id: u32,
3137 texture_id: Option<u32>,
3138 radius: f32,
3139 uv_rect: Rect,
3140 slice: [f32; 4],
3141 glyph_time: [f32; 2],
3142 ) {
3143 let scissor = self.clip_stack.last().copied();
3144
3145 let material = if material_id == 7 {
3146 if let cvkg_core::DrawMaterial::Glass {
3147 blur_radius,
3148 ior_override,
3149 } = self.current_draw_material
3150 {
3151 cvkg_core::DrawMaterial::Glass {
3152 blur_radius,
3153 ior_override,
3154 }
3155 } else {
3156 cvkg_core::DrawMaterial::Glass {
3157 blur_radius: 20.0,
3158 ior_override: 0.0,
3159 }
3160 }
3161 } else if material_id == 6 {
3162 cvkg_core::DrawMaterial::TopUI
3163 } else {
3164 cvkg_core::DrawMaterial::Opaque
3169 };
3170
3171 let (translation, scale_transform, rotation, _, _) = self.current_transform();
3172 let (blur_radius, ior_override) = if let cvkg_core::DrawMaterial::Glass {
3173 blur_radius,
3174 ior_override,
3175 } = material
3176 {
3177 (blur_radius, ior_override)
3178 } else {
3179 (0.0, 0.0)
3180 };
3181
3182 let current_instance_data = InstanceData {
3183 translation,
3184 scale: scale_transform,
3185 rotation,
3186 blur_radius,
3187 ior_override,
3188 };
3189
3190 let last_call = self.draw_calls.last();
3194 let needs_new_call = self.draw_calls.is_empty()
3195 || last_call.unwrap().scissor_rect != scissor
3196 || last_call.unwrap().material != material
3197 || self.instance_data.last() != Some(¤t_instance_data);
3198
3199 if needs_new_call {
3200 self.current_texture_id = Some(0); self.instance_data.push(current_instance_data);
3202 self.draw_calls.push(DrawCall {
3203 target_id: None,
3204 texture_id: self.current_texture_id,
3205 scissor_rect: scissor,
3206 index_start: self.indices.len() as u32,
3207 index_count: 0,
3208 material,
3209 instance_start: (self.instance_data.len() - 1) as u32,
3210 });
3211 }
3212
3213 let scale = self.current_scale_factor();
3214 let snap = |v: f32| (v * scale).round() / scale;
3215
3216 let base_idx = self.vertices.len() as u32;
3217 let x1 = snap(rect.x);
3218 let y1 = snap(rect.y);
3219 let x2 = snap(rect.x + rect.width);
3220 let y2 = snap(rect.y + rect.height);
3221 let z = self.current_z;
3222 let normal = [0.0, 0.0, 1.0];
3223 let screen = [self.current_width() as f32, self.current_height() as f32];
3224 let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
3225 x: -10000.0,
3226 y: -10000.0,
3227 width: 20000.0,
3228 height: 20000.0,
3229 });
3230 let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
3231
3232 let tex_index = texture_id.unwrap_or(0);
3233
3234 self.vertices.push(Vertex {
3235 position: [x1, y1, z],
3236 normal,
3237 uv: [uv_rect.x, uv_rect.y],
3238 color,
3239 material_id,
3240 radius,
3241 slice,
3242 logical: [0.0, 0.0],
3243 size: [rect.width, rect.height],
3244 clip,
3245 tex_index,
3246 });
3247 self.vertices.push(Vertex {
3248 position: [x2, y1, z],
3249 normal,
3250 uv: [uv_rect.x + uv_rect.width, uv_rect.y],
3251 color,
3252 material_id,
3253 radius,
3254 slice,
3255 logical: [rect.width, 0.0],
3256 size: [rect.width, rect.height],
3257 clip,
3258 tex_index,
3259 });
3260 self.vertices.push(Vertex {
3261 position: [x2, y2, z],
3262 normal,
3263 uv: [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
3264 color,
3265 material_id,
3266 radius,
3267 slice,
3268 logical: [rect.width, rect.height],
3269 size: [rect.width, rect.height],
3270 clip,
3271 tex_index,
3272 });
3273 self.vertices.push(Vertex {
3274 position: [x1, y2, z],
3275 normal,
3276 uv: [uv_rect.x, uv_rect.y + uv_rect.height],
3277 color,
3278 material_id,
3279 radius,
3280 slice,
3281 logical: [0.0, rect.height],
3282 size: [rect.width, rect.height],
3283 clip,
3284 tex_index,
3285 });
3286
3287 self.indices.extend_from_slice(&[
3288 base_idx,
3289 base_idx + 1,
3290 base_idx + 2,
3291 base_idx,
3292 base_idx + 2,
3293 base_idx + 3,
3294 ]);
3295
3296 if let Some(call) = self.draw_calls.last_mut() {
3297 call.index_count += 6;
3298 }
3299 }
3300
3301 pub fn end_frame(&mut self, mut encoder: wgpu::CommandEncoder) {
3316 struct ActiveFrameResources {
3317 surface_texture: Option<wgpu::SurfaceTexture>,
3318 target_view: wgpu::TextureView,
3319 scene_texture: wgpu::TextureView,
3320 scene_msaa_texture: wgpu::TextureView,
3321 depth_texture_view: wgpu::TextureView,
3322 blur_env_bind_group_a: wgpu::BindGroup,
3323 blur_env_bind_group_b: wgpu::BindGroup,
3324 bloom_env_bind_group_a: wgpu::BindGroup,
3325 bloom_env_bind_group_b: wgpu::BindGroup,
3326 }
3327
3328 let res = if let Some(window_id) = self.current_window {
3329 let Some(ctx) = self.surfaces.get(&window_id) else {
3330 log::error!("[GPU] Missing surface context for end_frame");
3331 return;
3332 };
3333 let frame = match ctx.surface.get_current_texture() {
3334 wgpu::CurrentSurfaceTexture::Success(t) => t,
3335 wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
3336 ctx.surface.configure(&self.device, &ctx.config);
3337 t
3338 }
3339 other => {
3340 log::warn!(
3341 "[GPU] Surface texture acquisition failed ({:?}), reconfiguring surface",
3342 other
3343 );
3344 ctx.surface.configure(&self.device, &ctx.config);
3345 self.queue.submit(std::iter::once(encoder.finish()));
3346 return;
3347 }
3348 };
3349 let view = frame
3350 .texture
3351 .create_view(&wgpu::TextureViewDescriptor::default());
3352
3353 ActiveFrameResources {
3354 surface_texture: Some(frame),
3355 target_view: view,
3356 scene_texture: ctx.scene_texture.clone(),
3357 scene_msaa_texture: ctx.scene_msaa_texture.clone(),
3358 depth_texture_view: ctx.depth_texture_view.clone(),
3359 blur_env_bind_group_a: ctx.blur_env_bind_group_a.clone(),
3360 blur_env_bind_group_b: ctx.blur_env_bind_group_b.clone(),
3361 bloom_env_bind_group_a: ctx.bloom_env_bind_group_a.clone(),
3362 bloom_env_bind_group_b: ctx.bloom_env_bind_group_b.clone(),
3363 }
3364 } else {
3365 let Some(ctx) = self.headless_context.as_ref() else {
3366 log::error!("[GPU] No headless context for end_frame");
3367 return;
3368 };
3369
3370 ActiveFrameResources {
3371 surface_texture: None,
3372 target_view: ctx.output_view.clone(),
3373 scene_texture: ctx.scene_texture.clone(),
3374 scene_msaa_texture: ctx.scene_msaa_texture.clone(),
3375 depth_texture_view: ctx.depth_texture_view.clone(),
3376 blur_env_bind_group_a: ctx.blur_env_bind_group_a.clone(),
3377 blur_env_bind_group_b: ctx.blur_env_bind_group_b.clone(),
3378 bloom_env_bind_group_a: ctx.bloom_env_bind_group_a.clone(),
3379 bloom_env_bind_group_b: ctx.bloom_env_bind_group_b.clone(),
3380 }
3381 };
3382
3383 let has_glass = self
3385 .draw_calls
3386 .iter()
3387 .any(|c| matches!(c.material, cvkg_core::DrawMaterial::Glass { .. }));
3388 let has_bloom = self.bloom_enabled;
3389 let has_accessibility =
3390 self.color_blind_mode != crate::color_blindness::ColorBlindMode::Normal;
3391
3392 let (blur_id, bloom_id) = if let Some(window_id) = self.current_window {
3402 let ctx = self.surfaces.get(&window_id).unwrap();
3403 (ctx.blur_tex_a, ctx.bloom_tex_a)
3404 } else {
3405 let ctx = self.headless_context.as_ref().unwrap();
3406 (ctx.blur_tex_a, ctx.bloom_tex_a)
3407 };
3408 self.registry.alias(kvasir::nodes::RES_BLUR_A, blur_id);
3409 self.registry.alias(kvasir::nodes::RES_BLOOM_A, bloom_id);
3410 self.registry
3411 .alias_view(kvasir::nodes::RES_SCENE, res.scene_texture.clone());
3412 self.registry.alias_view(
3413 kvasir::nodes::RES_SCENE_MSAA,
3414 res.scene_msaa_texture.clone(),
3415 );
3416
3417 let scale = self.current_scale_factor();
3418 let scale_bits = scale.to_bits();
3419 let active_offscreens_count = self.active_offscreens.len();
3420 let portal_regions_count = self.portal_regions.len();
3421 let width = self.current_width();
3422 let height = self.current_height();
3423 let has_volumetric = self.volumetric_enabled;
3424
3425 let use_cache = if let Some(ref cached) = self.cached_graph_plan {
3426 cached.matches(
3427 has_glass,
3428 has_bloom,
3429 has_accessibility,
3430 has_volumetric,
3431 active_offscreens_count,
3432 portal_regions_count,
3433 width,
3434 height,
3435 scale_bits,
3436 )
3437 } else {
3438 false
3439 };
3440
3441 if !use_cache {
3442 let render_graph = kvasir::nodes::build_render_graph(&kvasir::nodes::RenderGraphConfig {
3443 has_glass,
3444 has_bloom,
3445 has_accessibility,
3446 has_volumetric,
3447 active_offscreens: &self.active_offscreens,
3448 portal_regions: &self.portal_regions.iter().cloned().collect::<Vec<_>>(),
3449 width,
3450 height,
3451 scale,
3452 });
3453 let planner = kvasir::planner::ExecutionPlanner::new(&render_graph);
3454 let compiled_plan = planner.compile().expect("RenderGraph cycle detected!");
3455
3456 self.cached_graph_plan = Some(kvasir::graph_cache::CachedGraphPlan {
3457 has_glass,
3458 has_bloom,
3459 has_accessibility,
3460 has_volumetric,
3461 active_offscreens_count,
3462 portal_regions_count,
3463 width,
3464 height,
3465 scale_bits,
3466 graph: render_graph,
3467 plan: compiled_plan,
3468 });
3469 }
3470
3471 let cached = self.cached_graph_plan.as_ref().unwrap();
3472 for &pass_id in &cached.plan {
3473 if let Some(node) = cached.graph.node(pass_id) {
3474 log::trace!("[Kvasir] Executing node: {}", node.label());
3475 let mut ctx = kvasir::node::ExecutionContext {
3476 device: &self.device,
3477 queue: &self.queue,
3478 encoder: &mut encoder,
3479 registry: &self.registry,
3480 renderer: self,
3481 target_view: &res.target_view,
3482 depth_view: &res.depth_texture_view,
3483 blur_env_bind_group_a: &res.blur_env_bind_group_a,
3484 blur_env_bind_group_b: &res.blur_env_bind_group_b,
3485 bloom_env_bind_group_a: &res.bloom_env_bind_group_a,
3486 bloom_env_bind_group_b: &res.bloom_env_bind_group_b,
3487 scale_factor: scale,
3488 };
3489 node.execute(&mut ctx);
3490 }
3491 }
3492
3493 self.staging_command_buffers.push(encoder.finish());
3498
3499 if let (Some(q), Some(b), Some(rb)) = (
3501 &self.skuld_queries,
3502 &self.skuld_buffer,
3503 &self.skuld_read_buffer,
3504 ) {
3505 let mut resolve_encoder =
3506 self.device
3507 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
3508 label: Some("Skuld Resolve Encoder"),
3509 });
3510 resolve_encoder.resolve_query_set(q, 0..2, b, 0);
3511 resolve_encoder.copy_buffer_to_buffer(b, 0, rb, 0, 16);
3512 self.staging_command_buffers.push(resolve_encoder.finish());
3513 }
3514
3515 let cmds = std::mem::take(&mut self.staging_command_buffers);
3516 self.queue.submit(cmds);
3517 self.telemetry.frame_time_ms = self.last_frame_start.elapsed().as_secs_f32() * 1000.0;
3518 self.update_vram_telemetry();
3519
3520 if let Some(f) = res.surface_texture {
3521 f.present();
3522 }
3523 }
3524}
3525
3526impl Drop for SurtrRenderer {
3527 fn drop(&mut self) {
3528 let _ = self.device.poll(wgpu::PollType::Wait {
3530 submission_index: None,
3531 timeout: None,
3532 });
3533 }
3534}
3535
3536impl SurtrRenderer {
3537 pub fn submit_buckets(&mut self, buckets: &cvkg_compositor::CommandBuckets) {
3546 let mut active_offscreens = Vec::new();
3548 let mut current_target_id = None;
3549
3550 for cmd in &buckets.scene_commands {
3551 match cmd {
3552 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
3553 self.set_material(cvkg_core::DrawMaterial::Opaque);
3554 self.submit_routed(routed, current_target_id);
3555 }
3556 cvkg_compositor::engine::RenderCommand::PushOffscreen {
3557 source_layer,
3558 material,
3559 bounds,
3560 } => {
3561 current_target_id = Some(source_layer.0);
3562
3563 let width = (bounds.width).max(1.0) as u32;
3565 let height = (bounds.height).max(1.0) as u32;
3566 self.registry
3567 .allocate_offscreen(&self.device, source_layer.0, [width, height]);
3568
3569 if let cvkg_compositor::Material::ShaderEffect {
3570 effect_name,
3571 params_json: _,
3572 ..
3573 } = material
3574 {
3575 active_offscreens.push(crate::types::OffscreenEffectConfig {
3576 target_id: source_layer.0,
3577 effect: effect_name.clone(),
3578 blend_mode: 0, effect_args: [0.0; 16], });
3581 }
3582 }
3583 cvkg_compositor::engine::RenderCommand::PopOffscreen => {
3584 current_target_id = None;
3585 }
3586 }
3587 }
3588 self.active_offscreens = active_offscreens;
3589
3590 for cmd in &buckets.glass_commands {
3592 if let cvkg_compositor::engine::RenderCommand::Draw(routed) = cmd {
3593 let core_material = match routed.material {
3594 cvkg_compositor::Material::Opaque => cvkg_core::DrawMaterial::Opaque,
3595 cvkg_compositor::Material::Glass {
3596 blur_radius,
3597 depth_index: _,
3598 } => cvkg_core::DrawMaterial::Glass {
3599 blur_radius,
3600 ior_override: 0.0,
3601 },
3602 cvkg_compositor::Material::Overlay => cvkg_core::DrawMaterial::TopUI,
3603 _ => cvkg_core::DrawMaterial::Opaque,
3604 };
3605 self.set_material(core_material);
3606 self.submit_routed(routed, None);
3607 }
3608 }
3609
3610 for cmd in &buckets.overlay_commands {
3612 if let cvkg_compositor::engine::RenderCommand::Draw(routed) = cmd {
3613 self.set_material(cvkg_core::DrawMaterial::TopUI);
3614 self.submit_routed(routed, None);
3615 }
3616 }
3617 }
3618
3619 pub(crate) fn submit_routed(
3621 &mut self,
3622 routed: &cvkg_compositor::RoutedDrawCommand,
3623 target_id: Option<u64>,
3624 ) {
3625 let cmd = &routed.command;
3626 if cmd.index_count == 0 {
3627 return;
3628 }
3629 let material = match &routed.material {
3630 cvkg_compositor::Material::Glass { blur_radius, .. } => {
3631 cvkg_core::DrawMaterial::Glass {
3632 blur_radius: *blur_radius,
3633 ior_override: 0.0,
3634 }
3635 }
3636 cvkg_compositor::Material::Overlay => cvkg_core::DrawMaterial::TopUI,
3637 _ => cvkg_core::DrawMaterial::Opaque,
3638 };
3639 self.draw_calls.push(DrawCall {
3640 texture_id: cmd.texture_id,
3641 scissor_rect: cmd.scissor_rect,
3642 index_start: cmd.index_start,
3643 index_count: cmd.index_count,
3644 material,
3645 target_id,
3646 instance_start: cmd.instance_id,
3647 });
3648 }
3649}
3650
3651impl SurtrRenderer {
3652 pub(crate) fn apply_opacity(&self, mut color: [f32; 4]) -> [f32; 4] {
3654 if let Some(&alpha) = self.opacity_stack.last() {
3655 color[3] *= alpha;
3656 }
3657 color
3658 }
3659
3660 pub fn load_svg(&mut self, name: &str, data: &[u8]) {
3662 if self.svg_cache.contains(name) {
3663 return;
3664 }
3665
3666 let mut opt = usvg::Options::default();
3667 opt.fontdb_mut().load_system_fonts();
3668 let tree = match usvg::Tree::from_data(data, &opt) {
3669 Ok(t) => t,
3670 Err(e) => {
3671 log::error!("Failed to parse SVG '{}': {:?}, skipping load", name, e);
3672 return;
3673 }
3674 };
3675
3676 let view_box = Rect {
3677 x: 0.0,
3678 y: 0.0,
3679 width: tree.size().width(),
3680 height: tree.size().height(),
3681 };
3682
3683 let parsed_animations = parse_svg_animations(data);
3684
3685 let mut vertices = Vec::new();
3686 let mut indices = Vec::new();
3687 let mut fill_tessellator = FillTessellator::new();
3688 let mut stroke_tessellator = StrokeTessellator::new();
3689 let mut finalized_animations = Vec::new();
3690
3691 for child in tree.root().children() {
3692 let mut tess_params = TessellateParams {
3693 fill_tessellator: &mut fill_tessellator,
3694 stroke_tessellator: &mut stroke_tessellator,
3695 vertices: &mut vertices,
3696 indices: &mut indices,
3697 parsed_animations: &parsed_animations,
3698 finalized_animations: &mut finalized_animations,
3699 };
3700 self.tessellate_node(child, &mut tess_params);
3701 }
3702
3703 self.svg_cache.put(
3704 name.to_string(),
3705 SvgModel {
3706 vertices,
3707 indices,
3708 view_box,
3709 animations: finalized_animations,
3710 },
3711 );
3712 self.svg_trees.put(name.to_string(), tree);
3713 }
3714
3715 pub(crate) fn tessellate_node(&self, node: &usvg::Node, params: &mut TessellateParams<'_>) {
3716 let start_idx = params.vertices.len();
3717 let node_id = match node {
3718 usvg::Node::Group(g) => g.id().to_string(),
3719 usvg::Node::Path(p) => p.id().to_string(),
3720 _ => String::new(),
3721 };
3722
3723 if let usvg::Node::Group(ref group) = *node {
3724 for child in group.children() {
3725 let mut child_params = TessellateParams {
3726 fill_tessellator: params.fill_tessellator,
3727 stroke_tessellator: params.stroke_tessellator,
3728 vertices: params.vertices,
3729 indices: params.indices,
3730 parsed_animations: params.parsed_animations,
3731 finalized_animations: params.finalized_animations,
3732 };
3733 self.tessellate_node(child, &mut child_params);
3734 }
3735 } else if let usvg::Node::Path(ref path) = *node {
3736 let has_fill = path.fill().is_some();
3737 let has_stroke = path.stroke().is_some();
3738
3739 if !has_fill && !has_stroke {
3741 log::debug!("SVG path '{}' has no fill or stroke, skipping", node_id);
3742 return;
3743 }
3744
3745 let lyon_path = usvg_to_lyon(path, node.abs_transform());
3746 let screen = [4096.0, 4096.0]; let clip = [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY]; if has_fill && let Some(fill) = path.fill() {
3751 let color = match fill.paint() {
3752 usvg::Paint::Color(c) => [
3753 c.red as f32 / 255.0,
3754 c.green as f32 / 255.0,
3755 c.blue as f32 / 255.0,
3756 fill.opacity().get(),
3757 ],
3758 usvg::Paint::LinearGradient(_)
3759 | usvg::Paint::RadialGradient(_)
3760 | usvg::Paint::Pattern(_) => {
3761 log::warn!(
3762 "SVG path '{}' uses gradient/pattern fill which is not supported, using white fallback",
3763 node_id
3764 );
3765 [1.0, 1.0, 1.0, 1.0]
3766 }
3767 };
3768
3769 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
3770 let base_vertex_idx = params.vertices.len() as u32;
3771
3772 if let Err(e) = params.fill_tessellator.tessellate_path(
3773 &lyon_path,
3774 &FillOptions::default(),
3775 &mut BuffersBuilder::new(&mut buffers, SceneVertexConstructor { color }),
3776 ) {
3777 log::warn!(
3778 "SVG fill tessellation failed for path '{}': {:?}, skipping",
3779 node_id,
3780 e
3781 );
3782 return;
3783 }
3784
3785 params.vertices.extend(buffers.vertices);
3786 for idx in buffers.indices {
3787 params.indices.push(base_vertex_idx + idx);
3788 }
3789 }
3790
3791 if has_stroke && let Some(stroke) = path.stroke() {
3793 let base_vertex_idx = params.vertices.len() as u32;
3794 let stroke_width = stroke.width().get(); let color = match stroke.paint() {
3796 usvg::Paint::Color(c) => [
3797 c.red as f32 / 255.0,
3798 c.green as f32 / 255.0,
3799 c.blue as f32 / 255.0,
3800 stroke.opacity().get(),
3801 ],
3802 usvg::Paint::LinearGradient(_)
3803 | usvg::Paint::RadialGradient(_)
3804 | usvg::Paint::Pattern(_) => {
3805 log::warn!(
3806 "SVG path '{}' uses gradient/pattern stroke which is not supported, using white fallback",
3807 node_id
3808 );
3809 [1.0, 1.0, 1.0, 1.0]
3810 }
3811 };
3812
3813 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
3814
3815 let path_length = lyon::algorithms::length::approximate_length(&lyon_path, 0.1);
3816
3817 if let Err(e) = params.stroke_tessellator.tessellate_path(
3818 &lyon_path,
3819 &StrokeOptions::default().with_line_width(stroke_width),
3820 &mut BuffersBuilder::new(
3821 &mut buffers,
3822 CustomStrokeVertexConstructor { color, clip, path_length },
3823 ),
3824 ) {
3825 log::warn!(
3826 "SVG stroke tessellation failed for path '{}': {:?}, skipping",
3827 node_id,
3828 e
3829 );
3830 return;
3831 }
3832
3833 params.vertices.extend(buffers.vertices);
3834 for idx in buffers.indices {
3835 params.indices.push(base_vertex_idx + idx);
3836 }
3837 }
3838 }
3839
3840 let end_idx = params.vertices.len();
3841 if !node_id.is_empty() && start_idx < end_idx {
3842 for anim in params.parsed_animations {
3843 if anim.target_id == node_id {
3844 let mut final_anim = anim.clone();
3845 final_anim.vertex_range = start_idx..end_idx;
3846 params.finalized_animations.push(final_anim);
3847 }
3848 }
3849 }
3850 }
3851
3852 pub fn draw_svg(&mut self, name: &str, rect: Rect, color: Option<[f32; 4]>, material_id: u32) {
3854 let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
3855 x: -10000.0,
3856 y: -10000.0,
3857 width: 20000.0,
3858 height: 20000.0,
3859 });
3860 let scale = self.current_scale_factor();
3861 let screen_w = self.current_width() as f32 / scale;
3862 let screen_h = self.current_height() as f32 / scale;
3863
3864 if rect.x > clip_rect.x + clip_rect.width
3865 || rect.x + rect.width < clip_rect.x
3866 || rect.y > clip_rect.y + clip_rect.height
3867 || rect.y + rect.height < clip_rect.y
3868 {
3869 return;
3870 }
3871
3872 log::info!("DRAW_SVG '{}' called with rect: {:?}, model_view_box: {:?}", name, rect, self.svg_cache.get(name).map(|m| m.view_box));
3873
3874 if rect.x > screen_w
3875 || rect.x + rect.width < 0.0
3876 || rect.y > screen_h
3877 || rect.y + rect.height < 0.0
3878 {
3879 return;
3880 }
3881
3882 let model = if let Some(m) = self.svg_cache.get(name) {
3883 m.clone()
3884 } else {
3885 return;
3886 };
3887
3888 let _scale_x = rect.width / model.view_box.width;
3889 let _scale_y = rect.height / model.view_box.height;
3890 let base_idx = self.vertices.len() as u32;
3891 let screen = [self.current_width() as f32, self.current_height() as f32];
3892 let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
3893 x: -10000.0,
3894 y: -10000.0,
3895 width: 20000.0,
3896 height: 20000.0,
3897 });
3898 let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
3899 let scale = self.current_scale_factor();
3900 let snap = |v: f32| (v * scale).round() / scale;
3901
3902 let mut local_vertices = model.vertices.clone();
3903 for anim in &model.animations {
3904 let t = (self.current_scene.time % anim.duration) / anim.duration;
3905 let val = anim.from_val + (anim.to_val - anim.from_val) * t;
3906
3907 if anim.attribute_name == "transform" {
3908 let mut min_x = f32::MAX;
3910 let mut min_y = f32::MAX;
3911 let mut max_x = f32::MIN;
3912 let mut max_y = f32::MIN;
3913 for i in anim.vertex_range.clone() {
3914 let p = local_vertices[i].position;
3915 if p[0] < min_x {
3916 min_x = p[0];
3917 }
3918 if p[1] < min_y {
3919 min_y = p[1];
3920 }
3921 if p[0] > max_x {
3922 max_x = p[0];
3923 }
3924 if p[1] > max_y {
3925 max_y = p[1];
3926 }
3927 }
3928 let cx = (min_x + max_x) * 0.5;
3929 let cy = (min_y + max_y) * 0.5;
3930
3931 let c = val.to_radians().cos();
3932 let s = val.to_radians().sin();
3933
3934 for i in anim.vertex_range.clone() {
3935 let p = local_vertices[i].position;
3936 let dx = p[0] - cx;
3937 let dy = p[1] - cy;
3938 local_vertices[i].position[0] = cx + dx * c - dy * s;
3939 local_vertices[i].position[1] = cy + dx * s + dy * c;
3940 }
3941 } else if anim.attribute_name == "opacity" {
3942 for i in anim.vertex_range.clone() {
3943 local_vertices[i].color[3] = val;
3944 }
3945 } else if anim.attribute_name == "stroke-dashoffset" {
3946 for i in anim.vertex_range.clone() {
3951 let v = &mut local_vertices[i];
3952 v.slice[3] = 1.0 - val;
3953 }
3954 }
3955 }
3956
3957 let (blur_radius, ior_override) = if material_id == 7 {
3958 if let cvkg_core::DrawMaterial::Glass {
3959 blur_radius,
3960 ior_override,
3961 } = self.current_draw_material
3962 {
3963 (blur_radius, ior_override)
3964 } else {
3965 (20.0, 0.0)
3966 }
3967 } else {
3968 (0.0, 0.0)
3969 };
3970 for mut v in local_vertices {
3971 let rel_x = (v.position[0] - model.view_box.x) / model.view_box.width;
3972 let rel_y = (v.position[1] - model.view_box.y) / model.view_box.height;
3973
3974 v.position[0] = snap(rect.x + rel_x * rect.width);
3975 v.position[1] = snap(rect.y + rel_y * rect.height);
3976 v.position[2] = self.current_z;
3977 v.logical = [v.position[0], v.position[1]];
3978
3979 v.clip = clip;
3980 v.material_id = material_id;
3981
3982 if let Some(override_color) = color {
3983 let mut c = override_color;
3984 c[3] *= v.color[3]; v.color = self.apply_opacity(c);
3986 } else {
3987 v.color = self.apply_opacity(v.color);
3988 }
3989 self.vertices.push(v);
3990 }
3991
3992 for idx in &model.indices {
3993 self.indices.push(base_idx + *idx);
3994 }
3995
3996 let material = match material_id {
3997 7 => cvkg_core::DrawMaterial::Glass {
3998 blur_radius,
3999 ior_override,
4000 },
4001 0 => cvkg_core::DrawMaterial::Opaque,
4002 _ => cvkg_core::DrawMaterial::TopUI,
4003 };
4004 let tid = self.get_texture_id("__mega_heim");
4005
4006 let (translation, scale_transform, rotation, _, _) = self.current_transform();
4007 let current_instance_data = InstanceData {
4008 translation,
4009 scale: scale_transform,
4010 rotation,
4011 blur_radius,
4012 ior_override,
4013 };
4014
4015 let last_call = self.draw_calls.last();
4016 let needs_new_call = self.draw_calls.is_empty()
4017 || self.current_texture_id != tid
4018 || last_call.unwrap().scissor_rect != self.clip_stack.last().copied()
4019 || last_call.unwrap().material != material
4020 || self.instance_data.last() != Some(¤t_instance_data);
4021
4022 if needs_new_call {
4023 self.current_texture_id = tid;
4024 self.instance_data.push(current_instance_data);
4025 self.draw_calls.push(DrawCall {
4026 target_id: None,
4027 texture_id: tid,
4028 scissor_rect: self.clip_stack.last().copied(),
4029 index_start: (self.indices.len() - model.indices.len()) as u32,
4030 index_count: 0,
4031 material,
4032 instance_start: (self.instance_data.len() - 1) as u32,
4033 });
4034 }
4035
4036 if let Some(call) = self.draw_calls.last_mut() {
4037 call.index_count += model.indices.len() as u32;
4038 }
4039 }
4040
4041 pub async fn forge_headless(width: u32, height: u32) -> Self {
4043 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
4044 backends: wgpu::Backends::all(),
4045 flags: wgpu::InstanceFlags::default(),
4046 backend_options: wgpu::BackendOptions::default(),
4047 display: None,
4048 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
4049 });
4050
4051 log::info!("[GPU] Requesting HighPerformance adapter (headless)...");
4053 let mut adapter = instance
4054 .request_adapter(&wgpu::RequestAdapterOptions {
4055 power_preference: wgpu::PowerPreference::HighPerformance,
4056 compatible_surface: None,
4057 force_fallback_adapter: false,
4058 })
4059 .await
4060 .ok();
4061
4062 if adapter.is_none() {
4063 log::warn!(
4064 "[GPU] HighPerformance adapter failed (possible Bumblebee/Optimus), trying LowPower..."
4065 );
4066 adapter = instance
4067 .request_adapter(&wgpu::RequestAdapterOptions {
4068 power_preference: wgpu::PowerPreference::LowPower,
4069 compatible_surface: None,
4070 force_fallback_adapter: false,
4071 })
4072 .await
4073 .ok();
4074 }
4075
4076 if adapter.is_none() {
4077 log::warn!("[GPU] Hardware adapters failed, trying Software fallback...");
4078 adapter = instance
4079 .request_adapter(&wgpu::RequestAdapterOptions {
4080 power_preference: wgpu::PowerPreference::LowPower,
4081 compatible_surface: None,
4082 force_fallback_adapter: true,
4083 })
4084 .await
4085 .ok();
4086 }
4087
4088 let adapter = adapter.expect("Failed to find a suitable GPU for Surtr");
4089 let info = adapter.get_info();
4090 log::info!(
4091 "[GPU] Selected adapter: {} ({:?}) on backend: {:?}",
4092 info.name,
4093 info.device_type,
4094 info.backend
4095 );
4096 log::info!("[GPU] Driver info: {} - {}", info.driver, info.driver_info);
4097 let required_features = adapter.features()
4098 & (wgpu::Features::TIMESTAMP_QUERY
4099 | wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
4100 | wgpu::Features::TEXTURE_BINDING_ARRAY);
4101
4102 let (device, queue) = adapter
4103 .request_device(&wgpu::DeviceDescriptor {
4104 label: Some("Surtr Headless Forge"),
4105 required_features,
4106 required_limits: wgpu::Limits {
4107 max_bindings_per_bind_group: adapter
4108 .limits()
4109 .max_bindings_per_bind_group
4110 .min(256),
4111 max_binding_array_elements_per_shader_stage: adapter
4112 .limits()
4113 .max_binding_array_elements_per_shader_stage
4114 .min(256),
4115 ..wgpu::Limits::default()
4116 },
4117 memory_hints: wgpu::MemoryHints::default(),
4118 experimental_features: wgpu::ExperimentalFeatures::disabled(),
4119 trace: wgpu::Trace::Off,
4120 })
4121 .await
4122 .expect("Failed to create Surtr device");
4123
4124 let instance = Arc::new(instance);
4125 let adapter = Arc::new(adapter);
4126
4127 device.on_uncaptured_error(Arc::new(|error| {
4128 log::error!(
4129 "[GPU] Uncaptured device error (Device Lost or Panic): {:?}",
4130 error
4131 );
4132 }));
4133
4134 let device = Arc::new(device);
4135 let queue = Arc::new(queue);
4136
4137 Self::forge_internal(
4138 instance,
4139 adapter,
4140 device,
4141 queue,
4142 None,
4143 Some((width, height, wgpu::TextureFormat::Rgba8UnormSrgb)),
4144 )
4145 .await
4146 }
4147
4148 pub async fn capture_frame(&self) -> Result<Vec<u8>, String> {
4150 let ctx = self
4151 .headless_context
4152 .as_ref()
4153 .ok_or("Headless context required for capture")?;
4154 let current_width = self.current_width();
4155 let current_height = self.current_height();
4156
4157 let u32_size = std::mem::size_of::<u32>() as u32;
4158 let width = ctx.width;
4159 let height = ctx.height;
4160 let bytes_per_row = width * u32_size;
4161 let padding = (256 - (bytes_per_row % 256)) % 256;
4162 let padded_bytes_per_row = bytes_per_row + padding;
4163
4164 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
4165 label: Some("Capture Buffer"),
4166 size: (padded_bytes_per_row as u64 * height as u64),
4167 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
4168 mapped_at_creation: false,
4169 });
4170
4171 let mut encoder = self
4172 .device
4173 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
4174 label: Some("Capture Encoder"),
4175 });
4176
4177 encoder.copy_texture_to_buffer(
4178 wgpu::TexelCopyTextureInfo {
4179 texture: &ctx.output_texture,
4180 mip_level: 0,
4181 origin: wgpu::Origin3d::ZERO,
4182 aspect: wgpu::TextureAspect::All,
4183 },
4184 wgpu::TexelCopyBufferInfo {
4185 buffer: &output_buffer,
4186 layout: wgpu::TexelCopyBufferLayout {
4187 offset: 0,
4188 bytes_per_row: Some(padded_bytes_per_row),
4189 rows_per_image: Some(height),
4190 },
4191 },
4192 wgpu::Extent3d {
4193 width,
4194 height,
4195 depth_or_array_layers: 1,
4196 },
4197 );
4198
4199 self.queue.submit(Some(encoder.finish()));
4200
4201 let buffer_slice = output_buffer.slice(..);
4202 let (sender, receiver) = futures::channel::oneshot::channel();
4203 buffer_slice.map_async(wgpu::MapMode::Read, move |v| {
4204 let _ = sender.send(v);
4205 });
4206
4207 let _ = self.device.poll(wgpu::PollType::Wait {
4208 submission_index: None,
4209 timeout: None,
4210 });
4211
4212 if let Ok(Ok(_)) = receiver.await {
4213 let data = buffer_slice.get_mapped_range();
4214 let mut result = Vec::with_capacity((width * height * 4) as usize);
4215
4216 for y in 0..height {
4217 let start = (y * padded_bytes_per_row) as usize;
4218 let end = start + bytes_per_row as usize;
4219 result.extend_from_slice(&data[start..end]);
4220 }
4221
4222 log::trace!(
4223 "[GPU] capture_frame: data len={}, first 4 bytes={:?}",
4224 data.len(),
4225 &data[0..4.min(data.len())]
4226 );
4227
4228 drop(data);
4229 output_buffer.unmap();
4230 Ok(result)
4231 } else {
4232 Err("Failed to capture frame".to_string())
4233 }
4234 }
4235
4236 pub(crate) fn current_width(&self) -> u32 {
4237 if let Some(id) = self.current_window {
4238 self.surfaces.get(&id).map(|s| s.config.width).unwrap_or(1)
4239 } else {
4240 self.headless_context.as_ref().map(|h| h.width).unwrap_or(1)
4241 }
4242 }
4243
4244 pub(crate) fn current_height(&self) -> u32 {
4245 if let Some(id) = self.current_window {
4246 self.surfaces.get(&id).map(|s| s.config.height).unwrap_or(1)
4247 } else {
4248 self.headless_context
4249 .as_ref()
4250 .map(|h| h.height)
4251 .unwrap_or(1)
4252 }
4253 }
4254
4255 pub(crate) fn current_scale_factor(&self) -> f32 {
4256 if let Some(id) = self.current_window {
4257 self.surfaces
4258 .get(&id)
4259 .map(|s| s.scale_factor)
4260 .unwrap_or(1.0)
4261 } else {
4262 self.headless_context
4263 .as_ref()
4264 .map(|h| h.scale_factor)
4265 .unwrap_or(1.0)
4266 }
4267 }
4268
4269 pub(crate) fn current_time(&self) -> f32 {
4272 self.start_time.elapsed().as_secs_f32()
4273 }
4274
4275 pub(crate) fn find_filter<'a>(
4277 tree: &'a usvg::Tree,
4278 filter_id: &str,
4279 ) -> Option<&'a usvg::filter::Filter> {
4280 tree.filters()
4281 .iter()
4282 .find(|f| f.id() == filter_id)
4283 .map(|arc| arc.as_ref())
4284 }
4285}
4286
4287#[cfg(test)]
4288mod wgsl_tests {
4289 #[test]
4290 fn test_wgsl() {
4291 let source = include_str!("shaders/effects.wgsl");
4292 let mut frontend = naga::front::wgsl::Frontend::new();
4293 match frontend.parse(source) {
4294 Ok(_) => println!("WGSL parsed successfully!"),
4295 Err(e) => {
4296 panic!("WGSL parsing failed: \n{}", e.emit_to_string(source));
4297 }
4298 }
4299 }
4300}