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