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