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