Skip to main content

cvkg_render_gpu/
lib.rs

1//! # CVKG Agentic Development Guidelines (v1.2)
2//!
3//! All AI agents contributing to this crate MUST follow ALL seven rules:
4//!
5//! ── Karpathy Guidelines (1–4) ────────────────────────────────────────────
6//! 1. THINK FIRST     — State assumptions. Surface ambiguity. Push back on complexity.
7//! 2. STAY SIMPLE     — Minimum code. No speculative features. No unasked-for abstractions.
8//! 3. BE SURGICAL     — Touch only what's required. Own your orphans. Don't improve neighbors.
9//! 4. VERIFY GOALS    — Turn tasks into checkable criteria. Loop until they pass. Never commit broken.
10//!
11//! ── CVKG Extended Protocols (5–7) ────────────────────────────────────────
12//! 5. TRIPLE-PASS     — Read the target, its surrounding context, and its full call graph
13//!                      at least THREE TIMES before making any edit or revision.
14//! 6. COMMENT ALL     — Every major pub fn, unsafe block, and non-trivial algorithm in
15//!                      every .rs/.ts/.h/.wgsl file MUST have a descriptive doc comment.
16//!                      Comments describe WHY and WHAT CONTRACT, not HOW mechanically.
17//! 7. MONITOR LOOPS   — Check every tool call / command for progress every 30 seconds.
18//!                      After 3 consecutive identical failures, stop, write BLOCKED.md,
19//!                      and move to unblocked work. Never silently accept a broken state.
20//!
21//! Sources:
22//!   Karpathy: https://github.com/multica-ai/andrej-karpathy-skills
23//!   CVKG Extended: Section 2 of the CVKG Design Specification
24#![allow(clippy::type_complexity, clippy::unwrap_or_default)]
25
26//! # Surtr Render Pipeline
27//!
28//! The "Fiery Giant" of the CVKG architecture. This is the authoritative GPU renderer
29//! powered by `wgpu`. It manages the heat of the GPU to forge high-fidelity
30//! "Berserker" aesthetics.
31//!
32//! - **The Flaming Sword**: Command submission and synchronization.
33//! - **Muspelheim Passes**: Multi-pass Gaussian blur and bloom for Bifrost/Gungnir.
34//! - **Reclaim & Quench**: LRU-based cache eviction and atlas recycling.
35
36use cvkg_core::Rect;
37use lru::LruCache;
38use std::num::NonZeroUsize;
39use std::sync::Arc;
40
41#[derive(Clone, Copy)]
42struct SkylineSegment {
43    x: u32,
44    y: u32,
45    w: u32,
46}
47
48struct YggdrasilPacker {
49    width: u32,
50    height: u32,
51    skyline: Vec<SkylineSegment>,
52}
53
54impl YggdrasilPacker {
55    fn new(width: u32, height: u32) -> Self {
56        Self {
57            width,
58            height,
59            skyline: vec![SkylineSegment { x: 0, y: 0, w: width }],
60        }
61    }
62
63    fn pack(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
64        if w > self.width || h > self.height { return None; }
65        
66        let mut best_idx = None;
67        let mut best_y = u32::MAX;
68        let mut best_w = u32::MAX;
69        
70        for i in 0..self.skyline.len() {
71            let seg = &self.skyline[i];
72            if seg.x + w > self.width { continue; }
73            
74            let mut y = seg.y;
75            let mut remaining = w;
76            let mut j = i;
77            let mut fits = true;
78            
79            while remaining > 0 {
80                if j >= self.skyline.len() { fits = false; break; }
81                let s = &self.skyline[j];
82                y = y.max(s.y);
83                if y + h > self.height { fits = false; break; }
84                if s.w >= remaining { break; }
85                remaining -= s.w;
86                j += 1;
87            }
88            
89            if fits {
90                if y < best_y || (y == best_y && seg.w < best_w) {
91                    best_y = y;
92                    best_idx = Some(i);
93                    best_w = seg.w;
94                }
95            }
96        }
97        
98        if let Some(idx) = best_idx {
99            let x = self.skyline[idx].x;
100            let y = best_y;
101            
102            let new_seg = SkylineSegment { x, y: y + h, w };
103            let mut remaining = w;
104            let insert_idx = idx;
105            
106            while remaining > 0 {
107                if self.skyline[insert_idx].w <= remaining {
108                    remaining -= self.skyline[insert_idx].w;
109                    self.skyline.remove(insert_idx);
110                } else {
111                    self.skyline[insert_idx].x += remaining;
112                    self.skyline[insert_idx].w -= remaining;
113                    remaining = 0;
114                }
115            }
116            self.skyline.insert(insert_idx, new_seg);
117            
118            let mut i = 0;
119            while i < self.skyline.len() - 1 {
120                if self.skyline[i].y == self.skyline[i+1].y {
121                    let w = self.skyline[i+1].w;
122                    self.skyline[i].w += w;
123                    self.skyline.remove(i+1);
124                } else {
125                    i += 1;
126                }
127            }
128            
129            return Some((x, y));
130        }
131        None
132    }
133
134
135
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_shelf_packer_basic() {
144        let mut packer = YggdrasilPacker::new(100, 100);
145
146        // Pack first item
147        assert_eq!(packer.pack(10, 10), Some((0, 0)));
148
149        // Pack second item on same shelf
150        assert_eq!(packer.pack(20, 15), Some((10, 0)));
151    }
152
153    #[test]
154    fn test_shelf_packer_wrap() {
155        let mut packer = YggdrasilPacker::new(100, 100);
156        packer.pack(60, 10);
157
158        // This should trigger a new shelf
159        assert_eq!(packer.pack(50, 20), Some((0, 10)));
160    }
161
162    #[test]
163    fn test_parse_svg_animations() {
164        let svg = r##"
165            <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
166                <g id="spinner">
167                    <animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="2s" />
168                </g>
169                <circle id="pulse">
170                    <animate attributeName="opacity" from="0.5" to="1.0" dur="0.5s" />
171                </circle>
172                <!-- Edge cases: xlink:href, ms suffix, values list -->
173                <rect>
174                    <animate xlink:href="#myRect" attributeName="x" values="10; 20; 30" dur="500ms" />
175                </rect>
176            </svg>
177        "##;
178        let anims = parse_svg_animations(svg.as_bytes());
179        assert_eq!(anims.len(), 3);
180        
181        assert_eq!(anims[0].target_id, "spinner");
182        assert_eq!(anims[0].attribute_name, "transform");
183        assert_eq!(anims[0].duration, 2.0);
184        assert_eq!(anims[0].from_val, 0.0);
185        assert_eq!(anims[0].to_val, 360.0);
186
187        assert_eq!(anims[1].target_id, "pulse");
188        assert_eq!(anims[1].attribute_name, "opacity");
189        assert_eq!(anims[1].duration, 0.5);
190        assert_eq!(anims[1].from_val, 0.5);
191        assert_eq!(anims[1].to_val, 1.0);
192
193        assert_eq!(anims[2].target_id, "myRect");
194        assert_eq!(anims[2].attribute_name, "x");
195        assert_eq!(anims[2].duration, 0.5); // 500ms parsed as 0.5
196        assert_eq!(anims[2].from_val, 10.0);
197        assert_eq!(anims[2].to_val, 30.0);
198    }
199
200    #[test]
201    fn test_shelf_packer_full() {
202        let mut packer = YggdrasilPacker::new(10, 10);
203        assert_eq!(packer.pack(11, 5), None);
204        assert_eq!(packer.pack(5, 11), None);
205    }
206}
207
208use cvkg_core::{LAYOUT_DIRTY, Mesh, Renderer};
209use std::sync::atomic::Ordering;
210const WGSL_SRC: &str = concat!(
211    include_str!("shaders/common.wgsl"),
212    include_str!("shaders/shapes.wgsl"),
213    include_str!("shaders/bifrost.wgsl"),
214    include_str!("shaders/bloom.wgsl")
215);
216
217/// SvgModel — A collection of tessellated triangles representing a vector icon.
218#[derive(Clone, Debug)]
219pub struct SvgModel {
220    pub vertices: Vec<Vertex>,
221    pub indices: Vec<u32>,
222    pub view_box: Rect,
223    pub animations: Vec<SvgAnimation>,
224}
225
226#[derive(Clone, Debug)]
227pub struct SvgAnimation {
228    pub target_id: String,
229    pub attribute_name: String,
230    pub from_val: f32,
231    pub to_val: f32,
232    pub duration: f32,
233    pub vertex_range: std::ops::Range<usize>,
234}
235
236// ShieldWall — re-export AccessKit types so callers can build tree updates
237// without depending on accesskit directly.
238pub use accesskit::{
239    ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler, Node, NodeId, Role, Tree,
240    TreeId, TreeUpdate,
241};
242pub use accesskit_winit::Adapter as ShieldWallAdapter;
243
244// Re-export ColorTheme and SceneUniforms for cvkg-render-gpu users
245pub use cvkg_core::{ColorTheme, SceneUniforms};
246
247use lyon::tessellation::{
248    BuffersBuilder, FillOptions, FillTessellator, FillVertex, FillVertexConstructor, StrokeOptions,
249    StrokeTessellator, StrokeVertex, StrokeVertexConstructor, VertexBuffers,
250};
251
252#[repr(C)]
253#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
254pub struct Vertex {
255    pub position: [f32; 3],
256    pub normal: [f32; 3],
257    pub uv: [f32; 2],
258    pub color: [f32; 4],
259    pub mode: u32,
260    pub radius: f32,
261    pub slice: [f32; 4],
262    pub logical: [f32; 2],
263    pub size: [f32; 2],
264    pub screen: [f32; 2],
265    pub clip: [f32; 4], // [x, y, width, height]
266    pub translation: [f32; 2],
267    pub scale: [f32; 2],
268    pub rotation: f32,
269    pub tex_index: u32,
270}
271
272/// Represents a single batched GPU draw call.
273/// Batches are broken whenever the active texture or primitive mode changes.
274#[derive(Debug, Clone)]
275struct DrawCall {
276    pub texture_id: Option<u32>,
277    pub scissor_rect: Option<Rect>,
278    pub index_start: u32,
279    pub index_count: u32,
280    /// Material routing tag — determines which pass this draw call is routed to
281    /// in the multi-pass Backdrop Capture pipeline.
282    pub material: cvkg_core::DrawMaterial,
283}
284
285#[derive(Debug, Clone, Copy)]
286struct ShadowState {
287    pub radius: f32,
288    pub color: [f32; 4],
289    pub _offset: [f32; 2],
290}
291
292impl Vertex {
293    const ATTRIBUTES: [wgpu::VertexAttribute; 15] = wgpu::vertex_attr_array![
294        0 => Float32x3, // position
295        1 => Float32x3, // normal
296        2 => Float32x2, // uv
297        3 => Float32x4, // color
298        4 => Uint32,    // mode
299        5 => Float32,   // radius
300        6 => Float32x4, // slice
301        7 => Float32x2, // logical
302        8 => Float32x2, // size
303        9 => Float32x2, // screen
304        10 => Float32x4, // clip
305        11 => Float32x2, // translation
306        12 => Float32x2, // scale
307        13 => Float32,   // rotation
308        14 => Uint32     // tex_index
309    ];
310
311    fn desc() -> wgpu::VertexBufferLayout<'static> {
312        wgpu::VertexBufferLayout {
313            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
314            step_mode: wgpu::VertexStepMode::Vertex,
315            attributes: &Self::ATTRIBUTES,
316        }
317    }
318}
319
320/// SurtrRenderer implements the high-performance GPU backend.
321#[allow(dead_code)]
322pub struct SurtrRenderer {
323    instance: Arc<wgpu::Instance>,
324    adapter: Arc<wgpu::Adapter>,
325    device: Arc<wgpu::Device>,
326    queue: Arc<wgpu::Queue>,
327
328    // Multi-Window Surface Management
329    surfaces: std::collections::HashMap<winit::window::WindowId, SurfaceContext>,
330    current_window: Option<winit::window::WindowId>,
331    pub headless_context: Option<HeadlessContext>,
332
333    // Mega-Atlas (Shared across all windows)
334    text_engine: cvkg_runic_text::RunicTextEngine,
335    mega_atlas_tex: wgpu::Texture,
336    #[allow(dead_code)]
337    mega_atlas_view: wgpu::TextureView,
338    _mega_atlas_sampler: wgpu::Sampler,
339    mega_atlas_bind_group: wgpu::BindGroup,
340    text_cache: LruCache<u64, (Rect, f32, f32)>,
341    atlas_packer: YggdrasilPacker,
342    image_uv_registry: LruCache<String, Rect>,
343    texture_registry: LruCache<String, u32>,
344    texture_views: Vec<wgpu::TextureView>,
345    dummy_sampler: wgpu::Sampler,
346    svg_cache: LruCache<String, SvgModel>,
347    /// Parsed SVG trees for serialization and filter application.
348    svg_trees: LruCache<String, usvg::Tree>,
349    /// WGPU device for filter operations (cloned from main device).
350    filter_device: Option<Arc<wgpu::Device>>,
351    /// WGPU queue for filter operations (cloned from main queue).
352    filter_queue: Option<Arc<wgpu::Queue>>,
353    /// Clamp-to-edge sampler for SVG filter operations.
354    filter_sampler: wgpu::Sampler,
355    /// SVG filter evaluation engine.
356    filter_engine: Option<cvkg_svg_filters::FilterEngine>,
357    /// Pending filter batches accumulated during tessellation.
358    filter_batches: Vec<cvkg_svg_filters::FilterNode>,
359
360    // Niflheim Resources (Shared)
361    dummy_texture_bind_group: wgpu::BindGroup,
362    dummy_env_bind_group: wgpu::BindGroup,
363    texture_bind_group_layout: wgpu::BindGroupLayout,
364    texture_bind_groups: Vec<wgpu::BindGroup>,
365    shared_elements: LruCache<String, cvkg_core::Rect>,
366
367    // The Forge's Anvil (GPU Buffers)
368    vertex_buffer: wgpu::Buffer,
369    index_buffer: wgpu::Buffer,
370    vertices: Vec<Vertex>,
371    indices: Vec<u32>,
372    staging_belt: wgpu::util::StagingBelt,
373    staging_command_buffers: Vec<wgpu::CommandBuffer>,
374    draw_calls: Vec<DrawCall>,
375    current_texture_id: Option<u32>,
376
377    // Opacity & Clip Stacks
378    opacity_stack: Vec<f32>,
379    clip_stack: Vec<Rect>,
380    slice_stack: Vec<(f32, f32)>,
381    shadow_stack: Vec<ShadowState>,
382
383    // The Forge's Heart (Shared Berserker State)
384    theme_buffer: wgpu::Buffer,
385    scene_buffer: wgpu::Buffer,
386    berserker_bind_group: wgpu::BindGroup,
387    #[allow(dead_code)]
388    berserker_bind_group_layout: wgpu::BindGroupLayout,
389    start_time: std::time::Instant,
390    current_theme: ColorTheme,
391    current_scene: SceneUniforms,
392    current_z: f32,
393
394    // Muspelheim Pipelines (Shared)
395    pipeline: wgpu::RenderPipeline,
396    background_pipeline: wgpu::RenderPipeline,
397    bloom_extract_pipeline: wgpu::RenderPipeline,
398    blur_h_pipeline: wgpu::RenderPipeline,
399    blur_v_pipeline: wgpu::RenderPipeline,
400    composite_pipeline: wgpu::RenderPipeline,
401    env_bind_group_layout: wgpu::BindGroupLayout,
402
403    // Telemetry
404    pub telemetry: cvkg_core::TelemetryData,
405
406    /// Configuration for render-loop frame timing and degradation strategies.
407    pub frame_budget: cvkg_core::FrameBudget,
408    /// Instant at the start of the last redraw, used for measuring frame timings.
409    pub last_redraw_start: std::time::Instant,
410    /// Instant at the start of the last frame, used for frame_time_ms calculation.
411    pub last_frame_start: std::time::Instant,
412
413    // VRAM Tracking (Bytes)
414    vram_buffers_bytes: u64,
415    vram_textures_bytes: u64,
416
417    // Debugging
418    _debug_layout: bool,
419
420    // Transform Stack — stores full affine matrices for correct SVG transform composition.
421    transform_stack: Vec<glam::Mat3>,
422    /// Whether a redraw has been requested for the next frame.
423    pub redraw_requested: bool,
424
425    // Timestamp Queries (Norse: Skuld = future/time/debt)
426    skuld_queries: Option<wgpu::QuerySet>,
427    skuld_buffer: Option<wgpu::Buffer>,
428    skuld_read_buffer: Option<wgpu::Buffer>,
429    skuld_period: f32,
430    pub last_gpu_time_ns: u64,
431
432    // VDOM node stack for hierarchy tracking
433    vnode_stack: Vec<(Rect, &'static str)>,
434
435    /// Event handlers registered during render passes.
436    /// Maps "event_type" -> list of handlers.
437    event_handlers: std::collections::HashMap<
438        String,
439        Vec<std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>>,
440    >,
441
442    // ══════════════════════════════════════════════════════════════════════════
443    // Backdrop Capture Architecture — Kawase Blur Pyramid
444    // ══════════════════════════════════════════════════════════════════════════
445    /// Off-screen texture holding the mip-chain blur pyramid for glass sampling.
446    glass_blur_texture: wgpu::Texture,
447    /// Per-mip-level views into glass_blur_texture.
448    glass_blur_views: Vec<wgpu::TextureView>,
449    /// Bind groups for the downsample pass (one per mip level).
450    glass_blur_down_bind_groups: Vec<wgpu::BindGroup>,
451    /// Bind groups for the upsample pass (one per mip level).
452    glass_blur_up_bind_groups: Vec<wgpu::BindGroup>,
453    /// Uniform buffer for blur parameters (src_size, mip_level, kernel_width, mode).
454    glass_blur_uniform_buffer: wgpu::Buffer,
455    /// Render pipeline for Kawase downsample passes.
456    glass_blur_pipeline: wgpu::RenderPipeline,
457    /// Render pipeline for Kawase upsample passes.
458    glass_blur_upsample_pipeline: wgpu::RenderPipeline,
459    /// Bind group layout for blur passes (uniform + texture + sampler).
460    glass_blur_bind_group_layout: wgpu::BindGroupLayout,
461    /// Bind group layout for reading blur output in glass composite pass.
462    glass_output_bind_group_layout: wgpu::BindGroupLayout,
463    /// Current material state — draw calls are tagged with this material.
464    current_draw_material: cvkg_core::DrawMaterial,
465    /// Number of mip levels in the glass blur pyramid.
466    blur_pyramid_mip_count: u32,
467}
468
469struct SurfaceContext {
470    surface: wgpu::Surface<'static>,
471    config: wgpu::SurfaceConfiguration,
472    scene_texture: wgpu::TextureView,
473    scene_bind_group: wgpu::BindGroup,
474    scene_texture_bind_group: wgpu::BindGroup,
475    depth_texture_view: wgpu::TextureView,
476    blur_texture_a: wgpu::TextureView,
477    blur_texture_b: wgpu::TextureView,
478    blur_bind_group_a: wgpu::BindGroup,
479    blur_bind_group_b: wgpu::BindGroup,
480    blur_env_bind_group_a: wgpu::BindGroup,
481    scale_factor: f32,
482    sampler: wgpu::Sampler,
483}
484
485/// HeadlessContext — A rendering target for surface-less execution.
486pub struct HeadlessContext {
487    pub scene_texture: wgpu::TextureView,
488    pub scene_bind_group: wgpu::BindGroup,
489    pub scene_texture_bind_group: wgpu::BindGroup,
490    pub depth_texture_view: wgpu::TextureView,
491    pub blur_texture_a: wgpu::TextureView,
492    pub blur_texture_b: wgpu::TextureView,
493    pub blur_bind_group_a: wgpu::BindGroup,
494    pub blur_bind_group_b: wgpu::BindGroup,
495    pub blur_env_bind_group_a: wgpu::BindGroup,
496    pub scale_factor: f32,
497    pub sampler: wgpu::Sampler,
498    pub width: u32,
499    pub height: u32,
500    pub output_texture: wgpu::Texture,
501    pub output_view: wgpu::TextureView,
502}
503
504const MAX_VERTICES: usize = 100_000;
505const MAX_INDICES: usize = 150_000;
506
507impl SurtrRenderer {
508    /// forge — Initializes the Surtr GPU renderer from a winit window.
509    ///
510    /// This method performs the following:
511    /// 1. Negotiates a wgpu surface and adapter.
512    /// 2. Forges the Muspelheim multi-pass pipeline layouts.
513    /// 3. Initializes the Berserker state buffers and texture registries.
514    pub async fn forge(window: Arc<winit::window::Window>) -> Self {
515        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
516            backends: wgpu::Backends::all(),
517            flags: wgpu::InstanceFlags::default(),
518            backend_options: wgpu::BackendOptions::default(),
519            display: None,
520            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
521        });
522
523        let surface = instance
524            .create_surface(window.clone())
525            .expect("Failed to create surface");
526
527        // Request adapter with robust multi-stage fallback for Bumblebee/Optimus compatibility
528        println!("[GPU] Requesting HighPerformance adapter...");
529
530        let mut adapter = None;
531
532        // Manual override for driver/adapter selection (e.g. forcing amdgpu-pro over RADV)
533        if let Ok(filter) = std::env::var("WGPU_ADAPTER_NAME") {
534            let adapters = instance.enumerate_adapters(wgpu::Backends::all()).await;
535            println!("[GPU] Available adapters:");
536            for a in &adapters {
537                let info = a.get_info();
538                println!(
539                    "  - Name: '{}' | Driver: '{}' | Backend: {:?}",
540                    info.name, info.driver, info.backend
541                );
542            }
543
544            adapter = adapters.into_iter().find(|a| {
545                let info = a.get_info();
546                let match_found = info.name.to_lowercase().contains(&filter.to_lowercase())
547                    || info.driver.to_lowercase().contains(&filter.to_lowercase());
548                if match_found {
549                    println!(
550                        "[GPU] Manual selection match: {} | Driver: {}",
551                        info.name, info.driver
552                    );
553                }
554                match_found
555            });
556
557            if adapter.is_some() {
558                println!(
559                    "[GPU] Forced adapter selection via WGPU_ADAPTER_NAME='{}'",
560                    filter
561                );
562            } else {
563                println!(
564                    "[GPU] WGPU_ADAPTER_NAME='{}' provided but no matching adapter found. Falling back...",
565                    filter
566                );
567            }
568        }
569
570        if adapter.is_none() {
571            adapter = instance
572                .request_adapter(&wgpu::RequestAdapterOptions {
573                    power_preference: wgpu::PowerPreference::HighPerformance,
574                    compatible_surface: Some(&surface),
575                    force_fallback_adapter: false,
576                })
577                .await
578                .ok();
579        }
580
581        if adapter.is_none() {
582            println!(
583                "[GPU] HighPerformance adapter failed (possible Bumblebee/Optimus), trying LowPower..."
584            );
585            adapter = instance
586                .request_adapter(&wgpu::RequestAdapterOptions {
587                    power_preference: wgpu::PowerPreference::LowPower,
588                    compatible_surface: Some(&surface),
589                    force_fallback_adapter: false,
590                })
591                .await
592                .ok();
593        }
594
595        if adapter.is_none() {
596            println!("[GPU] Hardware adapters failed, trying Software fallback...");
597            adapter = instance
598                .request_adapter(&wgpu::RequestAdapterOptions {
599                    power_preference: wgpu::PowerPreference::LowPower,
600                    compatible_surface: Some(&surface),
601                    force_fallback_adapter: true,
602                })
603                .await
604                .ok();
605        }
606
607        let adapter = adapter.expect("Failed to find a suitable GPU for Surtr");
608        let info = adapter.get_info();
609        println!(
610            "[GPU] Selected adapter: {} ({:?}) on backend: {:?}",
611            info.name, info.device_type, info.backend
612        );
613        println!("[GPU] Driver info: {} - {}", info.driver, info.driver_info);
614        let supports_timestamps = adapter.features().contains(wgpu::Features::TIMESTAMP_QUERY);
615        let mut required_features = wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
616            | wgpu::Features::TEXTURE_BINDING_ARRAY;
617        if supports_timestamps {
618            required_features |= wgpu::Features::TIMESTAMP_QUERY;
619        }
620
621        let (device, queue) = adapter
622            .request_device(&wgpu::DeviceDescriptor {
623                label: Some("Surtr Forge"),
624                required_features,
625                required_limits: wgpu::Limits {
626                    max_bindings_per_bind_group: 256,
627                    max_binding_array_elements_per_shader_stage: 256,
628                    ..wgpu::Limits::default()
629                },
630                memory_hints: wgpu::MemoryHints::default(),
631                experimental_features: wgpu::ExperimentalFeatures::disabled(),
632                trace: wgpu::Trace::Off,
633            })
634            .await
635            .expect("Failed to create Surtr device");
636
637        let instance = Arc::new(instance);
638        let adapter = Arc::new(adapter);
639        
640        device.on_uncaptured_error(Arc::new(|error| {
641            log::error!("[GPU] Uncaptured device error (Device Lost or Panic): {:?}", error);
642            // In a full recovery scenario, we would signal the event loop to rebuild the GPU context
643        }));
644
645        let device = Arc::new(device);
646        let queue = Arc::new(queue);
647
648        let size = window.inner_size();
649        // Ensure we have valid dimensions - Wayland may return 0 for not-yet-committed surfaces
650        let width = if size.width > 0 { size.width } else { 1280 };
651        let height = if size.height > 0 { size.height } else { 720 };
652        let surface_caps = surface.get_capabilities(&adapter);
653        let surface_format = if surface_caps.formats.is_empty() {
654            log::error!("[GPU] CRITICAL: No compatible surface formats found for this adapter!");
655            log::error!(
656                "[GPU] Adapter: {} | Backend: {:?}",
657                adapter.get_info().name,
658                adapter.get_info().backend
659            );
660            // Fallback to a common format to avoid immediate panic, though configuration may still fail
661            wgpu::TextureFormat::Rgba8UnormSrgb
662        } else {
663            surface_caps
664                .formats
665                .iter()
666                .find(|f| f.is_srgb())
667                .copied()
668                .unwrap_or(surface_caps.formats[0])
669        };
670
671        // Dynamic capability selection for robust Wayland/X11 rendering
672        let present_mode = if surface_caps
673            .present_modes
674            .contains(&wgpu::PresentMode::Mailbox)
675        {
676            wgpu::PresentMode::Mailbox
677        } else {
678            log::warn!("[GPU] Mailbox not supported, falling back to Fifo (V-Sync)");
679            wgpu::PresentMode::Fifo
680        };
681
682        let alpha_mode = if surface_caps
683            .alpha_modes
684            .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
685        {
686            wgpu::CompositeAlphaMode::PostMultiplied
687        } else if surface_caps
688            .alpha_modes
689            .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
690        {
691            wgpu::CompositeAlphaMode::PreMultiplied
692        } else {
693            surface_caps.alpha_modes[0]
694        };
695
696        log::info!(
697            "[GPU] Configuring surface: {}x{} | {:?} | {:?}",
698            width,
699            height,
700            present_mode,
701            alpha_mode
702        );
703
704        let config = wgpu::SurfaceConfiguration {
705            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
706            format: surface_format,
707            width,
708            height,
709            present_mode,
710            alpha_mode,
711            view_formats: vec![],
712            desired_maximum_frame_latency: 2,
713        };
714        surface.configure(&device, &config);
715        log::info!("[GPU] Surface configuration successful.");
716
717        let renderer = Self::forge_internal(
718            instance,
719            adapter,
720            device,
721            queue,
722            Some((window, surface, config)),
723            None,
724        )
725        .await;
726        log::info!("[GPU] Forge internal complete.");
727        renderer
728    }
729
730    async fn forge_internal(
731        instance: Arc<wgpu::Instance>,
732        adapter: Arc<wgpu::Adapter>,
733        device: Arc<wgpu::Device>,
734        queue: Arc<wgpu::Queue>,
735        surface_info: Option<(
736            Arc<winit::window::Window>,
737            wgpu::Surface<'static>,
738            wgpu::SurfaceConfiguration,
739        )>,
740        headless_info: Option<(u32, u32, wgpu::TextureFormat)>,
741    ) -> Self {
742        let format = if let Some((_, _, ref config)) = surface_info {
743            config.format
744        } else if let Some((_, _, f)) = headless_info {
745            f
746        } else {
747            wgpu::TextureFormat::Rgba8UnormSrgb
748        };
749
750        let supports_timestamps = adapter.features().contains(wgpu::Features::TIMESTAMP_QUERY);
751        let skuld_period = queue.get_timestamp_period();
752        let (skuld_queries, skuld_buffer, skuld_read_buffer) = if supports_timestamps {
753            let q = device.create_query_set(&wgpu::QuerySetDescriptor {
754                label: Some("Skuld Timestamp Queries"),
755                count: 2,
756                ty: wgpu::QueryType::Timestamp,
757            });
758            let b = device.create_buffer(&wgpu::BufferDescriptor {
759                label: Some("Skuld Query Buffer"),
760                size: 16,
761                usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
762                mapped_at_creation: false,
763            });
764            let rb = device.create_buffer(&wgpu::BufferDescriptor {
765                label: Some("Skuld Read Buffer"),
766                size: 16,
767                usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
768                mapped_at_creation: false,
769            });
770            (Some(q), Some(b), Some(rb))
771        } else {
772            (None, None, None)
773        };
774
775        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
776            label: Some("Muspelheim Main Shader"),
777            source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(WGSL_SRC)),
778        });
779
780        // Niflheim Bind Group Layout (for textures/samplers)
781        let texture_bind_group_layout =
782            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
783                entries: &[
784                    wgpu::BindGroupLayoutEntry {
785                        binding: 0,
786                        visibility: wgpu::ShaderStages::FRAGMENT,
787                        ty: wgpu::BindingType::Texture {
788                            multisampled: false,
789                            view_dimension: wgpu::TextureViewDimension::D2,
790                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
791                        },
792                        count: std::num::NonZeroU32::new(256),
793                    },
794                    wgpu::BindGroupLayoutEntry {
795                        binding: 1,
796                        visibility: wgpu::ShaderStages::FRAGMENT,
797                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
798                        count: None,
799                    },
800                ],
801                label: Some("Niflheim Texture Bind Group Layout"),
802            });
803
804        // Environment Bind Group Layout (for blurred background / Bifrost)
805        // Environment Bind Group Layout (for blurred background / Bifrost)
806        let env_bind_group_layout =
807            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
808                entries: &[
809                    wgpu::BindGroupLayoutEntry {
810                        binding: 0,
811                        visibility: wgpu::ShaderStages::FRAGMENT,
812                        ty: wgpu::BindingType::Texture {
813                            multisampled: false,
814                            view_dimension: wgpu::TextureViewDimension::D2,
815                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
816                        },
817                        count: None,
818                    },
819                    wgpu::BindGroupLayoutEntry {
820                        binding: 1,
821                        visibility: wgpu::ShaderStages::FRAGMENT,
822                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
823                        count: None,
824                    },
825                ],
826                label: Some("Surtr Environment Bind Group Layout"),
827            });
828
829        let berserker_bind_group_layout =
830            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
831                entries: &[
832                    wgpu::BindGroupLayoutEntry {
833                        binding: 0,
834                        visibility: wgpu::ShaderStages::FRAGMENT,
835                        ty: wgpu::BindingType::Buffer {
836                            ty: wgpu::BufferBindingType::Uniform,
837                            has_dynamic_offset: false,
838                            min_binding_size: None,
839                        },
840                        count: None,
841                    },
842                    wgpu::BindGroupLayoutEntry {
843                        binding: 1,
844                        visibility: wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::VERTEX,
845                        ty: wgpu::BindingType::Buffer {
846                            ty: wgpu::BufferBindingType::Uniform,
847                            has_dynamic_offset: false,
848                            min_binding_size: None,
849                        },
850                        count: None,
851                    },
852                ],
853                label: Some("Surtr Berserker Bind Group Layout"),
854            });
855
856        // Pipeline setup
857        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
858            label: Some("Surtr Main Pipeline Layout"),
859            bind_group_layouts: &[
860                Some(&texture_bind_group_layout),
861                Some(&env_bind_group_layout),
862                Some(&berserker_bind_group_layout),
863            ],
864            immediate_size: 0,
865        });
866
867        // Specialized layout for post-processing (Bloom Extract, Blur) which only need Group 0 + Globals
868        let post_process_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
869            label: Some("Muspelheim Post Process Layout"),
870            bind_group_layouts: &[
871                Some(&texture_bind_group_layout),
872                Some(&env_bind_group_layout),
873                Some(&berserker_bind_group_layout),
874            ],
875            immediate_size: 0,
876        });
877
878        // Specialized layout for composite (Blur + Scene)
879        let composite_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
880            label: Some("Muspelheim Composite Layout"),
881            bind_group_layouts: &[
882                Some(&texture_bind_group_layout),
883                Some(&env_bind_group_layout),
884                Some(&berserker_bind_group_layout),
885            ],
886            immediate_size: 0,
887        });
888
889        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
890            label: Some("Surtr Main Pipeline"),
891            layout: Some(&pipeline_layout),
892            vertex: wgpu::VertexState {
893                module: &shader,
894                entry_point: Some("vs_main"),
895                buffers: &[Vertex::desc()],
896                compilation_options: wgpu::PipelineCompilationOptions::default(),
897            },
898            fragment: Some(wgpu::FragmentState {
899                module: &shader,
900                entry_point: Some("fs_main"),
901                targets: &[Some(wgpu::ColorTargetState {
902                    format,
903                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
904                    write_mask: wgpu::ColorWrites::ALL,
905                })],
906                compilation_options: wgpu::PipelineCompilationOptions::default(),
907            }),
908            primitive: wgpu::PrimitiveState::default(),
909            depth_stencil: Some(wgpu::DepthStencilState {
910                format: wgpu::TextureFormat::Depth32Float,
911                depth_write_enabled: Some(true),
912                depth_compare: Some(wgpu::CompareFunction::LessEqual),
913                stencil: wgpu::StencilState::default(),
914                bias: wgpu::DepthBiasState::default(),
915            }),
916            multisample: wgpu::MultisampleState::default(),
917            multiview_mask: None,
918            cache: None,
919        });
920
921        let background_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
922            label: Some("Surtr Background Pipeline"),
923            layout: Some(&pipeline_layout),
924            vertex: wgpu::VertexState {
925                module: &shader,
926                entry_point: Some("vs_fullscreen"),
927                buffers: &[],
928                compilation_options: wgpu::PipelineCompilationOptions::default(),
929            },
930            fragment: Some(wgpu::FragmentState {
931                module: &shader,
932                entry_point: Some("fs_background"),
933                targets: &[Some(wgpu::ColorTargetState {
934                    format,
935                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
936                    write_mask: wgpu::ColorWrites::ALL,
937                })],
938                compilation_options: wgpu::PipelineCompilationOptions::default(),
939            }),
940            primitive: wgpu::PrimitiveState::default(),
941            depth_stencil: Some(wgpu::DepthStencilState {
942                format: wgpu::TextureFormat::Depth32Float,
943                depth_write_enabled: Some(true),
944                depth_compare: Some(wgpu::CompareFunction::Always),
945                stencil: wgpu::StencilState::default(),
946                bias: wgpu::DepthBiasState::default(),
947            }),
948            multisample: wgpu::MultisampleState::default(),
949            multiview_mask: None,
950            cache: None,
951        });
952
953        // Muspelheim Bloom Extract Pipeline
954        let bloom_extract_pipeline =
955            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
956                label: Some("Muspelheim Bloom Extract"),
957                layout: Some(&post_process_layout),
958                vertex: wgpu::VertexState {
959                    module: &shader,
960                    entry_point: Some("vs_fullscreen"),
961                    buffers: &[],
962                    compilation_options: wgpu::PipelineCompilationOptions::default(),
963                },
964                fragment: Some(wgpu::FragmentState {
965                    module: &shader,
966                    entry_point: Some("fs_bloom_extract"),
967                    targets: &[Some(wgpu::ColorTargetState {
968                        format,
969                        blend: None,
970                        write_mask: wgpu::ColorWrites::ALL,
971                    })],
972                    compilation_options: wgpu::PipelineCompilationOptions::default(),
973                }),
974                primitive: wgpu::PrimitiveState::default(),
975                depth_stencil: None,
976                multisample: wgpu::MultisampleState::default(),
977                multiview_mask: None,
978                cache: None,
979            });
980
981        // Muspelheim Blur Pipelines (H and V)
982        let blur_h_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
983            label: Some("Muspelheim Horizontal Blur"),
984            layout: Some(&post_process_layout),
985            vertex: wgpu::VertexState {
986                module: &shader,
987                entry_point: Some("vs_fullscreen"),
988                buffers: &[],
989                compilation_options: wgpu::PipelineCompilationOptions::default(),
990            },
991            fragment: Some(wgpu::FragmentState {
992                module: &shader,
993                entry_point: Some("fs_blur_h"),
994                targets: &[Some(wgpu::ColorTargetState {
995                    format,
996                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
997                    write_mask: wgpu::ColorWrites::ALL,
998                })],
999                compilation_options: wgpu::PipelineCompilationOptions::default(),
1000            }),
1001            primitive: wgpu::PrimitiveState::default(),
1002            depth_stencil: None,
1003            multisample: wgpu::MultisampleState::default(),
1004            multiview_mask: None,
1005            cache: None,
1006        });
1007
1008        let blur_v_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1009            label: Some("Muspelheim Vertical Blur"),
1010            layout: Some(&post_process_layout),
1011            vertex: wgpu::VertexState {
1012                module: &shader,
1013                entry_point: Some("vs_fullscreen"),
1014                buffers: &[],
1015                compilation_options: wgpu::PipelineCompilationOptions::default(),
1016            },
1017            fragment: Some(wgpu::FragmentState {
1018                module: &shader,
1019                entry_point: Some("fs_blur_v"),
1020                targets: &[Some(wgpu::ColorTargetState {
1021                    format,
1022                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1023                    write_mask: wgpu::ColorWrites::ALL,
1024                })],
1025                compilation_options: wgpu::PipelineCompilationOptions::default(),
1026            }),
1027            primitive: wgpu::PrimitiveState::default(),
1028            depth_stencil: None,
1029            multisample: wgpu::MultisampleState::default(),
1030            multiview_mask: None,
1031            cache: None,
1032        });
1033
1034        // Muspelheim Composite Pipeline (additive blend onto screen)
1035        let composite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1036            label: Some("Muspelheim Composite"),
1037            layout: Some(&composite_layout),
1038            vertex: wgpu::VertexState {
1039                module: &shader,
1040                entry_point: Some("vs_fullscreen"),
1041                buffers: &[],
1042                compilation_options: wgpu::PipelineCompilationOptions::default(),
1043            },
1044            fragment: Some(wgpu::FragmentState {
1045                module: &shader,
1046                entry_point: Some("fs_composite"),
1047                targets: &[Some(wgpu::ColorTargetState {
1048                    format,
1049                    // Additive blend: src + dst — glow lights up the scene
1050                    blend: Some(wgpu::BlendState {
1051                        color: wgpu::BlendComponent {
1052                            src_factor: wgpu::BlendFactor::One,
1053                            dst_factor: wgpu::BlendFactor::One,
1054                            operation: wgpu::BlendOperation::Add,
1055                        },
1056                        alpha: wgpu::BlendComponent {
1057                            src_factor: wgpu::BlendFactor::One,
1058                            dst_factor: wgpu::BlendFactor::One,
1059                            operation: wgpu::BlendOperation::Add,
1060                        },
1061                    }),
1062                    write_mask: wgpu::ColorWrites::ALL,
1063                })],
1064                compilation_options: wgpu::PipelineCompilationOptions::default(),
1065            }),
1066            primitive: wgpu::PrimitiveState::default(),
1067            depth_stencil: None,
1068            multisample: wgpu::MultisampleState::default(),
1069            multiview_mask: None,
1070            cache: None,
1071        });
1072
1073        // Forge the Mega-Atlas (4096x4096 RGBA for production batching)
1074        let mega_atlas_tex = device.create_texture(&wgpu::TextureDescriptor {
1075            label: Some("Surtr Mega-Atlas"),
1076            size: wgpu::Extent3d {
1077                width: 4096,
1078                height: 4096,
1079                depth_or_array_layers: 1,
1080            },
1081            mip_level_count: 1,
1082            sample_count: 1,
1083            dimension: wgpu::TextureDimension::D2,
1084            format: wgpu::TextureFormat::Rgba8UnormSrgb,
1085            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1086            view_formats: &[],
1087        });
1088        let mega_atlas_view_obj =
1089            mega_atlas_tex.create_view(&wgpu::TextureViewDescriptor::default());
1090        let text_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1091            address_mode_u: wgpu::AddressMode::ClampToEdge,
1092            address_mode_v: wgpu::AddressMode::ClampToEdge,
1093            mag_filter: wgpu::FilterMode::Linear, // Use linear for images
1094            min_filter: wgpu::FilterMode::Linear,
1095            ..Default::default()
1096        });
1097
1098        // Forge the Niflheim Dummy Texture (1x1 White)
1099        let dummy_size = wgpu::Extent3d {
1100            width: 1,
1101            height: 1,
1102            depth_or_array_layers: 1,
1103        };
1104        let dummy_texture = device.create_texture(&wgpu::TextureDescriptor {
1105            label: Some("Niflheim Dummy Texture"),
1106            size: dummy_size,
1107            mip_level_count: 1,
1108            sample_count: 1,
1109            dimension: wgpu::TextureDimension::D2,
1110            format: wgpu::TextureFormat::Rgba8UnormSrgb,
1111            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1112            view_formats: &[],
1113        });
1114        queue.write_texture(
1115            wgpu::TexelCopyTextureInfo {
1116                texture: &dummy_texture,
1117                mip_level: 0,
1118                origin: wgpu::Origin3d::ZERO,
1119                aspect: wgpu::TextureAspect::All,
1120            },
1121            &[0, 0, 0, 255],
1122            wgpu::TexelCopyBufferLayout {
1123                offset: 0,
1124                bytes_per_row: Some(4),
1125                rows_per_image: Some(1),
1126            },
1127            dummy_size,
1128        );
1129
1130        let dummy_view = dummy_texture.create_view(&wgpu::TextureViewDescriptor::default());
1131        let dummy_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1132            address_mode_u: wgpu::AddressMode::ClampToEdge,
1133            address_mode_v: wgpu::AddressMode::ClampToEdge,
1134            address_mode_w: wgpu::AddressMode::ClampToEdge,
1135            mag_filter: wgpu::FilterMode::Linear,
1136            min_filter: wgpu::FilterMode::Nearest,
1137            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
1138            ..Default::default()
1139        });
1140
1141        let mut texture_views_list: Vec<wgpu::TextureView> =
1142            (0..256).map(|_| dummy_view.clone()).collect();
1143        texture_views_list[0] = mega_atlas_view_obj.clone();
1144
1145        let views_refs: Vec<&wgpu::TextureView> = texture_views_list.iter().collect();
1146        let mega_atlas_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1147            layout: &texture_bind_group_layout,
1148            entries: &[
1149                wgpu::BindGroupEntry {
1150                    binding: 0,
1151                    resource: wgpu::BindingResource::TextureViewArray(&views_refs),
1152                },
1153                wgpu::BindGroupEntry {
1154                    binding: 1,
1155                    resource: wgpu::BindingResource::Sampler(&text_sampler),
1156                },
1157            ],
1158            label: Some("Mega-Atlas Bind Group"),
1159        });
1160
1161        let dummy_views_refs: Vec<&wgpu::TextureView> = (0..256).map(|_| &dummy_view).collect();
1162        let dummy_texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1163            layout: &texture_bind_group_layout,
1164            entries: &[
1165                wgpu::BindGroupEntry {
1166                    binding: 0,
1167                    resource: wgpu::BindingResource::TextureViewArray(&dummy_views_refs),
1168                },
1169                wgpu::BindGroupEntry {
1170                    binding: 1,
1171                    resource: wgpu::BindingResource::Sampler(&dummy_sampler),
1172                },
1173            ],
1174            label: Some("Dummy Texture Bind Group"),
1175        });
1176
1177        let dummy_env_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1178            layout: &env_bind_group_layout,
1179            entries: &[
1180                wgpu::BindGroupEntry {
1181                    binding: 0,
1182                    resource: wgpu::BindingResource::TextureView(&dummy_view),
1183                },
1184                wgpu::BindGroupEntry {
1185                    binding: 1,
1186                    resource: wgpu::BindingResource::Sampler(&dummy_sampler),
1187                },
1188            ],
1189            label: Some("Dummy Env Bind Group"),
1190        });
1191
1192        let mut texture_registry = std::collections::HashMap::new();
1193        let mut texture_bind_groups = Vec::new();
1194
1195        texture_registry.insert("__mega_atlas".to_string(), 0);
1196        texture_bind_groups.push(mega_atlas_bind_group.clone());
1197
1198        // Forge the Anvil (Buffers)
1199        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1200            label: Some("Surtr Vertex Anvil"),
1201            size: (MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64,
1202            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1203            mapped_at_creation: false,
1204        });
1205
1206        let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1207            label: Some("Surtr Index Anvil"),
1208            size: (MAX_INDICES * std::mem::size_of::<u32>()) as u64,
1209            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1210            mapped_at_creation: false,
1211        });
1212
1213        // Forge the Heart (Berserker Uniforms)
1214        let current_theme = ColorTheme::default();
1215        use wgpu::util::DeviceExt;
1216        let theme_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1217            label: Some("Surtr Theme Buffer"),
1218            contents: bytemuck::bytes_of(&current_theme),
1219            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1220        });
1221
1222        let (width, height, scale_factor) = if let Some((ref window, _, ref config)) = surface_info
1223        {
1224            (config.width, config.height, window.scale_factor() as f32)
1225        } else if let Some((w, h, _)) = headless_info {
1226            (w, h, 1.0)
1227        } else {
1228            (1280, 720, 1.0)
1229        };
1230
1231        let mut current_scene =
1232            SceneUniforms::new(width as f32 / scale_factor, height as f32 / scale_factor);
1233        current_scene.scale_factor = scale_factor;
1234        let scene_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1235            label: Some("Surtr Scene Buffer"),
1236            contents: bytemuck::bytes_of(&current_scene),
1237            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1238        });
1239
1240        let berserker_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1241            layout: &berserker_bind_group_layout,
1242            entries: &[
1243                wgpu::BindGroupEntry {
1244                    binding: 0,
1245                    resource: theme_buffer.as_entire_binding(),
1246                },
1247                wgpu::BindGroupEntry {
1248                    binding: 1,
1249                    resource: scene_buffer.as_entire_binding(),
1250                },
1251            ],
1252            label: Some("Surtr Berserker Bind Group"),
1253        });
1254
1255        let mut surfaces = std::collections::HashMap::new();
1256        let mut current_window = None;
1257        let mut headless_context = None;
1258
1259        if let Some((window, surface, config)) = surface_info {
1260            let window_id = window.id();
1261            let ctx = Self::create_surface_context(
1262                &device,
1263                surface,
1264                config,
1265                &env_bind_group_layout,
1266                &texture_bind_group_layout,
1267                scale_factor,
1268            );
1269            surfaces.insert(window_id, ctx);
1270            current_window = Some(window_id);
1271        } else if let Some((w, h, f)) = headless_info {
1272            headless_context = Some(Self::create_headless_context(
1273                &device,
1274                w,
1275                h,
1276                f,
1277                &env_bind_group_layout,
1278                &texture_bind_group_layout,
1279            ));
1280        }
1281
1282        let staging_belt = wgpu::util::StagingBelt::new((*device).clone(), 1024 * 1024);
1283
1284        // Clone bind group layouts before they are moved into the Self struct
1285        let glass_blur_bind_group_layout = env_bind_group_layout.clone();
1286        let glass_output_bind_group_layout = env_bind_group_layout.clone();
1287        let glass_blur_pipeline = pipeline.clone();
1288        let glass_blur_upsample_pipeline = pipeline.clone();
1289
1290        // Create glass blur pyramid resources (must be before Self struct, which moves device)
1291        let glass_blur_texture = device.create_texture(&wgpu::TextureDescriptor {
1292            label: Some("Glass Blur Pyramid"),
1293            size: wgpu::Extent3d {
1294                width: width.max(1),
1295                height: height.max(1),
1296                depth_or_array_layers: 1,
1297            },
1298            mip_level_count: 1,
1299            sample_count: 1,
1300            dimension: wgpu::TextureDimension::D2,
1301            format: wgpu::TextureFormat::Rgba16Float,
1302            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
1303            view_formats: &[],
1304        });
1305        let glass_blur_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1306            label: Some("Glass Blur Uniform"),
1307            size: 64,
1308            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1309            mapped_at_creation: false,
1310        });
1311
1312        Self {
1313            instance,
1314            adapter,
1315            device: device.clone(),
1316            queue: queue.clone(),
1317            surfaces,
1318            current_window,
1319            headless_context,
1320            pipeline,
1321            bloom_extract_pipeline,
1322            blur_h_pipeline,
1323            blur_v_pipeline,
1324            composite_pipeline,
1325            env_bind_group_layout,
1326            text_engine: cvkg_runic_text::RunicTextEngine::default(),
1327            mega_atlas_tex,
1328            mega_atlas_view: mega_atlas_view_obj,
1329            _mega_atlas_sampler: text_sampler,
1330            mega_atlas_bind_group,
1331            text_cache: LruCache::new(NonZeroUsize::new(2048).unwrap()),
1332            atlas_packer: YggdrasilPacker::new(4096, 4096),
1333            image_uv_registry: LruCache::new(NonZeroUsize::new(256).unwrap()),
1334            texture_registry: LruCache::new(NonZeroUsize::new(255).unwrap()),
1335            texture_views: texture_views_list,
1336            dummy_sampler,
1337            svg_cache: LruCache::new(NonZeroUsize::new(128).unwrap()),
1338            svg_trees: LruCache::new(NonZeroUsize::new(128).unwrap()),
1339            filter_device: Some(device.clone()),
1340            filter_queue: Some(queue.clone()),
1341            filter_sampler: device.create_sampler(&wgpu::SamplerDescriptor {
1342                label: Some("SVG Filter Sampler"),
1343                address_mode_u: wgpu::AddressMode::ClampToEdge,
1344                address_mode_v: wgpu::AddressMode::ClampToEdge,
1345                address_mode_w: wgpu::AddressMode::ClampToEdge,
1346                mag_filter: wgpu::FilterMode::Linear,
1347                min_filter: wgpu::FilterMode::Linear,
1348                mipmap_filter: wgpu::MipmapFilterMode::Linear,
1349                ..Default::default()
1350            }),
1351            filter_engine: None,
1352            filter_batches: Vec::new(),
1353            dummy_texture_bind_group,
1354            dummy_env_bind_group,
1355            texture_bind_group_layout,
1356            texture_bind_groups,
1357            shared_elements: LruCache::new(NonZeroUsize::new(1024).unwrap()),
1358            vertex_buffer,
1359            index_buffer,
1360            vertices: Vec::with_capacity(MAX_VERTICES),
1361            indices: Vec::with_capacity(MAX_INDICES),
1362            draw_calls: Vec::new(),
1363            current_texture_id: None,
1364            opacity_stack: vec![1.0],
1365            clip_stack: Vec::new(),
1366            slice_stack: Vec::new(),
1367            shadow_stack: Vec::new(),
1368            theme_buffer,
1369            scene_buffer,
1370            berserker_bind_group,
1371            berserker_bind_group_layout,
1372            start_time: std::time::Instant::now(),
1373            current_theme,
1374            current_scene,
1375            background_pipeline,
1376            current_z: 0.0,
1377            telemetry: cvkg_core::TelemetryData::default(),
1378            last_frame_start: std::time::Instant::now(),
1379            last_redraw_start: std::time::Instant::now(),
1380            frame_budget: cvkg_core::FrameBudget::default(),
1381            vram_buffers_bytes: 0,
1382            vram_textures_bytes: 0,
1383            _debug_layout: false,
1384            transform_stack: Vec::new(),
1385            redraw_requested: false,
1386            skuld_queries,
1387            skuld_buffer,
1388            skuld_read_buffer,
1389            skuld_period,
1390            last_gpu_time_ns: 0,
1391            vnode_stack: Vec::new(),
1392            event_handlers: std::collections::HashMap::new(),
1393            staging_belt,
1394            staging_command_buffers: Vec::new(),
1395            // Backdrop Capture Architecture — Kawase Blur Pyramid
1396            glass_blur_texture,
1397            glass_blur_views: Vec::new(),
1398            glass_blur_down_bind_groups: Vec::new(),
1399            glass_blur_up_bind_groups: Vec::new(),
1400            glass_blur_uniform_buffer,
1401            glass_blur_pipeline,
1402            glass_blur_upsample_pipeline,
1403            glass_blur_bind_group_layout,
1404            glass_output_bind_group_layout,
1405            current_draw_material: cvkg_core::DrawMaterial::Opaque,
1406            blur_pyramid_mip_count: 1,
1407        }
1408    }
1409
1410    fn rebuild_texture_array_bind_group(&mut self) {
1411        let views: Vec<&wgpu::TextureView> = self.texture_views.iter().collect();
1412        self.mega_atlas_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1413            layout: &self.texture_bind_group_layout,
1414            entries: &[
1415                wgpu::BindGroupEntry {
1416                    binding: 0,
1417                    resource: wgpu::BindingResource::TextureViewArray(&views),
1418                },
1419                wgpu::BindGroupEntry {
1420                    binding: 1,
1421                    resource: wgpu::BindingResource::Sampler(&self.dummy_sampler),
1422                },
1423            ],
1424            label: Some("Surtr Texture Array Bind Group"),
1425        });
1426    }
1427
1428    /// Update VRAM telemetry based on currently allocated resources.
1429    fn update_vram_telemetry(&mut self) {
1430        // Calculate Buffer VRAM
1431        let mut buffer_bytes = 0;
1432        buffer_bytes += (MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64;
1433        buffer_bytes += (MAX_INDICES * std::mem::size_of::<u32>()) as u64;
1434        buffer_bytes += std::mem::size_of::<cvkg_core::ColorTheme>() as u64;
1435        buffer_bytes += std::mem::size_of::<cvkg_core::SceneUniforms>() as u64;
1436        self.vram_buffers_bytes = buffer_bytes;
1437
1438        // Calculate Texture VRAM
1439        let mut texture_bytes = 0;
1440        texture_bytes += 4096 * 4096 * 4; // Mega Atlas (RGBA8)
1441        texture_bytes += 4; // Dummy (RGBA8)
1442
1443        // Add Texture Array VRAM
1444        for _ in &self.texture_views {
1445            // Approximation: 1MB per texture
1446            texture_bytes += 1024 * 1024 * 4;
1447        }
1448
1449        for ctx in self.surfaces.values() {
1450            let bpp = 4;
1451            let surface_bytes = (ctx.config.width * ctx.config.height * bpp) as u64;
1452            texture_bytes += surface_bytes * 3; // scene, blur_a, blur_b
1453            texture_bytes += (ctx.config.width * ctx.config.height * 4) as u64; // depth (Depth32Float)
1454        }
1455
1456        self.vram_textures_bytes = texture_bytes;
1457
1458        self.telemetry.vram_buffers_mb = buffer_bytes as f32 / 1_048_576.0;
1459        self.telemetry.vram_textures_mb = texture_bytes as f32 / 1_048_576.0;
1460        self.telemetry.vram_pipelines_mb = 0.0;
1461        self.telemetry.vram_usage_mb =
1462            self.telemetry.vram_buffers_mb + self.telemetry.vram_textures_mb;
1463    }
1464
1465    /// Get real-time performance telemetry.
1466    pub fn get_telemetry(&self) -> cvkg_core::TelemetryData {
1467        self.telemetry.clone()
1468    }
1469
1470    /// resize — Reconfigures a specific surface and its internal textures.
1471    pub fn resize(
1472        &mut self,
1473        window_id: winit::window::WindowId,
1474        width: u32,
1475        height: u32,
1476        scale_factor: f32,
1477    ) {
1478        if width > 0
1479            && height > 0
1480            && let Some(ctx) = self.surfaces.get_mut(&window_id)
1481        {
1482            ctx.config.width = width;
1483            ctx.config.height = height;
1484            ctx.scale_factor = scale_factor;
1485            ctx.surface.configure(&self.device, &ctx.config);
1486
1487            // Re-create Muspelheim textures for this surface
1488            let texture_desc = wgpu::TextureDescriptor {
1489                label: Some("Surtr Scene Texture"),
1490                size: wgpu::Extent3d {
1491                    width,
1492                    height,
1493                    depth_or_array_layers: 1,
1494                },
1495                mip_level_count: 1,
1496                sample_count: 1,
1497                dimension: wgpu::TextureDimension::D2,
1498                format: ctx.config.format,
1499                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1500                    | wgpu::TextureUsages::TEXTURE_BINDING,
1501                view_formats: &[],
1502            };
1503
1504            let scene_tex = self.device.create_texture(&texture_desc);
1505            ctx.scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
1506
1507            let blur_tex_a = self.device.create_texture(&texture_desc);
1508            ctx.blur_texture_a = blur_tex_a.create_view(&wgpu::TextureViewDescriptor::default());
1509
1510            let blur_tex_b = self.device.create_texture(&texture_desc);
1511            ctx.blur_texture_b = blur_tex_b.create_view(&wgpu::TextureViewDescriptor::default());
1512
1513            // Re-create bind groups for this surface
1514            ctx.scene_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1515                layout: &self.env_bind_group_layout,
1516                entries: &[
1517                    wgpu::BindGroupEntry {
1518                        binding: 0,
1519                        resource: wgpu::BindingResource::TextureView(&ctx.scene_texture),
1520                    },
1521                    wgpu::BindGroupEntry {
1522                        binding: 1,
1523                        resource: wgpu::BindingResource::Sampler(&ctx.sampler),
1524                    },
1525                ],
1526                label: Some("Scene Bind Group Resize"),
1527            });
1528
1529            let scene_views: Vec<&wgpu::TextureView> =
1530                (0..256).map(|_| &ctx.scene_texture).collect();
1531            ctx.scene_texture_bind_group =
1532                self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1533                    layout: &self.texture_bind_group_layout,
1534                    entries: &[
1535                        wgpu::BindGroupEntry {
1536                            binding: 0,
1537                            resource: wgpu::BindingResource::TextureViewArray(&scene_views),
1538                        },
1539                        wgpu::BindGroupEntry {
1540                            binding: 1,
1541                            resource: wgpu::BindingResource::Sampler(&ctx.sampler),
1542                        },
1543                    ],
1544                    label: Some("Scene Texture Bind Group Resize"),
1545                });
1546
1547            let blur_views_a: Vec<&wgpu::TextureView> =
1548                (0..256).map(|_| &ctx.blur_texture_a).collect();
1549            ctx.blur_bind_group_a = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1550                layout: &self.texture_bind_group_layout,
1551                entries: &[
1552                    wgpu::BindGroupEntry {
1553                        binding: 0,
1554                        resource: wgpu::BindingResource::TextureViewArray(&blur_views_a),
1555                    },
1556                    wgpu::BindGroupEntry {
1557                        binding: 1,
1558                        resource: wgpu::BindingResource::Sampler(&ctx.sampler),
1559                    },
1560                ],
1561                label: Some("Blur Bind Group A Resize"),
1562            });
1563
1564            let blur_views_b: Vec<&wgpu::TextureView> =
1565                (0..256).map(|_| &ctx.blur_texture_b).collect();
1566            ctx.blur_bind_group_b = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1567                layout: &self.texture_bind_group_layout,
1568                entries: &[
1569                    wgpu::BindGroupEntry {
1570                        binding: 0,
1571                        resource: wgpu::BindingResource::TextureViewArray(&blur_views_b),
1572                    },
1573                    wgpu::BindGroupEntry {
1574                        binding: 1,
1575                        resource: wgpu::BindingResource::Sampler(&ctx.sampler),
1576                    },
1577                ],
1578                label: Some("Blur Bind Group B Resize"),
1579            });
1580
1581            ctx.blur_env_bind_group_a = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1582                layout: &self.env_bind_group_layout,
1583                entries: &[
1584                    wgpu::BindGroupEntry {
1585                        binding: 0,
1586                        resource: wgpu::BindingResource::TextureView(&ctx.blur_texture_a),
1587                    },
1588                    wgpu::BindGroupEntry {
1589                        binding: 1,
1590                        resource: wgpu::BindingResource::Sampler(&ctx.sampler),
1591                    },
1592                ],
1593                label: Some("Blur Env Bind Group A Resize"),
1594            });
1595
1596            let depth_texture = self.device.create_texture(&wgpu::TextureDescriptor {
1597                label: Some("Surtr Depth Texture"),
1598                size: wgpu::Extent3d {
1599                    width,
1600                    height,
1601                    depth_or_array_layers: 1,
1602                },
1603                mip_level_count: 1,
1604                sample_count: 1,
1605                dimension: wgpu::TextureDimension::D2,
1606                format: wgpu::TextureFormat::Depth32Float,
1607                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1608                view_formats: &[],
1609            });
1610            ctx.depth_texture_view =
1611                depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
1612        }
1613    }
1614
1615    /// begin_frame_headless — Strike the flaming sword to begin a new GPU frame for headless rendering.
1616    pub fn begin_frame_headless(&mut self) -> wgpu::CommandEncoder {
1617        self.current_window = None;
1618        self.vertices.clear();
1619        self.indices.clear();
1620        self.draw_calls.clear();
1621        self.filter_batches.clear();
1622        self.shared_elements.clear();
1623        self.current_texture_id = None;
1624        self.opacity_stack = vec![1.0];
1625        self.clip_stack.clear();
1626        self.slice_stack.clear();
1627        self.transform_stack.clear();
1628        self.current_z = 0.0;
1629        self.vnode_stack.clear();
1630        self.event_handlers.clear();
1631
1632        self.last_frame_start = std::time::Instant::now();
1633        self.telemetry.draw_calls = 0;
1634        self.telemetry.vertices = 0;
1635
1636        let ctx = self
1637            .headless_context
1638            .as_ref()
1639            .expect("Headless context not initialized");
1640        let time = self.start_time.elapsed().as_secs_f32();
1641        let logical_w = ctx.width as f32 / ctx.scale_factor;
1642        let logical_h = ctx.height as f32 / ctx.scale_factor;
1643        let dt = time - self.current_scene.time;
1644        self.current_scene.time = time;
1645        self.current_scene.delta_time = dt;
1646        self.current_scene.resolution = [logical_w, logical_h];
1647        self.current_scene.scale_factor = ctx.scale_factor;
1648        self.current_scene.proj =
1649            glam::Mat4::orthographic_lh(0.0, logical_w, logical_h, 0.0, -1000.0, 1000.0);
1650
1651        self.queue.write_buffer(
1652            &self.scene_buffer,
1653            0,
1654            bytemuck::bytes_of(&self.current_scene),
1655        );
1656
1657        self.device
1658            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1659                label: Some("Surtr Headless Command Encoder"),
1660            })
1661    }
1662
1663    /// begin_frame — Strike the flaming sword to begin a new GPU frame for a specific window.
1664    pub fn begin_frame(&mut self, window_id: winit::window::WindowId) -> wgpu::CommandEncoder {
1665        // Skuld: Read the timestamps from the previous frame
1666        if let Some(rb) = &self.skuld_read_buffer {
1667            let slice = rb.slice(..);
1668            let (tx, rx) = std::sync::mpsc::channel();
1669            slice.map_async(wgpu::MapMode::Read, move |r| tx.send(r).unwrap());
1670            
1671            // Poll to ensure mapping is complete
1672            self.device.poll(wgpu::PollType::Wait {
1673                submission_index: None,
1674                timeout: None,
1675            }).unwrap();
1676            
1677            if rx.recv().is_ok() {
1678                let data = slice.get_mapped_range();
1679                let timestamps: [u64; 2] = bytemuck::cast_slice(&data).try_into().unwrap_or([0, 0]);
1680                drop(data);
1681                rb.unmap();
1682                
1683                if timestamps[1] > timestamps[0] {
1684                    let diff_ticks = timestamps[1] - timestamps[0];
1685                    self.last_gpu_time_ns = (diff_ticks as f64 * self.skuld_period as f64) as u64;
1686                    // println!("[Skuld] GPU Time: {} ms", self.last_gpu_time_ns as f64 / 1_000_000.0);
1687                }
1688            }
1689        }
1690
1691        self.staging_belt.recall();
1692        self.current_window = Some(window_id);
1693        self.vertices.clear();
1694        self.indices.clear();
1695        self.draw_calls.clear();
1696        self.shared_elements.clear();
1697        self.current_texture_id = None;
1698        self.opacity_stack = vec![1.0];
1699        self.clip_stack.clear();
1700        self.slice_stack.clear();
1701        self.transform_stack.clear();
1702        self.current_z = 0.0;
1703        self.vnode_stack.clear();
1704        self.event_handlers.clear();
1705
1706        self.last_frame_start = std::time::Instant::now();
1707        self.telemetry.draw_calls = 0;
1708        self.telemetry.vertices = 0;
1709
1710        let ctx = self
1711            .surfaces
1712            .get(&window_id)
1713            .expect("Window not registered");
1714        let time = self.start_time.elapsed().as_secs_f32();
1715        let logical_w = ctx.config.width as f32 / ctx.scale_factor;
1716        let logical_h = ctx.config.height as f32 / ctx.scale_factor;
1717        let dt = time - self.current_scene.time;
1718        self.current_scene.time = time;
1719        self.current_scene.delta_time = dt;
1720        self.current_scene.resolution = [logical_w, logical_h];
1721        self.current_scene.scale_factor = ctx.scale_factor;
1722        self.current_scene.proj =
1723            glam::Mat4::orthographic_lh(0.0, logical_w, logical_h, 0.0, -1000.0, 1000.0);
1724
1725        self.queue.write_buffer(
1726            &self.scene_buffer,
1727            0,
1728            bytemuck::bytes_of(&self.current_scene),
1729        );
1730
1731        self.device
1732            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1733                label: Some("Surtr Command Encoder"),
1734            })
1735    }
1736
1737    /// register_window — Attaches a new OS window to the shared GPU context.
1738    pub fn register_window(&mut self, window: Arc<winit::window::Window>) {
1739        let size = window.inner_size();
1740        let surface = self
1741            .instance
1742            .create_surface(window.clone())
1743            .expect("Failed to create surface");
1744        let caps = surface.get_capabilities(&self.adapter);
1745        let format = caps.formats[0];
1746        let config = wgpu::SurfaceConfiguration {
1747            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1748            format,
1749            width: size.width,
1750            height: size.height,
1751            present_mode: wgpu::PresentMode::Mailbox,
1752            alpha_mode: caps.alpha_modes[0],
1753            view_formats: vec![],
1754            desired_maximum_frame_latency: 2,
1755        };
1756        surface.configure(&self.device, &config);
1757
1758        let ctx = Self::create_surface_context(
1759            &self.device,
1760            surface,
1761            config,
1762            &self.env_bind_group_layout,
1763            &self.texture_bind_group_layout,
1764            window.scale_factor() as f32,
1765        );
1766
1767        self.surfaces.insert(window.id(), ctx);
1768    }
1769
1770    fn create_headless_context(
1771        device: &wgpu::Device,
1772        width: u32,
1773        height: u32,
1774        format: wgpu::TextureFormat,
1775        env_bind_group_layout: &wgpu::BindGroupLayout,
1776        texture_bind_group_layout: &wgpu::BindGroupLayout,
1777    ) -> HeadlessContext {
1778        let texture_desc = wgpu::TextureDescriptor {
1779            label: Some("Surtr Headless Scene Texture"),
1780            size: wgpu::Extent3d {
1781                width,
1782                height,
1783                depth_or_array_layers: 1,
1784            },
1785            mip_level_count: 1,
1786            sample_count: 1,
1787            dimension: wgpu::TextureDimension::D2,
1788            format,
1789            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1790                | wgpu::TextureUsages::TEXTURE_BINDING
1791                | wgpu::TextureUsages::COPY_SRC,
1792            view_formats: &[],
1793        };
1794
1795        let scene_tex = device.create_texture(&texture_desc);
1796        let scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
1797
1798        let blur_width = (width / 2).max(1);
1799        let blur_height = (height / 2).max(1);
1800        let blur_texture_desc = wgpu::TextureDescriptor {
1801            label: Some("Surtr Blur Texture"),
1802            size: wgpu::Extent3d {
1803                width: blur_width,
1804                height: blur_height,
1805                depth_or_array_layers: 1,
1806            },
1807            mip_level_count: 1,
1808            sample_count: 1,
1809            dimension: wgpu::TextureDimension::D2,
1810            format,
1811            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1812                | wgpu::TextureUsages::TEXTURE_BINDING
1813                | wgpu::TextureUsages::COPY_SRC,
1814            view_formats: &[],
1815        };
1816
1817        let blur_tex_a = device.create_texture(&blur_texture_desc);
1818        let blur_texture_a = blur_tex_a.create_view(&wgpu::TextureViewDescriptor::default());
1819
1820        let blur_tex_b = device.create_texture(&blur_texture_desc);
1821        let blur_texture_b = blur_tex_b.create_view(&wgpu::TextureViewDescriptor::default());
1822
1823        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1824            address_mode_u: wgpu::AddressMode::ClampToEdge,
1825            address_mode_v: wgpu::AddressMode::ClampToEdge,
1826            mag_filter: wgpu::FilterMode::Linear,
1827            min_filter: wgpu::FilterMode::Linear,
1828            ..Default::default()
1829        });
1830
1831        let scene_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1832            layout: env_bind_group_layout,
1833            entries: &[
1834                wgpu::BindGroupEntry {
1835                    binding: 0,
1836                    resource: wgpu::BindingResource::TextureView(&scene_texture),
1837                },
1838                wgpu::BindGroupEntry {
1839                    binding: 1,
1840                    resource: wgpu::BindingResource::Sampler(&sampler),
1841                },
1842            ],
1843            label: Some("Headless Scene Bind Group"),
1844        });
1845
1846        let scene_views: Vec<&wgpu::TextureView> = (0..256).map(|_| &scene_texture).collect();
1847        let scene_texture_bind_group = 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(&scene_views),
1853                },
1854                wgpu::BindGroupEntry {
1855                    binding: 1,
1856                    resource: wgpu::BindingResource::Sampler(&sampler),
1857                },
1858            ],
1859            label: Some("Headless Scene Texture Bind Group"),
1860        });
1861
1862        let blur_views_a: Vec<&wgpu::TextureView> = (0..256).map(|_| &blur_texture_a).collect();
1863        let blur_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
1864            layout: texture_bind_group_layout,
1865            entries: &[
1866                wgpu::BindGroupEntry {
1867                    binding: 0,
1868                    resource: wgpu::BindingResource::TextureViewArray(&blur_views_a),
1869                },
1870                wgpu::BindGroupEntry {
1871                    binding: 1,
1872                    resource: wgpu::BindingResource::Sampler(&sampler),
1873                },
1874            ],
1875            label: Some("Headless Blur Bind Group A"),
1876        });
1877
1878        let blur_views_b: Vec<&wgpu::TextureView> = (0..256).map(|_| &blur_texture_b).collect();
1879        let blur_bind_group_b = 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(&blur_views_b),
1885                },
1886                wgpu::BindGroupEntry {
1887                    binding: 1,
1888                    resource: wgpu::BindingResource::Sampler(&sampler),
1889                },
1890            ],
1891            label: Some("Headless Blur Bind Group B"),
1892        });
1893
1894        let blur_env_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
1895            layout: env_bind_group_layout,
1896            entries: &[
1897                wgpu::BindGroupEntry {
1898                    binding: 0,
1899                    resource: wgpu::BindingResource::TextureView(&blur_texture_a),
1900                },
1901                wgpu::BindGroupEntry {
1902                    binding: 1,
1903                    resource: wgpu::BindingResource::Sampler(&sampler),
1904                },
1905            ],
1906            label: Some("Headless Blur Env Bind Group A"),
1907        });
1908
1909        let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
1910            label: Some("Headless Depth Texture"),
1911            size: wgpu::Extent3d {
1912                width,
1913                height,
1914                depth_or_array_layers: 1,
1915            },
1916            mip_level_count: 1,
1917            sample_count: 1,
1918            dimension: wgpu::TextureDimension::D2,
1919            format: wgpu::TextureFormat::Depth32Float,
1920            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1921            view_formats: &[],
1922        });
1923        let depth_texture_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
1924
1925        let output_texture = device.create_texture(&wgpu::TextureDescriptor {
1926            label: Some("Headless Output 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,
1936            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1937                | wgpu::TextureUsages::COPY_DST
1938                | wgpu::TextureUsages::COPY_SRC,
1939            view_formats: &[],
1940        });
1941        let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
1942
1943        HeadlessContext {
1944            scene_texture,
1945            scene_bind_group,
1946            scene_texture_bind_group,
1947            depth_texture_view,
1948            blur_texture_a,
1949            blur_texture_b,
1950            blur_bind_group_a,
1951            blur_bind_group_b,
1952            blur_env_bind_group_a,
1953            scale_factor: 1.0,
1954            sampler,
1955            width,
1956            height,
1957            output_texture,
1958            output_view,
1959        }
1960    }
1961
1962    fn create_surface_context(
1963        device: &wgpu::Device,
1964        surface: wgpu::Surface<'static>,
1965        config: wgpu::SurfaceConfiguration,
1966        env_bind_group_layout: &wgpu::BindGroupLayout,
1967        texture_bind_group_layout: &wgpu::BindGroupLayout,
1968        scale_factor: f32,
1969    ) -> SurfaceContext {
1970        let width = config.width;
1971        let height = config.height;
1972
1973        let texture_desc = wgpu::TextureDescriptor {
1974            label: Some("Surtr Scene Texture"),
1975            size: wgpu::Extent3d {
1976                width,
1977                height,
1978                depth_or_array_layers: 1,
1979            },
1980            mip_level_count: 1,
1981            sample_count: 1,
1982            dimension: wgpu::TextureDimension::D2,
1983            format: config.format,
1984            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
1985            view_formats: &[],
1986        };
1987
1988        let scene_tex = device.create_texture(&texture_desc);
1989        let scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
1990
1991        let blur_width = (width / 2).max(1);
1992        let blur_height = (height / 2).max(1);
1993        let blur_texture_desc = wgpu::TextureDescriptor {
1994            label: Some("Surtr Blur Texture"),
1995            size: wgpu::Extent3d {
1996                width: blur_width,
1997                height: blur_height,
1998                depth_or_array_layers: 1,
1999            },
2000            mip_level_count: 1,
2001            sample_count: 1,
2002            dimension: wgpu::TextureDimension::D2,
2003            format: config.format,
2004            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2005                | wgpu::TextureUsages::TEXTURE_BINDING
2006                | wgpu::TextureUsages::COPY_SRC,
2007            view_formats: &[],
2008        };
2009
2010        let blur_tex_a = device.create_texture(&blur_texture_desc);
2011        let blur_texture_a = blur_tex_a.create_view(&wgpu::TextureViewDescriptor::default());
2012
2013        let blur_tex_b = device.create_texture(&blur_texture_desc);
2014        let blur_texture_b = blur_tex_b.create_view(&wgpu::TextureViewDescriptor::default());
2015
2016        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
2017            address_mode_u: wgpu::AddressMode::ClampToEdge,
2018            address_mode_v: wgpu::AddressMode::ClampToEdge,
2019            mag_filter: wgpu::FilterMode::Linear,
2020            min_filter: wgpu::FilterMode::Linear,
2021            ..Default::default()
2022        });
2023
2024        let scene_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2025            layout: env_bind_group_layout,
2026            entries: &[
2027                wgpu::BindGroupEntry {
2028                    binding: 0,
2029                    resource: wgpu::BindingResource::TextureView(&scene_texture),
2030                },
2031                wgpu::BindGroupEntry {
2032                    binding: 1,
2033                    resource: wgpu::BindingResource::Sampler(&sampler),
2034                },
2035            ],
2036            label: Some("Scene Bind Group"),
2037        });
2038
2039        let scene_views: Vec<&wgpu::TextureView> = (0..256).map(|_| &scene_texture).collect();
2040        let scene_texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2041            layout: texture_bind_group_layout,
2042            entries: &[
2043                wgpu::BindGroupEntry {
2044                    binding: 0,
2045                    resource: wgpu::BindingResource::TextureViewArray(&scene_views),
2046                },
2047                wgpu::BindGroupEntry {
2048                    binding: 1,
2049                    resource: wgpu::BindingResource::Sampler(&sampler),
2050                },
2051            ],
2052            label: Some("Scene Texture Bind Group"),
2053        });
2054
2055        let blur_views_a: Vec<&wgpu::TextureView> = (0..256).map(|_| &blur_texture_a).collect();
2056        let blur_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
2057            layout: texture_bind_group_layout,
2058            entries: &[
2059                wgpu::BindGroupEntry {
2060                    binding: 0,
2061                    resource: wgpu::BindingResource::TextureViewArray(&blur_views_a),
2062                },
2063                wgpu::BindGroupEntry {
2064                    binding: 1,
2065                    resource: wgpu::BindingResource::Sampler(&sampler),
2066                },
2067            ],
2068            label: Some("Blur Bind Group A"),
2069        });
2070
2071        let blur_views_b: Vec<&wgpu::TextureView> = (0..256).map(|_| &blur_texture_b).collect();
2072        let blur_bind_group_b = 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(&blur_views_b),
2078                },
2079                wgpu::BindGroupEntry {
2080                    binding: 1,
2081                    resource: wgpu::BindingResource::Sampler(&sampler),
2082                },
2083            ],
2084            label: Some("Blur Bind Group B"),
2085        });
2086
2087        let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
2088            label: Some("Surtr Depth Texture"),
2089            size: wgpu::Extent3d {
2090                width,
2091                height,
2092                depth_or_array_layers: 1,
2093            },
2094            mip_level_count: 1,
2095            sample_count: 1,
2096            dimension: wgpu::TextureDimension::D2,
2097            format: wgpu::TextureFormat::Depth32Float,
2098            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
2099            view_formats: &[],
2100        });
2101        let depth_texture_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
2102
2103        let blur_env_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
2104            layout: env_bind_group_layout,
2105            entries: &[
2106                wgpu::BindGroupEntry {
2107                    binding: 0,
2108                    resource: wgpu::BindingResource::TextureView(&blur_texture_a),
2109                },
2110                wgpu::BindGroupEntry {
2111                    binding: 1,
2112                    resource: wgpu::BindingResource::Sampler(&sampler),
2113                },
2114            ],
2115            label: Some("Blur Env Bind Group A"),
2116        });
2117
2118        SurfaceContext {
2119            surface,
2120            config,
2121            scene_texture,
2122            scene_bind_group,
2123            scene_texture_bind_group,
2124            depth_texture_view,
2125            blur_texture_a,
2126            blur_texture_b,
2127            blur_bind_group_a,
2128            blur_bind_group_b,
2129            blur_env_bind_group_a,
2130            scale_factor,
2131            sampler,
2132        }
2133    }
2134
2135    pub fn reset_time(&mut self) {
2136        self.start_time = std::time::Instant::now();
2137    }
2138
2139    /// reclaim_vram — Atomic recycling of the Mega-Atlas and all associated caches.
2140    /// This prevents OOM and silent failures by quenching the atlas when full.
2141    pub fn reclaim_vram(&mut self) {
2142        log::warn!("[GPU] Yggdrasil Compaction: Compacting Mega-Atlas...");
2143        
2144        let new_mega_atlas_tex = self.device.create_texture(&wgpu::TextureDescriptor {
2145            label: Some("Yggdrasil Mega-Atlas (Compacted)"),
2146            size: wgpu::Extent3d { width: 4096, height: 4096, depth_or_array_layers: 1 },
2147            mip_level_count: 1,
2148            sample_count: 1,
2149            dimension: wgpu::TextureDimension::D2,
2150            format: wgpu::TextureFormat::Rgba8UnormSrgb,
2151            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC,
2152            view_formats: &[],
2153        });
2154        
2155        let mut new_packer = YggdrasilPacker::new(4096, 4096);
2156        let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2157            label: Some("Atlas Compaction Encoder"),
2158        });
2159        
2160        let image_entries: Vec<(String, Rect)> = self.image_uv_registry.iter().map(|(k, v)| (k.clone(), *v)).collect();
2161        for (name, old_uv) in image_entries {
2162            if let Some(&tex_idx) = self.texture_registry.get(&name) {
2163                if tex_idx == 0 {
2164                    let w_px = (old_uv.width * 4096.0).round() as u32;
2165                    let h_px = (old_uv.height * 4096.0).round() as u32;
2166                    let old_x_px = (old_uv.x * 4096.0).round() as u32;
2167                    let old_y_px = (old_uv.y * 4096.0).round() as u32;
2168                    
2169                    if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
2170                        encoder.copy_texture_to_texture(
2171                            wgpu::TexelCopyTextureInfo {
2172                                texture: &self.mega_atlas_tex,
2173                                mip_level: 0,
2174                                origin: wgpu::Origin3d { x: old_x_px, y: old_y_px, z: 0 },
2175                                aspect: wgpu::TextureAspect::All,
2176                            },
2177                            wgpu::TexelCopyTextureInfo {
2178                                texture: &new_mega_atlas_tex,
2179                                mip_level: 0,
2180                                origin: wgpu::Origin3d { x: new_x, y: new_y, z: 0 },
2181                                aspect: wgpu::TextureAspect::All,
2182                            },
2183                            wgpu::Extent3d { width: w_px, height: h_px, depth_or_array_layers: 1 },
2184                        );
2185                        
2186                        let new_uv = Rect {
2187                            x: new_x as f32 / 4096.0,
2188                            y: new_y as f32 / 4096.0,
2189                            width: old_uv.width,
2190                            height: old_uv.height,
2191                        };
2192                        self.image_uv_registry.put(name.clone(), new_uv);
2193                    }
2194                }
2195            }
2196        }
2197        
2198        let text_entries: Vec<(u64, (Rect, f32, f32))> = self.text_cache.iter().map(|(k, v)| (*k, *v)).collect();
2199        for (hash, (old_uv, w_f, h_f)) in text_entries {
2200            let w_px = (old_uv.width * 4096.0).round() as u32;
2201            let h_px = (old_uv.height * 4096.0).round() as u32;
2202            let old_x_px = (old_uv.x * 4096.0).round() as u32;
2203            let old_y_px = (old_uv.y * 4096.0).round() as u32;
2204            
2205            if let Some((new_x, new_y)) = new_packer.pack(w_px, h_px) {
2206                encoder.copy_texture_to_texture(
2207                    wgpu::TexelCopyTextureInfo {
2208                        texture: &self.mega_atlas_tex,
2209                        mip_level: 0,
2210                        origin: wgpu::Origin3d { x: old_x_px, y: old_y_px, z: 0 },
2211                        aspect: wgpu::TextureAspect::All,
2212                    },
2213                    wgpu::TexelCopyTextureInfo {
2214                        texture: &new_mega_atlas_tex,
2215                        mip_level: 0,
2216                        origin: wgpu::Origin3d { x: new_x, y: new_y, z: 0 },
2217                        aspect: wgpu::TextureAspect::All,
2218                    },
2219                    wgpu::Extent3d { width: w_px, height: h_px, depth_or_array_layers: 1 },
2220                );
2221                
2222                let new_uv = Rect {
2223                    x: new_x as f32 / 4096.0,
2224                    y: new_y as f32 / 4096.0,
2225                    width: old_uv.width,
2226                    height: old_uv.height,
2227                };
2228                self.text_cache.put(hash, (new_uv, w_f, h_f));
2229            }
2230        }
2231        
2232        self.queue.submit(std::iter::once(encoder.finish()));
2233        
2234        self.mega_atlas_tex = new_mega_atlas_tex;
2235        let mega_atlas_view_obj = self.mega_atlas_tex.create_view(&wgpu::TextureViewDescriptor::default());
2236        self.texture_views[0] = mega_atlas_view_obj.clone();
2237        
2238        self.rebuild_texture_array_bind_group();
2239        
2240        if !self.texture_bind_groups.is_empty() {
2241            self.texture_bind_groups[0] = self.mega_atlas_bind_group.clone();
2242        }
2243        
2244        self.atlas_packer = new_packer;
2245        self.telemetry.vram_exhausted = false;
2246    }
2247
2248    fn shatter_internal(
2249        &mut self,
2250        rect: Rect,
2251        pieces: u32,
2252        force: f32,
2253        color: [f32; 4],
2254        mode: u32,
2255    ) {
2256        // High-Fidelity Variable Particle Density
2257        let count = (pieces as f32).sqrt().ceil() as u32;
2258        let dw = rect.width / count as f32;
2259        let dh = rect.height / count as f32;
2260
2261        let c = self.apply_opacity(color);
2262
2263        for y in 0..count {
2264            for x in 0..count {
2265                let shard_rect = Rect {
2266                    x: rect.x + x as f32 * dw,
2267                    y: rect.y + y as f32 * dh,
2268                    width: dw,
2269                    height: dh,
2270                };
2271
2272                let uv = Rect {
2273                    x: x as f32 / count as f32,
2274                    y: y as f32 / count as f32,
2275                    width: 1.0 / count as f32,
2276                    height: 1.0 / count as f32,
2277                };
2278
2279                self.fill_rect_with_full_params(shard_rect, c, mode, None, force, uv);
2280            }
2281        }
2282    }
2283
2284    fn recursive_bolt(&mut self, from: [f32; 2], to: [f32; 2], depth: u32, color: [f32; 4]) {
2285        if depth == 0 {
2286            self.draw_lightning_segment(from, to, color);
2287            return;
2288        }
2289
2290        let mid_x = (from[0] + to[0]) * 0.5;
2291        let mid_y = (from[1] + to[1]) * 0.5;
2292
2293        let dx = to[0] - from[0];
2294        let dy = to[1] - from[1];
2295        let len = (dx * dx + dy * dy).sqrt();
2296
2297        // Perpendicular offset for jaggedness
2298        let offset_scale = len * 0.15;
2299        let seed = (from[0] * 12.9898 + from[1] * 78.233 + (depth as f32) * 37.11)
2300            .sin()
2301            .fract();
2302        let offset_x = -dy / len * (seed - 0.5) * offset_scale;
2303        let offset_y = dx / len * (seed - 0.5) * offset_scale;
2304
2305        let mid = [mid_x + offset_x, mid_y + offset_y];
2306
2307        self.recursive_bolt(from, mid, depth - 1, color);
2308        self.recursive_bolt(mid, to, depth - 1, color);
2309
2310        // 20% chance of a secondary branch
2311        if depth > 2 && seed > 0.8 {
2312            let branch_to = [
2313                mid[0] + offset_x * 2.0 + (seed * 100.0).sin() * 50.0,
2314                mid[1] + offset_y * 2.0 + (seed * 100.0).cos() * 50.0,
2315            ];
2316            self.recursive_bolt(mid, branch_to, depth - 2, color);
2317        }
2318    }
2319
2320    fn draw_lightning_segment(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
2321        let dx = to[0] - from[0];
2322        let dy = to[1] - from[1];
2323        let len = (dx * dx + dy * dy).sqrt();
2324        if len < 0.001 {
2325            return;
2326        }
2327
2328        let glow_width = 32.0;
2329        let core_width = 4.0;
2330        let c = self.apply_opacity(color);
2331
2332        // 1. Render Volumetric Glow (Cyan)
2333        let gnx = -dy / len * glow_width * 0.5;
2334        let gny = dx / len * glow_width * 0.5;
2335        let gp1 = [from[0] + gnx, from[1] + gny];
2336        let gp2 = [to[0] + gnx, to[1] + gny];
2337        let gp3 = [to[0] - gnx, to[1] - gny];
2338        let gp4 = [from[0] - gnx, from[1] - gny];
2339        self.push_oriented_quad(
2340            [gp1, gp2, gp3, gp4],
2341            c,
2342            9,
2343            Rect {
2344                x: 0.0,
2345                y: 0.0,
2346                width: 1.0,
2347                height: 1.0,
2348            },
2349        );
2350
2351        // 2. Render Blinding Core (White)
2352        let cnx = -dy / len * core_width * 0.5;
2353        let cny = dx / len * core_width * 0.5;
2354        let cp1 = [from[0] + cnx, from[1] + cny];
2355        let cp2 = [to[0] + cnx, to[1] + cny];
2356        let cp3 = [to[0] - cnx, to[1] - cny];
2357        let cp4 = [from[0] - cnx, from[1] - cny];
2358        self.push_oriented_quad(
2359            [cp1, cp2, cp3, cp4],
2360            [1.0, 1.0, 1.0, c[3]],
2361            0,
2362            Rect {
2363                x: 0.0,
2364                y: 0.0,
2365                width: 1.0,
2366                height: 1.0,
2367            },
2368        );
2369    }
2370
2371    fn push_oriented_quad(
2372        &mut self,
2373        points: [[f32; 2]; 4],
2374        color: [f32; 4],
2375        mode: u32,
2376        uv_rect: Rect,
2377    ) {
2378        let scissor = self.clip_stack.last().copied();
2379        let texture_id = None; // Oriented quads like lightning don't use textures yet
2380
2381        if self.draw_calls.is_empty()
2382            || self.current_texture_id != texture_id
2383            || self.draw_calls.last().unwrap().scissor_rect != scissor
2384        {
2385            self.current_texture_id = texture_id;
2386            self.draw_calls.push(DrawCall {
2387                texture_id,
2388                scissor_rect: scissor,
2389                index_start: self.indices.len() as u32,
2390                index_count: 0,
2391                material: if mode == 7 {
2392                    cvkg_core::DrawMaterial::Glass { blur_radius: 20.0 }
2393                } else if mode == 6 {
2394                    cvkg_core::DrawMaterial::TopUI
2395                } else {
2396                    cvkg_core::DrawMaterial::Opaque
2397                },
2398            });
2399        }
2400
2401        let uvs = [
2402            [uv_rect.x, uv_rect.y],
2403            [uv_rect.x + uv_rect.width, uv_rect.y],
2404            [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
2405            [uv_rect.x, uv_rect.y + uv_rect.height],
2406        ];
2407
2408        let screen = [self.current_width() as f32, self.current_height() as f32];
2409        let rect = Rect {
2410            x: points[0][0],
2411            y: points[0][1],
2412            width: 1.0,
2413            height: 1.0,
2414        };
2415
2416        for i in 0..4 {
2417            let px = points[i][0];
2418            let py = points[i][1];
2419
2420            let (translation, scale_transform, rotation, _, _) = self.current_transform();
2421            self.vertices.push(Vertex {
2422                position: [px, py, 0.0],
2423                normal: [0.0, 0.0, 1.0],
2424                uv: uvs[i],
2425                color,
2426                mode,
2427                radius: 0.0,
2428                slice: [0.0, 0.0, 0.0, 1.0],
2429                logical: [px - rect.x, py - rect.y],
2430                size: [rect.width, rect.height],
2431                screen,
2432                clip: [-10000.0, -10000.0, 20000.0, 20000.0],
2433                translation,
2434                scale: scale_transform,
2435                rotation,
2436                tex_index: 0,
2437            });
2438        }
2439
2440        if let Some(call) = self.draw_calls.last_mut() {
2441            call.index_count += 6;
2442        }
2443    }
2444    fn get_texture_id(&mut self, name: &str) -> Option<u32> {
2445        self.texture_registry.get(name).copied()
2446    }
2447
2448    /// fill_rect_with_mode — Specialized rectangle drawing with mode-specific shader logic.
2449    pub fn fill_rect_with_mode(
2450        &mut self,
2451        rect: Rect,
2452        color: [f32; 4],
2453        mode: u32,
2454        texture_id: Option<u32>,
2455    ) {
2456        self.fill_rect_with_full_params(
2457            rect,
2458            color,
2459            mode,
2460            texture_id,
2461            0.0,
2462            Rect {
2463                x: 0.0,
2464                y: 0.0,
2465                width: 1.0,
2466                height: 1.0,
2467            },
2468        );
2469    }
2470
2471    fn fill_rect_with_full_params(
2472        &mut self,
2473        rect: Rect,
2474        color: [f32; 4],
2475        mode: u32,
2476        texture_id: Option<u32>,
2477        radius: f32,
2478        uv_rect: Rect,
2479    ) {
2480        // If a shadow is active, draw it first
2481        if let Some(shadow) = self.shadow_stack.last().copied()
2482            && shadow.color[3] > 0.001
2483        {
2484            Renderer::draw_drop_shadow(
2485                self,
2486                rect,
2487                radius,
2488                shadow.color,
2489                shadow.radius,
2490                0.0, // Spread
2491            );
2492        }
2493
2494        let slice = self
2495            .slice_stack
2496            .last()
2497            .copied()
2498            .map(|(a, o)| [a, o, 1.0, 1.0])
2499            .unwrap_or([0.0, 0.0, 0.0, 1.0]);
2500        self.fill_rect_with_full_params_and_slice(
2501            rect, color, mode, texture_id, radius, uv_rect, slice,
2502        );
2503    }
2504
2505    #[allow(clippy::too_many_arguments)]
2506    fn fill_rect_with_full_params_and_slice(
2507        &mut self,
2508        rect: Rect,
2509        color: [f32; 4],
2510        mode: u32,
2511        texture_id: Option<u32>,
2512        radius: f32,
2513        uv_rect: Rect,
2514        slice: [f32; 4],
2515    ) {
2516        let scissor = self.clip_stack.last().copied();
2517
2518        let material = if mode == 7 {
2519            cvkg_core::DrawMaterial::Glass { blur_radius: 20.0 }
2520        } else if mode == 6 {
2521            cvkg_core::DrawMaterial::TopUI
2522        } else {
2523            self.current_draw_material
2524        };
2525
2526        // Batching: check if we need to start a new DrawCall
2527        // With Texture Array, we no longer need to break batches when the texture changes,
2528        // as long as they are all part of the same array bind group (Group 0).
2529        let last_call = self.draw_calls.last();
2530        let needs_new_call = self.draw_calls.is_empty()
2531            || last_call.unwrap().scissor_rect != scissor
2532            || last_call.unwrap().material != material;
2533
2534        if needs_new_call {
2535            self.current_texture_id = Some(0); // All textures are now in the binding array at Group 0
2536            self.draw_calls.push(DrawCall {
2537                texture_id: self.current_texture_id,
2538                scissor_rect: scissor,
2539                index_start: self.indices.len() as u32,
2540                index_count: 0,
2541                material,
2542            });
2543        }
2544
2545        let scale = self.current_scale_factor();
2546        let snap = |v: f32| (v * scale).round() / scale;
2547
2548        let base_idx = self.vertices.len() as u32;
2549        let x1 = snap(rect.x);
2550        let y1 = snap(rect.y);
2551        let x2 = snap(rect.x + rect.width);
2552        let y2 = snap(rect.y + rect.height);
2553        let z = self.current_z;
2554        let normal = [0.0, 0.0, 1.0];
2555        let screen = [self.current_width() as f32, self.current_height() as f32];
2556        let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
2557            x: -10000.0,
2558            y: -10000.0,
2559            width: 20000.0,
2560            height: 20000.0,
2561        });
2562        let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
2563
2564        let (translation, scale_transform, rotation, _, _) = self.current_transform();
2565
2566        let tex_index = texture_id.unwrap_or(0);
2567
2568        self.vertices.push(Vertex {
2569            position: [x1, y1, z],
2570            normal,
2571            uv: [uv_rect.x, uv_rect.y],
2572            color,
2573            mode,
2574            radius,
2575            slice,
2576            logical: [0.0, 0.0],
2577            size: [rect.width, rect.height],
2578            screen,
2579            clip,
2580            translation,
2581            scale: scale_transform,
2582            rotation,
2583            tex_index,
2584        });
2585        self.vertices.push(Vertex {
2586            position: [x2, y1, z],
2587            normal,
2588            uv: [uv_rect.x + uv_rect.width, uv_rect.y],
2589            color,
2590            mode,
2591            radius,
2592            slice,
2593            logical: [rect.width, 0.0],
2594            size: [rect.width, rect.height],
2595            screen,
2596            clip,
2597            translation,
2598            scale: scale_transform,
2599            rotation,
2600            tex_index,
2601        });
2602        self.vertices.push(Vertex {
2603            position: [x2, y2, z],
2604            normal,
2605            uv: [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
2606            color,
2607            mode,
2608            radius,
2609            slice,
2610            logical: [rect.width, rect.height],
2611            size: [rect.width, rect.height],
2612            screen,
2613            clip,
2614            translation,
2615            scale: scale_transform,
2616            rotation,
2617            tex_index,
2618        });
2619        self.vertices.push(Vertex {
2620            position: [x1, y2, z],
2621            normal,
2622            uv: [uv_rect.x, uv_rect.y + uv_rect.height],
2623            color,
2624            mode,
2625            radius,
2626            slice,
2627            logical: [0.0, rect.height],
2628            size: [rect.width, rect.height],
2629            screen,
2630            clip,
2631            translation,
2632            scale: scale_transform,
2633            rotation,
2634            tex_index,
2635        });
2636
2637        self.indices.extend_from_slice(&[
2638            base_idx,
2639            base_idx + 1,
2640            base_idx + 2,
2641            base_idx,
2642            base_idx + 2,
2643            base_idx + 3,
2644        ]);
2645
2646        if let Some(call) = self.draw_calls.last_mut() {
2647            call.index_count += 6;
2648        }
2649    }
2650
2651    /// end_frame — Quench the blade by submitting the full Muspelheim multi-pass effect.
2652    pub fn end_frame(&mut self, mut encoder: wgpu::CommandEncoder) {
2653        let (
2654            surface_texture,
2655            target_view,
2656            ctx_scene_texture,
2657            ctx_depth_texture_view,
2658            ctx_blur_env_bind_group_a,
2659            ctx_scene_texture_bind_group,
2660            ctx_blur_texture_a,
2661            ctx_blur_texture_b,
2662            _ctx_sampler,
2663            ctx_blur_bind_group_a,
2664            ctx_blur_bind_group_b,
2665            scale,
2666        ) = if let Some(window_id) = self.current_window {
2667            let ctx = self
2668                .surfaces
2669                .get(&window_id)
2670                .expect("Missing surface context");
2671            let frame = match ctx.surface.get_current_texture() {
2672                wgpu::CurrentSurfaceTexture::Success(t) => t,
2673                wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
2674                    ctx.surface.configure(&self.device, &ctx.config);
2675                    t
2676                }
2677                _ => {
2678                    log::warn!("[GPU] Surface texture acquisition failed, reconfiguring surface");
2679                    ctx.surface.configure(&self.device, &ctx.config);
2680                    return;
2681                }
2682            };
2683            let view = frame
2684                .texture
2685                .create_view(&wgpu::TextureViewDescriptor::default());
2686            (
2687                Some(frame),
2688                view,
2689                &ctx.scene_texture,
2690                &ctx.depth_texture_view,
2691                &ctx.blur_env_bind_group_a,
2692                &ctx.scene_texture_bind_group,
2693                &ctx.blur_texture_a,
2694                &ctx.blur_texture_b,
2695                &ctx.sampler,
2696                &ctx.blur_bind_group_a,
2697                &ctx.blur_bind_group_b,
2698                ctx.scale_factor,
2699            )
2700        } else {
2701            let ctx = self
2702                .headless_context
2703                .as_ref()
2704                .expect("No headless context for end_frame");
2705            (
2706                None,
2707                ctx.output_view.clone(),
2708                &ctx.scene_texture,
2709                &ctx.depth_texture_view,
2710                &ctx.blur_env_bind_group_a,
2711                &ctx.scene_texture_bind_group,
2712                &ctx.blur_texture_a,
2713                &ctx.blur_texture_b,
2714                &ctx.sampler,
2715                &ctx.blur_bind_group_a,
2716                &ctx.blur_bind_group_b,
2717                self.current_scale_factor(),
2718            )
2719        };
2720
2721        // ── Pass 1: Opaque Background & Atmosphere ──────────────────────────
2722        {
2723            let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2724                label: Some("Surtr P1 Opaque Background"),
2725                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2726                    view: ctx_scene_texture,
2727                    resolve_target: None,
2728                    ops: wgpu::Operations {
2729                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2730                        store: wgpu::StoreOp::Store,
2731                    },
2732                    depth_slice: None,
2733                })],
2734                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2735                    view: ctx_depth_texture_view,
2736                    depth_ops: Some(wgpu::Operations {
2737                        load: wgpu::LoadOp::Clear(0.0), // Reversed-Z
2738                        store: wgpu::StoreOp::Store,
2739                    }),
2740                    stencil_ops: None,
2741                }),
2742                timestamp_writes: self.skuld_queries.as_ref().map(|q| wgpu::RenderPassTimestampWrites {
2743                    query_set: q,
2744                    beginning_of_pass_write_index: Some(0),
2745                    end_of_pass_write_index: None,
2746                }),
2747                occlusion_query_set: None,
2748                multiview_mask: None,
2749            });
2750
2751            // 1a. Background Atmosphere
2752            p.set_pipeline(&self.background_pipeline);
2753            p.set_bind_group(0, &self.dummy_texture_bind_group, &[]);
2754            p.set_bind_group(1, ctx_blur_env_bind_group_a, &[]); // Use previous frame's blur for background depth
2755            p.set_bind_group(2, &self.berserker_bind_group, &[]);
2756            p.draw(0..6, 0..1);
2757
2758            // 1b. Opaque Main Elements (non-glass, non-ui)
2759            if !self.draw_calls.is_empty() {
2760                p.set_pipeline(&self.pipeline);
2761                p.set_vertex_buffer(0, self.vertex_buffer.slice(..));
2762                p.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2763                p.set_bind_group(1, &self.dummy_env_bind_group, &[]);
2764                p.set_bind_group(2, &self.berserker_bind_group, &[]);
2765
2766                for call in self.draw_calls.iter().filter(|c| matches!(c.material, cvkg_core::DrawMaterial::Opaque)) {
2767                    let bg = if let Some(id) = call.texture_id {
2768                        if id == 0 {
2769                            &self.mega_atlas_bind_group
2770                        } else {
2771                            self.texture_bind_groups
2772                                .get(id as usize)
2773                                .unwrap_or(&self.dummy_texture_bind_group)
2774                        }
2775                    } else {
2776                        &self.dummy_texture_bind_group
2777                    };
2778                    p.set_bind_group(0, bg, &[]);
2779                    p.draw_indexed(
2780                        call.index_start..call.index_start + call.index_count,
2781                        0,
2782                        0..1,
2783                    );
2784                    self.telemetry.draw_calls += 1;
2785                    self.telemetry.vertices += call.index_count;
2786                }
2787            }
2788        }
2789
2790        // ── Pass 2: Backdrop Blur (Bifrost) ──────────────────────────────────
2791        // Capture the background into blur_texture_b
2792        {
2793            // First extract into texture_a
2794            let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2795                label: Some("Surtr Blur Extract"),
2796                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2797                    view: ctx_blur_texture_a,
2798                    resolve_target: None,
2799                    ops: wgpu::Operations {
2800                        load: wgpu::LoadOp::Clear(wgpu::Color {
2801                            r: 0.0,
2802                            g: 0.0,
2803                            b: 0.0,
2804                            a: 0.0,
2805                        }),
2806                        store: wgpu::StoreOp::Store,
2807                    },
2808                    depth_slice: None,
2809                })],
2810                ..Default::default()
2811            });
2812            p.set_pipeline(&self.bloom_extract_pipeline); // Use extract as a direct copy for now
2813            p.set_bind_group(0, ctx_scene_texture_bind_group, &[]);
2814            p.set_bind_group(1, &self.dummy_env_bind_group, &[]);
2815            p.set_bind_group(2, &self.berserker_bind_group, &[]);
2816            p.draw(0..6, 0..1);
2817        }
2818
2819        let blur_iters: u32 = 4;
2820        for _i in 0..blur_iters {
2821            {
2822                let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2823                    label: Some("Blur H"),
2824                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2825                        view: ctx_blur_texture_b,
2826                        resolve_target: None,
2827                        ops: wgpu::Operations {
2828                            load: wgpu::LoadOp::Clear(wgpu::Color {
2829                                r: 0.0,
2830                                g: 0.0,
2831                                b: 0.0,
2832                                a: 0.0,
2833                            }),
2834                            store: wgpu::StoreOp::Store,
2835                        },
2836                        depth_slice: None,
2837                    })],
2838                    ..Default::default()
2839                });
2840                p.set_pipeline(&self.blur_h_pipeline);
2841                p.set_bind_group(0, ctx_blur_bind_group_a, &[]);
2842                p.set_bind_group(1, &self.dummy_env_bind_group, &[]);
2843                p.set_bind_group(2, &self.berserker_bind_group, &[]);
2844                p.draw(0..6, 0..1);
2845            }
2846            {
2847                let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2848                    label: Some("Blur V"),
2849                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2850                        view: ctx_blur_texture_a,
2851                        resolve_target: None,
2852                        ops: wgpu::Operations {
2853                            load: wgpu::LoadOp::Clear(wgpu::Color {
2854                                r: 0.0,
2855                                g: 0.0,
2856                                b: 0.0,
2857                                a: 0.0,
2858                            }),
2859                            store: wgpu::StoreOp::Store,
2860                        },
2861                        depth_slice: None,
2862                    })],
2863                    ..Default::default()
2864                });
2865                p.set_pipeline(&self.blur_v_pipeline);
2866                p.set_bind_group(0, ctx_blur_bind_group_b, &[]);
2867                p.set_bind_group(1, &self.dummy_env_bind_group, &[]);
2868                p.set_bind_group(2, &self.berserker_bind_group, &[]);
2869                p.draw(0..6, 0..1);
2870            }
2871        }
2872
2873        // 1. Finalize the PRE-parallel work (Background & Atmosphere)
2874        self.staging_command_buffers.push(encoder.finish());
2875
2876        let rt_w = self.current_width() as i32;
2877        let rt_h = self.current_height() as i32;
2878
2879        // 2. Parallel Encoding Phase: Glass & UI ─────────────────────────────
2880        // We utilize rayon to record these independent passes in parallel.
2881        let (glass_cb, ui_cb) = rayon::join(
2882            || {
2883                let mut glass_encoder =
2884                    self.device
2885                        .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2886                            label: Some("Parallel Glass Encoder"),
2887                        });
2888                {
2889                    let mut p = glass_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2890                        label: Some("Surtr P3 Liquid Glass"),
2891                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2892                            view: ctx_scene_texture,
2893                            resolve_target: None,
2894                            ops: wgpu::Operations {
2895                                load: wgpu::LoadOp::Load,
2896                                store: wgpu::StoreOp::Store,
2897                            },
2898                            depth_slice: None,
2899                        })],
2900                        depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2901                            view: ctx_depth_texture_view,
2902                            depth_ops: Some(wgpu::Operations {
2903                                load: wgpu::LoadOp::Load,
2904                                store: wgpu::StoreOp::Store,
2905                            }),
2906                            stencil_ops: None,
2907                        }),
2908                        ..Default::default()
2909                    });
2910
2911                    p.set_pipeline(&self.pipeline);
2912                    p.set_vertex_buffer(0, self.vertex_buffer.slice(..));
2913                    p.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2914                    p.set_bind_group(1, ctx_blur_env_bind_group_a, &[]);
2915                    p.set_bind_group(2, &self.berserker_bind_group, &[]);
2916
2917                    for call in self.draw_calls.iter().filter(|c| matches!(c.material, cvkg_core::DrawMaterial::Glass { .. })) {
2918                        let bg = if let Some(id) = call.texture_id {
2919                            if id == 0 {
2920                                &self.mega_atlas_bind_group
2921                            } else {
2922                                self.texture_bind_groups
2923                                    .get(id as usize)
2924                                    .unwrap_or(&self.dummy_texture_bind_group)
2925                            }
2926                        } else {
2927                            &self.dummy_texture_bind_group
2928                        };
2929                        p.set_bind_group(0, bg, &[]);
2930                        if let Some(rect) = call.scissor_rect {
2931                            // Scissor rect clamping logic:
2932                            // wgpu validation requires that the scissor rect is entirely contained within
2933                            // the physical render target dimensions and has a non-zero area (width > 0, height > 0).
2934                            // We compute the physical boundaries using the current render target size and scale factor,
2935                            // intersect/clamp them to the render target viewport, and fallback to a minimal 1x1 region
2936                            // if the intersection results in zero/negative area.
2937                            if rt_w > 0 && rt_h > 0 {
2938                                let x1 = (rect.x * scale).round() as i32;
2939                                let y1 = (rect.y * scale).round() as i32;
2940                                let x2 = ((rect.x + rect.width) * scale).round() as i32;
2941                                let y2 = ((rect.y + rect.height) * scale).round() as i32;
2942
2943                                let x1_clamped = x1.clamp(0, rt_w);
2944                                let y1_clamped = y1.clamp(0, rt_h);
2945                                let x2_clamped = x2.clamp(0, rt_w);
2946                                let y2_clamped = y2.clamp(0, rt_h);
2947
2948                                let w = x2_clamped - x1_clamped;
2949                                let h = y2_clamped - y1_clamped;
2950
2951                                if w > 0 && h > 0 {
2952                                    p.set_scissor_rect(
2953                                        x1_clamped as u32,
2954                                        y1_clamped as u32,
2955                                        w as u32,
2956                                        h as u32,
2957                                    );
2958                                } else {
2959                                    p.set_scissor_rect(0, 0, 1, 1);
2960                                }
2961                            }
2962                        }
2963                        p.draw_indexed(
2964                            call.index_start..call.index_start + call.index_count,
2965                            0,
2966                            0..1,
2967                        );
2968                    }
2969                }
2970                glass_encoder.finish()
2971            },
2972            || {
2973                let mut ui_encoder =
2974                    self.device
2975                        .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2976                            label: Some("Parallel UI Encoder"),
2977                        });
2978                {
2979                    let mut p = ui_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2980                        label: Some("Surtr P4 UI Layer"),
2981                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2982                            view: ctx_scene_texture,
2983                            resolve_target: None,
2984                            ops: wgpu::Operations {
2985                                load: wgpu::LoadOp::Load,
2986                                store: wgpu::StoreOp::Store,
2987                            },
2988                            depth_slice: None,
2989                        })],
2990                        depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2991                            view: ctx_depth_texture_view,
2992                            depth_ops: Some(wgpu::Operations {
2993                                load: wgpu::LoadOp::Load,
2994                                store: wgpu::StoreOp::Store,
2995                            }),
2996                            stencil_ops: None,
2997                        }),
2998                        ..Default::default()
2999                    });
3000
3001                    p.set_pipeline(&self.pipeline);
3002                    p.set_vertex_buffer(0, self.vertex_buffer.slice(..));
3003                    p.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3004                    p.set_bind_group(1, &self.dummy_env_bind_group, &[]);
3005                    p.set_bind_group(2, &self.berserker_bind_group, &[]);
3006
3007                    for call in self.draw_calls.iter().filter(|c| matches!(c.material, cvkg_core::DrawMaterial::TopUI)) {
3008                        let bg = if let Some(id) = call.texture_id {
3009                            if id == 0 {
3010                                &self.mega_atlas_bind_group
3011                            } else {
3012                                self.texture_bind_groups
3013                                    .get(id as usize)
3014                                    .unwrap_or(&self.dummy_texture_bind_group)
3015                            }
3016                        } else {
3017                            &self.dummy_texture_bind_group
3018                        };
3019                        p.set_bind_group(0, bg, &[]);
3020                        if let Some(rect) = call.scissor_rect {
3021                            // Scissor rect clamping logic:
3022                            // wgpu validation requires that the scissor rect is entirely contained within
3023                            // the physical render target dimensions and has a non-zero area (width > 0, height > 0).
3024                            // We compute the physical boundaries using the current render target size and scale factor,
3025                            // intersect/clamp them to the render target viewport, and fallback to a minimal 1x1 region
3026                            // if the intersection results in zero/negative area.
3027                            if rt_w > 0 && rt_h > 0 {
3028                                let x1 = (rect.x * scale).round() as i32;
3029                                let y1 = (rect.y * scale).round() as i32;
3030                                let x2 = ((rect.x + rect.width) * scale).round() as i32;
3031                                let y2 = ((rect.y + rect.height) * scale).round() as i32;
3032
3033                                let x1_clamped = x1.clamp(0, rt_w);
3034                                let y1_clamped = y1.clamp(0, rt_h);
3035                                let x2_clamped = x2.clamp(0, rt_w);
3036                                let y2_clamped = y2.clamp(0, rt_h);
3037
3038                                let w = x2_clamped - x1_clamped;
3039                                let h = y2_clamped - y1_clamped;
3040
3041                                if w > 0 && h > 0 {
3042                                    p.set_scissor_rect(
3043                                        x1_clamped as u32,
3044                                        y1_clamped as u32,
3045                                        w as u32,
3046                                        h as u32,
3047                                    );
3048                                } else {
3049                                    p.set_scissor_rect(0, 0, 1, 1);
3050                                }
3051                            }
3052                        }
3053                        p.draw_indexed(
3054                            call.index_start..call.index_start + call.index_count,
3055                            0,
3056                            0..1,
3057                        );
3058                    }
3059                }
3060                ui_encoder.finish()
3061            },
3062        );
3063
3064        self.staging_command_buffers.push(glass_cb);
3065        self.staging_command_buffers.push(ui_cb);
3066
3067        // Update telemetry for parallel work
3068        let glass_calls = self.draw_calls.iter().filter(|c| matches!(c.material, cvkg_core::DrawMaterial::Glass { .. })).count();
3069        let glass_verts: u32 = self
3070            .draw_calls
3071            .iter()
3072            .filter(|c| matches!(c.material, cvkg_core::DrawMaterial::Glass { .. }))
3073            .map(|c| c.index_count)
3074            .sum();
3075        let ui_calls = self.draw_calls.iter().filter(|c| matches!(c.material, cvkg_core::DrawMaterial::TopUI)).count();
3076        let ui_verts: u32 = self
3077            .draw_calls
3078            .iter()
3079            .filter(|c| matches!(c.material, cvkg_core::DrawMaterial::TopUI))
3080            .map(|c| c.index_count)
3081            .sum();
3082        self.telemetry.draw_calls += (glass_calls + ui_calls) as u32;
3083        self.telemetry.vertices += glass_verts + ui_verts;
3084
3085        // 3. Start POST-parallel work (Bloom & Composite)
3086        let mut post_encoder =
3087            self.device
3088                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
3089                    label: Some("Surtr Post-Process Encoder"),
3090                });
3091
3092        // ── Pass 5: Bloom Extract (Complete Scene) ──────────────────────────
3093        {
3094            let mut p = post_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3095                label: Some("Surtr Bloom Extract"),
3096                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3097                    view: ctx_blur_texture_a,
3098                    resolve_target: None,
3099                    ops: wgpu::Operations {
3100                        load: wgpu::LoadOp::Clear(wgpu::Color {
3101                            r: 0.0,
3102                            g: 0.0,
3103                            b: 0.0,
3104                            a: 0.0,
3105                        }),
3106                        store: wgpu::StoreOp::Store,
3107                    },
3108                    depth_slice: None,
3109                })],
3110                ..Default::default()
3111            });
3112            p.set_pipeline(&self.bloom_extract_pipeline);
3113            p.set_bind_group(0, ctx_scene_texture_bind_group, &[]);
3114            p.set_bind_group(1, &self.dummy_env_bind_group, &[]);
3115            p.set_bind_group(2, &self.berserker_bind_group, &[]);
3116            p.draw(0..6, 0..1);
3117        }
3118
3119        // ── Pass 6: Blur Bloom ──────────────────────────────────────────────
3120        for _ in 0..2 {
3121            {
3122                let mut p = post_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3123                    label: Some("Bloom Blur H"),
3124                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3125                        view: ctx_blur_texture_b,
3126                        resolve_target: None,
3127                        ops: wgpu::Operations {
3128                            load: wgpu::LoadOp::Clear(wgpu::Color {
3129                                r: 0.0,
3130                                g: 0.0,
3131                                b: 0.0,
3132                                a: 0.0,
3133                            }),
3134                            store: wgpu::StoreOp::Store,
3135                        },
3136                        depth_slice: None,
3137                    })],
3138                    ..Default::default()
3139                });
3140                p.set_pipeline(&self.blur_h_pipeline);
3141                p.set_bind_group(0, ctx_blur_bind_group_a, &[]);
3142                p.set_bind_group(1, &self.dummy_env_bind_group, &[]);
3143                p.set_bind_group(2, &self.berserker_bind_group, &[]);
3144                p.draw(0..6, 0..1);
3145            }
3146            {
3147                let mut p = post_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3148                    label: Some("Bloom Blur V"),
3149                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3150                        view: ctx_blur_texture_a,
3151                        resolve_target: None,
3152                        ops: wgpu::Operations {
3153                            load: wgpu::LoadOp::Clear(wgpu::Color {
3154                                r: 0.0,
3155                                g: 0.0,
3156                                b: 0.0,
3157                                a: 0.0,
3158                            }),
3159                            store: wgpu::StoreOp::Store,
3160                        },
3161                        depth_slice: None,
3162                    })],
3163                    ..Default::default()
3164                });
3165                p.set_pipeline(&self.blur_v_pipeline);
3166                p.set_bind_group(0, ctx_blur_bind_group_b, &[]);
3167                p.set_bind_group(1, &self.dummy_env_bind_group, &[]);
3168                p.set_bind_group(2, &self.berserker_bind_group, &[]);
3169                p.draw(0..6, 0..1);
3170            }
3171        }
3172
3173        // ── Pass 7: Composite & Tone Map ────────────────────────────────────
3174        {
3175            let mut p = post_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3176                label: Some("Surtr P7 Composite"),
3177                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3178                    view: &target_view,
3179                    resolve_target: None,
3180                    ops: wgpu::Operations {
3181                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
3182                        store: wgpu::StoreOp::Store,
3183                    },
3184                    depth_slice: None,
3185                })],
3186                depth_stencil_attachment: None,
3187                timestamp_writes: self.skuld_queries.as_ref().map(|q| wgpu::RenderPassTimestampWrites {
3188                    query_set: q,
3189                    beginning_of_pass_write_index: None,
3190                    end_of_pass_write_index: Some(1),
3191                }),
3192                occlusion_query_set: None,
3193                multiview_mask: None,
3194            });
3195            p.set_pipeline(&self.composite_pipeline);
3196            p.set_bind_group(0, ctx_scene_texture_bind_group, &[]);
3197            p.set_bind_group(1, ctx_blur_env_bind_group_a, &[]);
3198            p.set_bind_group(2, &self.berserker_bind_group, &[]);
3199            p.draw(0..6, 0..1);
3200            self.telemetry.draw_calls += 1;
3201        }
3202
3203        self.telemetry.frame_time_ms = self.last_frame_start.elapsed().as_secs_f32() * 1000.0;
3204        self.update_vram_telemetry();
3205
3206        // Skuld: Resolve timestamps
3207        if let (Some(q), Some(b), Some(rb)) = (
3208            &self.skuld_queries,
3209            &self.skuld_buffer,
3210            &self.skuld_read_buffer,
3211        ) {
3212            post_encoder.resolve_query_set(q, 0..2, b, 0);
3213            post_encoder.copy_buffer_to_buffer(b, 0, rb, 0, 16);
3214        }
3215
3216        // Finalize post-parallel work
3217        self.staging_command_buffers.push(post_encoder.finish());
3218
3219        // Atomic submission: all blocks in correct sequence
3220        let cmds = std::mem::take(&mut self.staging_command_buffers);
3221        self.queue.submit(cmds);
3222        if let Some(f) = surface_texture {
3223            f.present();
3224        }
3225    }
3226}
3227
3228impl cvkg_core::ElapsedTime for SurtrRenderer {
3229    fn delta_time(&self) -> f32 {
3230        self.current_scene.delta_time
3231    }
3232
3233    fn elapsed_time(&self) -> f32 {
3234        self.start_time.elapsed().as_secs_f32()
3235    }
3236}
3237
3238impl SurtrRenderer {
3239    /// load_image_to_atlas — Packs a raw asset into the Mega-Atlas.
3240    /// This is used for common icons to enable aggressive batching (1 draw call).
3241    pub fn load_image_to_atlas(&mut self, name: &str, data: &[u8]) {
3242        if self.image_uv_registry.contains(name) {
3243            return;
3244        }
3245        let img_result = image::load_from_memory(data);
3246        let img = match img_result {
3247            Ok(img) => img.to_rgba8(),
3248            Err(e) => {
3249                log::error!("Failed to load image {} to atlas: {}", name, e);
3250                return;
3251            }
3252        };
3253        let (width, height) = img.dimensions();
3254
3255        // Pack into atlas
3256        if let Some((x, y)) = self.atlas_packer.pack(width, height) {
3257            let uv_rect = Rect {
3258                x: x as f32 / 4096.0,
3259                y: y as f32 / 4096.0,
3260                width: width as f32 / 4096.0,
3261                height: height as f32 / 4096.0,
3262            };
3263
3264            // Upload to GPU
3265            self.queue.write_texture(
3266                wgpu::TexelCopyTextureInfo {
3267                    texture: &self.mega_atlas_tex,
3268                    mip_level: 0,
3269                    origin: wgpu::Origin3d { x, y, z: 0 },
3270                    aspect: wgpu::TextureAspect::All,
3271                },
3272                &img,
3273                wgpu::TexelCopyBufferLayout {
3274                    offset: 0,
3275                    bytes_per_row: Some(4 * width),
3276                    rows_per_image: Some(height),
3277                },
3278                wgpu::Extent3d {
3279                    width,
3280                    height,
3281                    depth_or_array_layers: 1,
3282                },
3283            );
3284
3285            self.image_uv_registry.put(name.to_string(), uv_rect);
3286            self.texture_registry.put(name.to_string(), 0); // Index 0 is the dummy white texture
3287            log::debug!(
3288                "[Surtr] Packed '{}' into Mega-Atlas at ({}, {})",
3289                name,
3290                x,
3291                y
3292            );
3293        } else {
3294            log::warn!(
3295                "ATLAS_FULL: Failed to pack '{}' into Mega-Atlas. Falling back to Texture Array.",
3296                name
3297            );
3298            self.load_image(name, data);
3299        }
3300    }
3301
3302    /// Shapes a text string using a predefined system font stack.
3303    ///
3304    /// # Contract
3305    /// Evaluates text shaping with fallbacks: queries "SF Pro Text", "SF Pro", "Inter",
3306    /// "Helvetica Neue", "Helvetica", "Arial", and defaults back to "sans-serif".
3307    /// This ensures visual typographic consistency across platforms where specific
3308    /// branding faces may or may not be installed.
3309    fn shape_text_with_stack(&mut self, text: &str, size: f32) -> cvkg_runic_text::ShapedText {
3310        let mut style = cvkg_runic_text::TextStyle::new("SF Pro Text", size);
3311        style.fallback_families = vec![
3312            "SF Pro".to_string(),
3313            "Inter".to_string(),
3314            "Helvetica Neue".to_string(),
3315            "Helvetica".to_string(),
3316            "Arial".to_string(),
3317            "sans-serif".to_string(),
3318        ];
3319        let spans = vec![cvkg_runic_text::TextSpan::new(text, style)];
3320        self.text_engine.shape_layout(&spans, None, cvkg_runic_text::TextAlign::Start, cvkg_runic_text::TextOverflow::WordWrap)
3321            .unwrap_or_else(|_| cvkg_runic_text::ShapedText {
3322                glyphs: Vec::new(),
3323                lines: Vec::new(),
3324                width: 0.0,
3325                height: 0.0,
3326                text: text.to_string(),
3327                spans: Vec::new(),
3328                has_rtl: false,
3329                ascent: 0.0,
3330                descent: 0.0,
3331                line_gap: 0.0,
3332                grapheme_boundaries: vec![],
3333            })
3334    }
3335}
3336
3337impl cvkg_core::Renderer for SurtrRenderer {
3338    fn is_over_budget(&self) -> bool {
3339        self.frame_budget.allow_degradation
3340            && self.last_frame_start.elapsed().as_secs_f32() * 1000.0 > self.frame_budget.target_ms
3341    }
3342
3343    /// fill_rect — Standard rectangle drawing method.
3344    fn prewarm_vram(&mut self, assets: Vec<(String, Vec<u8>)>) {
3345        log::info!(
3346            "[Surtr] Pre-warming Mega-Atlas with {} assets...",
3347            assets.len()
3348        );
3349        for (name, data) in assets {
3350            self.load_image_to_atlas(&name, &data);
3351        }
3352    }
3353
3354    fn fill_rect(&mut self, rect: Rect, color: [f32; 4]) {
3355        self.fill_rect_with_mode(rect, self.apply_opacity(color), 0, None);
3356    }
3357
3358    fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4]) {
3359        self.fill_rect_with_full_params(
3360            rect,
3361            self.apply_opacity(color),
3362            3,
3363            None,
3364            radius,
3365            Rect {
3366                x: 0.0,
3367                y: 0.0,
3368                width: 1.0,
3369                height: 1.0,
3370            },
3371        );
3372    }
3373
3374    fn fill_ellipse(&mut self, rect: Rect, color: [f32; 4]) {
3375        self.fill_rect_with_full_params(
3376            rect,
3377            self.apply_opacity(color),
3378            4,
3379            None,
3380            0.0,
3381            Rect {
3382                x: 0.0,
3383                y: 0.0,
3384                width: 1.0,
3385                height: 1.0,
3386            },
3387        );
3388    }
3389
3390    fn draw_3d_cube(&mut self, rect: Rect, color: [f32; 4], rotation: [f32; 3]) {
3391        self.fill_rect_with_full_params_and_slice(
3392            rect,
3393            self.apply_opacity(color),
3394            21,
3395            None,
3396            0.0,
3397            Rect {
3398                x: 0.0,
3399                y: 0.0,
3400                width: 1.0,
3401                height: 1.0,
3402            },
3403            [rotation[0], rotation[1], rotation[2], 0.0],
3404        );
3405    }
3406
3407    fn bifrost(&mut self, rect: Rect, blur: f32, _saturation: f32, opacity: f32) {
3408        // Calculate screen-space UVs for high-fidelity global refraction
3409        let screen_uv = Rect {
3410            x: rect.x / self.current_width() as f32,
3411            y: rect.y / self.current_height() as f32,
3412            width: rect.width / self.current_width() as f32,
3413            height: rect.height / self.current_height() as f32,
3414        };
3415        // Use mode 7 for high-fidelity background blur sampling
3416        // Use the blur parameter as corner radius for the glass panel
3417        self.fill_rect_with_full_params(rect, [1.0, 1.0, 1.0, opacity], 7, None, blur, screen_uv);
3418    }
3419
3420    fn gungnir(&mut self, rect: Rect, color: [f32; 4], radius: f32, intensity: f32) {
3421        // Create neon glow effect using additive blending
3422        // This renders a glowing aura around the element
3423        let center_x = rect.x + rect.width * 0.5;
3424        let center_y = rect.y + rect.height * 0.5;
3425        let max_dim = rect.width.max(rect.height) * 0.5 + radius;
3426
3427        // Draw expanding glow layers
3428        for i in 0..8 {
3429            let alpha = intensity / (i as f32 + 1.0) * 0.3;
3430            let glow_color = [color[0], color[1], color[2], alpha];
3431            self.fill_rect_with_mode(
3432                Rect {
3433                    x: center_x - max_dim - i as f32 * 2.0,
3434                    y: center_y - max_dim - i as f32 * 2.0,
3435                    width: max_dim * 2.0 + i as f32 * 4.0,
3436                    height: max_dim * 2.0 + i as f32 * 4.0,
3437                },
3438                glow_color,
3439                8, // Mode for additive blending
3440                None,
3441            );
3442        }
3443    }
3444
3445    /// Renders a dynamic glowing hover boundary field around a hit target.
3446    ///
3447    /// # Contract
3448    /// Expands the bounding box of the visual target by `radius` to establish
3449    /// a continuous proximity glow. Uses blending mode 18 (GPU drop shadow/glow)
3450    /// to rasterize the glow with specialized radius-to-margin uv coordinate mappings.
3451    fn mani_glow(&mut self, rect: Rect, color: [f32; 4], radius: f32) {
3452        let margin = radius;
3453        let glow_rect = Rect {
3454            x: rect.x - margin,
3455            y: rect.y - margin,
3456            width: rect.width + 2.0 * margin,
3457            height: rect.height + 2.0 * margin,
3458        };
3459        let uv_rect = Rect {
3460            x: margin,
3461            y: radius,
3462            width: 0.0,
3463            height: 0.0,
3464        };
3465        self.fill_rect_with_full_params(glow_rect, self.apply_opacity(color), 18, None, 8.0, uv_rect);
3466    }
3467
3468
3469    fn stroke_rect(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
3470        let c = self.apply_opacity(color);
3471        let hw = stroke_width;
3472        // Top, bottom, left, right edge bars
3473        self.fill_rect_with_mode(
3474            Rect {
3475                x: rect.x,
3476                y: rect.y,
3477                width: rect.width,
3478                height: hw,
3479            },
3480            c,
3481            1,
3482            None,
3483        );
3484        self.fill_rect_with_mode(
3485            Rect {
3486                x: rect.x,
3487                y: rect.y + rect.height - hw,
3488                width: rect.width,
3489                height: hw,
3490            },
3491            c,
3492            1,
3493            None,
3494        );
3495        self.fill_rect_with_mode(
3496            Rect {
3497                x: rect.x,
3498                y: rect.y,
3499                width: hw,
3500                height: rect.height,
3501            },
3502            c,
3503            1,
3504            None,
3505        );
3506        self.fill_rect_with_mode(
3507            Rect {
3508                x: rect.x + rect.width - hw,
3509                y: rect.y,
3510                width: hw,
3511                height: rect.height,
3512            },
3513            c,
3514            1,
3515            None,
3516        );
3517    }
3518
3519    fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4], stroke_width: f32) {
3520        self.fill_rect_with_full_params(
3521            rect,
3522            self.apply_opacity(color),
3523            17,
3524            None,
3525            radius,
3526            Rect {
3527                x: stroke_width,
3528                y: 0.0,
3529                width: 0.0,
3530                height: 0.0,
3531            },
3532        );
3533    }
3534
3535    fn stroke_ellipse(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
3536        // Tessellate an ellipse stroke using Lyon's StrokeTessellator.
3537        let cx = rect.x + rect.width / 2.0;
3538        let cy = rect.y + rect.height / 2.0;
3539        let rx = rect.width / 2.0;
3540        let ry = rect.height / 2.0;
3541
3542        // Build an ellipse path using Lyon
3543        let mut builder = lyon::path::Path::builder();
3544        if rx > 0.0 && ry > 0.0 {
3545            // Approximate ellipse with 64 segments
3546            let segments = 64;
3547            for i in 0..segments {
3548                let angle = 2.0 * std::f32::consts::PI * (i as f32) / (segments as f32);
3549                let x = cx + rx * angle.cos();
3550                let y = cy + ry * angle.sin();
3551                if i == 0 {
3552                    builder.begin(lyon::math::point(x, y));
3553                } else {
3554                    builder.line_to(lyon::math::point(x, y));
3555                }
3556            }
3557            builder.close();
3558        }
3559        let path = builder.build();
3560        self.stroke_path(&path, color, stroke_width);
3561    }
3562
3563    fn draw_linear_gradient(
3564        &mut self,
3565        rect: Rect,
3566        start_color: [f32; 4],
3567        end_color: [f32; 4],
3568        angle: f32,
3569    ) {
3570        self.fill_rect_with_full_params_and_slice(
3571            rect,
3572            self.apply_opacity(start_color),
3573            15,
3574            None,
3575            0.0,
3576            Rect {
3577                x: angle,
3578                y: 0.0,
3579                width: 1.0,
3580                height: 1.0,
3581            },
3582            end_color,
3583        );
3584    }
3585
3586    fn draw_radial_gradient(&mut self, rect: Rect, inner_color: [f32; 4], outer_color: [f32; 4]) {
3587        self.fill_rect_with_full_params_and_slice(
3588            rect,
3589            self.apply_opacity(inner_color),
3590            16,
3591            None,
3592            0.0,
3593            Rect {
3594                x: 0.0,
3595                y: 0.0,
3596                width: 1.0,
3597                height: 1.0,
3598            },
3599            outer_color,
3600        );
3601    }
3602
3603    fn draw_drop_shadow(
3604        &mut self,
3605        rect: Rect,
3606        radius: f32,
3607        color: [f32; 4],
3608        blur: f32,
3609        spread: f32,
3610    ) {
3611        let margin = blur + spread;
3612        let inflated = Rect {
3613            x: rect.x - margin,
3614            y: rect.y - margin,
3615            width: rect.width + margin * 2.0,
3616            height: rect.height + margin * 2.0,
3617        };
3618        // uv.x = total margin (for SDF offset), uv.y = blur width (for falloff)
3619        self.fill_rect_with_full_params(
3620            inflated,
3621            self.apply_opacity(color),
3622            18,
3623            None,
3624            radius,
3625            Rect {
3626                x: margin,
3627                y: blur,
3628                width: 0.0,
3629                height: 0.0,
3630            },
3631        );
3632    }
3633
3634    fn stroke_dashed_rounded_rect(
3635        &mut self,
3636        rect: Rect,
3637        radius: f32,
3638        color: [f32; 4],
3639        width: f32,
3640        dash: f32,
3641        gap: f32,
3642    ) {
3643        self.fill_rect_with_full_params(
3644            rect,
3645            self.apply_opacity(color),
3646            19,
3647            None,
3648            radius,
3649            Rect {
3650                x: width,
3651                y: dash,
3652                width: gap,
3653                height: 0.0,
3654            },
3655        );
3656    }
3657
3658    fn draw_9slice(
3659        &mut self,
3660        image_name: &str,
3661        rect: Rect,
3662        left: f32,
3663        top: f32,
3664        right: f32,
3665        bottom: f32,
3666    ) {
3667        let c = self.apply_opacity([1.0, 1.0, 1.0, 1.0]);
3668        let tid = self.get_texture_id(image_name);
3669        self.fill_rect_with_full_params(
3670            rect,
3671            c,
3672            20,
3673            tid,
3674            bottom,
3675            Rect {
3676                x: left,
3677                y: top,
3678                width: right,
3679                height: 0.0,
3680            },
3681        );
3682    }
3683
3684    fn draw_line(
3685        &mut self,
3686        x1: f32,
3687        y1: f32,
3688        x2: f32,
3689        y2: f32,
3690        color: [f32; 4],
3691        stroke_width: f32,
3692    ) {
3693        let dx = x2 - x1;
3694        let dy = y2 - y1;
3695        let len = (dx * dx + dy * dy).sqrt();
3696        if len < 0.001 {
3697            return;
3698        }
3699
3700        let c = self.apply_opacity(color);
3701        let tid = self.get_texture_id("__mega_atlas");
3702
3703        self.fill_rect_with_mode(
3704            Rect {
3705                x: (x1 + x2) / 2.0 - len / 2.0,
3706                y: (y1 + y2) / 2.0 - stroke_width / 2.0,
3707                width: len,
3708                height: stroke_width,
3709            },
3710            c,
3711            1, // Gungnir Mode for glowing lines
3712            tid,
3713        );
3714    }
3715
3716    fn draw_image(&mut self, image_name: &str, rect: Rect) {
3717        let tid = self
3718            .get_texture_id(image_name)
3719            .or_else(|| self.get_texture_id("__mega_atlas"));
3720        let uv_rect = self
3721            .image_uv_registry
3722            .get(image_name)
3723            .copied()
3724            .unwrap_or(Rect {
3725                x: 0.0,
3726                y: 0.0,
3727                width: 1.0,
3728                height: 1.0,
3729            });
3730        self.fill_rect_with_full_params(rect, [1.0, 1.0, 1.0, 1.0], 2, tid, 0.0, uv_rect);
3731    }
3732
3733
3734    fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
3735        // High-DPI: Shape and rasterize at the physical scale factor for maximum sharpness.
3736        let scaled_size = size * self.current_scale_factor();
3737        let shaped = self.shape_text_with_stack(text, scaled_size);
3738        let c = self.apply_opacity(color);
3739
3740        for glyph in shaped.glyphs {
3741            let cache_key = glyph.cache_key;
3742
3743            let (uv_rect, w, h) = if let Some(info) = self.text_cache.get(&cache_key) {
3744                *info
3745            } else {
3746                if let Some(image) = self.text_engine.rasterize(cache_key) {
3747                    let gw = image.width;
3748                    let gh = image.height;
3749
3750                    let pack_res = self.atlas_packer.pack(gw, gh);
3751                    let (nx, ny) = if let Some(pos) = pack_res {
3752                        pos
3753                    } else {
3754                        // RECLAIM & RETRY: Atlas is full, quench the forge and try again.
3755                        self.reclaim_vram();
3756                        self.atlas_packer.pack(gw, gh).unwrap_or((0, 0))
3757                    };
3758
3759                    let mut rgba_data = Vec::with_capacity((gw * gh * 4) as usize);
3760                    for alpha in &image.data {
3761                        rgba_data.push(255);
3762                        rgba_data.push(255);
3763                        rgba_data.push(255);
3764                        rgba_data.push(*alpha);
3765                    }
3766
3767                    self.queue.write_texture(
3768                        wgpu::TexelCopyTextureInfo {
3769                            texture: &self.mega_atlas_tex,
3770                            mip_level: 0,
3771                            origin: wgpu::Origin3d { x: nx, y: ny, z: 0 },
3772                            aspect: wgpu::TextureAspect::All,
3773                        },
3774                        &rgba_data,
3775                        wgpu::TexelCopyBufferLayout {
3776                            offset: 0,
3777                            bytes_per_row: Some(gw * 4),
3778                            rows_per_image: Some(gh),
3779                        },
3780                        wgpu::Extent3d {
3781                            width: gw,
3782                            height: gh,
3783                            depth_or_array_layers: 1,
3784                        },
3785                    );
3786
3787                    let info = (
3788                        Rect {
3789                            x: nx as f32 / 4096.0,
3790                            y: ny as f32 / 4096.0,
3791                            width: gw as f32 / 4096.0,
3792                            height: gh as f32 / 4096.0,
3793                        },
3794                        gw as f32,
3795                        gh as f32,
3796                    );
3797                    self.text_cache.put(cache_key, info);
3798                    info
3799                } else {
3800                    (Rect::zero(), 0.0, 0.0)
3801                }
3802            };
3803
3804            if w > 0.0 {
3805                // Map physical glyph dimensions and positions back to logical units
3806                // so the logical orthographic projection matrix places them correctly.
3807                let glyph_rect = Rect {
3808                    x: x + glyph.x / self.current_scale_factor(),
3809                    y: y + glyph.y / self.current_scale_factor(),
3810                    width: w / self.current_scale_factor(),
3811                    height: h / self.current_scale_factor(),
3812                };
3813                let tid = self.get_texture_id("__mega_atlas");
3814                self.fill_rect_with_full_params(glyph_rect, c, 6, tid, 0.0, uv_rect);
3815            }
3816        }
3817    }
3818
3819    /// measure_text — Calculates the dimensions of a text string without rendering.
3820    fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
3821        let shaped = self.shape_text_with_stack(text, size);
3822        (shaped.width, shaped.height)
3823    }
3824
3825    fn shape_rich_text(
3826        &mut self,
3827        spans: &[cvkg_runic_text::TextSpan],
3828        max_width: Option<f32>,
3829        align: cvkg_runic_text::TextAlign,
3830        overflow: cvkg_runic_text::TextOverflow,
3831    ) -> Option<cvkg_runic_text::ShapedText> {
3832        let sf = self.current_scale_factor();
3833        let mut scaled_spans = spans.to_vec();
3834        for span in &mut scaled_spans {
3835            span.style.font_size *= sf;
3836            if span.style.fallback_families.is_empty() {
3837                span.style.fallback_families = vec![
3838                    "SF Pro".to_string(),
3839                    "Inter".to_string(),
3840                    "Helvetica Neue".to_string(),
3841                    "Helvetica".to_string(),
3842                    "Arial".to_string(),
3843                    "sans-serif".to_string(),
3844                ];
3845            }
3846        }
3847        let scaled_max_width = max_width.map(|w| w * sf);
3848        self.text_engine.shape_layout(&scaled_spans, scaled_max_width, align, overflow).ok()
3849    }
3850
3851    fn draw_shaped_text(&mut self, shaped: &cvkg_runic_text::ShapedText, x: f32, y: f32) {
3852        for glyph in &shaped.glyphs {
3853            let byte_idx = shaped.grapheme_boundaries.get(glyph.cluster as usize).copied().unwrap_or(0);
3854            let mut span_color = [1.0, 1.0, 1.0, 1.0];
3855            for span in &shaped.spans {
3856                if byte_idx >= span.byte_offset && byte_idx < span.byte_offset + span.text.len() {
3857                    span_color = [
3858                        span.style.color[0] as f32 / 255.0,
3859                        span.style.color[1] as f32 / 255.0,
3860                        span.style.color[2] as f32 / 255.0,
3861                        span.style.color[3] as f32 / 255.0,
3862                    ];
3863                    break;
3864                }
3865            }
3866            let c = self.apply_opacity(span_color);
3867
3868            let cache_key = glyph.cache_key;
3869            let (uv_rect, w, h) = if let Some(info) = self.text_cache.get(&cache_key) {
3870                *info
3871            } else {
3872                if let Some(image) = self.text_engine.rasterize(cache_key) {
3873                    let gw = image.width;
3874                    let gh = image.height;
3875
3876                    let pack_res = self.atlas_packer.pack(gw, gh);
3877                    let (nx, ny) = if let Some(pos) = pack_res {
3878                        pos
3879                    } else {
3880                        self.reclaim_vram();
3881                        self.atlas_packer.pack(gw, gh).unwrap_or((0, 0))
3882                    };
3883
3884                    let mut rgba_data = Vec::with_capacity((gw * gh * 4) as usize);
3885                    for alpha in &image.data {
3886                        rgba_data.push(255);
3887                        rgba_data.push(255);
3888                        rgba_data.push(255);
3889                        rgba_data.push(*alpha);
3890                    }
3891
3892                    self.queue.write_texture(
3893                        wgpu::TexelCopyTextureInfo {
3894                            texture: &self.mega_atlas_tex,
3895                            mip_level: 0,
3896                            origin: wgpu::Origin3d { x: nx, y: ny, z: 0 },
3897                            aspect: wgpu::TextureAspect::All,
3898                        },
3899                        &rgba_data,
3900                        wgpu::TexelCopyBufferLayout {
3901                            offset: 0,
3902                            bytes_per_row: Some(gw * 4),
3903                            rows_per_image: Some(gh),
3904                        },
3905                        wgpu::Extent3d {
3906                            width: gw,
3907                            height: gh,
3908                            depth_or_array_layers: 1,
3909                        },
3910                    );
3911
3912                    let info = (
3913                        Rect {
3914                            x: nx as f32 / 4096.0,
3915                            y: ny as f32 / 4096.0,
3916                            width: gw as f32 / 4096.0,
3917                            height: gh as f32 / 4096.0,
3918                        },
3919                        gw as f32,
3920                        gh as f32,
3921                    );
3922                    self.text_cache.put(cache_key, info);
3923                    info
3924                } else {
3925                    (Rect::zero(), 0.0, 0.0)
3926                }
3927            };
3928
3929            if w > 0.0 {
3930                let sf = self.current_scale_factor();
3931                let glyph_rect = Rect {
3932                    x: x + glyph.x / sf,
3933                    y: y + glyph.y / sf,
3934                    width: w / sf,
3935                    height: h / sf,
3936                };
3937                let tid = self.get_texture_id("__mega_atlas");
3938                self.fill_rect_with_full_params(glyph_rect, c, 6, tid, 0.0, uv_rect);
3939            }
3940        }
3941    }
3942
3943    fn draw_texture(&mut self, texture_id: u32, rect: Rect) {
3944        self.fill_rect_with_full_params(
3945            rect,
3946            [1.0, 1.0, 1.0, 1.0],
3947            2,
3948            Some(texture_id),
3949            0.0,
3950            Rect {
3951                x: 0.0,
3952                y: 0.0,
3953                width: 1.0,
3954                height: 1.0,
3955            },
3956        );
3957    }
3958
3959    /// load_image — Proactively pushes a raw asset into the Mega-Atlas.
3960    /// load_image — Proactively pushes a raw asset into the Texture Array.
3961    fn load_image(&mut self, name: &str, data: &[u8]) {
3962        if self.image_uv_registry.contains(name) {
3963            return;
3964        }
3965        let img_result = image::load_from_memory(data);
3966        let img = match img_result {
3967            Ok(img) => img.to_rgba8(),
3968            Err(e) => {
3969                log::error!("Failed to load image {}: {}", name, e);
3970                image::RgbaImage::from_pixel(1, 1, image::Rgba([0, 0, 0, 255]))
3971            }
3972        };
3973        let (width, height) = img.dimensions();
3974
3975        let size = wgpu::Extent3d {
3976            width,
3977            height,
3978            depth_or_array_layers: 1,
3979        };
3980        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
3981            label: Some(&format!("Texture Array Layer: {}", name)),
3982            size,
3983            mip_level_count: 1,
3984            sample_count: 1,
3985            dimension: wgpu::TextureDimension::D2,
3986            format: wgpu::TextureFormat::Rgba8UnormSrgb,
3987            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3988            view_formats: &[],
3989        });
3990
3991        self.queue.write_texture(
3992            wgpu::TexelCopyTextureInfo {
3993                texture: &texture,
3994                mip_level: 0,
3995                origin: wgpu::Origin3d::ZERO,
3996                aspect: wgpu::TextureAspect::All,
3997            },
3998            &img,
3999            wgpu::TexelCopyBufferLayout {
4000                offset: 0,
4001                bytes_per_row: Some(4 * width),
4002                rows_per_image: Some(height),
4003            },
4004            size,
4005        );
4006
4007        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
4008
4009        // Slot allocation (Skip index 0 which is the dummy/atlas)
4010        let index = if self.texture_registry.len() < 255 {
4011            (self.texture_registry.len() + 1) as u32
4012        } else {
4013            // Evict the least recently used texture
4014            if let Some((old_name, old_index)) = self.texture_registry.pop_lru() {
4015                self.image_uv_registry.pop(&old_name);
4016                old_index
4017            } else {
4018                1 // Fallback
4019            }
4020        };
4021
4022        self.texture_views[index as usize] = view;
4023        self.image_uv_registry.put(
4024            name.to_string(),
4025            Rect {
4026                x: 0.0,
4027                y: 0.0,
4028                width: 1.0,
4029                height: 1.0,
4030            },
4031        );
4032        self.texture_registry.put(name.to_string(), index);
4033        self.rebuild_texture_array_bind_group();
4034    }
4035
4036    fn push_clip_rect(&mut self, rect: Rect) {
4037        self.clip_stack.push(rect);
4038    }
4039
4040    fn pop_clip_rect(&mut self) {
4041        self.clip_stack.pop();
4042    }
4043
4044    fn current_clip_rect(&self) -> Rect {
4045        self.clip_stack.last().copied().unwrap_or(Rect::new(
4046            0.0,
4047            0.0,
4048            self.current_width() as f32,
4049            self.current_height() as f32,
4050        ))
4051    }
4052
4053    fn memoize(&mut self, _id: u64, _data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer)) {
4054        render_fn(self);
4055    }
4056
4057    fn push_opacity(&mut self, opacity: f32) {
4058        let current = self.opacity_stack.last().copied().unwrap_or(1.0);
4059        self.opacity_stack.push(current * opacity);
4060    }
4061
4062    fn pop_opacity(&mut self) {
4063        self.opacity_stack.pop();
4064    }
4065
4066    fn push_shadow(&mut self, radius: f32, color: [f32; 4], offset: [f32; 2]) {
4067        self.shadow_stack.push(ShadowState {
4068            radius,
4069            color,
4070            _offset: offset,
4071        });
4072    }
4073
4074    fn pop_shadow(&mut self) {
4075        self.shadow_stack.pop();
4076    }
4077
4078    fn push_transform(&mut self, translation: [f32; 2], scale: [f32; 2], rotation: f32) {
4079        let c = rotation.cos();
4080        let sn = rotation.sin();
4081        let affine = glam::Mat3::from_cols(
4082            glam::Vec3::new(c * scale[0], sn * scale[0], 0.0),
4083            glam::Vec3::new(-sn * scale[1], c * scale[1], 0.0),
4084            glam::Vec3::new(translation[0], translation[1], 1.0),
4085        );
4086
4087        let parent = self.transform_stack.last().copied().unwrap_or(glam::Mat3::IDENTITY);
4088        self.transform_stack.push(parent * affine);
4089    }
4090
4091    fn push_affine(&mut self, transform: [f32; 6]) {
4092        let affine = glam::Mat3::from_cols(
4093            glam::Vec3::new(transform[0], transform[1], 0.0),
4094            glam::Vec3::new(transform[2], transform[3], 0.0),
4095            glam::Vec3::new(transform[4], transform[5], 1.0),
4096        );
4097        let parent = self.transform_stack.last().copied().unwrap_or(glam::Mat3::IDENTITY);
4098        self.transform_stack.push(parent * affine);
4099    }
4100
4101    fn pop_transform(&mut self) {
4102        self.transform_stack.pop();
4103    }
4104
4105
4106
4107    fn set_theme(&mut self, theme: ColorTheme) {
4108        self.current_theme = theme;
4109        self.queue
4110            .write_buffer(&self.theme_buffer, 0, bytemuck::bytes_of(&theme));
4111    }
4112
4113    fn set_rage(&mut self, rage: f32) {
4114        self.current_scene.berzerker_rage = rage;
4115        // scene_buffer is updated every frame in begin_frame, so no need to write here
4116    }
4117
4118    fn trigger_shatter_event(&mut self, origin: [f32; 2], force: f32) {
4119        self.current_scene.shatter_origin = origin;
4120        self.current_scene.shatter_time = self.current_scene.time;
4121        self.current_scene.shatter_force = force;
4122    }
4123
4124    fn set_scene_preset(&mut self, preset: u32) {
4125        self.current_scene.scene_type = preset;
4126    }
4127
4128    /// push_mjolnir_slice — Pushes a geometric clipping plane onto the stack.
4129    /// All subsequent draw calls will be sliced by this plane until it is popped.
4130    fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
4131        self.slice_stack.push((angle, offset));
4132    }
4133
4134    /// pop_mjolnir_slice — Removes the top-most geometric clipping plane from the stack.
4135    fn pop_mjolnir_slice(&mut self) {
4136        self.slice_stack.pop();
4137    }
4138
4139    fn mjolnir_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
4140        self.shatter_internal(rect, pieces, force, color, 8);
4141    }
4142
4143    fn mjolnir_fluid_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
4144        self.shatter_internal(rect, pieces, force, color, 11);
4145    }
4146
4147    fn draw_mjolnir_bolt(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
4148        self.recursive_bolt(from, to, 4, color);
4149    }
4150
4151    fn upload_data_texture(&mut self, id: &str, data: &[f32], width: u32, height: u32) {
4152        let size = wgpu::Extent3d {
4153            width,
4154            height,
4155            depth_or_array_layers: 1,
4156        };
4157        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
4158            label: Some(id),
4159            size,
4160            mip_level_count: 1,
4161            sample_count: 1,
4162            dimension: wgpu::TextureDimension::D2,
4163            format: wgpu::TextureFormat::R32Float,
4164            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4165            view_formats: &[],
4166        });
4167        self.queue.write_texture(
4168            wgpu::TexelCopyTextureInfo {
4169                texture: &texture,
4170                mip_level: 0,
4171                origin: wgpu::Origin3d::ZERO,
4172                aspect: wgpu::TextureAspect::All,
4173            },
4174            bytemuck::cast_slice(data),
4175            wgpu::TexelCopyBufferLayout {
4176                offset: 0,
4177                bytes_per_row: Some(4 * width),
4178                rows_per_image: Some(height),
4179            },
4180            size,
4181        );
4182        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
4183        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
4184            address_mode_u: wgpu::AddressMode::ClampToEdge,
4185            address_mode_v: wgpu::AddressMode::ClampToEdge,
4186            mag_filter: wgpu::FilterMode::Linear,
4187            ..Default::default()
4188        });
4189        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
4190            layout: &self.texture_bind_group_layout,
4191            entries: &[
4192                wgpu::BindGroupEntry {
4193                    binding: 0,
4194                    resource: wgpu::BindingResource::TextureViewArray(&vec![&view; 256]),
4195                },
4196                wgpu::BindGroupEntry {
4197                    binding: 1,
4198                    resource: wgpu::BindingResource::Sampler(&sampler),
4199                },
4200            ],
4201            label: Some(id),
4202        });
4203        self.texture_bind_groups.push(bind_group);
4204        let tid = (self.texture_bind_groups.len() - 1) as u32;
4205        self.texture_registry.put(id.to_string(), tid);
4206    }
4207
4208    fn draw_heatmap(&mut self, texture_id: &str, rect: Rect, _palette: &str) {
4209        let tid = self.get_texture_id(texture_id);
4210        self.fill_rect_with_mode(rect, [1.0, 1.0, 1.0, 1.0], 12, tid);
4211    }
4212
4213    fn draw_mesh(&mut self, mesh: &Mesh, color: [f32; 4], transform: glam::Mat4) {
4214        let base_idx = self.vertices.len() as u32;
4215        let screen = [self.current_width() as f32, self.current_height() as f32];
4216
4217        for i in 0..mesh.vertices.len() {
4218            let pos = transform.transform_point3(glam::Vec3::from(mesh.vertices[i]));
4219            let norm = transform.transform_vector3(glam::Vec3::from(mesh.normals[i]));
4220
4221            let (translation, scale_transform, rotation, _, _) = self.current_transform();
4222            self.vertices.push(Vertex {
4223                position: pos.to_array(),
4224                normal: norm.to_array(),
4225                uv: [0.0, 0.0],
4226                color,
4227                mode: 13, // Mode 13: 3D Surface
4228                radius: 0.0,
4229                slice: [0.0, 0.0, 0.0, 1.0],
4230                logical: [0.0, 0.0],
4231                size: [0.0, 0.0],
4232                screen,
4233                clip: [-10000.0, -10000.0, 20000.0, 20000.0],
4234                translation,
4235                scale: scale_transform,
4236                rotation,
4237                tex_index: 0,
4238            });
4239        }
4240
4241        for idx in &mesh.indices {
4242            self.indices.push(base_idx + idx);
4243        }
4244
4245        if self.draw_calls.is_empty() || self.current_texture_id.is_some() {
4246            self.current_texture_id = None;
4247            self.draw_calls.push(DrawCall {
4248                texture_id: None,
4249                scissor_rect: self.clip_stack.last().copied(),
4250                index_start: (self.indices.len() as u32) - (mesh.indices.len() as u32),
4251                index_count: mesh.indices.len() as u32,
4252                material: cvkg_core::DrawMaterial::Opaque,
4253            });
4254        } else {
4255            self.draw_calls.last_mut().unwrap().index_count += mesh.indices.len() as u32;
4256        }
4257    }
4258
4259    fn register_shared_element(&mut self, id: &str, rect: Rect) {
4260        self.shared_elements.put(id.to_string(), rect);
4261    }
4262
4263    fn set_z_index(&mut self, z: f32) {
4264        self.current_z = z;
4265    }
4266
4267    fn set_material(&mut self, material: cvkg_core::DrawMaterial) {
4268        self.current_draw_material = material;
4269    }
4270
4271    fn current_material(&self) -> cvkg_core::DrawMaterial {
4272        self.current_draw_material
4273    }
4274
4275    fn get_z_index(&self) -> f32 {
4276        self.current_z
4277    }
4278
4279    fn request_redraw(&mut self) {
4280        self.redraw_requested = true;
4281    }
4282
4283    fn push_vnode(&mut self, rect: Rect, name: &'static str) {
4284        self.vnode_stack.push((rect, name));
4285    }
4286
4287    fn pop_vnode(&mut self) {
4288        self.vnode_stack.pop();
4289    }
4290
4291    fn register_handler(
4292        &mut self,
4293        event_type: &str,
4294        handler: std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>,
4295    ) {
4296        self.event_handlers
4297            .entry(event_type.to_string())
4298            .or_insert_with(Vec::new)
4299            .push(handler);
4300    }
4301
4302    fn serialize_svg(&mut self, name: &str) -> Result<String, String> {
4303        let tree = self.svg_trees.get(name)
4304            .ok_or_else(|| format!("SVG '{}' not found", name))?;
4305        let config = cvkg_svg_serialize::SerializerConfig::default();
4306        let mut serializer = cvkg_svg_serialize::SvgSerializer::with_config(config);
4307        serializer.serialize(tree)
4308            .map_err(|e| format!("SVG serialization failed: {}", e))
4309    }
4310
4311    fn apply_svg_filter(
4312        &mut self,
4313        name: &str,
4314        filter_id: &str,
4315        region: Rect,
4316    ) -> Result<String, String> {
4317        let tree = self.svg_trees.get(name)
4318            .ok_or_else(|| format!("SVG '{}' not found", name))?;
4319        let filter = Self::find_filter(tree, filter_id)
4320            .ok_or_else(|| format!("Filter '{}' not found in SVG '{}'", filter_id, name))?;
4321        let config = cvkg_svg_serialize::SerializerConfig::default();
4322        let mut serializer = cvkg_svg_serialize::SvgSerializer::with_config(config);
4323        serializer.serialize(tree)
4324            .map_err(|e| format!("SVG filter serialization failed: {}", e))
4325    }
4326}
4327
4328// ── Inherent methods on SurtrRenderer (not part of the Renderer trait) ──
4329
4330impl SurtrRenderer {
4331    /// Clear all registered event handlers. Call at the start of each frame
4332    /// before re-rendering the component tree.
4333    pub fn clear_event_handlers(&mut self) {
4334        self.event_handlers.clear();
4335    }
4336
4337    /// Get all registered event handlers for a specific event type.
4338    pub fn get_handlers(
4339        &self,
4340        event_type: &str,
4341    ) -> Option<&Vec<std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>>> {
4342        self.event_handlers.get(event_type)
4343    }
4344
4345    /// Compute per-vertex transform values from the current matrix.
4346    /// Extracts translation, scale, rotation, and skew from the affine matrix
4347    /// so the existing vertex shader fields still work correctly.
4348    pub(crate) fn current_transform(&self) -> ([f32; 2], [f32; 2], f32, f32, f32) {
4349        // Returns (translation, scale, rotation, skew_x, skew_y)
4350        let m = self.transform_stack.last().copied().unwrap_or(glam::Mat3::IDENTITY);
4351        let t = [m.z_axis.x, m.z_axis.y];
4352        // Extract scale and rotation from the 2x2 submatrix
4353        let a = m.x_axis.x;
4354        let b = m.x_axis.y;
4355        let c = m.y_axis.x;
4356        let d = m.y_axis.y;
4357        let sx = (a * a + b * b).sqrt();
4358        let sy = (c * c + d * d).sqrt();
4359        let rotation = b.atan2(a);
4360        // Skew: the angle between the basis vectors minus 90 degrees
4361        let skew_x = (a * c + b * d) / (sx * sy); // sin(skew)
4362        (t, [sx, sy], rotation, skew_x, 0.0)
4363    }
4364
4365    pub fn stroke_path(&mut self, path: &lyon::path::Path, color: [f32; 4], stroke_width: f32) {
4366        let c = self.apply_opacity(color);
4367        let mut tessellator = StrokeTessellator::new();
4368        let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
4369        let base_vertex_idx = self.vertices.len() as u32;
4370
4371        let (translation, scale, rotation, _, _) = self.current_transform();
4372        let screen = [self.current_width() as f32, self.current_height() as f32];
4373        let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
4374            x: -10000.0,
4375            y: -10000.0,
4376            width: 20000.0,
4377            height: 20000.0,
4378        });
4379        let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
4380
4381        tessellator
4382            .tessellate_path(
4383                path,
4384                &StrokeOptions::default().with_line_width(stroke_width),
4385                &mut BuffersBuilder::new(
4386                    &mut buffers,
4387                    CustomStrokeVertexConstructor {
4388                        color: c,
4389                        translation,
4390                        scale,
4391                        rotation,
4392                        screen,
4393                        clip,
4394                    },
4395                ),
4396            )
4397            .unwrap();
4398
4399        self.vertices.extend(buffers.vertices);
4400        for idx in &buffers.indices {
4401            self.indices.push(base_vertex_idx + *idx);
4402        }
4403
4404        let material = self.current_material();
4405        let tid = self.get_texture_id("__mega_atlas");
4406
4407        let last_call = self.draw_calls.last();
4408        let needs_new_call = self.draw_calls.is_empty()
4409            || self.current_texture_id != tid
4410            || last_call.unwrap().scissor_rect != self.clip_stack.last().copied()
4411            || last_call.unwrap().material != material;
4412
4413        if needs_new_call {
4414            self.current_texture_id = tid;
4415            self.draw_calls.push(DrawCall {
4416                texture_id: tid,
4417                scissor_rect: self.clip_stack.last().copied(),
4418                index_start: base_vertex_idx,
4419                index_count: buffers.indices.len() as u32,
4420                material,
4421            });
4422        } else if let Some(call) = self.draw_calls.last_mut() {
4423            call.index_count += buffers.indices.len() as u32;
4424        }
4425    }
4426}
4427
4428pub fn parse_svg_animations(data: &[u8]) -> Vec<SvgAnimation> {
4429    let mut parsed_animations = Vec::new();
4430    if let Ok(xml_doc) = roxmltree::Document::parse(std::str::from_utf8(data).unwrap_or("")) {
4431        for node in xml_doc.descendants() {
4432            if node.tag_name().name() == "animateTransform" || node.tag_name().name() == "animate" {
4433                let target_id = node
4434                    .attribute("href")
4435                    .or_else(|| node.attribute(("http://www.w3.org/1999/xlink", "href")))
4436                    .or_else(|| node.attribute("xlink:href"))
4437                    .or_else(|| node.parent_element().and_then(|p| p.attribute("id")))
4438                    .unwrap_or("")
4439                    .trim_start_matches('#')
4440                    .to_string();
4441
4442                if !target_id.is_empty() {
4443                    let dur_str = node.attribute("dur").unwrap_or("1s");
4444                    let duration = if dur_str.ends_with("ms") {
4445                        dur_str.trim_end_matches("ms").parse::<f32>().unwrap_or(1000.0) / 1000.0
4446                    } else {
4447                        dur_str.trim_end_matches('s').parse::<f32>().unwrap_or(1.0)
4448                    };
4449
4450                    let (from_val, to_val) = if let Some(values) = node.attribute("values") {
4451                        let parts: Vec<&str> = values.split(';').collect();
4452                        if parts.len() >= 2 {
4453                            let f = parts[0].trim().parse::<f32>().unwrap_or(0.0);
4454                            let t = parts[parts.len() - 1].trim().parse::<f32>().unwrap_or(0.0);
4455                            (f, t)
4456                        } else {
4457                            (0.0, 360.0) // Fallback defaults
4458                        }
4459                    } else {
4460                        let f = node.attribute("from").unwrap_or("0").parse::<f32>().unwrap_or(0.0);
4461                        let t = node.attribute("to").unwrap_or("360").parse::<f32>().unwrap_or(360.0);
4462                        (f, t)
4463                    };
4464
4465                    let attr = node.attribute("attributeName").unwrap_or("transform").to_string();
4466
4467                    parsed_animations.push(SvgAnimation {
4468                        target_id,
4469                        attribute_name: attr,
4470                        from_val,
4471                        to_val,
4472                        duration,
4473                        vertex_range: 0..0, // Will be filled during tessellation
4474                    });
4475                }
4476            }
4477        }
4478    }
4479    parsed_animations
4480}
4481
4482// --- SVG Helpers ---
4483
4484fn usvg_to_lyon(path: &usvg::Path) -> lyon::path::Path {
4485    let mut builder = lyon::path::Path::builder();
4486    for segment in path.data().segments() {
4487        match segment {
4488            usvg::tiny_skia_path::PathSegment::MoveTo(p) => {
4489                builder.begin(lyon::math::point(p.x, p.y));
4490            }
4491            usvg::tiny_skia_path::PathSegment::LineTo(p) => {
4492                builder.line_to(lyon::math::point(p.x, p.y));
4493            }
4494            usvg::tiny_skia_path::PathSegment::QuadTo(p1, p) => {
4495                builder.quadratic_bezier_to(
4496                    lyon::math::point(p1.x, p1.y),
4497                    lyon::math::point(p.x, p.y),
4498                );
4499            }
4500            usvg::tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => {
4501                builder.cubic_bezier_to(
4502                    lyon::math::point(p1.x, p1.y),
4503                    lyon::math::point(p2.x, p2.y),
4504                    lyon::math::point(p.x, p.y),
4505                );
4506            }
4507            usvg::tiny_skia_path::PathSegment::Close => {
4508                builder.end(true);
4509            }
4510        }
4511    }
4512    builder.build()
4513}
4514
4515struct SceneVertexConstructor {
4516    color: [f32; 4],
4517    translation: [f32; 2],
4518    scale: [f32; 2],
4519    rotation: f32,
4520}
4521
4522/// Vertex constructor for stroke tessellation -- includes screen and clip for transform.
4523struct CustomStrokeVertexConstructor {
4524    color: [f32; 4],
4525    translation: [f32; 2],
4526    scale: [f32; 2],
4527    rotation: f32,
4528    screen: [f32; 2],
4529    clip: [f32; 4],
4530}
4531
4532impl StrokeVertexConstructor<Vertex> for CustomStrokeVertexConstructor {
4533    fn new_vertex(&mut self, vertex: StrokeVertex) -> Vertex {
4534        let pos = vertex.position();
4535        Vertex {
4536            position: [pos.x, pos.y, 0.0],
4537            normal: [0.0, 0.0, 1.0],
4538            uv: [0.0, 0.0],
4539            color: self.color,
4540            mode: 0,
4541            radius: 0.0,
4542            slice: [0.0, 0.0, 0.0, 1.0],
4543            logical: [pos.x, pos.y],
4544            size: [1.0, 1.0],
4545            screen: self.screen,
4546            clip: self.clip,
4547            translation: self.translation,
4548            scale: self.scale,
4549            rotation: self.rotation,
4550            tex_index: 0,
4551        }
4552    }
4553}
4554
4555impl FillVertexConstructor<Vertex> for SceneVertexConstructor {
4556    fn new_vertex(&mut self, vertex: FillVertex) -> Vertex {
4557        Vertex {
4558            position: [vertex.position().x, vertex.position().y, 0.0],
4559            normal: [0.0, 0.0, 1.0],
4560            uv: [0.0, 0.0],
4561            color: self.color,
4562            mode: 0,
4563            radius: 0.0,
4564            slice: [0.0, 0.0, 0.0, 1.0],
4565            logical: [vertex.position().x, vertex.position().y],
4566            size: [1.0, 1.0],
4567            screen: [0.0, 0.0],
4568            clip: [-10000.0, -10000.0, 20000.0, 20000.0],
4569            translation: self.translation,
4570            scale: self.scale,
4571            rotation: self.rotation,
4572            tex_index: 0,
4573        }
4574    }
4575}
4576
4577impl Drop for SurtrRenderer {
4578    fn drop(&mut self) {
4579        // Ensure GPU is idle before dropping to avoid Swapchain semaphore panics
4580        let _ = self.device.poll(wgpu::PollType::Wait {
4581            submission_index: None,
4582            timeout: None,
4583        });
4584    }
4585}
4586
4587impl SurtrRenderer {
4588    /// Submit pre-routed draw command buckets from the cvkg-compositor.
4589    ///
4590    /// Accepts `CommandBuckets` produced by `CompositorEngine::flatten_and_route()`
4591    /// and submits draw calls in the correct pass order for the Backdrop Capture
4592    /// Architecture:
4593    /// 1. Scene commands (opaque) → Scene Capture pass
4594    /// 2. Glass commands → Material Composite pass (samples blur pyramid)
4595    /// 3. Overlay commands → Top-Level Foreground pass
4596    pub fn submit_buckets(&mut self, buckets: &cvkg_compositor::CommandBuckets) {
4597        // Scene pass — opaque draw calls
4598        for routed in &buckets.scene_commands {
4599            self.set_material(cvkg_core::DrawMaterial::Opaque);
4600            self.submit_routed(routed);
4601        }
4602
4603        // Glass pass — glassmorphism draw calls sampling blur pyramid
4604        for routed in &buckets.glass_commands {
4605            let core_material = match routed.material {
4606                cvkg_compositor::Material::Opaque => cvkg_core::DrawMaterial::Opaque,
4607                cvkg_compositor::Material::Glass { blur_radius } => {
4608                    cvkg_core::DrawMaterial::Glass { blur_radius }
4609                }
4610                cvkg_compositor::Material::Overlay => cvkg_core::DrawMaterial::TopUI,
4611                _ => cvkg_core::DrawMaterial::Opaque,
4612            };
4613            self.set_material(core_material);
4614            self.submit_routed(routed);
4615        }
4616
4617        // Overlay pass — foreground UI (crisp text, icons, edge lighting)
4618        for routed in &buckets.overlay_commands {
4619            self.set_material(cvkg_core::DrawMaterial::TopUI);
4620            self.submit_routed(routed);
4621        }
4622    }
4623
4624    /// Submit a single routed draw command through the internal pipeline.
4625    fn submit_routed(&mut self, routed: &cvkg_compositor::RoutedDrawCommand) {
4626        let cmd = &routed.command;
4627        self.fill_rect_with_full_params(
4628            cvkg_core::Rect::new(0.0, 0.0, 1.0, 1.0),
4629            [1.0, 1.0, 1.0, 1.0],
4630            0,
4631            cmd.texture_id,
4632            0.0,
4633            cvkg_core::Rect::new(0.0, 0.0, 1.0, 1.0),
4634        );
4635    }
4636}
4637
4638impl cvkg_core::FrameRenderer<wgpu::CommandEncoder> for SurtrRenderer {
4639    fn begin_frame(&mut self) -> wgpu::CommandEncoder {
4640        cvkg_core::begin_render_phase();
4641        let id = self
4642            .current_window
4643            .expect("No target window set for frame. Call set_target_window first.");
4644        self.begin_frame(id)
4645    }
4646
4647    fn render_frame(&mut self) {
4648        // Visual Lint: If layout was dirtied during the render phase (layout thrashing),
4649        // draw a 10px red border as a warning flash.
4650        if LAYOUT_DIRTY.swap(false, Ordering::AcqRel)
4651            && let Some(window_id) = self.current_window
4652            && let Some(surface_ctx) = self.surfaces.get(&window_id)
4653        {
4654            let w = surface_ctx.config.width as f32;
4655            let h = surface_ctx.config.height as f32;
4656            let border_rect = cvkg_core::Rect {
4657                x: 0.0,
4658                y: 0.0,
4659                width: w,
4660                height: h,
4661            };
4662            // Draw a thick red border to signal layout-thrashing
4663            self.stroke_rect(border_rect, [1.0, 0.0, 0.0, 1.0], 10.0);
4664        }
4665
4666        // Dynamic Buffer Growth (Up to 4x capacity)
4667        let req_v_size = (self.vertices.len() * std::mem::size_of::<Vertex>()) as u64;
4668        let mut cur_v_size = self.vertex_buffer.size();
4669        let max_v_size = (MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64 * 4;
4670        
4671        if req_v_size > cur_v_size {
4672            while cur_v_size < req_v_size && cur_v_size < max_v_size {
4673                cur_v_size *= 2;
4674            }
4675            if req_v_size > max_v_size {
4676                log::error!("Exceeded dynamic vertex buffer max capacity! Capping geometry.");
4677                self.vertices.truncate((max_v_size / std::mem::size_of::<Vertex>() as u64) as usize);
4678                cur_v_size = max_v_size;
4679            }
4680            log::info!("Growing vertex buffer to {} bytes", cur_v_size);
4681            self.vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
4682                label: Some("Vertex Buffer (Grown)"),
4683                size: cur_v_size,
4684                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
4685                mapped_at_creation: false,
4686            });
4687        }
4688
4689        let req_i_size = (self.indices.len() * std::mem::size_of::<u32>()) as u64;
4690        let mut cur_i_size = self.index_buffer.size();
4691        let max_i_size = (MAX_INDICES * std::mem::size_of::<u32>()) as u64 * 4;
4692        
4693        if req_i_size > cur_i_size {
4694            while cur_i_size < req_i_size && cur_i_size < max_i_size {
4695                cur_i_size *= 2;
4696            }
4697            if req_i_size > max_i_size {
4698                log::error!("Exceeded dynamic index buffer max capacity! Capping geometry.");
4699                self.indices.truncate((max_i_size / std::mem::size_of::<u32>() as u64) as usize);
4700                cur_i_size = max_i_size;
4701            }
4702            log::info!("Growing index buffer to {} bytes", cur_i_size);
4703            self.index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
4704                label: Some("Index Buffer (Grown)"),
4705                size: cur_i_size,
4706                usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
4707                mapped_at_creation: false,
4708            });
4709        }
4710
4711        // Forge Submission: Sync all geometry to GPU using StagingBelt with a dedicated encoder
4712        let mut staging_encoder =
4713            self.device
4714                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
4715                    label: Some("Surtr Staging Encoder"),
4716                });
4717
4718        let mut has_writes = false;
4719
4720        if !self.vertices.is_empty() {
4721            let v_bytes = bytemuck::cast_slice(&self.vertices);
4722            self.staging_belt
4723                .write_buffer(
4724                    &mut staging_encoder,
4725                    &self.vertex_buffer,
4726                    0,
4727                    wgpu::BufferSize::new(v_bytes.len() as u64).unwrap(),
4728                )
4729                .copy_from_slice(v_bytes);
4730            has_writes = true;
4731        }
4732
4733        if !self.indices.is_empty() {
4734            let i_bytes = bytemuck::cast_slice(&self.indices);
4735            self.staging_belt
4736                .write_buffer(
4737                    &mut staging_encoder,
4738                    &self.index_buffer,
4739                    0,
4740                    wgpu::BufferSize::new(i_bytes.len() as u64).unwrap(),
4741                )
4742                .copy_from_slice(i_bytes);
4743            has_writes = true;
4744        }
4745
4746        if has_writes {
4747            self.staging_belt.finish();
4748            self.staging_command_buffers.push(staging_encoder.finish());
4749        }
4750
4751        // Update Time & Uniforms (Direct write is fine for small uniforms)
4752        self.current_scene.time = self.start_time.elapsed().as_secs_f32();
4753        self.queue.write_buffer(
4754            &self.scene_buffer,
4755            0,
4756            bytemuck::bytes_of(&self.current_scene),
4757        );
4758        self.queue.write_buffer(
4759            &self.theme_buffer,
4760            0,
4761            bytemuck::bytes_of(&self.current_theme),
4762        );
4763    }
4764
4765    fn end_frame(&mut self, encoder: wgpu::CommandEncoder) {
4766        Self::end_frame(self, encoder);
4767        cvkg_core::end_render_phase();
4768    }
4769}
4770
4771impl SurtrRenderer {
4772    /// Returns the current effective opacity (product of all stacked values).
4773    fn apply_opacity(&self, mut color: [f32; 4]) -> [f32; 4] {
4774        if let Some(&alpha) = self.opacity_stack.last() {
4775            color[3] *= alpha;
4776        }
4777        color
4778    }
4779
4780    /// load_svg — Parses an SVG file and tessellates its paths into GPU triangles.
4781    pub fn load_svg(&mut self, name: &str, data: &[u8]) {
4782        let opt = usvg::Options::default();
4783        let tree = usvg::Tree::from_data(data, &opt).expect("Failed to parse SVG");
4784
4785        let view_box = Rect {
4786            x: 0.0,
4787            y: 0.0,
4788            width: tree.size().width(),
4789            height: tree.size().height(),
4790        };
4791
4792        let parsed_animations = parse_svg_animations(data);
4793
4794        let mut vertices = Vec::new();
4795        let mut indices = Vec::new();
4796        let mut tessellator = FillTessellator::new();
4797        let mut finalized_animations = Vec::new();
4798
4799        for child in tree.root().children() {
4800            self.tessellate_node(child, &mut tessellator, &mut vertices, &mut indices, &parsed_animations, &mut finalized_animations);
4801        }
4802
4803        self.svg_cache.put(
4804            name.to_string(),
4805            SvgModel {
4806                vertices,
4807                indices,
4808                view_box,
4809                animations: finalized_animations,
4810            },
4811        );
4812        self.svg_trees.put(name.to_string(), tree);
4813    }
4814
4815    fn tessellate_node(
4816        &self,
4817        node: &usvg::Node,
4818        tessellator: &mut FillTessellator,
4819        vertices: &mut Vec<Vertex>,
4820        indices: &mut Vec<u32>,
4821        parsed_animations: &[SvgAnimation],
4822        finalized_animations: &mut Vec<SvgAnimation>,
4823    ) {
4824        let start_idx = vertices.len();
4825        let node_id = match node {
4826            usvg::Node::Group(g) => g.id().to_string(),
4827            usvg::Node::Path(p) => p.id().to_string(),
4828            _ => String::new(),
4829        };
4830
4831        if let usvg::Node::Group(ref group) = *node {
4832            for child in group.children() {
4833                self.tessellate_node(child, tessellator, vertices, indices, parsed_animations, finalized_animations);
4834            }
4835        } else if let usvg::Node::Path(ref path) = *node
4836            && let Some(fill) = path.fill()
4837        {
4838            let color = match fill.paint() {
4839                usvg::Paint::Color(c) => [
4840                    c.red as f32 / 255.0,
4841                    c.green as f32 / 255.0,
4842                    c.blue as f32 / 255.0,
4843                    fill.opacity().get(),
4844                ],
4845                _ => [1.0, 1.0, 1.0, 1.0],
4846            };
4847
4848            let lyon_path = usvg_to_lyon(path);
4849            let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
4850            let base_vertex_idx = vertices.len() as u32;
4851
4852            tessellator
4853                .tessellate_path(
4854                    &lyon_path,
4855                    &FillOptions::default(),
4856                    &mut BuffersBuilder::new(
4857                        &mut buffers,
4858                        SceneVertexConstructor {
4859                            color,
4860                            translation: [0.0, 0.0],
4861                            scale: [1.0, 1.0],
4862                            rotation: 0.0,
4863                        },
4864                    ),
4865                )
4866                .unwrap();
4867
4868            vertices.extend(buffers.vertices);
4869            for idx in buffers.indices {
4870                indices.push(base_vertex_idx + idx);
4871            }
4872        }
4873
4874        let end_idx = vertices.len();
4875        if !node_id.is_empty() && start_idx < end_idx {
4876            for anim in parsed_animations {
4877                if anim.target_id == node_id {
4878                    let mut final_anim = anim.clone();
4879                    final_anim.vertex_range = start_idx..end_idx;
4880                    finalized_animations.push(final_anim);
4881                }
4882            }
4883        }
4884    }
4885
4886    /// draw_svg — Renders a pre-loaded SVG icon at the specified logical rect.
4887    pub fn draw_svg(&mut self, name: &str, rect: Rect, color: Option<[f32; 4]>, mode: u32) {
4888        let model = if let Some(m) = self.svg_cache.get(name) {
4889            m.clone()
4890        } else {
4891            return;
4892        };
4893
4894        let _scale_x = rect.width / model.view_box.width;
4895        let _scale_y = rect.height / model.view_box.height;
4896        let base_idx = self.vertices.len() as u32;
4897        let screen = [self.current_width() as f32, self.current_height() as f32];
4898        let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
4899            x: -10000.0,
4900            y: -10000.0,
4901            width: 20000.0,
4902            height: 20000.0,
4903        });
4904        let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
4905        let scale = self.current_scale_factor();
4906        let snap = |v: f32| (v * scale).round() / scale;
4907
4908        let mut local_vertices = model.vertices.clone();
4909        for anim in &model.animations {
4910            let t = (self.current_scene.time % anim.duration) / anim.duration;
4911            let val = anim.from_val + (anim.to_val - anim.from_val) * t;
4912
4913            if anim.attribute_name == "transform" {
4914                // assume rotation
4915                let mut min_x = f32::MAX;
4916                let mut min_y = f32::MAX;
4917                let mut max_x = f32::MIN;
4918                let mut max_y = f32::MIN;
4919                for i in anim.vertex_range.clone() {
4920                    let p = local_vertices[i].position;
4921                    if p[0] < min_x { min_x = p[0]; }
4922                    if p[1] < min_y { min_y = p[1]; }
4923                    if p[0] > max_x { max_x = p[0]; }
4924                    if p[1] > max_y { max_y = p[1]; }
4925                }
4926                let cx = (min_x + max_x) * 0.5;
4927                let cy = (min_y + max_y) * 0.5;
4928                
4929                let c = val.to_radians().cos();
4930                let s = val.to_radians().sin();
4931                
4932                for i in anim.vertex_range.clone() {
4933                    let p = local_vertices[i].position;
4934                    let dx = p[0] - cx;
4935                    let dy = p[1] - cy;
4936                    local_vertices[i].position[0] = cx + dx * c - dy * s;
4937                    local_vertices[i].position[1] = cy + dx * s + dy * c;
4938                }
4939            } else if anim.attribute_name == "opacity" {
4940                for i in anim.vertex_range.clone() {
4941                    local_vertices[i].color[3] = val;
4942                }
4943            }
4944        }
4945
4946        for mut v in local_vertices {
4947            let rel_x = (v.position[0] - model.view_box.x) / model.view_box.width;
4948            let rel_y = (v.position[1] - model.view_box.y) / model.view_box.height;
4949
4950            v.position[0] = snap(rect.x + rel_x * rect.width);
4951            v.position[1] = snap(rect.y + rel_y * rect.height);
4952            v.position[2] = self.current_z;
4953            v.logical = [v.position[0], v.position[1]];
4954            v.screen = screen;
4955            v.clip = clip;
4956            v.mode = mode;
4957
4958            if let Some(override_color) = color {
4959                let mut c = override_color;
4960                c[3] *= v.color[3]; // preserve animated opacity
4961                v.color = self.apply_opacity(c);
4962            } else {
4963                v.color = self.apply_opacity(v.color);
4964            }
4965            self.vertices.push(v);
4966        }
4967
4968        for idx in &model.indices {
4969            self.indices.push(base_idx + *idx);
4970        }
4971
4972        let material = if mode == 7 {
4973            cvkg_core::DrawMaterial::Glass { blur_radius: 20.0 }
4974        } else {
4975            cvkg_core::DrawMaterial::TopUI
4976        };
4977        let tid = self.get_texture_id("__mega_atlas");
4978
4979        let last_call = self.draw_calls.last();
4980        let needs_new_call = self.draw_calls.is_empty()
4981            || self.current_texture_id != tid
4982            || last_call.unwrap().scissor_rect != self.clip_stack.last().copied()
4983            || last_call.unwrap().material != material;
4984
4985        if needs_new_call {
4986            self.current_texture_id = tid;
4987            self.draw_calls.push(DrawCall {
4988                texture_id: tid,
4989                scissor_rect: self.clip_stack.last().copied(),
4990                index_start: (self.indices.len() - model.indices.len()) as u32,
4991                index_count: 0,
4992                material,
4993            });
4994        }
4995
4996        if let Some(call) = self.draw_calls.last_mut() {
4997            call.index_count += model.indices.len() as u32;
4998        }
4999    }
5000
5001    /// forge_headless — Initializes Surtr without a window for visual regression testing.
5002    pub async fn forge_headless(width: u32, height: u32) -> Self {
5003        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
5004            backends: wgpu::Backends::all(),
5005            flags: wgpu::InstanceFlags::default(),
5006            backend_options: wgpu::BackendOptions::default(),
5007            display: None,
5008            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
5009        });
5010
5011        // Request adapter with robust multi-stage fallback for Bumblebee/Optimus compatibility
5012        println!("[GPU] Requesting HighPerformance adapter...");
5013        let mut adapter = instance
5014            .request_adapter(&wgpu::RequestAdapterOptions {
5015                power_preference: wgpu::PowerPreference::HighPerformance,
5016                compatible_surface: None,
5017                force_fallback_adapter: false,
5018            })
5019            .await
5020            .ok();
5021
5022        if adapter.is_none() {
5023            println!(
5024                "[GPU] HighPerformance adapter failed (possible Bumblebee/Optimus), trying LowPower..."
5025            );
5026            adapter = instance
5027                .request_adapter(&wgpu::RequestAdapterOptions {
5028                    power_preference: wgpu::PowerPreference::LowPower,
5029                    compatible_surface: None,
5030                    force_fallback_adapter: false,
5031                })
5032                .await
5033                .ok();
5034        }
5035
5036        if adapter.is_none() {
5037            println!("[GPU] Hardware adapters failed, trying Software fallback...");
5038            adapter = instance
5039                .request_adapter(&wgpu::RequestAdapterOptions {
5040                    power_preference: wgpu::PowerPreference::LowPower,
5041                    compatible_surface: None,
5042                    force_fallback_adapter: true,
5043                })
5044                .await
5045                .ok();
5046        }
5047
5048        let adapter = adapter.expect("Failed to find a suitable GPU for Surtr");
5049        let info = adapter.get_info();
5050        println!(
5051            "[GPU] Selected adapter: {} ({:?}) on backend: {:?}",
5052            info.name, info.device_type, info.backend
5053        );
5054        println!("[GPU] Driver info: {} - {}", info.driver, info.driver_info);
5055        let supports_timestamps = adapter.features().contains(wgpu::Features::TIMESTAMP_QUERY);
5056        let mut required_features = wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
5057            | wgpu::Features::TEXTURE_BINDING_ARRAY;
5058        if supports_timestamps {
5059            required_features |= wgpu::Features::TIMESTAMP_QUERY;
5060        }
5061
5062        let (device, queue) = adapter
5063            .request_device(&wgpu::DeviceDescriptor {
5064                label: Some("Surtr Headless Forge"),
5065                required_features,
5066                required_limits: wgpu::Limits {
5067                    max_bindings_per_bind_group: 256,
5068                    max_binding_array_elements_per_shader_stage: 256,
5069                    ..wgpu::Limits::default()
5070                },
5071                memory_hints: wgpu::MemoryHints::default(),
5072                experimental_features: wgpu::ExperimentalFeatures::disabled(),
5073                trace: wgpu::Trace::Off,
5074            })
5075            .await
5076            .expect("Failed to create Surtr device");
5077
5078        let instance = Arc::new(instance);
5079        let adapter = Arc::new(adapter);
5080        
5081        device.on_uncaptured_error(Arc::new(|error| {
5082            log::error!("[GPU] Uncaptured device error (Device Lost or Panic): {:?}", error);
5083        }));
5084
5085        let device = Arc::new(device);
5086        let queue = Arc::new(queue);
5087
5088        Self::forge_internal(
5089            instance,
5090            adapter,
5091            device,
5092            queue,
5093            None,
5094            Some((width, height, wgpu::TextureFormat::Rgba8UnormSrgb)),
5095        )
5096        .await
5097    }
5098
5099    /// capture_frame — Read back the rendered frame as a byte buffer (RGBA8).
5100    pub async fn capture_frame(&self) -> Result<Vec<u8>, String> {
5101        let ctx = self
5102            .headless_context
5103            .as_ref()
5104            .ok_or("Headless context required for capture")?;
5105        let u32_size = std::mem::size_of::<u32>() as u32;
5106        let width = ctx.width;
5107        let height = ctx.height;
5108        let bytes_per_row = width * u32_size;
5109        let padding = (256 - (bytes_per_row % 256)) % 256;
5110        let padded_bytes_per_row = bytes_per_row + padding;
5111
5112        let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
5113            label: Some("Capture Buffer"),
5114            size: (padded_bytes_per_row as u64 * height as u64),
5115            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
5116            mapped_at_creation: false,
5117        });
5118
5119        let mut encoder = self
5120            .device
5121            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
5122                label: Some("Capture Encoder"),
5123            });
5124
5125        encoder.copy_texture_to_buffer(
5126            wgpu::TexelCopyTextureInfo {
5127                texture: &ctx.output_texture,
5128                mip_level: 0,
5129                origin: wgpu::Origin3d::ZERO,
5130                aspect: wgpu::TextureAspect::All,
5131            },
5132            wgpu::TexelCopyBufferInfo {
5133                buffer: &output_buffer,
5134                layout: wgpu::TexelCopyBufferLayout {
5135                    offset: 0,
5136                    bytes_per_row: Some(padded_bytes_per_row),
5137                    rows_per_image: Some(height),
5138                },
5139            },
5140            wgpu::Extent3d {
5141                width,
5142                height,
5143                depth_or_array_layers: 1,
5144            },
5145        );
5146
5147        self.queue.submit(Some(encoder.finish()));
5148
5149        let buffer_slice = output_buffer.slice(..);
5150        let (sender, receiver) = futures::channel::oneshot::channel();
5151        buffer_slice.map_async(wgpu::MapMode::Read, move |v| {
5152            let _ = sender.send(v);
5153        });
5154
5155        let _ = self.device.poll(wgpu::PollType::Wait {
5156            submission_index: None,
5157            timeout: None,
5158        });
5159
5160        if let Ok(Ok(_)) = receiver.await {
5161            let data = buffer_slice.get_mapped_range();
5162            let mut result = Vec::with_capacity((width * height * 4) as usize);
5163
5164            for y in 0..height {
5165                let start = (y * padded_bytes_per_row) as usize;
5166                let end = start + bytes_per_row as usize;
5167                result.extend_from_slice(&data[start..end]);
5168            }
5169
5170            drop(data);
5171            output_buffer.unmap();
5172            Ok(result)
5173        } else {
5174            Err("Failed to capture frame".to_string())
5175        }
5176    }
5177
5178    fn current_width(&self) -> u32 {
5179        if let Some(id) = self.current_window {
5180            self.surfaces.get(&id).unwrap().config.width
5181        } else {
5182            self.headless_context.as_ref().unwrap().width
5183        }
5184    }
5185
5186    fn current_height(&self) -> u32 {
5187        if let Some(id) = self.current_window {
5188            self.surfaces.get(&id).unwrap().config.height
5189        } else {
5190            self.headless_context.as_ref().unwrap().height
5191        }
5192    }
5193
5194    fn current_scale_factor(&self) -> f32 {
5195        if let Some(id) = self.current_window {
5196            self.surfaces.get(&id).unwrap().scale_factor
5197        } else {
5198            self.headless_context.as_ref().unwrap().scale_factor
5199        }
5200    }
5201
5202    /// Find a filter by ID in the SVG tree's filter list.
5203    fn find_filter<'a>(tree: &'a usvg::Tree, filter_id: &str) -> Option<&'a usvg::filter::Filter> {
5204        tree.filters()
5205            .iter()
5206            .find(|f| f.id() == filter_id)
5207            .map(|arc| arc.as_ref())
5208    }
5209}