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 = std::collections::HashMap::new();
1199 let mut texture_bind_groups = Vec::new();
1200
1201 texture_registry.insert("__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: LruCache::new(NonZeroUsize::new(256).unwrap()),
1591 texture_registry: LruCache::new(NonZeroUsize::new(255).unwrap()),
1592 texture_views: texture_views_list,
1593 dummy_sampler,
1594 svg_cache: LruCache::new(NonZeroUsize::new(128).unwrap()),
1595 svg_trees: LruCache::new(NonZeroUsize::new(128).unwrap()),
1596 filter_engine: Some(
1597 cvkg_svg_filters::FilterEngine::new(cvkg_svg_filters::GpuContext {
1598 device: device.clone(),
1599 queue: queue.clone(),
1600 })
1601 .expect("Failed to create SVG filter engine"),
1602 ),
1603 filter_batches: Vec::new(),
1604 dummy_texture_bind_group,
1605 dummy_env_bind_group,
1606 texture_bind_group_layout,
1607 texture_bind_groups,
1608 shared_elements: LruCache::new(NonZeroUsize::new(1024).unwrap()),
1609 vertex_buffer,
1610 index_buffer,
1611 instance_buffer,
1612 vertices: Vec::with_capacity(MAX_VERTICES),
1613 indices: Vec::with_capacity(MAX_INDICES),
1614 instance_data: Vec::with_capacity(MAX_VERTICES / 4),
1615 draw_calls: Vec::new(),
1616 current_texture_id: None,
1617 opacity_stack: vec![1.0],
1618 clip_stack: Vec::new(),
1619 slice_stack: Vec::new(),
1620 shadow_stack: Vec::new(),
1621 theme_buffer,
1622 scene_buffer,
1623 berserker_bind_group,
1624 berserker_bind_group_layout,
1625 start_time: std::time::Instant::now(),
1626 current_theme,
1627 current_scene,
1628 background_pipeline,
1629 current_z: 0.0,
1630 telemetry: cvkg_core::TelemetryData::default(),
1631 last_frame_start: std::time::Instant::now(),
1632 last_redraw_start: std::time::Instant::now(),
1633 frame_budget: cvkg_core::FrameBudget::default(),
1634 capture_staging_buffer: None,
1635 compositor_index_cursor: 0,
1636 vram_buffers_bytes: 0,
1637 vram_textures_bytes: 0,
1638 _debug_layout: false,
1639 transform_stack: Vec::new(),
1640 redraw_requested: false,
1641 skuld_queries,
1642 skuld_buffer,
1643 skuld_read_buffer,
1644 skuld_period,
1645 last_gpu_time_ns: 0,
1646 vnode_stack: Vec::new(),
1647 event_handlers: std::collections::HashMap::new(),
1648 staging_belt,
1649 staging_command_buffers: Vec::new(),
1650 glass_output_bind_group_layout,
1651 current_draw_material: cvkg_core::DrawMaterial::Opaque,
1652 portal_regions: VecDeque::new(),
1653 cached_graph_plan: None,
1654 memo_cache: std::collections::HashMap::new(),
1655 bloom_enabled: true,
1656 volumetric_enabled: false,
1657 color_blind_mode: crate::color_blindness::ColorBlindMode::Normal,
1658 color_blind_intensity: 1.0,
1659 color_blind_pipeline,
1660 volumetric_pipeline,
1661 volumetric_bind_group_layout: volumetric_bgl,
1662 volumetric_uniform_buffer,
1663 color_blind_bind_group_layout: color_blind_bgl,
1664 color_blind_uniform_buffer,
1665 sampler,
1666 kawase_down_pipeline,
1667 kawase_up_pipeline,
1668 kawase_bind_group_layout: kawase_bgl,
1669 kawase_uniform: device.create_buffer(&wgpu::BufferDescriptor {
1670 label: Some("Kawase Persistent Uniform"),
1671 size: 32,
1672 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1673 mapped_at_creation: false,
1674 }),
1675 bind_group_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
1676 texture_view_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
1677 }
1678 }
1679
1680 pub(crate) fn rebuild_texture_array_bind_group(&mut self) {
1681 let views: Vec<&wgpu::TextureView> = self.texture_views.iter().collect();
1682 self.mega_heim_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1683 layout: &self.texture_bind_group_layout,
1684 entries: &[
1685 wgpu::BindGroupEntry {
1686 binding: 0,
1687 resource: wgpu::BindingResource::TextureViewArray(&views),
1688 },
1689 wgpu::BindGroupEntry {
1690 binding: 1,
1691 resource: wgpu::BindingResource::Sampler(&self.dummy_sampler),
1692 },
1693 ],
1694 label: Some("Surtr Texture Array Bind Group"),
1695 });
1696 }
1697
1698 pub(crate) fn update_vram_telemetry(&mut self) {
1700 let mut buffer_bytes = 0;
1702 buffer_bytes += (MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64;
1703 buffer_bytes += (MAX_INDICES * std::mem::size_of::<u32>()) as u64;
1704 buffer_bytes += std::mem::size_of::<cvkg_core::ColorTheme>() as u64;
1705 buffer_bytes += std::mem::size_of::<cvkg_core::SceneUniforms>() as u64;
1706 self.vram_buffers_bytes = buffer_bytes;
1707
1708 let mut texture_bytes = 0;
1710 texture_bytes += 4096 * 4096 * 4; texture_bytes += 4; for ctx in self.surfaces.values() {
1714 let bpp = 4;
1715 let surface_bytes = (ctx.config.width * ctx.config.height * bpp) as u64;
1716 texture_bytes += surface_bytes * 3; }
1718
1719 self.vram_textures_bytes = texture_bytes;
1720
1721 self.telemetry.vram_buffers_mb = buffer_bytes as f32 / 1_048_576.0;
1722 self.telemetry.vram_textures_mb = texture_bytes as f32 / 1_048_576.0;
1723 self.telemetry.vram_pipelines_mb = 0.0;
1724 self.telemetry.vram_usage_mb =
1725 self.telemetry.vram_buffers_mb + self.telemetry.vram_textures_mb;
1726 }
1727
1728 pub fn get_telemetry(&self) -> cvkg_core::TelemetryData {
1730 self.telemetry.clone()
1731 }
1732
1733 pub fn resize(
1735 &mut self,
1736 window_id: winit::window::WindowId,
1737 width: u32,
1738 height: u32,
1739 scale_factor: f32,
1740 ) {
1741 if width > 0
1742 && height > 0
1743 && let Some(ctx) = self.surfaces.get_mut(&window_id)
1744 {
1745 if ctx.config.width == width && ctx.config.height == height {
1746 return;
1748 }
1749
1750 log::info!("[GPU] Reconfiguring surface: {}x{}", width, height);
1751 self.bind_group_cache.lock().unwrap().clear();
1752 self.texture_view_cache.lock().unwrap().clear();
1753 self.shaped_text_cache.clear();
1754 ctx.config.width = width;
1755 ctx.config.height = height;
1756 ctx.scale_factor = scale_factor;
1757 ctx.surface.configure(&self.device, &ctx.config);
1758
1759 let texture_desc = wgpu::TextureDescriptor {
1761 label: Some("Surtr Scene Texture"),
1762 size: wgpu::Extent3d {
1763 width,
1764 height,
1765 depth_or_array_layers: 1,
1766 },
1767 mip_level_count: 1,
1768 sample_count: 1,
1769 dimension: wgpu::TextureDimension::D2,
1770 format: wgpu::TextureFormat::Rgba16Float,
1771 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1772 | wgpu::TextureUsages::TEXTURE_BINDING,
1773 view_formats: &[],
1774 };
1775
1776 let scene_tex = self.device.create_texture(&texture_desc);
1777
1778 let msaa_desc = wgpu::TextureDescriptor {
1779 label: Some("Scene MSAA"),
1780 size: texture_desc.size,
1781 mip_level_count: 1,
1782 sample_count: 4,
1783 dimension: wgpu::TextureDimension::D2,
1784 format: wgpu::TextureFormat::Rgba16Float,
1785 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1786 view_formats: &[],
1787 };
1788 let scene_msaa_tex = self.device.create_texture(&msaa_desc);
1789 ctx.scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
1790 ctx.scene_msaa_texture =
1791 scene_msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
1792
1793 self.registry.remove_image(ctx.blur_tex_a);
1794 self.registry.remove_image(ctx.blur_tex_b);
1795 self.registry.remove_image(ctx.bloom_tex_a);
1796 self.registry.remove_image(ctx.bloom_tex_b);
1797
1798 let blur_width = (width / 2).max(1);
1799 let blur_height = (height / 2).max(1);
1800
1801 let blur_desc_a = crate::kvasir::resource::ResourceDescriptor {
1802 label: Some("Surtr Blur Texture A".into()),
1803 kind: crate::kvasir::resource::ResourceKind::Image {
1804 format: ctx.config.format,
1805 width: blur_width,
1806 height: blur_height,
1807 mip_level_count: 5,
1808 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1809 | wgpu::TextureUsages::TEXTURE_BINDING
1810 | wgpu::TextureUsages::COPY_SRC,
1811 },
1812 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
1813 };
1814 ctx.blur_tex_a = self.registry.allocate_image(&self.device, &blur_desc_a);
1815
1816 let blur_desc_b = crate::kvasir::resource::ResourceDescriptor {
1817 label: Some("Surtr Blur Texture B".into()),
1818 kind: crate::kvasir::resource::ResourceKind::Image {
1819 format: ctx.config.format,
1820 width: blur_width,
1821 height: blur_height,
1822 mip_level_count: 5,
1823 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1824 | wgpu::TextureUsages::TEXTURE_BINDING
1825 | wgpu::TextureUsages::COPY_SRC,
1826 },
1827 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
1828 };
1829 ctx.blur_tex_b = self.registry.allocate_image(&self.device, &blur_desc_b);
1830
1831 let bloom_desc_a = crate::kvasir::resource::ResourceDescriptor {
1832 label: Some("Surtr Bloom Texture A".into()),
1833 kind: crate::kvasir::resource::ResourceKind::Image {
1834 format: ctx.config.format,
1835 width: blur_width,
1836 height: blur_height,
1837 mip_level_count: 5,
1838 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1839 | wgpu::TextureUsages::TEXTURE_BINDING
1840 | wgpu::TextureUsages::COPY_SRC,
1841 },
1842 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
1843 };
1844 ctx.bloom_tex_a = self.registry.allocate_image(&self.device, &bloom_desc_a);
1845
1846 let bloom_desc_b = crate::kvasir::resource::ResourceDescriptor {
1847 label: Some("Surtr Bloom Texture B".into()),
1848 kind: crate::kvasir::resource::ResourceKind::Image {
1849 format: ctx.config.format,
1850 width: blur_width,
1851 height: blur_height,
1852 mip_level_count: 5,
1853 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1854 | wgpu::TextureUsages::TEXTURE_BINDING
1855 | wgpu::TextureUsages::COPY_SRC,
1856 },
1857 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
1858 };
1859 ctx.bloom_tex_b = self.registry.allocate_image(&self.device, &bloom_desc_b);
1860
1861 ctx.scene_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1863 layout: &self.env_bind_group_layout,
1864 entries: &[
1865 wgpu::BindGroupEntry {
1866 binding: 0,
1867 resource: wgpu::BindingResource::TextureView(&ctx.scene_texture),
1868 },
1869 wgpu::BindGroupEntry {
1870 binding: 1,
1871 resource: wgpu::BindingResource::Sampler(&ctx.sampler),
1872 },
1873 ],
1874 label: Some("Scene Bind Group Resize"),
1875 });
1876
1877 let scene_views: Vec<&wgpu::TextureView> =
1878 (0..256).map(|_| &ctx.scene_texture).collect();
1879 ctx.scene_texture_bind_group =
1880 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1881 layout: &self.texture_bind_group_layout,
1882 entries: &[
1883 wgpu::BindGroupEntry {
1884 binding: 0,
1885 resource: wgpu::BindingResource::TextureViewArray(&scene_views),
1886 },
1887 wgpu::BindGroupEntry {
1888 binding: 1,
1889 resource: wgpu::BindingResource::Sampler(&ctx.sampler),
1890 },
1891 ],
1892 label: Some("Scene Texture Bind Group Resize"),
1893 });
1894
1895 let depth_texture = self.device.create_texture(&wgpu::TextureDescriptor {
1896 label: Some("Surtr Depth Texture"),
1897 size: wgpu::Extent3d {
1898 width,
1899 height,
1900 depth_or_array_layers: 1,
1901 },
1902 mip_level_count: 1,
1903 sample_count: 1,
1904 dimension: wgpu::TextureDimension::D2,
1905 format: wgpu::TextureFormat::Depth32Float,
1906 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1907 view_formats: &[],
1908 });
1909 ctx.depth_texture_view =
1910 depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
1911 }
1912 }
1913
1914 pub fn begin_frame_headless(&mut self) -> wgpu::CommandEncoder {
1916 self.current_window = None;
1917 self.vertices.clear();
1918 self.indices.clear();
1919 self.instance_data.clear();
1920 self.draw_calls.clear();
1921 self.filter_batches.clear();
1922 self.shared_elements.clear();
1923 self.current_texture_id = None;
1924 self.opacity_stack = vec![1.0];
1925 self.clip_stack.clear();
1926 self.slice_stack.clear();
1927 self.transform_stack.clear();
1928 self.portal_regions.clear(); self.current_z = 0.0;
1930 self.compositor_index_cursor = self.indices.len() as u32;
1931 self.vnode_stack.clear();
1932 self.event_handlers.clear();
1933
1934 self.memo_cache.clear();
1936
1937 self.last_frame_start = std::time::Instant::now();
1938 self.telemetry.draw_calls = 0;
1939 self.telemetry.vertices = 0;
1940
1941 self.staging_belt.recall();
1943
1944 let ctx = self
1945 .headless_context
1946 .as_ref()
1947 .expect("Headless context not initialized");
1948 let time = self.start_time.elapsed().as_secs_f32();
1949 let logical_w = ctx.width as f32 / ctx.scale_factor;
1950 let logical_h = ctx.height as f32 / ctx.scale_factor;
1951 let dt = time - self.current_scene.time;
1952 self.current_scene.time = time;
1953 self.current_scene.delta_time = dt;
1954 self.current_scene.resolution = [logical_w, logical_h];
1955 self.current_scene.scale_factor = ctx.scale_factor;
1956 self.current_scene.proj =
1957 glam::Mat4::orthographic_lh(0.0, logical_w, logical_h, 0.0, -1000.0, 1000.0);
1958
1959 self.queue.write_buffer(
1960 &self.scene_buffer,
1961 0,
1962 bytemuck::bytes_of(&self.current_scene),
1963 );
1964
1965 self.device
1966 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1967 label: Some("Surtr Headless Command Encoder"),
1968 })
1969 }
1970
1971 pub fn begin_frame(&mut self, window_id: winit::window::WindowId) -> wgpu::CommandEncoder {
1973 if let Some(rx) = &self.ai_material_rx {
1975 while let Ok(res) = rx.try_recv() {
1976 match res {
1977 Ok(_) => log::info!("[Surtr] Received AI generated material"),
1978 Err(e) => log::warn!("[Surtr] AI material generation error: {:?}", e),
1979 }
1980 }
1981 }
1982
1983 if let Some(rb) = &self.skuld_read_buffer {
1985 let slice = rb.slice(..);
1986 let (tx, rx) = std::sync::mpsc::channel();
1987 slice.map_async(wgpu::MapMode::Read, move |r| {
1988 let _ = tx.send(r);
1989 });
1990
1991 self.device
1993 .poll(wgpu::PollType::Wait {
1994 submission_index: None,
1995 timeout: None,
1996 })
1997 .unwrap();
1998
1999 if rx.recv().is_ok() {
2000 let data = slice.get_mapped_range();
2001 let timestamps: [u64; 2] = bytemuck::cast_slice(&data).try_into().unwrap_or([0, 0]);
2002 drop(data);
2003 rb.unmap();
2004
2005 if timestamps[1] > timestamps[0] {
2006 let diff_ticks = timestamps[1] - timestamps[0];
2007 self.last_gpu_time_ns = (diff_ticks as f64 * self.skuld_period as f64) as u64;
2008 log::trace!(
2009 "[Skuld] GPU time: {} ms",
2010 self.last_gpu_time_ns as f64 / 1_000_000.0
2011 );
2012 }
2013 }
2014 }
2015
2016 self.staging_belt.recall();
2017 self.current_window = Some(window_id);
2018 self.vertices.clear();
2019 self.indices.clear();
2020 self.instance_data.clear();
2021 self.draw_calls.clear();
2022 self.filter_batches.clear();
2023 self.shared_elements.clear();
2024 self.current_texture_id = None;
2025 self.opacity_stack = vec![1.0];
2026 self.clip_stack.clear();
2027 self.slice_stack.clear();
2028 self.transform_stack.clear();
2029 self.portal_regions.clear(); self.current_z = 0.0;
2031 self.vnode_stack.clear();
2032 self.event_handlers.clear();
2033
2034 self.memo_cache.clear();
2036
2037 self.last_frame_start = std::time::Instant::now();
2038 self.telemetry.draw_calls = 0;
2039 self.telemetry.vertices = 0;
2040
2041 let ctx = self
2042 .surfaces
2043 .get(&window_id)
2044 .expect("Window not registered");
2045 let time = self.start_time.elapsed().as_secs_f32();
2046 let logical_w = ctx.config.width as f32 / ctx.scale_factor;
2047 let logical_h = ctx.config.height as f32 / ctx.scale_factor;
2048 let dt = time - self.current_scene.time;
2049 self.current_scene.time = time;
2050 self.current_scene.delta_time = dt;
2051 self.current_scene.resolution = [logical_w, logical_h];
2052 self.current_scene.scale_factor = ctx.scale_factor;
2053 self.current_scene.proj =
2054 glam::Mat4::orthographic_lh(0.0, logical_w, logical_h, 0.0, -1000.0, 1000.0);
2055
2056 self.queue.write_buffer(
2057 &self.scene_buffer,
2058 0,
2059 bytemuck::bytes_of(&self.current_scene),
2060 );
2061
2062 self.device
2063 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2064 label: Some("Surtr Command Encoder"),
2065 })
2066 }
2067
2068 pub fn register_window(&mut self, window: Arc<winit::window::Window>) {
2070 let size = window.inner_size();
2071 let surface = self
2072 .instance
2073 .create_surface(window.clone())
2074 .expect("Failed to create surface");
2075 let caps = surface.get_capabilities(&self.adapter);
2076 let format = caps.formats[0];
2077
2078 let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Mailbox) {
2080 wgpu::PresentMode::Mailbox
2081 } else {
2082 log::warn!("[GPU] Mailbox not supported, falling back to Fifo (V-Sync)");
2083 wgpu::PresentMode::Fifo
2084 };
2085
2086 let alpha_mode = if caps
2087 .alpha_modes
2088 .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
2089 {
2090 wgpu::CompositeAlphaMode::PostMultiplied
2091 } else if caps
2092 .alpha_modes
2093 .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
2094 {
2095 wgpu::CompositeAlphaMode::PreMultiplied
2096 } else {
2097 caps.alpha_modes[0]
2098 };
2099
2100 log::info!(
2101 "[GPU] Configuring surface: {}x{} | {:?} | {:?}",
2102 size.width,
2103 size.height,
2104 present_mode,
2105 alpha_mode
2106 );
2107
2108 let config = wgpu::SurfaceConfiguration {
2109 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2110 format,
2111 width: size.width,
2112 height: size.height,
2113 present_mode,
2114 alpha_mode,
2115 view_formats: vec![],
2116 desired_maximum_frame_latency: 1,
2117 };
2118 surface.configure(&self.device, &config);
2119
2120 let ctx = Self::create_surface_context(
2121 &self.device,
2122 surface,
2123 config,
2124 &self.env_bind_group_layout,
2125 &self.texture_bind_group_layout,
2126 window.scale_factor() as f32,
2127 &mut self.registry,
2128 );
2129
2130 self.surfaces.insert(window.id(), ctx);
2131 }
2132
2133 pub(crate) fn create_headless_context(
2134 device: &wgpu::Device,
2135 width: u32,
2136 height: u32,
2137 format: wgpu::TextureFormat,
2138 env_bind_group_layout: &wgpu::BindGroupLayout,
2139 texture_bind_group_layout: &wgpu::BindGroupLayout,
2140 registry: &mut crate::kvasir::registry::ResourceRegistry,
2141 ) -> HeadlessContext {
2142 let texture_desc = wgpu::TextureDescriptor {
2143 label: Some("Surtr Headless Scene Texture"),
2144 size: wgpu::Extent3d {
2145 width,
2146 height,
2147 depth_or_array_layers: 1,
2148 },
2149 mip_level_count: 1,
2150 sample_count: 1,
2151 dimension: wgpu::TextureDimension::D2,
2152 format: wgpu::TextureFormat::Rgba16Float,
2153 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2154 | wgpu::TextureUsages::TEXTURE_BINDING
2155 | wgpu::TextureUsages::COPY_SRC,
2156 view_formats: &[],
2157 };
2158
2159 let scene_tex = device.create_texture(&texture_desc);
2160
2161 let msaa_desc = wgpu::TextureDescriptor {
2162 label: Some("Scene MSAA"),
2163 size: texture_desc.size,
2164 mip_level_count: 1,
2165 sample_count: 4,
2166 dimension: wgpu::TextureDimension::D2,
2167 format: wgpu::TextureFormat::Rgba16Float,
2168 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2169 view_formats: &[],
2170 };
2171 let scene_msaa_tex = device.create_texture(&msaa_desc);
2172 let scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
2173 let scene_msaa_texture =
2174 scene_msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
2175
2176 let blur_width = (width / 2).max(1);
2177 let blur_height = (height / 2).max(1);
2178 let blur_desc_a = crate::kvasir::resource::ResourceDescriptor {
2179 label: Some("Headless Blur Texture A".into()),
2180 kind: crate::kvasir::resource::ResourceKind::Image {
2181 format,
2182 width: blur_width,
2183 height: blur_height,
2184 mip_level_count: 5,
2185 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2186 | wgpu::TextureUsages::TEXTURE_BINDING
2187 | wgpu::TextureUsages::COPY_SRC,
2188 },
2189 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2190 };
2191 let blur_tex_a = registry.allocate_image(device, &blur_desc_a);
2192
2193 let blur_desc_b = crate::kvasir::resource::ResourceDescriptor {
2194 label: Some("Headless Blur Texture B".into()),
2195 kind: crate::kvasir::resource::ResourceKind::Image {
2196 format,
2197 width: blur_width,
2198 height: blur_height,
2199 mip_level_count: 5,
2200 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2201 | wgpu::TextureUsages::TEXTURE_BINDING
2202 | wgpu::TextureUsages::COPY_SRC,
2203 },
2204 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2205 };
2206 let blur_tex_b = registry.allocate_image(device, &blur_desc_b);
2207
2208 let bloom_desc_a = crate::kvasir::resource::ResourceDescriptor {
2209 label: Some("Headless Bloom Texture A".into()),
2210 kind: crate::kvasir::resource::ResourceKind::Image {
2211 format,
2212 width: blur_width,
2213 height: blur_height,
2214 mip_level_count: 5,
2215 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2216 | wgpu::TextureUsages::TEXTURE_BINDING
2217 | wgpu::TextureUsages::COPY_SRC,
2218 },
2219 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2220 };
2221 let bloom_tex_a = registry.allocate_image(device, &bloom_desc_a);
2222
2223 let bloom_desc_b = crate::kvasir::resource::ResourceDescriptor {
2224 label: Some("Headless Bloom Texture B".into()),
2225 kind: crate::kvasir::resource::ResourceKind::Image {
2226 format,
2227 width: blur_width,
2228 height: blur_height,
2229 mip_level_count: 5,
2230 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2231 | wgpu::TextureUsages::TEXTURE_BINDING
2232 | wgpu::TextureUsages::COPY_SRC,
2233 },
2234 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2235 };
2236 let bloom_tex_b = registry.allocate_image(device, &bloom_desc_b);
2237
2238 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
2239 address_mode_u: wgpu::AddressMode::ClampToEdge,
2240 address_mode_v: wgpu::AddressMode::ClampToEdge,
2241 mag_filter: wgpu::FilterMode::Linear,
2242 min_filter: wgpu::FilterMode::Linear,
2243 ..Default::default()
2244 });
2245
2246 let scene_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2247 layout: env_bind_group_layout,
2248 entries: &[
2249 wgpu::BindGroupEntry {
2250 binding: 0,
2251 resource: wgpu::BindingResource::TextureView(&scene_texture),
2252 },
2253 wgpu::BindGroupEntry {
2254 binding: 1,
2255 resource: wgpu::BindingResource::Sampler(&sampler),
2256 },
2257 ],
2258 label: Some("Headless Scene Bind Group"),
2259 });
2260
2261 let blur_view_a = registry.get_texture_view(blur_tex_a).unwrap();
2262 let blur_view_b = registry.get_texture_view(blur_tex_b).unwrap();
2263 let bloom_view_a = registry.get_texture_view(bloom_tex_a).unwrap();
2264 let bloom_view_b = registry.get_texture_view(bloom_tex_b).unwrap();
2265
2266 let blur_env_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
2267 layout: env_bind_group_layout,
2268 entries: &[
2269 wgpu::BindGroupEntry {
2270 binding: 0,
2271 resource: wgpu::BindingResource::TextureView(&blur_view_a),
2272 },
2273 wgpu::BindGroupEntry {
2274 binding: 1,
2275 resource: wgpu::BindingResource::Sampler(&sampler),
2276 },
2277 ],
2278 label: Some("Headless Blur Env Bind Group A"),
2279 });
2280 let blur_env_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
2281 layout: env_bind_group_layout,
2282 entries: &[
2283 wgpu::BindGroupEntry {
2284 binding: 0,
2285 resource: wgpu::BindingResource::TextureView(&blur_view_b),
2286 },
2287 wgpu::BindGroupEntry {
2288 binding: 1,
2289 resource: wgpu::BindingResource::Sampler(&sampler),
2290 },
2291 ],
2292 label: Some("Headless Blur Env Bind Group B"),
2293 });
2294 let bloom_env_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
2295 layout: env_bind_group_layout,
2296 entries: &[
2297 wgpu::BindGroupEntry {
2298 binding: 0,
2299 resource: wgpu::BindingResource::TextureView(&bloom_view_a),
2300 },
2301 wgpu::BindGroupEntry {
2302 binding: 1,
2303 resource: wgpu::BindingResource::Sampler(&sampler),
2304 },
2305 ],
2306 label: Some("Headless Bloom Env Bind Group A"),
2307 });
2308 let bloom_env_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
2309 layout: env_bind_group_layout,
2310 entries: &[
2311 wgpu::BindGroupEntry {
2312 binding: 0,
2313 resource: wgpu::BindingResource::TextureView(&bloom_view_b),
2314 },
2315 wgpu::BindGroupEntry {
2316 binding: 1,
2317 resource: wgpu::BindingResource::Sampler(&sampler),
2318 },
2319 ],
2320 label: Some("Headless Bloom Env Bind Group B"),
2321 });
2322
2323 let scene_views: Vec<&wgpu::TextureView> = (0..256).map(|_| &scene_texture).collect();
2324 let scene_texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2325 layout: texture_bind_group_layout,
2326 entries: &[
2327 wgpu::BindGroupEntry {
2328 binding: 0,
2329 resource: wgpu::BindingResource::TextureViewArray(&scene_views),
2330 },
2331 wgpu::BindGroupEntry {
2332 binding: 1,
2333 resource: wgpu::BindingResource::Sampler(&sampler),
2334 },
2335 ],
2336 label: Some("Headless Scene Texture Bind Group"),
2337 });
2338
2339 let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
2340 label: Some("Headless Depth Texture"),
2341 size: wgpu::Extent3d {
2342 width,
2343 height,
2344 depth_or_array_layers: 1,
2345 },
2346 mip_level_count: 1,
2347 sample_count: 4,
2348 dimension: wgpu::TextureDimension::D2,
2349 format: wgpu::TextureFormat::Depth32Float,
2350 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2351 view_formats: &[],
2352 });
2353 let depth_texture_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
2354
2355 let output_texture = device.create_texture(&wgpu::TextureDescriptor {
2356 label: Some("Headless Output Texture"),
2357 size: wgpu::Extent3d {
2358 width,
2359 height,
2360 depth_or_array_layers: 1,
2361 },
2362 mip_level_count: 1,
2363 sample_count: 1,
2364 dimension: wgpu::TextureDimension::D2,
2365 format,
2366 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2367 | wgpu::TextureUsages::COPY_DST
2368 | wgpu::TextureUsages::COPY_SRC,
2369 view_formats: &[],
2370 });
2371 let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
2372
2373 crate::types::HeadlessContext {
2374 scene_texture,
2375 scene_msaa_texture,
2376 scene_bind_group,
2377 scene_texture_bind_group,
2378 depth_texture_view,
2379 blur_tex_a,
2380 blur_tex_b,
2381 bloom_tex_a,
2382 bloom_tex_b,
2383 blur_env_bind_group_a,
2384 blur_env_bind_group_b,
2385 bloom_env_bind_group_a,
2386 bloom_env_bind_group_b,
2387 scale_factor: 1.0,
2388 sampler,
2389 width,
2390 height,
2391 output_texture,
2392 output_view,
2393 }
2394 }
2395
2396 pub(crate) fn create_surface_context(
2397 device: &wgpu::Device,
2398 surface: wgpu::Surface<'static>,
2399 config: wgpu::SurfaceConfiguration,
2400 env_bind_group_layout: &wgpu::BindGroupLayout,
2401 texture_bind_group_layout: &wgpu::BindGroupLayout,
2402 scale_factor: f32,
2403 registry: &mut crate::kvasir::registry::ResourceRegistry,
2404 ) -> SurfaceContext {
2405 let width = config.width;
2406 let height = config.height;
2407
2408 let texture_desc = wgpu::TextureDescriptor {
2409 label: Some("Surtr Scene Texture"),
2410 size: wgpu::Extent3d {
2411 width,
2412 height,
2413 depth_or_array_layers: 1,
2414 },
2415 mip_level_count: 1,
2416 sample_count: 1,
2417 dimension: wgpu::TextureDimension::D2,
2418 format: wgpu::TextureFormat::Rgba16Float,
2419 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
2420 view_formats: &[],
2421 };
2422
2423 let scene_tex = device.create_texture(&texture_desc);
2424
2425 let msaa_desc = wgpu::TextureDescriptor {
2426 label: Some("Scene MSAA"),
2427 size: texture_desc.size,
2428 mip_level_count: 1,
2429 sample_count: 4,
2430 dimension: wgpu::TextureDimension::D2,
2431 format: wgpu::TextureFormat::Rgba16Float,
2432 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2433 view_formats: &[],
2434 };
2435 let scene_msaa_tex = device.create_texture(&msaa_desc);
2436 let scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
2437 let scene_msaa_texture =
2438 scene_msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
2439
2440 let blur_width = (config.width / 2).max(1);
2441 let blur_height = (config.height / 2).max(1);
2442 let blur_desc_a = crate::kvasir::resource::ResourceDescriptor {
2443 label: Some("Surface Blur Texture A".into()),
2444 kind: crate::kvasir::resource::ResourceKind::Image {
2445 format: config.format,
2446 width: blur_width,
2447 height: blur_height,
2448 mip_level_count: 5,
2449 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2450 | wgpu::TextureUsages::TEXTURE_BINDING
2451 | wgpu::TextureUsages::COPY_SRC,
2452 },
2453 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2454 };
2455 let blur_tex_a = registry.allocate_image(device, &blur_desc_a);
2456
2457 let blur_desc_b = crate::kvasir::resource::ResourceDescriptor {
2458 label: Some("Surface Blur Texture B".into()),
2459 kind: crate::kvasir::resource::ResourceKind::Image {
2460 format: config.format,
2461 width: blur_width,
2462 height: blur_height,
2463 mip_level_count: 5,
2464 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2465 | wgpu::TextureUsages::TEXTURE_BINDING
2466 | wgpu::TextureUsages::COPY_SRC,
2467 },
2468 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2469 };
2470 let blur_tex_b = registry.allocate_image(device, &blur_desc_b);
2471
2472 let bloom_desc_a = crate::kvasir::resource::ResourceDescriptor {
2473 label: Some("Surface Bloom Texture A".into()),
2474 kind: crate::kvasir::resource::ResourceKind::Image {
2475 format: config.format,
2476 width: blur_width,
2477 height: blur_height,
2478 mip_level_count: 5,
2479 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2480 | wgpu::TextureUsages::TEXTURE_BINDING
2481 | wgpu::TextureUsages::COPY_SRC,
2482 },
2483 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2484 };
2485 let bloom_tex_a = registry.allocate_image(device, &bloom_desc_a);
2486
2487 let bloom_desc_b = crate::kvasir::resource::ResourceDescriptor {
2488 label: Some("Surface Bloom Texture B".into()),
2489 kind: crate::kvasir::resource::ResourceKind::Image {
2490 format: config.format,
2491 width: blur_width,
2492 height: blur_height,
2493 mip_level_count: 5,
2494 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2495 | wgpu::TextureUsages::TEXTURE_BINDING
2496 | wgpu::TextureUsages::COPY_SRC,
2497 },
2498 lifetime: crate::kvasir::resource::ResourceLifetime::Persistent,
2499 };
2500 let bloom_tex_b = registry.allocate_image(device, &bloom_desc_b);
2501
2502 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
2503 address_mode_u: wgpu::AddressMode::ClampToEdge,
2504 address_mode_v: wgpu::AddressMode::ClampToEdge,
2505 mag_filter: wgpu::FilterMode::Linear,
2506 min_filter: wgpu::FilterMode::Linear,
2507 ..Default::default()
2508 });
2509
2510 let scene_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2511 layout: env_bind_group_layout,
2512 entries: &[
2513 wgpu::BindGroupEntry {
2514 binding: 0,
2515 resource: wgpu::BindingResource::TextureView(&scene_texture),
2516 },
2517 wgpu::BindGroupEntry {
2518 binding: 1,
2519 resource: wgpu::BindingResource::Sampler(&sampler),
2520 },
2521 ],
2522 label: Some("Scene Bind Group"),
2523 });
2524
2525 let blur_view_a = registry.get_texture_view(blur_tex_a).unwrap();
2526 let blur_view_b = registry.get_texture_view(blur_tex_b).unwrap();
2527 let bloom_view_a = registry.get_texture_view(bloom_tex_a).unwrap();
2528 let bloom_view_b = registry.get_texture_view(bloom_tex_b).unwrap();
2529
2530 let blur_env_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
2531 layout: env_bind_group_layout,
2532 entries: &[
2533 wgpu::BindGroupEntry {
2534 binding: 0,
2535 resource: wgpu::BindingResource::TextureView(&blur_view_a),
2536 },
2537 wgpu::BindGroupEntry {
2538 binding: 1,
2539 resource: wgpu::BindingResource::Sampler(&sampler),
2540 },
2541 ],
2542 label: Some("Blur Env Bind Group A"),
2543 });
2544 let blur_env_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
2545 layout: env_bind_group_layout,
2546 entries: &[
2547 wgpu::BindGroupEntry {
2548 binding: 0,
2549 resource: wgpu::BindingResource::TextureView(&blur_view_b),
2550 },
2551 wgpu::BindGroupEntry {
2552 binding: 1,
2553 resource: wgpu::BindingResource::Sampler(&sampler),
2554 },
2555 ],
2556 label: Some("Blur Env Bind Group B"),
2557 });
2558 let bloom_env_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
2559 layout: env_bind_group_layout,
2560 entries: &[
2561 wgpu::BindGroupEntry {
2562 binding: 0,
2563 resource: wgpu::BindingResource::TextureView(&bloom_view_a),
2564 },
2565 wgpu::BindGroupEntry {
2566 binding: 1,
2567 resource: wgpu::BindingResource::Sampler(&sampler),
2568 },
2569 ],
2570 label: Some("Bloom Env Bind Group A"),
2571 });
2572 let bloom_env_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
2573 layout: env_bind_group_layout,
2574 entries: &[
2575 wgpu::BindGroupEntry {
2576 binding: 0,
2577 resource: wgpu::BindingResource::TextureView(&bloom_view_b),
2578 },
2579 wgpu::BindGroupEntry {
2580 binding: 1,
2581 resource: wgpu::BindingResource::Sampler(&sampler),
2582 },
2583 ],
2584 label: Some("Bloom Env Bind Group B"),
2585 });
2586
2587 let scene_views: Vec<&wgpu::TextureView> = (0..256).map(|_| &scene_texture).collect();
2588 let scene_texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2589 layout: texture_bind_group_layout,
2590 entries: &[
2591 wgpu::BindGroupEntry {
2592 binding: 0,
2593 resource: wgpu::BindingResource::TextureViewArray(&scene_views),
2594 },
2595 wgpu::BindGroupEntry {
2596 binding: 1,
2597 resource: wgpu::BindingResource::Sampler(&sampler),
2598 },
2599 ],
2600 label: Some("Scene Texture Bind Group"),
2601 });
2602
2603 let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
2604 label: Some("Surtr Depth Texture"),
2605 size: wgpu::Extent3d {
2606 width,
2607 height,
2608 depth_or_array_layers: 1,
2609 },
2610 mip_level_count: 1,
2611 sample_count: 4,
2612 dimension: wgpu::TextureDimension::D2,
2613 format: wgpu::TextureFormat::Depth32Float,
2614 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
2615 view_formats: &[],
2616 });
2617 let depth_texture_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
2618
2619 crate::types::SurfaceContext {
2620 surface,
2621 config,
2622 scene_texture,
2623 scene_msaa_texture,
2624 scene_bind_group,
2625 scene_texture_bind_group,
2626 depth_texture_view,
2627 blur_tex_a,
2628 blur_tex_b,
2629 bloom_tex_a,
2630 bloom_tex_b,
2631 blur_env_bind_group_a,
2632 blur_env_bind_group_b,
2633 bloom_env_bind_group_a,
2634 bloom_env_bind_group_b,
2635 scale_factor,
2636 sampler,
2637 }
2638 }
2639
2640 pub fn reset_time(&mut self) {
2641 self.start_time = std::time::Instant::now();
2642 }
2643
2644 pub fn reclaim_vram(&mut self) {
2647 log::warn!("[GPU] Sundr Compaction: Compacting Mega-Heim...");
2648
2649 let new_mega_heim_tex = self.device.create_texture(&wgpu::TextureDescriptor {
2650 label: Some("Sundr Mega-Heim (Compacted)"),
2651 size: wgpu::Extent3d {
2652 width: 4096,
2653 height: 4096,
2654 depth_or_array_layers: 1,
2655 },
2656 mip_level_count: 1,
2657 sample_count: 1,
2658 dimension: wgpu::TextureDimension::D2,
2659 format: wgpu::TextureFormat::Rgba8UnormSrgb,
2660 usage: wgpu::TextureUsages::TEXTURE_BINDING
2661 | wgpu::TextureUsages::COPY_DST
2662 | wgpu::TextureUsages::COPY_SRC,
2663 view_formats: &[],
2664 });
2665
2666 let mut new_packer = SundrPacker::new(4096, 4096);
2667 let mut encoder = self
2668 .device
2669 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2670 label: Some("Heim Compaction Encoder"),
2671 });
2672
2673 let image_entries: Vec<(String, Rect)> = self
2674 .image_uv_registry
2675 .iter()
2676 .map(|(k, v)| (k.clone(), *v))
2677 .collect();
2678 for (name, old_uv) in image_entries {
2679 if let Some(&tex_idx) = self.texture_registry.get(&name)
2680 && tex_idx == 0
2681 {
2682 let w_px = (old_uv.width * 4096.0).round() as u32;
2683 let h_px = (old_uv.height * 4096.0).round() as u32;
2684 let old_x_px = (old_uv.x * 4096.0).round() as u32;
2685 let old_y_px = (old_uv.y * 4096.0).round() as u32;
2686
2687 if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
2688 encoder.copy_texture_to_texture(
2689 wgpu::TexelCopyTextureInfo {
2690 texture: &self.mega_heim_tex,
2691 mip_level: 0,
2692 origin: wgpu::Origin3d {
2693 x: old_x_px,
2694 y: old_y_px,
2695 z: 0,
2696 },
2697 aspect: wgpu::TextureAspect::All,
2698 },
2699 wgpu::TexelCopyTextureInfo {
2700 texture: &new_mega_heim_tex,
2701 mip_level: 0,
2702 origin: wgpu::Origin3d {
2703 x: new_x,
2704 y: new_y,
2705 z: 0,
2706 },
2707 aspect: wgpu::TextureAspect::All,
2708 },
2709 wgpu::Extent3d {
2710 width: w_px,
2711 height: h_px,
2712 depth_or_array_layers: 1,
2713 },
2714 );
2715
2716 let new_uv = Rect {
2717 x: new_x as f32 / 4096.0,
2718 y: new_y as f32 / 4096.0,
2719 width: old_uv.width,
2720 height: old_uv.height,
2721 };
2722 self.image_uv_registry.put(name.clone(), new_uv);
2723 }
2724 }
2725 }
2726
2727 let text_entries: Vec<(u64, (Rect, f32, f32, f32, f32))> =
2728 self.text_cache.iter().map(|(k, v)| (*k, *v)).collect();
2729 for (hash, (old_uv, w_f, h_f, x_off, y_off)) in text_entries {
2730 let w_px = (old_uv.width * 4096.0).round() as u32;
2731 let h_px = (old_uv.height * 4096.0).round() as u32;
2732 let old_x_px = (old_uv.x * 4096.0).round() as u32;
2733 let old_y_px = (old_uv.y * 4096.0).round() as u32;
2734
2735 if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
2736 encoder.copy_texture_to_texture(
2737 wgpu::TexelCopyTextureInfo {
2738 texture: &self.mega_heim_tex,
2739 mip_level: 0,
2740 origin: wgpu::Origin3d {
2741 x: old_x_px,
2742 y: old_y_px,
2743 z: 0,
2744 },
2745 aspect: wgpu::TextureAspect::All,
2746 },
2747 wgpu::TexelCopyTextureInfo {
2748 texture: &new_mega_heim_tex,
2749 mip_level: 0,
2750 origin: wgpu::Origin3d {
2751 x: new_x,
2752 y: new_y,
2753 z: 0,
2754 },
2755 aspect: wgpu::TextureAspect::All,
2756 },
2757 wgpu::Extent3d {
2758 width: w_px,
2759 height: h_px,
2760 depth_or_array_layers: 1,
2761 },
2762 );
2763
2764 let new_uv = Rect {
2765 x: new_x as f32 / 4096.0,
2766 y: new_y as f32 / 4096.0,
2767 width: old_uv.width,
2768 height: old_uv.height,
2769 };
2770 self.text_cache.put(hash, (new_uv, w_f, h_f, x_off, y_off));
2771 }
2772 }
2773
2774 self.queue.submit(std::iter::once(encoder.finish()));
2775
2776 self.mega_heim_tex = new_mega_heim_tex;
2777 let mega_heim_view_obj = self
2778 .mega_heim_tex
2779 .create_view(&wgpu::TextureViewDescriptor::default());
2780 self.texture_views[0] = mega_heim_view_obj.clone();
2781
2782 self.rebuild_texture_array_bind_group();
2783
2784 if !self.texture_bind_groups.is_empty() {
2785 self.texture_bind_groups[0] = self.mega_heim_bind_group.clone();
2786 }
2787
2788 self.heim_packer = new_packer;
2789 self.telemetry.vram_exhausted = false;
2790 }
2791
2792 pub(crate) fn shatter_internal(
2793 &mut self,
2794 rect: Rect,
2795 pieces: u32,
2796 force: f32,
2797 color: [f32; 4],
2798 material_id: u32,
2799 ) {
2800 let count = (pieces as f32).sqrt().ceil() as u32;
2802 let dw = rect.width / count as f32;
2803 let dh = rect.height / count as f32;
2804
2805 let c = self.apply_opacity(color);
2806
2807 let cx = rect.x + rect.width * 0.5;
2808 let cy = rect.y + rect.height * 0.5;
2809
2810 for y in 0..count {
2811 for x in 0..count {
2812 let init_x = rect.x + x as f32 * dw;
2813 let init_y = rect.y + y as f32 * dh;
2814
2815 let dx = (init_x + dw * 0.5) - cx;
2817 let dy = (init_y + dh * 0.5) - cy;
2818 let dist = (dx * dx + dy * dy).sqrt().max(1.0);
2819
2820 let nx = dx / dist;
2822 let ny = dy / dist;
2823
2824 let hash =
2826 ((x as f32 * 12.9898 + y as f32 * 78.233).sin().fract() * 43_758.547).fract();
2827 let hash2 =
2828 ((x as f32 * 37.11 + y as f32 * 149.87).sin().fract() * 23_412.19).fract();
2829
2830 let speed_var = 0.5 + hash * 1.5;
2831 let angle = ny.atan2(nx) + (hash2 - 0.5) * 0.6;
2832 let disp_x = angle.cos() * force * 50.0 * speed_var;
2833 let disp_y = angle.sin() * force * 50.0 * speed_var;
2834
2835 let gravity = force * force * 20.0;
2837
2838 let scale_factor = (1.0 - (force / 6.0).min(1.0)).max(0.0);
2841 let shard_w = dw * scale_factor;
2842 let shard_h = dh * scale_factor;
2843
2844 let displaced_x = init_x + disp_x + (dw - shard_w) * 0.5;
2845 let displaced_y = init_y + disp_y + gravity + (dh - shard_h) * 0.5;
2846
2847 let shard_rect = Rect {
2848 x: displaced_x,
2849 y: displaced_y,
2850 width: shard_w,
2851 height: shard_h,
2852 };
2853
2854 let uv = Rect {
2855 x: x as f32 / count as f32,
2856 y: y as f32 / count as f32,
2857 width: 1.0 / count as f32,
2858 height: 1.0 / count as f32,
2859 };
2860
2861 self.fill_rect_with_full_params(shard_rect, c, material_id, None, force, uv);
2862 }
2863 }
2864 }
2865
2866 pub(crate) fn recursive_bolt(
2867 &mut self,
2868 from: [f32; 2],
2869 to: [f32; 2],
2870 depth: u32,
2871 color: [f32; 4],
2872 ) {
2873 if depth == 0 {
2874 self.draw_lightning_segment(from, to, color);
2875 return;
2876 }
2877
2878 let mid_x = (from[0] + to[0]) * 0.5;
2879 let mid_y = (from[1] + to[1]) * 0.5;
2880
2881 let dx = to[0] - from[0];
2882 let dy = to[1] - from[1];
2883 let len = (dx * dx + dy * dy).sqrt();
2884
2885 if len < 1e-4 {
2886 return;
2887 }
2888
2889 let offset_scale = len * 0.15;
2891 let seed = (from[0] * 12.9898 + from[1] * 78.233 + (depth as f32) * 37.11)
2892 .sin()
2893 .fract();
2894 let offset_x = -dy / len * (seed - 0.5) * offset_scale;
2895 let offset_y = dx / len * (seed - 0.5) * offset_scale;
2896
2897 let mid = [mid_x + offset_x, mid_y + offset_y];
2898
2899 self.recursive_bolt(from, mid, depth - 1, color);
2900 self.recursive_bolt(mid, to, depth - 1, color);
2901
2902 if depth > 2 && seed > 0.8 {
2904 let branch_to = [
2905 mid[0] + offset_x * 2.0 + (seed * 100.0).sin() * 50.0,
2906 mid[1] + offset_y * 2.0 + (seed * 100.0).cos() * 50.0,
2907 ];
2908 self.recursive_bolt(mid, branch_to, depth - 2, color);
2909 }
2910 }
2911
2912 pub(crate) fn draw_lightning_segment(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
2913 let dx = to[0] - from[0];
2914 let dy = to[1] - from[1];
2915 let len = (dx * dx + dy * dy).sqrt();
2916 if len < 0.001 {
2917 return;
2918 }
2919
2920 let glow_width = 32.0;
2921 let core_width = 4.0;
2922 let c = self.apply_opacity(color);
2923
2924 let gnx = -dy / len * glow_width * 0.5;
2926 let gny = dx / len * glow_width * 0.5;
2927 let gp1 = [from[0] + gnx, from[1] + gny];
2928 let gp2 = [to[0] + gnx, to[1] + gny];
2929 let gp3 = [to[0] - gnx, to[1] - gny];
2930 let gp4 = [from[0] - gnx, from[1] - gny];
2931 self.push_oriented_quad(
2932 [gp1, gp2, gp3, gp4],
2933 c,
2934 9,
2935 Rect {
2936 x: 0.0,
2937 y: 0.0,
2938 width: 1.0,
2939 height: 1.0,
2940 },
2941 );
2942
2943 let cnx = -dy / len * core_width * 0.5;
2945 let cny = dx / len * core_width * 0.5;
2946 let cp1 = [from[0] + cnx, from[1] + cny];
2947 let cp2 = [to[0] + cnx, to[1] + cny];
2948 let cp3 = [to[0] - cnx, to[1] - cny];
2949 let cp4 = [from[0] - cnx, from[1] - cny];
2950 self.push_oriented_quad(
2951 [cp1, cp2, cp3, cp4],
2952 [1.0, 1.0, 1.0, c[3]],
2953 0,
2954 Rect {
2955 x: 0.0,
2956 y: 0.0,
2957 width: 1.0,
2958 height: 1.0,
2959 },
2960 );
2961 }
2962
2963 pub(crate) fn push_oriented_quad(
2964 &mut self,
2965 points: [[f32; 2]; 4],
2966 color: [f32; 4],
2967 material_id: u32,
2968 uv_rect: Rect,
2969 ) {
2970 let scissor = self.clip_stack.last().copied();
2971 let texture_id = None; let (translation, scale_transform, rotation, _, _) = self.current_transform();
2974 let current_instance_data = InstanceData {
2975 translation,
2976 scale: scale_transform,
2977 rotation,
2978 blur_radius: 0.0,
2979 ior_override: 0.0,
2980 };
2981
2982 if self.draw_calls.is_empty()
2983 || self.current_texture_id != texture_id
2984 || self.draw_calls.last().unwrap().scissor_rect != scissor
2985 || self.instance_data.last() != Some(¤t_instance_data)
2986 {
2987 self.current_texture_id = texture_id;
2988 self.instance_data.push(current_instance_data);
2989 self.draw_calls.push(DrawCall {
2990 target_id: None,
2991 texture_id,
2992 scissor_rect: scissor,
2993 index_start: self.indices.len() as u32,
2994 index_count: 0,
2995 material: if material_id == 7 {
2996 if let cvkg_core::DrawMaterial::Glass {
2997 blur_radius,
2998 ior_override,
2999 } = self.current_draw_material
3000 {
3001 cvkg_core::DrawMaterial::Glass {
3002 blur_radius,
3003 ior_override,
3004 }
3005 } else {
3006 cvkg_core::DrawMaterial::Glass {
3007 blur_radius: 20.0,
3008 ior_override: 0.0,
3009 }
3010 }
3011 } else if material_id == 6 {
3012 cvkg_core::DrawMaterial::TopUI
3013 } else {
3014 cvkg_core::DrawMaterial::Opaque
3015 },
3016 instance_start: (self.instance_data.len() - 1) as u32,
3017 });
3018 }
3019
3020 let uvs = [
3021 [uv_rect.x, uv_rect.y],
3022 [uv_rect.x + uv_rect.width, uv_rect.y],
3023 [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
3024 [uv_rect.x, uv_rect.y + uv_rect.height],
3025 ];
3026
3027 let screen = [self.current_width() as f32, self.current_height() as f32];
3028 let rect = Rect {
3029 x: points[0][0],
3030 y: points[0][1],
3031 width: 1.0,
3032 height: 1.0,
3033 };
3034
3035 for i in 0..4 {
3036 let px = points[i][0];
3037 let py = points[i][1];
3038
3039 let (translation, scale_transform, rotation, _, _) = self.current_transform();
3040 self.vertices.push(Vertex {
3041 position: [px, py, 0.0],
3042 normal: [0.0, 0.0, 1.0],
3043 uv: uvs[i],
3044 color,
3045 material_id,
3046 radius: 0.0,
3047 slice: [0.0, 0.0, 0.0, 1.0],
3048 logical: [px - rect.x, py - rect.y],
3049 size: [rect.width, rect.height],
3050 clip: [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY],
3051 tex_index: 0,
3052 });
3053 }
3054
3055 if let Some(call) = self.draw_calls.last_mut() {
3056 call.index_count += 6;
3057 }
3058 }
3059 pub(crate) fn get_texture_id(&mut self, name: &str) -> Option<u32> {
3060 self.texture_registry.get(name).copied()
3061 }
3062
3063 pub fn fill_rect_with_mode(
3065 &mut self,
3066 rect: Rect,
3067 color: [f32; 4],
3068 material_id: u32,
3069 texture_id: Option<u32>,
3070 ) {
3071 self.fill_rect_with_full_params(
3072 rect,
3073 color,
3074 material_id,
3075 texture_id,
3076 0.0,
3077 Rect {
3078 x: 0.0,
3079 y: 0.0,
3080 width: 1.0,
3081 height: 1.0,
3082 },
3083 );
3084 }
3085
3086 pub(crate) fn fill_rect_with_full_params(
3087 &mut self,
3088 rect: Rect,
3089 color: [f32; 4],
3090 material_id: u32,
3091 texture_id: Option<u32>,
3092 radius: f32,
3093 uv_rect: Rect,
3094 ) {
3095 if let Some(shadow) = self.shadow_stack.last().copied()
3097 && shadow.color[3] > 0.001
3098 {
3099 Renderer::draw_drop_shadow(
3100 self,
3101 rect,
3102 radius,
3103 shadow.color,
3104 shadow.radius,
3105 0.0, );
3107 }
3108
3109 let slice = self
3110 .slice_stack
3111 .last()
3112 .copied()
3113 .map(|(a, o)| [a, o, 1.0, 1.0])
3114 .unwrap_or([0.0, 0.0, 0.0, 1.0]);
3115 self.fill_rect_with_full_params_and_slice(
3116 rect,
3117 color,
3118 material_id,
3119 texture_id,
3120 radius,
3121 uv_rect,
3122 slice,
3123 [0.0, 0.0],
3124 );
3125 }
3126
3127 #[allow(clippy::too_many_arguments)]
3128 pub(crate) fn fill_rect_with_full_params_and_slice(
3129 &mut self,
3130 rect: Rect,
3131 color: [f32; 4],
3132 material_id: u32,
3133 texture_id: Option<u32>,
3134 radius: f32,
3135 uv_rect: Rect,
3136 slice: [f32; 4],
3137 glyph_time: [f32; 2],
3138 ) {
3139 let scissor = self.clip_stack.last().copied();
3140
3141 let material = if material_id == 7 {
3142 if let cvkg_core::DrawMaterial::Glass {
3143 blur_radius,
3144 ior_override,
3145 } = self.current_draw_material
3146 {
3147 cvkg_core::DrawMaterial::Glass {
3148 blur_radius,
3149 ior_override,
3150 }
3151 } else {
3152 cvkg_core::DrawMaterial::Glass {
3153 blur_radius: 20.0,
3154 ior_override: 0.0,
3155 }
3156 }
3157 } else if material_id == 6 {
3158 cvkg_core::DrawMaterial::TopUI
3159 } else if material_id == 0 {
3160 cvkg_core::DrawMaterial::Opaque
3161 } else {
3162 self.current_draw_material
3163 };
3164
3165 let (translation, scale_transform, rotation, _, _) = self.current_transform();
3166 let (blur_radius, ior_override) = if let cvkg_core::DrawMaterial::Glass {
3167 blur_radius,
3168 ior_override,
3169 } = material
3170 {
3171 (blur_radius, ior_override)
3172 } else {
3173 (0.0, 0.0)
3174 };
3175
3176 let current_instance_data = InstanceData {
3177 translation,
3178 scale: scale_transform,
3179 rotation,
3180 blur_radius,
3181 ior_override,
3182 };
3183
3184 let last_call = self.draw_calls.last();
3188 let needs_new_call = self.draw_calls.is_empty()
3189 || last_call.unwrap().scissor_rect != scissor
3190 || last_call.unwrap().material != material
3191 || self.instance_data.last() != Some(¤t_instance_data);
3192
3193 if needs_new_call {
3194 self.current_texture_id = Some(0); self.instance_data.push(current_instance_data);
3196 self.draw_calls.push(DrawCall {
3197 target_id: None,
3198 texture_id: self.current_texture_id,
3199 scissor_rect: scissor,
3200 index_start: self.indices.len() as u32,
3201 index_count: 0,
3202 material,
3203 instance_start: (self.instance_data.len() - 1) as u32,
3204 });
3205 }
3206
3207 let scale = self.current_scale_factor();
3208 let snap = |v: f32| (v * scale).round() / scale;
3209
3210 let base_idx = self.vertices.len() as u32;
3211 let x1 = snap(rect.x);
3212 let y1 = snap(rect.y);
3213 let x2 = snap(rect.x + rect.width);
3214 let y2 = snap(rect.y + rect.height);
3215 let z = self.current_z;
3216 let normal = [0.0, 0.0, 1.0];
3217 let screen = [self.current_width() as f32, self.current_height() as f32];
3218 let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
3219 x: -10000.0,
3220 y: -10000.0,
3221 width: 20000.0,
3222 height: 20000.0,
3223 });
3224 let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
3225
3226 let tex_index = texture_id.unwrap_or(0);
3227
3228 self.vertices.push(Vertex {
3229 position: [x1, y1, z],
3230 normal,
3231 uv: [uv_rect.x, uv_rect.y],
3232 color,
3233 material_id,
3234 radius,
3235 slice,
3236 logical: [0.0, 0.0],
3237 size: [rect.width, rect.height],
3238 clip,
3239 tex_index,
3240 });
3241 self.vertices.push(Vertex {
3242 position: [x2, y1, z],
3243 normal,
3244 uv: [uv_rect.x + uv_rect.width, uv_rect.y],
3245 color,
3246 material_id,
3247 radius,
3248 slice,
3249 logical: [rect.width, 0.0],
3250 size: [rect.width, rect.height],
3251 clip,
3252 tex_index,
3253 });
3254 self.vertices.push(Vertex {
3255 position: [x2, y2, z],
3256 normal,
3257 uv: [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
3258 color,
3259 material_id,
3260 radius,
3261 slice,
3262 logical: [rect.width, rect.height],
3263 size: [rect.width, rect.height],
3264 clip,
3265 tex_index,
3266 });
3267 self.vertices.push(Vertex {
3268 position: [x1, y2, z],
3269 normal,
3270 uv: [uv_rect.x, uv_rect.y + uv_rect.height],
3271 color,
3272 material_id,
3273 radius,
3274 slice,
3275 logical: [0.0, rect.height],
3276 size: [rect.width, rect.height],
3277 clip,
3278 tex_index,
3279 });
3280
3281 self.indices.extend_from_slice(&[
3282 base_idx,
3283 base_idx + 1,
3284 base_idx + 2,
3285 base_idx,
3286 base_idx + 2,
3287 base_idx + 3,
3288 ]);
3289
3290 if let Some(call) = self.draw_calls.last_mut() {
3291 call.index_count += 6;
3292 }
3293 }
3294
3295 pub fn end_frame(&mut self, mut encoder: wgpu::CommandEncoder) {
3310 struct ActiveFrameResources {
3311 surface_texture: Option<wgpu::SurfaceTexture>,
3312 target_view: wgpu::TextureView,
3313 scene_texture: wgpu::TextureView,
3314 scene_msaa_texture: wgpu::TextureView,
3315 depth_texture_view: wgpu::TextureView,
3316 blur_env_bind_group_a: wgpu::BindGroup,
3317 blur_env_bind_group_b: wgpu::BindGroup,
3318 bloom_env_bind_group_a: wgpu::BindGroup,
3319 bloom_env_bind_group_b: wgpu::BindGroup,
3320 }
3321
3322 let res = if let Some(window_id) = self.current_window {
3323 let Some(ctx) = self.surfaces.get(&window_id) else {
3324 log::error!("[GPU] Missing surface context for end_frame");
3325 return;
3326 };
3327 let frame = match ctx.surface.get_current_texture() {
3328 wgpu::CurrentSurfaceTexture::Success(t) => t,
3329 wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
3330 ctx.surface.configure(&self.device, &ctx.config);
3331 t
3332 }
3333 other => {
3334 log::warn!(
3335 "[GPU] Surface texture acquisition failed ({:?}), reconfiguring surface",
3336 other
3337 );
3338 ctx.surface.configure(&self.device, &ctx.config);
3339 self.queue.submit(std::iter::once(encoder.finish()));
3340 return;
3341 }
3342 };
3343 let view = frame
3344 .texture
3345 .create_view(&wgpu::TextureViewDescriptor::default());
3346
3347 ActiveFrameResources {
3348 surface_texture: Some(frame),
3349 target_view: view,
3350 scene_texture: ctx.scene_texture.clone(),
3351 scene_msaa_texture: ctx.scene_msaa_texture.clone(),
3352 depth_texture_view: ctx.depth_texture_view.clone(),
3353 blur_env_bind_group_a: ctx.blur_env_bind_group_a.clone(),
3354 blur_env_bind_group_b: ctx.blur_env_bind_group_b.clone(),
3355 bloom_env_bind_group_a: ctx.bloom_env_bind_group_a.clone(),
3356 bloom_env_bind_group_b: ctx.bloom_env_bind_group_b.clone(),
3357 }
3358 } else {
3359 let Some(ctx) = self.headless_context.as_ref() else {
3360 log::error!("[GPU] No headless context for end_frame");
3361 return;
3362 };
3363
3364 ActiveFrameResources {
3365 surface_texture: None,
3366 target_view: ctx.output_view.clone(),
3367 scene_texture: ctx.scene_texture.clone(),
3368 scene_msaa_texture: ctx.scene_msaa_texture.clone(),
3369 depth_texture_view: ctx.depth_texture_view.clone(),
3370 blur_env_bind_group_a: ctx.blur_env_bind_group_a.clone(),
3371 blur_env_bind_group_b: ctx.blur_env_bind_group_b.clone(),
3372 bloom_env_bind_group_a: ctx.bloom_env_bind_group_a.clone(),
3373 bloom_env_bind_group_b: ctx.bloom_env_bind_group_b.clone(),
3374 }
3375 };
3376
3377 let has_glass = self
3379 .draw_calls
3380 .iter()
3381 .any(|c| matches!(c.material, cvkg_core::DrawMaterial::Glass { .. }));
3382 let has_bloom = self.bloom_enabled;
3383 let has_accessibility =
3384 self.color_blind_mode != crate::color_blindness::ColorBlindMode::Normal;
3385
3386 let (blur_id, bloom_id) = if let Some(window_id) = self.current_window {
3396 let ctx = self.surfaces.get(&window_id).unwrap();
3397 (ctx.blur_tex_a, ctx.bloom_tex_a)
3398 } else {
3399 let ctx = self.headless_context.as_ref().unwrap();
3400 (ctx.blur_tex_a, ctx.bloom_tex_a)
3401 };
3402 self.registry.alias(kvasir::nodes::RES_BLUR_A, blur_id);
3403 self.registry.alias(kvasir::nodes::RES_BLOOM_A, bloom_id);
3404 self.registry
3405 .alias_view(kvasir::nodes::RES_SCENE, res.scene_texture.clone());
3406 self.registry.alias_view(
3407 kvasir::nodes::RES_SCENE_MSAA,
3408 res.scene_msaa_texture.clone(),
3409 );
3410
3411 let scale = self.current_scale_factor();
3412 let scale_bits = scale.to_bits();
3413 let active_offscreens_count = self.active_offscreens.len();
3414 let portal_regions_count = self.portal_regions.len();
3415 let width = self.current_width();
3416 let height = self.current_height();
3417 let has_volumetric = self.volumetric_enabled;
3418
3419 let use_cache = if let Some(ref cached) = self.cached_graph_plan {
3420 cached.matches(
3421 has_glass,
3422 has_bloom,
3423 has_accessibility,
3424 has_volumetric,
3425 active_offscreens_count,
3426 portal_regions_count,
3427 width,
3428 height,
3429 scale_bits,
3430 )
3431 } else {
3432 false
3433 };
3434
3435 if !use_cache {
3436 let render_graph = kvasir::nodes::build_render_graph(&kvasir::nodes::RenderGraphConfig {
3437 has_glass,
3438 has_bloom,
3439 has_accessibility,
3440 has_volumetric,
3441 active_offscreens: &self.active_offscreens,
3442 portal_regions: &self.portal_regions.iter().cloned().collect::<Vec<_>>(),
3443 width,
3444 height,
3445 scale,
3446 });
3447 let planner = kvasir::planner::ExecutionPlanner::new(&render_graph);
3448 let compiled_plan = planner.compile().expect("RenderGraph cycle detected!");
3449
3450 self.cached_graph_plan = Some(kvasir::graph_cache::CachedGraphPlan {
3451 has_glass,
3452 has_bloom,
3453 has_accessibility,
3454 has_volumetric,
3455 active_offscreens_count,
3456 portal_regions_count,
3457 width,
3458 height,
3459 scale_bits,
3460 graph: render_graph,
3461 plan: compiled_plan,
3462 });
3463 }
3464
3465 let cached = self.cached_graph_plan.as_ref().unwrap();
3466 for &pass_id in &cached.plan {
3467 if let Some(node) = cached.graph.node(pass_id) {
3468 log::trace!("[Kvasir] Executing node: {}", node.label());
3469 let mut ctx = kvasir::node::ExecutionContext {
3470 device: &self.device,
3471 queue: &self.queue,
3472 encoder: &mut encoder,
3473 registry: &self.registry,
3474 renderer: self,
3475 target_view: &res.target_view,
3476 depth_view: &res.depth_texture_view,
3477 blur_env_bind_group_a: &res.blur_env_bind_group_a,
3478 blur_env_bind_group_b: &res.blur_env_bind_group_b,
3479 bloom_env_bind_group_a: &res.bloom_env_bind_group_a,
3480 bloom_env_bind_group_b: &res.bloom_env_bind_group_b,
3481 scale_factor: scale,
3482 };
3483 node.execute(&mut ctx);
3484 }
3485 }
3486
3487 self.staging_command_buffers.push(encoder.finish());
3492
3493 if let (Some(q), Some(b), Some(rb)) = (
3495 &self.skuld_queries,
3496 &self.skuld_buffer,
3497 &self.skuld_read_buffer,
3498 ) {
3499 let mut resolve_encoder =
3500 self.device
3501 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
3502 label: Some("Skuld Resolve Encoder"),
3503 });
3504 resolve_encoder.resolve_query_set(q, 0..2, b, 0);
3505 resolve_encoder.copy_buffer_to_buffer(b, 0, rb, 0, 16);
3506 self.staging_command_buffers.push(resolve_encoder.finish());
3507 }
3508
3509 let cmds = std::mem::take(&mut self.staging_command_buffers);
3510 self.queue.submit(cmds);
3511 self.telemetry.frame_time_ms = self.last_frame_start.elapsed().as_secs_f32() * 1000.0;
3512 self.update_vram_telemetry();
3513
3514 if let Some(f) = res.surface_texture {
3515 f.present();
3516 }
3517 }
3518}
3519
3520impl Drop for SurtrRenderer {
3521 fn drop(&mut self) {
3522 let _ = self.device.poll(wgpu::PollType::Wait {
3524 submission_index: None,
3525 timeout: None,
3526 });
3527 }
3528}
3529
3530impl SurtrRenderer {
3531 pub fn submit_buckets(&mut self, buckets: &cvkg_compositor::CommandBuckets) {
3540 let mut active_offscreens = Vec::new();
3542 let mut current_target_id = None;
3543
3544 for cmd in &buckets.scene_commands {
3545 match cmd {
3546 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
3547 self.set_material(cvkg_core::DrawMaterial::Opaque);
3548 self.submit_routed(routed, current_target_id);
3549 }
3550 cvkg_compositor::engine::RenderCommand::PushOffscreen {
3551 source_layer,
3552 material,
3553 bounds,
3554 } => {
3555 current_target_id = Some(source_layer.0);
3556
3557 let width = (bounds.width).max(1.0) as u32;
3559 let height = (bounds.height).max(1.0) as u32;
3560 self.registry
3561 .allocate_offscreen(&self.device, source_layer.0, [width, height]);
3562
3563 if let cvkg_compositor::Material::ShaderEffect {
3564 effect_name,
3565 params_json: _,
3566 ..
3567 } = material
3568 {
3569 active_offscreens.push(crate::types::OffscreenEffectConfig {
3570 target_id: source_layer.0,
3571 effect: effect_name.clone(),
3572 blend_mode: 0, effect_args: [0.0; 16], });
3575 }
3576 }
3577 cvkg_compositor::engine::RenderCommand::PopOffscreen => {
3578 current_target_id = None;
3579 }
3580 }
3581 }
3582 self.active_offscreens = active_offscreens;
3583
3584 for cmd in &buckets.glass_commands {
3586 if let cvkg_compositor::engine::RenderCommand::Draw(routed) = cmd {
3587 let core_material = match routed.material {
3588 cvkg_compositor::Material::Opaque => cvkg_core::DrawMaterial::Opaque,
3589 cvkg_compositor::Material::Glass {
3590 blur_radius,
3591 depth_index: _,
3592 } => cvkg_core::DrawMaterial::Glass {
3593 blur_radius,
3594 ior_override: 0.0,
3595 },
3596 cvkg_compositor::Material::Overlay => cvkg_core::DrawMaterial::TopUI,
3597 _ => cvkg_core::DrawMaterial::Opaque,
3598 };
3599 self.set_material(core_material);
3600 self.submit_routed(routed, None);
3601 }
3602 }
3603
3604 for cmd in &buckets.overlay_commands {
3606 if let cvkg_compositor::engine::RenderCommand::Draw(routed) = cmd {
3607 self.set_material(cvkg_core::DrawMaterial::TopUI);
3608 self.submit_routed(routed, None);
3609 }
3610 }
3611 }
3612
3613 pub(crate) fn submit_routed(
3615 &mut self,
3616 routed: &cvkg_compositor::RoutedDrawCommand,
3617 target_id: Option<u64>,
3618 ) {
3619 let cmd = &routed.command;
3620 if cmd.index_count == 0 {
3621 return;
3622 }
3623 let material = match &routed.material {
3624 cvkg_compositor::Material::Glass { blur_radius, .. } => {
3625 cvkg_core::DrawMaterial::Glass {
3626 blur_radius: *blur_radius,
3627 ior_override: 0.0,
3628 }
3629 }
3630 cvkg_compositor::Material::Overlay => cvkg_core::DrawMaterial::TopUI,
3631 _ => cvkg_core::DrawMaterial::Opaque,
3632 };
3633 self.draw_calls.push(DrawCall {
3634 texture_id: cmd.texture_id,
3635 scissor_rect: cmd.scissor_rect,
3636 index_start: cmd.index_start,
3637 index_count: cmd.index_count,
3638 material,
3639 target_id,
3640 instance_start: cmd.instance_id,
3641 });
3642 }
3643}
3644
3645impl SurtrRenderer {
3646 pub(crate) fn apply_opacity(&self, mut color: [f32; 4]) -> [f32; 4] {
3648 if let Some(&alpha) = self.opacity_stack.last() {
3649 color[3] *= alpha;
3650 }
3651 color
3652 }
3653
3654 pub fn load_svg(&mut self, name: &str, data: &[u8]) {
3656 let opt = usvg::Options::default();
3657 let tree = match usvg::Tree::from_data(data, &opt) {
3658 Ok(t) => t,
3659 Err(e) => {
3660 log::error!("Failed to parse SVG '{}': {:?}, skipping load", name, e);
3661 return;
3662 }
3663 };
3664
3665 let view_box = Rect {
3666 x: 0.0,
3667 y: 0.0,
3668 width: tree.size().width(),
3669 height: tree.size().height(),
3670 };
3671
3672 let parsed_animations = parse_svg_animations(data);
3673
3674 let mut vertices = Vec::new();
3675 let mut indices = Vec::new();
3676 let mut fill_tessellator = FillTessellator::new();
3677 let mut stroke_tessellator = StrokeTessellator::new();
3678 let mut finalized_animations = Vec::new();
3679
3680 for child in tree.root().children() {
3681 let mut tess_params = TessellateParams {
3682 fill_tessellator: &mut fill_tessellator,
3683 stroke_tessellator: &mut stroke_tessellator,
3684 vertices: &mut vertices,
3685 indices: &mut indices,
3686 parsed_animations: &parsed_animations,
3687 finalized_animations: &mut finalized_animations,
3688 };
3689 self.tessellate_node(child, &mut tess_params);
3690 }
3691
3692 self.svg_cache.put(
3693 name.to_string(),
3694 SvgModel {
3695 vertices,
3696 indices,
3697 view_box,
3698 animations: finalized_animations,
3699 },
3700 );
3701 self.svg_trees.put(name.to_string(), tree);
3702 }
3703
3704 pub(crate) fn tessellate_node(&self, node: &usvg::Node, params: &mut TessellateParams<'_>) {
3705 let start_idx = params.vertices.len();
3706 let node_id = match node {
3707 usvg::Node::Group(g) => g.id().to_string(),
3708 usvg::Node::Path(p) => p.id().to_string(),
3709 _ => String::new(),
3710 };
3711
3712 if let usvg::Node::Group(ref group) = *node {
3713 for child in group.children() {
3714 let mut child_params = TessellateParams {
3715 fill_tessellator: params.fill_tessellator,
3716 stroke_tessellator: params.stroke_tessellator,
3717 vertices: params.vertices,
3718 indices: params.indices,
3719 parsed_animations: params.parsed_animations,
3720 finalized_animations: params.finalized_animations,
3721 };
3722 self.tessellate_node(child, &mut child_params);
3723 }
3724 } else if let usvg::Node::Path(ref path) = *node {
3725 let has_fill = path.fill().is_some();
3726 let has_stroke = path.stroke().is_some();
3727
3728 if !has_fill && !has_stroke {
3730 log::debug!("SVG path '{}' has no fill or stroke, skipping", node_id);
3731 return;
3732 }
3733
3734 let lyon_path = usvg_to_lyon(path);
3735 let screen = [4096.0, 4096.0]; let clip = [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY]; if has_fill && let Some(fill) = path.fill() {
3740 let color = match fill.paint() {
3741 usvg::Paint::Color(c) => [
3742 c.red as f32 / 255.0,
3743 c.green as f32 / 255.0,
3744 c.blue as f32 / 255.0,
3745 fill.opacity().get(),
3746 ],
3747 usvg::Paint::LinearGradient(_)
3748 | usvg::Paint::RadialGradient(_)
3749 | usvg::Paint::Pattern(_) => {
3750 log::warn!(
3751 "SVG path '{}' uses gradient/pattern fill which is not supported, using white fallback",
3752 node_id
3753 );
3754 [1.0, 1.0, 1.0, 1.0]
3755 }
3756 };
3757
3758 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
3759 let base_index_idx = params.indices.len() as u32;
3760
3761 if let Err(e) = params.fill_tessellator.tessellate_path(
3762 &lyon_path,
3763 &FillOptions::default(),
3764 &mut BuffersBuilder::new(&mut buffers, SceneVertexConstructor { color }),
3765 ) {
3766 log::warn!(
3767 "SVG fill tessellation failed for path '{}': {:?}, skipping",
3768 node_id,
3769 e
3770 );
3771 return;
3772 }
3773
3774 params.vertices.extend(buffers.vertices);
3775 for idx in buffers.indices {
3776 params.indices.push(base_index_idx + idx);
3777 }
3778 }
3779
3780 if has_stroke && let Some(stroke) = path.stroke() {
3782 let stroke_index_idx = params.indices.len() as u32; let stroke_width = stroke.width().get(); let color = match stroke.paint() {
3785 usvg::Paint::Color(c) => [
3786 c.red as f32 / 255.0,
3787 c.green as f32 / 255.0,
3788 c.blue as f32 / 255.0,
3789 stroke.opacity().get(),
3790 ],
3791 usvg::Paint::LinearGradient(_)
3792 | usvg::Paint::RadialGradient(_)
3793 | usvg::Paint::Pattern(_) => {
3794 log::warn!(
3795 "SVG path '{}' uses gradient/pattern stroke which is not supported, using white fallback",
3796 node_id
3797 );
3798 [1.0, 1.0, 1.0, 1.0]
3799 }
3800 };
3801
3802 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
3803
3804 if let Err(e) = params.stroke_tessellator.tessellate_path(
3805 &lyon_path,
3806 &StrokeOptions::default().with_line_width(stroke_width),
3807 &mut BuffersBuilder::new(
3808 &mut buffers,
3809 CustomStrokeVertexConstructor { color, clip },
3810 ),
3811 ) {
3812 log::warn!(
3813 "SVG stroke tessellation failed for path '{}': {:?}, skipping",
3814 node_id,
3815 e
3816 );
3817 return;
3818 }
3819
3820 params.vertices.extend(buffers.vertices);
3821 for idx in buffers.indices {
3822 params.indices.push(stroke_index_idx + idx);
3823 }
3824 }
3825 }
3826
3827 let end_idx = params.vertices.len();
3828 if !node_id.is_empty() && start_idx < end_idx {
3829 for anim in params.parsed_animations {
3830 if anim.target_id == node_id {
3831 let mut final_anim = anim.clone();
3832 final_anim.vertex_range = start_idx..end_idx;
3833 params.finalized_animations.push(final_anim);
3834 }
3835 }
3836 }
3837 }
3838
3839 pub fn draw_svg(&mut self, name: &str, rect: Rect, color: Option<[f32; 4]>, material_id: u32) {
3841 let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
3842 x: -10000.0,
3843 y: -10000.0,
3844 width: 20000.0,
3845 height: 20000.0,
3846 });
3847 let scale = self.current_scale_factor();
3848 let screen_w = self.current_width() as f32 / scale;
3849 let screen_h = self.current_height() as f32 / scale;
3850
3851 if rect.x > clip_rect.x + clip_rect.width
3852 || rect.x + rect.width < clip_rect.x
3853 || rect.y > clip_rect.y + clip_rect.height
3854 || rect.y + rect.height < clip_rect.y
3855 {
3856 return;
3857 }
3858 if rect.x > screen_w
3859 || rect.x + rect.width < 0.0
3860 || rect.y > screen_h
3861 || rect.y + rect.height < 0.0
3862 {
3863 return;
3864 }
3865
3866 let model = if let Some(m) = self.svg_cache.get(name) {
3867 m.clone()
3868 } else {
3869 return;
3870 };
3871
3872 let _scale_x = rect.width / model.view_box.width;
3873 let _scale_y = rect.height / model.view_box.height;
3874 let base_idx = self.vertices.len() as u32;
3875 let screen = [self.current_width() as f32, self.current_height() as f32];
3876 let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
3877 x: -10000.0,
3878 y: -10000.0,
3879 width: 20000.0,
3880 height: 20000.0,
3881 });
3882 let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
3883 let scale = self.current_scale_factor();
3884 let snap = |v: f32| (v * scale).round() / scale;
3885
3886 let mut local_vertices = model.vertices.clone();
3887 for anim in &model.animations {
3888 let t = (self.current_scene.time % anim.duration) / anim.duration;
3889 let val = anim.from_val + (anim.to_val - anim.from_val) * t;
3890
3891 if anim.attribute_name == "transform" {
3892 let mut min_x = f32::MAX;
3894 let mut min_y = f32::MAX;
3895 let mut max_x = f32::MIN;
3896 let mut max_y = f32::MIN;
3897 for i in anim.vertex_range.clone() {
3898 let p = local_vertices[i].position;
3899 if p[0] < min_x {
3900 min_x = p[0];
3901 }
3902 if p[1] < min_y {
3903 min_y = p[1];
3904 }
3905 if p[0] > max_x {
3906 max_x = p[0];
3907 }
3908 if p[1] > max_y {
3909 max_y = p[1];
3910 }
3911 }
3912 let cx = (min_x + max_x) * 0.5;
3913 let cy = (min_y + max_y) * 0.5;
3914
3915 let c = val.to_radians().cos();
3916 let s = val.to_radians().sin();
3917
3918 for i in anim.vertex_range.clone() {
3919 let p = local_vertices[i].position;
3920 let dx = p[0] - cx;
3921 let dy = p[1] - cy;
3922 local_vertices[i].position[0] = cx + dx * c - dy * s;
3923 local_vertices[i].position[1] = cy + dx * s + dy * c;
3924 }
3925 } else if anim.attribute_name == "opacity" {
3926 for i in anim.vertex_range.clone() {
3927 local_vertices[i].color[3] = val;
3928 }
3929 }
3930 }
3931
3932 let (blur_radius, ior_override) = if material_id == 7 {
3933 if let cvkg_core::DrawMaterial::Glass {
3934 blur_radius,
3935 ior_override,
3936 } = self.current_draw_material
3937 {
3938 (blur_radius, ior_override)
3939 } else {
3940 (20.0, 0.0)
3941 }
3942 } else {
3943 (0.0, 0.0)
3944 };
3945 for mut v in local_vertices {
3946 let rel_x = (v.position[0] - model.view_box.x) / model.view_box.width;
3947 let rel_y = (v.position[1] - model.view_box.y) / model.view_box.height;
3948
3949 v.position[0] = snap(rect.x + rel_x * rect.width);
3950 v.position[1] = snap(rect.y + rel_y * rect.height);
3951 v.position[2] = self.current_z;
3952 v.logical = [v.position[0], v.position[1]];
3953
3954 v.clip = clip;
3955 v.material_id = material_id;
3956
3957 if let Some(override_color) = color {
3958 let mut c = override_color;
3959 c[3] *= v.color[3]; v.color = self.apply_opacity(c);
3961 } else {
3962 v.color = self.apply_opacity(v.color);
3963 }
3964 self.vertices.push(v);
3965 }
3966
3967 for idx in &model.indices {
3968 self.indices.push(base_idx + *idx);
3969 }
3970
3971 let material = match material_id {
3972 7 => cvkg_core::DrawMaterial::Glass {
3973 blur_radius,
3974 ior_override,
3975 },
3976 0 => cvkg_core::DrawMaterial::Opaque,
3977 _ => cvkg_core::DrawMaterial::TopUI,
3978 };
3979 let tid = self.get_texture_id("__mega_heim");
3980
3981 let (translation, scale_transform, rotation, _, _) = self.current_transform();
3982 let current_instance_data = InstanceData {
3983 translation,
3984 scale: scale_transform,
3985 rotation,
3986 blur_radius,
3987 ior_override,
3988 };
3989
3990 let last_call = self.draw_calls.last();
3991 let needs_new_call = self.draw_calls.is_empty()
3992 || self.current_texture_id != tid
3993 || last_call.unwrap().scissor_rect != self.clip_stack.last().copied()
3994 || last_call.unwrap().material != material
3995 || self.instance_data.last() != Some(¤t_instance_data);
3996
3997 if needs_new_call {
3998 self.current_texture_id = tid;
3999 self.instance_data.push(current_instance_data);
4000 self.draw_calls.push(DrawCall {
4001 target_id: None,
4002 texture_id: tid,
4003 scissor_rect: self.clip_stack.last().copied(),
4004 index_start: (self.indices.len() - model.indices.len()) as u32,
4005 index_count: 0,
4006 material,
4007 instance_start: (self.instance_data.len() - 1) as u32,
4008 });
4009 }
4010
4011 if let Some(call) = self.draw_calls.last_mut() {
4012 call.index_count += model.indices.len() as u32;
4013 }
4014 }
4015
4016 pub async fn forge_headless(width: u32, height: u32) -> Self {
4018 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
4019 backends: wgpu::Backends::all(),
4020 flags: wgpu::InstanceFlags::default(),
4021 backend_options: wgpu::BackendOptions::default(),
4022 display: None,
4023 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
4024 });
4025
4026 log::info!("[GPU] Requesting HighPerformance adapter (headless)...");
4028 let mut adapter = instance
4029 .request_adapter(&wgpu::RequestAdapterOptions {
4030 power_preference: wgpu::PowerPreference::HighPerformance,
4031 compatible_surface: None,
4032 force_fallback_adapter: false,
4033 })
4034 .await
4035 .ok();
4036
4037 if adapter.is_none() {
4038 log::warn!(
4039 "[GPU] HighPerformance adapter failed (possible Bumblebee/Optimus), trying LowPower..."
4040 );
4041 adapter = instance
4042 .request_adapter(&wgpu::RequestAdapterOptions {
4043 power_preference: wgpu::PowerPreference::LowPower,
4044 compatible_surface: None,
4045 force_fallback_adapter: false,
4046 })
4047 .await
4048 .ok();
4049 }
4050
4051 if adapter.is_none() {
4052 log::warn!("[GPU] Hardware adapters failed, trying Software fallback...");
4053 adapter = instance
4054 .request_adapter(&wgpu::RequestAdapterOptions {
4055 power_preference: wgpu::PowerPreference::LowPower,
4056 compatible_surface: None,
4057 force_fallback_adapter: true,
4058 })
4059 .await
4060 .ok();
4061 }
4062
4063 let adapter = adapter.expect("Failed to find a suitable GPU for Surtr");
4064 let info = adapter.get_info();
4065 log::info!(
4066 "[GPU] Selected adapter: {} ({:?}) on backend: {:?}",
4067 info.name,
4068 info.device_type,
4069 info.backend
4070 );
4071 log::info!("[GPU] Driver info: {} - {}", info.driver, info.driver_info);
4072 let required_features = adapter.features()
4073 & (wgpu::Features::TIMESTAMP_QUERY
4074 | wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
4075 | wgpu::Features::TEXTURE_BINDING_ARRAY);
4076
4077 let (device, queue) = adapter
4078 .request_device(&wgpu::DeviceDescriptor {
4079 label: Some("Surtr Headless Forge"),
4080 required_features,
4081 required_limits: wgpu::Limits {
4082 max_bindings_per_bind_group: adapter
4083 .limits()
4084 .max_bindings_per_bind_group
4085 .min(256),
4086 max_binding_array_elements_per_shader_stage: adapter
4087 .limits()
4088 .max_binding_array_elements_per_shader_stage
4089 .min(256),
4090 ..wgpu::Limits::default()
4091 },
4092 memory_hints: wgpu::MemoryHints::default(),
4093 experimental_features: wgpu::ExperimentalFeatures::disabled(),
4094 trace: wgpu::Trace::Off,
4095 })
4096 .await
4097 .expect("Failed to create Surtr device");
4098
4099 let instance = Arc::new(instance);
4100 let adapter = Arc::new(adapter);
4101
4102 device.on_uncaptured_error(Arc::new(|error| {
4103 log::error!(
4104 "[GPU] Uncaptured device error (Device Lost or Panic): {:?}",
4105 error
4106 );
4107 }));
4108
4109 let device = Arc::new(device);
4110 let queue = Arc::new(queue);
4111
4112 Self::forge_internal(
4113 instance,
4114 adapter,
4115 device,
4116 queue,
4117 None,
4118 Some((width, height, wgpu::TextureFormat::Rgba8UnormSrgb)),
4119 )
4120 .await
4121 }
4122
4123 pub async fn capture_frame(&self) -> Result<Vec<u8>, String> {
4125 let ctx = self
4126 .headless_context
4127 .as_ref()
4128 .ok_or("Headless context required for capture")?;
4129 let current_width = self.current_width();
4130 let current_height = self.current_height();
4131
4132 let u32_size = std::mem::size_of::<u32>() as u32;
4133 let width = ctx.width;
4134 let height = ctx.height;
4135 let bytes_per_row = width * u32_size;
4136 let padding = (256 - (bytes_per_row % 256)) % 256;
4137 let padded_bytes_per_row = bytes_per_row + padding;
4138
4139 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
4140 label: Some("Capture Buffer"),
4141 size: (padded_bytes_per_row as u64 * height as u64),
4142 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
4143 mapped_at_creation: false,
4144 });
4145
4146 let mut encoder = self
4147 .device
4148 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
4149 label: Some("Capture Encoder"),
4150 });
4151
4152 encoder.copy_texture_to_buffer(
4153 wgpu::TexelCopyTextureInfo {
4154 texture: &ctx.output_texture,
4155 mip_level: 0,
4156 origin: wgpu::Origin3d::ZERO,
4157 aspect: wgpu::TextureAspect::All,
4158 },
4159 wgpu::TexelCopyBufferInfo {
4160 buffer: &output_buffer,
4161 layout: wgpu::TexelCopyBufferLayout {
4162 offset: 0,
4163 bytes_per_row: Some(padded_bytes_per_row),
4164 rows_per_image: Some(height),
4165 },
4166 },
4167 wgpu::Extent3d {
4168 width,
4169 height,
4170 depth_or_array_layers: 1,
4171 },
4172 );
4173
4174 self.queue.submit(Some(encoder.finish()));
4175
4176 let buffer_slice = output_buffer.slice(..);
4177 let (sender, receiver) = futures::channel::oneshot::channel();
4178 buffer_slice.map_async(wgpu::MapMode::Read, move |v| {
4179 let _ = sender.send(v);
4180 });
4181
4182 let _ = self.device.poll(wgpu::PollType::Wait {
4183 submission_index: None,
4184 timeout: None,
4185 });
4186
4187 if let Ok(Ok(_)) = receiver.await {
4188 let data = buffer_slice.get_mapped_range();
4189 let mut result = Vec::with_capacity((width * height * 4) as usize);
4190
4191 for y in 0..height {
4192 let start = (y * padded_bytes_per_row) as usize;
4193 let end = start + bytes_per_row as usize;
4194 result.extend_from_slice(&data[start..end]);
4195 }
4196
4197 log::trace!(
4198 "[GPU] capture_frame: data len={}, first 4 bytes={:?}",
4199 data.len(),
4200 &data[0..4.min(data.len())]
4201 );
4202
4203 drop(data);
4204 output_buffer.unmap();
4205 Ok(result)
4206 } else {
4207 Err("Failed to capture frame".to_string())
4208 }
4209 }
4210
4211 pub(crate) fn current_width(&self) -> u32 {
4212 if let Some(id) = self.current_window {
4213 self.surfaces.get(&id).map(|s| s.config.width).unwrap_or(1)
4214 } else {
4215 self.headless_context.as_ref().map(|h| h.width).unwrap_or(1)
4216 }
4217 }
4218
4219 pub(crate) fn current_height(&self) -> u32 {
4220 if let Some(id) = self.current_window {
4221 self.surfaces.get(&id).map(|s| s.config.height).unwrap_or(1)
4222 } else {
4223 self.headless_context
4224 .as_ref()
4225 .map(|h| h.height)
4226 .unwrap_or(1)
4227 }
4228 }
4229
4230 pub(crate) fn current_scale_factor(&self) -> f32 {
4231 if let Some(id) = self.current_window {
4232 self.surfaces
4233 .get(&id)
4234 .map(|s| s.scale_factor)
4235 .unwrap_or(1.0)
4236 } else {
4237 self.headless_context
4238 .as_ref()
4239 .map(|h| h.scale_factor)
4240 .unwrap_or(1.0)
4241 }
4242 }
4243
4244 pub(crate) fn current_time(&self) -> f32 {
4247 self.start_time.elapsed().as_secs_f32()
4248 }
4249
4250 pub(crate) fn find_filter<'a>(
4252 tree: &'a usvg::Tree,
4253 filter_id: &str,
4254 ) -> Option<&'a usvg::filter::Filter> {
4255 tree.filters()
4256 .iter()
4257 .find(|f| f.id() == filter_id)
4258 .map(|arc| arc.as_ref())
4259 }
4260}
4261
4262#[cfg(test)]
4263mod wgsl_tests {
4264 #[test]
4265 fn test_wgsl() {
4266 let source = include_str!("shaders/effects.wgsl");
4267 let mut frontend = naga::front::wgsl::Frontend::new();
4268 match frontend.parse(source) {
4269 Ok(_) => println!("WGSL parsed successfully!"),
4270 Err(e) => {
4271 panic!("WGSL parsing failed: \n{}", e.emit_to_string(source));
4272 }
4273 }
4274 }
4275}