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