Skip to main content

cvkg_render_gpu/
renderer.rs

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