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