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