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