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