Skip to main content

cvkg_render_gpu/
renderer.rs

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