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