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
25//! # Surtr Render Pipeline
26//!
27//! The "Fiery Giant" of the CVKG architecture. This is the authoritative GPU renderer
28//! powered by `wgpu`. It manages the heat of the GPU to forge high-fidelity
29//! "Berserker" aesthetics.
30//!
31//! - **The Flaming Sword**: Command submission and synchronization.
32//! - **Muspelheim Passes**: Multi-pass Gaussian blur and bloom for Bifrost/Gungnir.
33
34use cvkg_core::{ColorTheme, Mesh, Rect, SceneUniforms};
35use std::sync::Arc;
36
37// ShieldWall — re-export AccessKit types so callers can build tree updates
38// without depending on accesskit directly.
39pub use accesskit::{
40    ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler, Node, NodeId, Role, Tree,
41    TreeId, TreeUpdate,
42};
43pub use accesskit_winit::Adapter as ShieldWallAdapter;
44
45#[repr(C)]
46#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
47pub struct Vertex {
48    pub position: [f32; 3],
49    pub normal: [f32; 3],
50    pub uv: [f32; 2],
51    pub color: [f32; 4],
52    pub mode: u32,
53    pub radius: f32,
54    pub slice: [f32; 4],
55    pub logical: [f32; 2],
56    pub size: [f32; 2],
57    pub screen: [f32; 2],
58    pub clip: [f32; 4], // [x, y, width, height]
59}
60
61/// Represents a single batched GPU draw call.
62/// Batches are broken whenever the active texture or primitive mode changes.
63#[derive(Debug, Clone)]
64struct DrawCall {
65    pub texture_id: Option<u32>,
66    pub scissor_rect: Option<Rect>,
67    pub index_start: u32,
68    pub index_count: u32,
69    pub is_glass: bool,
70    pub is_ui: bool,
71}
72
73impl Vertex {
74    const ATTRIBUTES: [wgpu::VertexAttribute; 11] = wgpu::vertex_attr_array![
75        0 => Float32x3, // position
76        1 => Float32x3, // normal
77        2 => Float32x2, // uv
78        3 => Float32x4, // color
79        4 => Uint32,    // mode
80        5 => Float32,   // radius
81        6 => Float32x4, // slice
82        7 => Float32x2, // logical
83        8 => Float32x2, // size
84        9 => Float32x2, // screen
85        10 => Float32x4 // clip
86    ];
87
88    fn desc() -> wgpu::VertexBufferLayout<'static> {
89        wgpu::VertexBufferLayout {
90            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
91            step_mode: wgpu::VertexStepMode::Vertex,
92            attributes: &Self::ATTRIBUTES,
93        }
94    }
95}
96
97/// SurtrRenderer implements the high-performance GPU backend.
98pub struct SurtrRenderer {
99    device: Arc<wgpu::Device>,
100    queue: Arc<wgpu::Queue>,
101    surface: wgpu::Surface<'static>,
102    config: wgpu::SurfaceConfiguration,
103
104    // Text Forge
105    #[allow(dead_code)]
106    font_system: cosmic_text::FontSystem,
107    #[allow(dead_code)]
108    swash_cache: cosmic_text::SwashCache,
109    text_atlas_tex: wgpu::Texture,
110    #[allow(dead_code)]
111    text_atlas_view: wgpu::TextureView,
112    #[allow(dead_code)]
113    text_sampler: wgpu::Sampler,
114    text_cache: std::collections::HashMap<cosmic_text::CacheKey, (Rect, f32, f32)>,
115    text_atlas_pos: (u32, u32),
116
117    // Niflheim Resources
118    dummy_texture_bind_group: wgpu::BindGroup,
119    dummy_env_bind_group: wgpu::BindGroup,
120    texture_bind_group_layout: wgpu::BindGroupLayout,
121    texture_bind_groups: Vec<wgpu::BindGroup>,
122    texture_registry: std::collections::HashMap<String, u32>,
123    shared_elements: std::collections::HashMap<String, cvkg_core::Rect>,
124
125    // The Forge's Anvil (GPU Buffers)
126    vertex_buffer: wgpu::Buffer,
127    index_buffer: wgpu::Buffer,
128    vertices: Vec<Vertex>,
129    indices: Vec<u32>,
130    draw_calls: Vec<DrawCall>,
131    current_texture_id: Option<u32>,
132
133    // Opacity stack: each push multiplies into the current effective alpha.
134    opacity_stack: Vec<f32>,
135    // Clip rect stack: used for batched scissoring.
136    clip_stack: Vec<Rect>,
137    // Mjolnir Slice stack: (angle, offset)
138    slice_stack: Vec<(f32, f32)>,
139
140    // The Forge's Heart (Berserker State)
141    theme_buffer: wgpu::Buffer,
142    scene_buffer: wgpu::Buffer,
143    berserker_bind_group: wgpu::BindGroup,
144    #[allow(dead_code)]
145    berserker_bind_group_layout: wgpu::BindGroupLayout,
146    start_time: std::time::Instant,
147    current_theme: ColorTheme,
148    current_scene: SceneUniforms,
149
150    // Muspelheim Pipelines
151    pipeline: wgpu::RenderPipeline,
152    background_pipeline: wgpu::RenderPipeline,
153    bloom_extract_pipeline: wgpu::RenderPipeline,
154    blur_h_pipeline: wgpu::RenderPipeline,
155    blur_v_pipeline: wgpu::RenderPipeline,
156    composite_pipeline: wgpu::RenderPipeline,
157
158    /// Muspelheim Textures & Bind Groups
159    blur_texture_a: wgpu::TextureView,
160    blur_texture_b: wgpu::TextureView,
161    blur_bind_group_a: wgpu::BindGroup,
162    blur_bind_group_b: wgpu::BindGroup,
163    blur_env_bind_group_a: wgpu::BindGroup,
164    scene_texture: wgpu::TextureView,
165    scene_bind_group: wgpu::BindGroup,
166    scene_texture_bind_group: wgpu::BindGroup,
167    env_bind_group_layout: wgpu::BindGroupLayout,
168}
169
170const MAX_VERTICES: usize = 100_000;
171const MAX_INDICES: usize = 150_000;
172
173impl SurtrRenderer {
174    /// forge — Initializes the Surtr GPU renderer from a winit window.
175    ///
176    /// This method performs the following:
177    /// 1. Negotiates a wgpu surface and adapter.
178    /// 2. Forges the Muspelheim multi-pass pipeline layouts.
179    /// 3. Initializes the Berserker state buffers and texture registries.
180    pub async fn forge(window: Arc<winit::window::Window>) -> Self {
181        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
182            backends: wgpu::Backends::all(),
183            flags: wgpu::InstanceFlags::default(),
184            backend_options: wgpu::BackendOptions::default(),
185            display: None,
186            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
187        });
188
189        let surface = instance
190            .create_surface(window.clone())
191            .expect("Failed to create surface");
192
193        let adapter = instance
194            .request_adapter(&wgpu::RequestAdapterOptions {
195                power_preference: wgpu::PowerPreference::HighPerformance,
196                compatible_surface: Some(&surface),
197                force_fallback_adapter: false,
198            })
199            .await
200            .expect("Failed to find a suitable GPU for Surtr");
201
202        let (device, queue) = adapter
203            .request_device(&wgpu::DeviceDescriptor {
204                label: Some("Surtr Forge"),
205                required_features: wgpu::Features::empty(),
206                required_limits: wgpu::Limits::default(),
207                memory_hints: wgpu::MemoryHints::default(),
208                experimental_features: wgpu::ExperimentalFeatures::disabled(),
209                trace: wgpu::Trace::Off,
210            })
211            .await
212            .expect("Failed to create Surtr device");
213
214        let device = Arc::new(device);
215        let queue = Arc::new(queue);
216
217        let size = window.inner_size();
218        let surface_caps = surface.get_capabilities(&adapter);
219        let surface_format = surface_caps
220            .formats
221            .iter()
222            .find(|f| f.is_srgb())
223            .copied()
224            .unwrap_or(surface_caps.formats[0]);
225
226        let config = wgpu::SurfaceConfiguration {
227            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
228            format: surface_format,
229            width: size.width,
230            height: size.height,
231            present_mode: wgpu::PresentMode::Fifo,
232            alpha_mode: surface_caps.alpha_modes[0],
233            view_formats: vec![],
234            desired_maximum_frame_latency: 2,
235        };
236        surface.configure(&device, &config);
237
238        // Load the Muspelheim Shaders
239        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
240            label: Some("Muspelheim Main Shader"),
241            source: wgpu::ShaderSource::Wgsl(include_str!("shaders.wgsl").into()),
242        });
243
244        // Niflheim Bind Group Layout (for textures/samplers)
245        let texture_bind_group_layout =
246            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
247                entries: &[
248                    wgpu::BindGroupLayoutEntry {
249                        binding: 0,
250                        visibility: wgpu::ShaderStages::FRAGMENT,
251                        ty: wgpu::BindingType::Texture {
252                            multisampled: false,
253                            view_dimension: wgpu::TextureViewDimension::D2,
254                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
255                        },
256                        count: None,
257                    },
258                    wgpu::BindGroupLayoutEntry {
259                        binding: 1,
260                        visibility: wgpu::ShaderStages::FRAGMENT,
261                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
262                        count: None,
263                    },
264                ],
265                label: Some("Niflheim Texture Bind Group Layout"),
266            });
267
268        // Environment Bind Group Layout (for blurred background / Bifrost)
269        // Environment Bind Group Layout (for blurred background / Bifrost)
270        let env_bind_group_layout =
271            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
272                entries: &[
273                    wgpu::BindGroupLayoutEntry {
274                        binding: 0,
275                        visibility: wgpu::ShaderStages::FRAGMENT,
276                        ty: wgpu::BindingType::Texture {
277                            multisampled: false,
278                            view_dimension: wgpu::TextureViewDimension::D2,
279                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
280                        },
281                        count: None,
282                    },
283                    wgpu::BindGroupLayoutEntry {
284                        binding: 1,
285                        visibility: wgpu::ShaderStages::FRAGMENT,
286                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
287                        count: None,
288                    },
289                ],
290                label: Some("Surtr Environment Bind Group Layout"),
291            });
292
293        let berserker_bind_group_layout =
294            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
295                entries: &[
296                    wgpu::BindGroupLayoutEntry {
297                        binding: 0,
298                        visibility: wgpu::ShaderStages::FRAGMENT,
299                        ty: wgpu::BindingType::Buffer {
300                            ty: wgpu::BufferBindingType::Uniform,
301                            has_dynamic_offset: false,
302                            min_binding_size: None,
303                        },
304                        count: None,
305                    },
306                    wgpu::BindGroupLayoutEntry {
307                        binding: 1,
308                        visibility: wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::VERTEX,
309                        ty: wgpu::BindingType::Buffer {
310                            ty: wgpu::BufferBindingType::Uniform,
311                            has_dynamic_offset: false,
312                            min_binding_size: None,
313                        },
314                        count: None,
315                    },
316                ],
317                label: Some("Surtr Berserker Bind Group Layout"),
318            });
319
320        // Pipeline setup
321        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
322            label: Some("Surtr Main Pipeline Layout"),
323            bind_group_layouts: &[
324                Some(&texture_bind_group_layout),
325                Some(&env_bind_group_layout),
326                Some(&berserker_bind_group_layout),
327            ],
328            immediate_size: 0,
329        });
330
331        // Specialized layout for post-processing (Bloom Extract, Blur) which only need Group 0 + Globals
332        let post_process_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
333            label: Some("Muspelheim Post Process Layout"),
334            bind_group_layouts: &[
335                Some(&texture_bind_group_layout),
336                Some(&texture_bind_group_layout),
337                Some(&berserker_bind_group_layout),
338            ],
339            immediate_size: 0,
340        });
341
342        // Specialized layout for composite (Blur + Scene)
343        let composite_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
344            label: Some("Muspelheim Composite Layout"),
345            bind_group_layouts: &[
346                Some(&texture_bind_group_layout),
347                Some(&texture_bind_group_layout),
348                Some(&berserker_bind_group_layout),
349            ],
350            immediate_size: 0,
351        });
352
353        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
354            label: Some("Surtr Main Pipeline"),
355            layout: Some(&pipeline_layout),
356            vertex: wgpu::VertexState {
357                module: &shader,
358                entry_point: Some("vs_main"),
359                buffers: &[Vertex::desc()],
360                compilation_options: wgpu::PipelineCompilationOptions::default(),
361            },
362            fragment: Some(wgpu::FragmentState {
363                module: &shader,
364                entry_point: Some("fs_main"),
365                targets: &[Some(wgpu::ColorTargetState {
366                    format: config.format,
367                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
368                    write_mask: wgpu::ColorWrites::ALL,
369                })],
370                compilation_options: wgpu::PipelineCompilationOptions::default(),
371            }),
372            primitive: wgpu::PrimitiveState::default(),
373            depth_stencil: None,
374            multisample: wgpu::MultisampleState::default(),
375            multiview_mask: None,
376            cache: None,
377        });
378
379        let background_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
380            label: Some("Surtr Background Pipeline"),
381            layout: Some(&pipeline_layout),
382            vertex: wgpu::VertexState {
383                module: &shader,
384                entry_point: Some("vs_fullscreen"),
385                buffers: &[],
386                compilation_options: wgpu::PipelineCompilationOptions::default(),
387            },
388            fragment: Some(wgpu::FragmentState {
389                module: &shader,
390                entry_point: Some("fs_background"),
391                targets: &[Some(wgpu::ColorTargetState {
392                    format: config.format,
393                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
394                    write_mask: wgpu::ColorWrites::ALL,
395                })],
396                compilation_options: wgpu::PipelineCompilationOptions::default(),
397            }),
398            primitive: wgpu::PrimitiveState::default(),
399            depth_stencil: None,
400            multisample: wgpu::MultisampleState::default(),
401            multiview_mask: None,
402            cache: None,
403        });
404
405        // Muspelheim Bloom Extract Pipeline
406        let bloom_extract_pipeline =
407            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
408                label: Some("Muspelheim Bloom Extract"),
409                layout: Some(&post_process_layout),
410                vertex: wgpu::VertexState {
411                    module: &shader,
412                    entry_point: Some("vs_fullscreen"),
413                    buffers: &[],
414                    compilation_options: wgpu::PipelineCompilationOptions::default(),
415                },
416                fragment: Some(wgpu::FragmentState {
417                    module: &shader,
418                    entry_point: Some("fs_bloom_extract"),
419                    targets: &[Some(wgpu::ColorTargetState {
420                        format: config.format,
421                        blend: None,
422                        write_mask: wgpu::ColorWrites::ALL,
423                    })],
424                    compilation_options: wgpu::PipelineCompilationOptions::default(),
425                }),
426                primitive: wgpu::PrimitiveState::default(),
427                depth_stencil: None,
428                multisample: wgpu::MultisampleState::default(),
429                multiview_mask: None,
430                cache: None,
431            });
432
433        // Muspelheim Blur Pipelines (H and V)
434        let blur_h_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
435            label: Some("Muspelheim Horizontal Blur"),
436            layout: Some(&post_process_layout),
437            vertex: wgpu::VertexState {
438                module: &shader,
439                entry_point: Some("vs_fullscreen"),
440                buffers: &[],
441                compilation_options: wgpu::PipelineCompilationOptions::default(),
442            },
443            fragment: Some(wgpu::FragmentState {
444                module: &shader,
445                entry_point: Some("fs_blur_h"),
446                targets: &[Some(wgpu::ColorTargetState {
447                    format: config.format,
448                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
449                    write_mask: wgpu::ColorWrites::ALL,
450                })],
451                compilation_options: wgpu::PipelineCompilationOptions::default(),
452            }),
453            primitive: wgpu::PrimitiveState::default(),
454            depth_stencil: None,
455            multisample: wgpu::MultisampleState::default(),
456            multiview_mask: None,
457            cache: None,
458        });
459
460        let blur_v_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
461            label: Some("Muspelheim Vertical Blur"),
462            layout: Some(&post_process_layout),
463            vertex: wgpu::VertexState {
464                module: &shader,
465                entry_point: Some("vs_fullscreen"),
466                buffers: &[],
467                compilation_options: wgpu::PipelineCompilationOptions::default(),
468            },
469            fragment: Some(wgpu::FragmentState {
470                module: &shader,
471                entry_point: Some("fs_blur_v"),
472                targets: &[Some(wgpu::ColorTargetState {
473                    format: config.format,
474                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
475                    write_mask: wgpu::ColorWrites::ALL,
476                })],
477                compilation_options: wgpu::PipelineCompilationOptions::default(),
478            }),
479            primitive: wgpu::PrimitiveState::default(),
480            depth_stencil: None,
481            multisample: wgpu::MultisampleState::default(),
482            multiview_mask: None,
483            cache: None,
484        });
485
486        // Muspelheim Composite Pipeline (additive blend onto screen)
487        let composite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
488            label: Some("Muspelheim Composite"),
489            layout: Some(&composite_layout),
490            vertex: wgpu::VertexState {
491                module: &shader,
492                entry_point: Some("vs_fullscreen"),
493                buffers: &[],
494                compilation_options: wgpu::PipelineCompilationOptions::default(),
495            },
496            fragment: Some(wgpu::FragmentState {
497                module: &shader,
498                entry_point: Some("fs_composite"),
499                targets: &[Some(wgpu::ColorTargetState {
500                    format: config.format,
501                    // Additive blend: src + dst — glow lights up the scene
502                    blend: Some(wgpu::BlendState {
503                        color: wgpu::BlendComponent {
504                            src_factor: wgpu::BlendFactor::One,
505                            dst_factor: wgpu::BlendFactor::One,
506                            operation: wgpu::BlendOperation::Add,
507                        },
508                        alpha: wgpu::BlendComponent {
509                            src_factor: wgpu::BlendFactor::One,
510                            dst_factor: wgpu::BlendFactor::One,
511                            operation: wgpu::BlendOperation::Add,
512                        },
513                    }),
514                    write_mask: wgpu::ColorWrites::ALL,
515                })],
516                compilation_options: wgpu::PipelineCompilationOptions::default(),
517            }),
518            primitive: wgpu::PrimitiveState::default(),
519            depth_stencil: None,
520            multisample: wgpu::MultisampleState::default(),
521            multiview_mask: None,
522            cache: None,
523        });
524
525        // Muspelheim Intermediate Textures
526        let blur_tex_desc = wgpu::TextureDescriptor {
527            label: Some("Muspelheim Intermediate"),
528            size: wgpu::Extent3d {
529                width: config.width,
530                height: config.height,
531                depth_or_array_layers: 1,
532            },
533            mip_level_count: 1,
534            sample_count: 1,
535            dimension: wgpu::TextureDimension::D2,
536            format: config.format,
537            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
538            view_formats: &[],
539        };
540        let blur_texture_a_obj = device.create_texture(&blur_tex_desc);
541        let blur_texture_b_obj = device.create_texture(&blur_tex_desc);
542        let blur_texture_a =
543            blur_texture_a_obj.create_view(&wgpu::TextureViewDescriptor::default());
544        let blur_texture_b =
545            blur_texture_b_obj.create_view(&wgpu::TextureViewDescriptor::default());
546
547        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
548            address_mode_u: wgpu::AddressMode::ClampToEdge,
549            address_mode_v: wgpu::AddressMode::ClampToEdge,
550            mag_filter: wgpu::FilterMode::Linear,
551            ..Default::default()
552        });
553
554        let blur_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
555            layout: &texture_bind_group_layout,
556            entries: &[
557                wgpu::BindGroupEntry {
558                    binding: 0,
559                    resource: wgpu::BindingResource::TextureView(&blur_texture_a),
560                },
561                wgpu::BindGroupEntry {
562                    binding: 1,
563                    resource: wgpu::BindingResource::Sampler(&sampler),
564                },
565            ],
566            label: Some("Blur Bind Group A"),
567        });
568
569        let blur_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
570            layout: &texture_bind_group_layout,
571            entries: &[
572                wgpu::BindGroupEntry {
573                    binding: 0,
574                    resource: wgpu::BindingResource::TextureView(&blur_texture_b),
575                },
576                wgpu::BindGroupEntry {
577                    binding: 1,
578                    resource: wgpu::BindingResource::Sampler(&sampler),
579                },
580            ],
581            label: Some("Blur Bind Group B"),
582        });
583
584        let blur_env_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
585            layout: &env_bind_group_layout,
586            entries: &[
587                wgpu::BindGroupEntry {
588                    binding: 0,
589                    resource: wgpu::BindingResource::TextureView(&blur_texture_a),
590                },
591                wgpu::BindGroupEntry {
592                    binding: 1,
593                    resource: wgpu::BindingResource::Sampler(&sampler),
594                },
595            ],
596            label: Some("Blur Env Bind Group A"),
597        });
598
599        // Forge the Scene Capture Texture
600        let scene_texture_obj = device.create_texture(&blur_tex_desc);
601        let scene_texture = scene_texture_obj.create_view(&wgpu::TextureViewDescriptor::default());
602        let scene_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
603            layout: &env_bind_group_layout,
604            entries: &[
605                wgpu::BindGroupEntry {
606                    binding: 0,
607                    resource: wgpu::BindingResource::TextureView(&scene_texture),
608                },
609                wgpu::BindGroupEntry {
610                    binding: 1,
611                    resource: wgpu::BindingResource::Sampler(&sampler),
612                },
613            ],
614            label: Some("Scene Capture Bind Group"),
615        });
616
617        // Forge the Text Atlas (1024x1024 Alpha-only for speed)
618        let text_atlas_tex = device.create_texture(&wgpu::TextureDescriptor {
619            label: Some("Surtr Text Atlas"),
620            size: wgpu::Extent3d {
621                width: 1024,
622                height: 1024,
623                depth_or_array_layers: 1,
624            },
625            mip_level_count: 1,
626            sample_count: 1,
627            dimension: wgpu::TextureDimension::D2,
628            format: wgpu::TextureFormat::R8Unorm,
629            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
630            view_formats: &[],
631        });
632        let text_atlas = text_atlas_tex.create_view(&wgpu::TextureViewDescriptor::default());
633        let text_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
634            address_mode_u: wgpu::AddressMode::ClampToEdge,
635            address_mode_v: wgpu::AddressMode::ClampToEdge,
636            mag_filter: wgpu::FilterMode::Nearest,
637            min_filter: wgpu::FilterMode::Nearest,
638            ..Default::default()
639        });
640
641        // Clear the text atlas to transparency initially
642        queue.write_texture(
643            wgpu::TexelCopyTextureInfo {
644                texture: &text_atlas_tex,
645                mip_level: 0,
646                origin: wgpu::Origin3d::ZERO,
647                aspect: wgpu::TextureAspect::All,
648            },
649            &vec![0u8; 1024 * 1024],
650            wgpu::TexelCopyBufferLayout {
651                offset: 0,
652                bytes_per_row: Some(1024),
653                rows_per_image: Some(1024),
654            },
655            wgpu::Extent3d {
656                width: 1024,
657                height: 1024,
658                depth_or_array_layers: 1,
659            },
660        );
661
662        // Forge the Niflheim Dummy Texture (1x1 White)
663        let dummy_size = wgpu::Extent3d {
664            width: 1,
665            height: 1,
666            depth_or_array_layers: 1,
667        };
668        let dummy_texture = device.create_texture(&wgpu::TextureDescriptor {
669            label: Some("Niflheim Dummy Texture"),
670            size: dummy_size,
671            mip_level_count: 1,
672            sample_count: 1,
673            dimension: wgpu::TextureDimension::D2,
674            format: wgpu::TextureFormat::Rgba8UnormSrgb,
675            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
676            view_formats: &[],
677        });
678        queue.write_texture(
679            wgpu::TexelCopyTextureInfo {
680                texture: &dummy_texture,
681                mip_level: 0,
682                origin: wgpu::Origin3d::ZERO,
683                aspect: wgpu::TextureAspect::All,
684            },
685            &[255, 255, 255, 255],
686            wgpu::TexelCopyBufferLayout {
687                offset: 0,
688                bytes_per_row: Some(4),
689                rows_per_image: Some(1),
690            },
691            dummy_size,
692        );
693
694        let dummy_view = dummy_texture.create_view(&wgpu::TextureViewDescriptor::default());
695        let dummy_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
696            address_mode_u: wgpu::AddressMode::ClampToEdge,
697            address_mode_v: wgpu::AddressMode::ClampToEdge,
698            address_mode_w: wgpu::AddressMode::ClampToEdge,
699            mag_filter: wgpu::FilterMode::Linear,
700            min_filter: wgpu::FilterMode::Nearest,
701            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
702            ..Default::default()
703        });
704
705        let dummy_texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
706            layout: &texture_bind_group_layout,
707            entries: &[
708                wgpu::BindGroupEntry {
709                    binding: 0,
710                    resource: wgpu::BindingResource::TextureView(&dummy_view),
711                },
712                wgpu::BindGroupEntry {
713                    binding: 1,
714                    resource: wgpu::BindingResource::Sampler(&dummy_sampler),
715                },
716            ],
717            label: Some("Dummy Texture Bind Group"),
718        });
719
720        let dummy_env_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
721            layout: &env_bind_group_layout,
722            entries: &[
723                wgpu::BindGroupEntry {
724                    binding: 0,
725                    resource: wgpu::BindingResource::TextureView(&dummy_view),
726                },
727                wgpu::BindGroupEntry {
728                    binding: 1,
729                    resource: wgpu::BindingResource::Sampler(&dummy_sampler),
730                },
731            ],
732            label: Some("Dummy Env Bind Group"),
733        });
734
735        let mut texture_registry = std::collections::HashMap::new();
736        let mut texture_bind_groups = Vec::new();
737
738        let text_atlas_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
739            layout: &texture_bind_group_layout,
740            entries: &[
741                wgpu::BindGroupEntry {
742                    binding: 0,
743                    resource: wgpu::BindingResource::TextureView(&text_atlas),
744                },
745                wgpu::BindGroupEntry {
746                    binding: 1,
747                    resource: wgpu::BindingResource::Sampler(&text_sampler),
748                },
749            ],
750            label: Some("Text Atlas Bind Group"),
751        });
752        texture_registry.insert("__text_atlas".to_string(), texture_bind_groups.len() as u32);
753        texture_bind_groups.push(text_atlas_bg);
754
755        // Forge the Anvil (Buffers)
756        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
757            label: Some("Surtr Vertex Anvil"),
758            size: (MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64,
759            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
760            mapped_at_creation: false,
761        });
762
763        let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
764            label: Some("Surtr Index Anvil"),
765            size: (MAX_INDICES * std::mem::size_of::<u32>()) as u64,
766            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
767            mapped_at_creation: false,
768        });
769
770        // Register atlas
771
772        // Texture registry and bind groups already initialized above.
773
774        // Forge the Heart (Berserker Uniforms)
775        let current_theme = ColorTheme::default();
776        use wgpu::util::DeviceExt;
777        let theme_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
778            label: Some("Surtr Theme Buffer"),
779            contents: bytemuck::bytes_of(&current_theme),
780            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
781        });
782
783        let current_scene = SceneUniforms::new(config.width as f32, config.height as f32);
784        let scene_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
785            label: Some("Surtr Scene Buffer"),
786            contents: bytemuck::bytes_of(&current_scene),
787            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
788        });
789
790        let berserker_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
791            layout: &berserker_bind_group_layout,
792            entries: &[
793                wgpu::BindGroupEntry {
794                    binding: 0,
795                    resource: theme_buffer.as_entire_binding(),
796                },
797                wgpu::BindGroupEntry {
798                    binding: 1,
799                    resource: scene_buffer.as_entire_binding(),
800                },
801            ],
802            label: Some("Surtr Berserker Bind Group"),
803        });
804
805        let scene_texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
806            layout: &texture_bind_group_layout,
807            entries: &[
808                wgpu::BindGroupEntry {
809                    binding: 0,
810                    resource: wgpu::BindingResource::TextureView(&scene_texture),
811                },
812                wgpu::BindGroupEntry {
813                    binding: 1,
814                    resource: wgpu::BindingResource::Sampler(&sampler),
815                },
816            ],
817            label: Some("Scene Texture Bind Group (Group 0)"),
818        });
819
820        Self {
821            device,
822            queue,
823            surface,
824            config,
825            pipeline,
826            bloom_extract_pipeline,
827            blur_h_pipeline,
828            blur_v_pipeline,
829            composite_pipeline,
830            blur_texture_a,
831            blur_texture_b,
832            blur_bind_group_a,
833            blur_bind_group_b,
834            blur_env_bind_group_a,
835            scene_texture,
836            scene_bind_group,
837            scene_texture_bind_group,
838            env_bind_group_layout,
839            font_system: cosmic_text::FontSystem::new(),
840            swash_cache: cosmic_text::SwashCache::new(),
841            text_atlas_tex,
842            text_atlas_view: text_atlas,
843            text_sampler,
844            text_cache: std::collections::HashMap::new(),
845            text_atlas_pos: (0, 0),
846            dummy_texture_bind_group,
847            dummy_env_bind_group,
848            texture_bind_group_layout,
849            texture_bind_groups,
850            texture_registry,
851            shared_elements: std::collections::HashMap::new(),
852            vertex_buffer,
853            index_buffer,
854            vertices: Vec::with_capacity(MAX_VERTICES),
855            indices: Vec::with_capacity(MAX_INDICES),
856            draw_calls: Vec::new(),
857            current_texture_id: None,
858            opacity_stack: vec![1.0],
859            clip_stack: Vec::new(),
860            slice_stack: Vec::new(),
861            theme_buffer,
862            scene_buffer,
863            berserker_bind_group,
864            berserker_bind_group_layout,
865            start_time: std::time::Instant::now(),
866            current_theme,
867            current_scene,
868            background_pipeline,
869        }
870    }
871
872    /// resize — Reconfigures the surface and internal textures for a new resolution.
873    ///
874    /// This is a non-trivial algorithm that must ensure all intermediate Muspelheim textures
875    /// (Bloom, Blur, Scene Capture) are recreated with matching dimensions to prevent
876    /// coordinate drift in the composite pass.
877    pub fn resize(&mut self, width: u32, height: u32) {
878        if width > 0 && height > 0 {
879            self.config.width = width;
880            self.config.height = height;
881            self.surface.configure(&self.device, &self.config);
882
883            // Re-create Muspelheim textures
884            let texture_desc = wgpu::TextureDescriptor {
885                label: Some("Surtr Scene Texture"),
886                size: wgpu::Extent3d {
887                    width,
888                    height,
889                    depth_or_array_layers: 1,
890                },
891                mip_level_count: 1,
892                sample_count: 1,
893                dimension: wgpu::TextureDimension::D2,
894                format: self.config.format,
895                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
896                    | wgpu::TextureUsages::TEXTURE_BINDING,
897                view_formats: &[],
898            };
899
900            let scene_tex = self.device.create_texture(&texture_desc);
901            self.scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
902
903            let blur_tex_a = self.device.create_texture(&texture_desc);
904            self.blur_texture_a = blur_tex_a.create_view(&wgpu::TextureViewDescriptor::default());
905
906            let blur_tex_b = self.device.create_texture(&texture_desc);
907            self.blur_texture_b = blur_tex_b.create_view(&wgpu::TextureViewDescriptor::default());
908
909            // Re-create bind groups (using existing layouts)
910            let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
911                address_mode_u: wgpu::AddressMode::ClampToEdge,
912                address_mode_v: wgpu::AddressMode::ClampToEdge,
913                mag_filter: wgpu::FilterMode::Linear,
914                min_filter: wgpu::FilterMode::Linear,
915                ..Default::default()
916            });
917
918            self.scene_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
919                layout: &self.env_bind_group_layout,
920                entries: &[
921                    wgpu::BindGroupEntry {
922                        binding: 0,
923                        resource: wgpu::BindingResource::TextureView(&self.scene_texture),
924                    },
925                    wgpu::BindGroupEntry {
926                        binding: 1,
927                        resource: wgpu::BindingResource::Sampler(&sampler),
928                    },
929                ],
930                label: Some("Surtr Scene Bind Group Resize"),
931            });
932
933            self.blur_bind_group_a = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
934                layout: &self.texture_bind_group_layout,
935                entries: &[
936                    wgpu::BindGroupEntry {
937                        binding: 0,
938                        resource: wgpu::BindingResource::TextureView(&self.blur_texture_a),
939                    },
940                    wgpu::BindGroupEntry {
941                        binding: 1,
942                        resource: wgpu::BindingResource::Sampler(&sampler),
943                    },
944                ],
945                label: Some("Surtr Blur Bind Group A Resize"),
946            });
947
948            self.blur_bind_group_b = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
949                layout: &self.texture_bind_group_layout,
950                entries: &[
951                    wgpu::BindGroupEntry {
952                        binding: 0,
953                        resource: wgpu::BindingResource::TextureView(&self.blur_texture_b),
954                    },
955                    wgpu::BindGroupEntry {
956                        binding: 1,
957                        resource: wgpu::BindingResource::Sampler(&sampler),
958                    },
959                ],
960                label: Some("Surtr Blur Bind Group B Resize"),
961            });
962
963            self.scene_texture_bind_group =
964                self.device.create_bind_group(&wgpu::BindGroupDescriptor {
965                    layout: &self.texture_bind_group_layout,
966                    entries: &[
967                        wgpu::BindGroupEntry {
968                            binding: 0,
969                            resource: wgpu::BindingResource::TextureView(&self.scene_texture),
970                        },
971                        wgpu::BindGroupEntry {
972                            binding: 1,
973                            resource: wgpu::BindingResource::Sampler(&sampler),
974                        },
975                    ],
976                    label: Some("Scene Texture Bind Group Resize"),
977                });
978
979            self.blur_env_bind_group_a =
980                self.device.create_bind_group(&wgpu::BindGroupDescriptor {
981                    layout: &self.env_bind_group_layout,
982                    entries: &[
983                        wgpu::BindGroupEntry {
984                            binding: 0,
985                            resource: wgpu::BindingResource::TextureView(&self.blur_texture_a),
986                        },
987                        wgpu::BindGroupEntry {
988                            binding: 1,
989                            resource: wgpu::BindingResource::Sampler(&sampler),
990                        },
991                    ],
992                    label: Some("Blur Env Bind Group A Resize"),
993                });
994
995            self.current_scene.resolution = [width as f32, height as f32];
996        }
997    }
998
999    /// begin_frame — Strike the flaming sword to begin a new GPU frame.
1000    pub fn begin_frame(&mut self) -> wgpu::CommandEncoder {
1001        self.vertices.clear();
1002        self.indices.clear();
1003        self.draw_calls.clear();
1004        self.shared_elements.clear(); // Clear registry for the new frame
1005        self.current_texture_id = None;
1006
1007        let time = self.start_time.elapsed().as_secs_f32();
1008        let (width, height) = (self.config.width as f32, self.config.height as f32);
1009        let dt = time - self.current_scene.time;
1010        self.current_scene.time = time;
1011        self.current_scene.delta_time = dt;
1012        self.current_scene.resolution = [width, height];
1013        self.current_scene.proj =
1014            glam::Mat4::orthographic_lh(0.0, width, height, 0.0, -100.0, 100.0);
1015
1016        self.queue.write_buffer(
1017            &self.scene_buffer,
1018            0,
1019            bytemuck::bytes_of(&self.current_scene),
1020        );
1021
1022        self.device
1023            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1024                label: Some("Surtr's Flaming Sword"),
1025            })
1026    }
1027
1028    /// Reset the internal clock (for interactive effects)
1029    pub fn reset_time(&mut self) {
1030        self.start_time = std::time::Instant::now();
1031    }
1032
1033    fn shatter_internal(
1034        &mut self,
1035        rect: Rect,
1036        pieces: u32,
1037        force: f32,
1038        color: [f32; 4],
1039        mode: u32,
1040    ) {
1041        // High-Fidelity Variable Particle Density
1042        let count = (pieces as f32).sqrt().ceil() as u32;
1043        let dw = rect.width / count as f32;
1044        let dh = rect.height / count as f32;
1045
1046        let c = self.apply_opacity(color);
1047
1048        for y in 0..count {
1049            for x in 0..count {
1050                let shard_rect = Rect {
1051                    x: rect.x + x as f32 * dw,
1052                    y: rect.y + y as f32 * dh,
1053                    width: dw,
1054                    height: dh,
1055                };
1056
1057                let uv = Rect {
1058                    x: x as f32 / count as f32,
1059                    y: y as f32 / count as f32,
1060                    width: 1.0 / count as f32,
1061                    height: 1.0 / count as f32,
1062                };
1063
1064                self.fill_rect_with_full_params(shard_rect, c, mode, None, force, uv);
1065            }
1066        }
1067    }
1068
1069    fn recursive_bolt(&mut self, from: [f32; 2], to: [f32; 2], depth: u32, color: [f32; 4]) {
1070        if depth == 0 {
1071            self.draw_lightning_segment(from, to, color);
1072            return;
1073        }
1074
1075        let mid_x = (from[0] + to[0]) * 0.5;
1076        let mid_y = (from[1] + to[1]) * 0.5;
1077
1078        let dx = to[0] - from[0];
1079        let dy = to[1] - from[1];
1080        let len = (dx * dx + dy * dy).sqrt();
1081
1082        // Perpendicular offset for jaggedness
1083        let offset_scale = len * 0.15;
1084        let seed = (from[0] * 12.9898 + from[1] * 78.233 + (depth as f32) * 37.11)
1085            .sin()
1086            .fract();
1087        let offset_x = -dy / len * (seed - 0.5) * offset_scale;
1088        let offset_y = dx / len * (seed - 0.5) * offset_scale;
1089
1090        let mid = [mid_x + offset_x, mid_y + offset_y];
1091
1092        self.recursive_bolt(from, mid, depth - 1, color);
1093        self.recursive_bolt(mid, to, depth - 1, color);
1094
1095        // 20% chance of a secondary branch
1096        if depth > 2 && seed > 0.8 {
1097            let branch_to = [
1098                mid[0] + offset_x * 2.0 + (seed * 100.0).sin() * 50.0,
1099                mid[1] + offset_y * 2.0 + (seed * 100.0).cos() * 50.0,
1100            ];
1101            self.recursive_bolt(mid, branch_to, depth - 2, color);
1102        }
1103    }
1104
1105    fn draw_lightning_segment(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
1106        let dx = to[0] - from[0];
1107        let dy = to[1] - from[1];
1108        let len = (dx * dx + dy * dy).sqrt();
1109        if len < 0.001 {
1110            return;
1111        }
1112
1113        let glow_width = 32.0;
1114        let core_width = 4.0;
1115        let c = self.apply_opacity(color);
1116
1117        // 1. Render Volumetric Glow (Cyan)
1118        let gnx = -dy / len * glow_width * 0.5;
1119        let gny = dx / len * glow_width * 0.5;
1120        let gp1 = [from[0] + gnx, from[1] + gny];
1121        let gp2 = [to[0] + gnx, to[1] + gny];
1122        let gp3 = [to[0] - gnx, to[1] - gny];
1123        let gp4 = [from[0] - gnx, from[1] - gny];
1124        self.push_oriented_quad(
1125            [gp1, gp2, gp3, gp4],
1126            c,
1127            9,
1128            Rect {
1129                x: 0.0,
1130                y: 0.0,
1131                width: 1.0,
1132                height: 1.0,
1133            },
1134        );
1135
1136        // 2. Render Blinding Core (White)
1137        let cnx = -dy / len * core_width * 0.5;
1138        let cny = dx / len * core_width * 0.5;
1139        let cp1 = [from[0] + cnx, from[1] + cny];
1140        let cp2 = [to[0] + cnx, to[1] + cny];
1141        let cp3 = [to[0] - cnx, to[1] - cny];
1142        let cp4 = [from[0] - cnx, from[1] - cny];
1143        self.push_oriented_quad(
1144            [cp1, cp2, cp3, cp4],
1145            [1.0, 1.0, 1.0, c[3]],
1146            0,
1147            Rect {
1148                x: 0.0,
1149                y: 0.0,
1150                width: 1.0,
1151                height: 1.0,
1152            },
1153        );
1154    }
1155
1156    fn push_oriented_quad(
1157        &mut self,
1158        points: [[f32; 2]; 4],
1159        color: [f32; 4],
1160        mode: u32,
1161        uv_rect: Rect,
1162    ) {
1163        let scissor = self.clip_stack.last().copied();
1164        let texture_id = None; // Oriented quads like lightning don't use textures yet
1165
1166        if self.draw_calls.is_empty()
1167            || self.current_texture_id != texture_id
1168            || self.draw_calls.last().unwrap().scissor_rect != scissor
1169        {
1170            self.current_texture_id = texture_id;
1171            self.draw_calls.push(DrawCall {
1172                texture_id,
1173                scissor_rect: scissor,
1174                index_start: self.indices.len() as u32,
1175                index_count: 0,
1176                is_glass: mode == 7,
1177                is_ui: mode == 6,
1178            });
1179        }
1180
1181        let uvs = [
1182            [uv_rect.x, uv_rect.y],
1183            [uv_rect.x + uv_rect.width, uv_rect.y],
1184            [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
1185            [uv_rect.x, uv_rect.y + uv_rect.height],
1186        ];
1187
1188        let screen = [self.config.width as f32, self.config.height as f32];
1189        let rect = Rect {
1190            x: points[0][0],
1191            y: points[0][1],
1192            width: 1.0,
1193            height: 1.0,
1194        };
1195
1196        for i in 0..4 {
1197            let px = points[i][0];
1198            let py = points[i][1];
1199
1200            self.vertices.push(Vertex {
1201                position: [px, py, 0.0],
1202                normal: [0.0, 0.0, 1.0],
1203                uv: uvs[i],
1204                color,
1205                mode,
1206                radius: 0.0,
1207                slice: [0.0, 0.0, 0.0, 1.0],
1208                logical: [px - rect.x, py - rect.y],
1209                size: [rect.width, rect.height],
1210                screen,
1211                clip: [-10000.0, -10000.0, 20000.0, 20000.0],
1212            });
1213        }
1214
1215        if let Some(call) = self.draw_calls.last_mut() {
1216            call.index_count += 6;
1217        }
1218    }
1219    fn get_texture_id(&self, name: &str) -> Option<u32> {
1220        self.texture_registry.get(name).copied()
1221    }
1222
1223    /// fill_rect_with_mode — Specialized rectangle drawing with mode-specific shader logic.
1224    pub fn fill_rect_with_mode(
1225        &mut self,
1226        rect: Rect,
1227        color: [f32; 4],
1228        mode: u32,
1229        texture_id: Option<u32>,
1230    ) {
1231        self.fill_rect_with_full_params(
1232            rect,
1233            color,
1234            mode,
1235            texture_id,
1236            0.0,
1237            Rect {
1238                x: 0.0,
1239                y: 0.0,
1240                width: 1.0,
1241                height: 1.0,
1242            },
1243        );
1244    }
1245
1246    fn fill_rect_with_full_params(
1247        &mut self,
1248        rect: Rect,
1249        color: [f32; 4],
1250        mode: u32,
1251        texture_id: Option<u32>,
1252        radius: f32,
1253        uv_rect: Rect,
1254    ) {
1255        let slice = self
1256            .slice_stack
1257            .last()
1258            .copied()
1259            .map(|(a, o)| [a, o, 1.0, 1.0])
1260            .unwrap_or([0.0, 0.0, 0.0, 1.0]);
1261        self.fill_rect_with_full_params_and_slice(
1262            rect, color, mode, texture_id, radius, uv_rect, slice,
1263        );
1264    }
1265
1266    fn fill_rect_with_full_params_and_slice(
1267        &mut self,
1268        rect: Rect,
1269        color: [f32; 4],
1270        mode: u32,
1271        texture_id: Option<u32>,
1272        radius: f32,
1273        uv_rect: Rect,
1274        slice: [f32; 4],
1275    ) {
1276        let scissor = self.clip_stack.last().copied();
1277
1278        let is_glass = mode == 7;
1279        let is_ui = mode == 6;
1280
1281        // Batching: check if we need to start a new DrawCall
1282        let last_call = self.draw_calls.last();
1283        let needs_new_call = self.draw_calls.is_empty()
1284            || self.current_texture_id != texture_id
1285            || last_call.unwrap().scissor_rect != scissor
1286            || last_call.unwrap().is_glass != is_glass
1287            || last_call.unwrap().is_ui != is_ui;
1288
1289        if needs_new_call {
1290            self.current_texture_id = texture_id;
1291            self.draw_calls.push(DrawCall {
1292                texture_id,
1293                scissor_rect: scissor,
1294                index_start: self.indices.len() as u32,
1295                index_count: 0,
1296                is_glass,
1297                is_ui,
1298            });
1299        }
1300
1301        let base_idx = self.vertices.len() as u32;
1302        let x1 = rect.x;
1303        let y1 = rect.y;
1304        let x2 = rect.x + rect.width;
1305        let y2 = rect.y + rect.height;
1306        let z = 0.0;
1307        let normal = [0.0, 0.0, 1.0];
1308        let screen = [self.config.width as f32, self.config.height as f32];
1309        let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
1310            x: -10000.0,
1311            y: -10000.0,
1312            width: 20000.0,
1313            height: 20000.0,
1314        });
1315        let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
1316
1317        self.vertices.push(Vertex {
1318            position: [x1, y1, z],
1319            normal,
1320            uv: [uv_rect.x, uv_rect.y],
1321            color,
1322            mode,
1323            radius,
1324            slice,
1325            logical: [0.0, 0.0],
1326            size: [rect.width, rect.height],
1327            screen,
1328            clip,
1329        });
1330        self.vertices.push(Vertex {
1331            position: [x2, y1, z],
1332            normal,
1333            uv: [uv_rect.x + uv_rect.width, uv_rect.y],
1334            color,
1335            mode,
1336            radius,
1337            slice,
1338            logical: [rect.width, 0.0],
1339            size: [rect.width, rect.height],
1340            screen,
1341            clip,
1342        });
1343        self.vertices.push(Vertex {
1344            position: [x2, y2, z],
1345            normal,
1346            uv: [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
1347            color,
1348            mode,
1349            radius,
1350            slice,
1351            logical: [rect.width, rect.height],
1352            size: [rect.width, rect.height],
1353            screen,
1354            clip,
1355        });
1356        self.vertices.push(Vertex {
1357            position: [x1, y2, z],
1358            normal,
1359            uv: [uv_rect.x, uv_rect.y + uv_rect.height],
1360            color,
1361            mode,
1362            radius,
1363            slice,
1364            logical: [0.0, rect.height],
1365            size: [rect.width, rect.height],
1366            screen,
1367            clip,
1368        });
1369
1370        self.indices.extend_from_slice(&[
1371            base_idx,
1372            base_idx + 1,
1373            base_idx + 2,
1374            base_idx,
1375            base_idx + 2,
1376            base_idx + 3,
1377        ]);
1378
1379        if let Some(call) = self.draw_calls.last_mut() {
1380            call.index_count += 6;
1381        }
1382    }
1383
1384    /// end_frame — Quench the blade by submitting the full Muspelheim multi-pass effect.
1385    pub fn end_frame(&mut self, mut encoder: wgpu::CommandEncoder) {
1386        self.queue
1387            .write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&self.vertices));
1388        self.queue
1389            .write_buffer(&self.index_buffer, 0, bytemuck::cast_slice(&self.indices));
1390
1391        let frame = match self.surface.get_current_texture() {
1392            wgpu::CurrentSurfaceTexture::Success(t) => t,
1393            wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
1394                self.surface.configure(&self.device, &self.config);
1395                t
1396            }
1397            wgpu::CurrentSurfaceTexture::Timeout
1398            | wgpu::CurrentSurfaceTexture::Outdated
1399            | wgpu::CurrentSurfaceTexture::Lost
1400            | wgpu::CurrentSurfaceTexture::Occluded
1401            | wgpu::CurrentSurfaceTexture::Validation => {
1402                self.surface.configure(&self.device, &self.config);
1403                return;
1404            }
1405        };
1406        let screen = frame
1407            .texture
1408            .create_view(&wgpu::TextureViewDescriptor::default());
1409
1410        // ── Pass 1: Opaque Background & Atmosphere ──────────────────────────
1411        {
1412            let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1413                label: Some("Surtr P1 Opaque Background"),
1414                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1415                    view: &self.scene_texture,
1416                    resolve_target: None,
1417                    ops: wgpu::Operations {
1418                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1419                        store: wgpu::StoreOp::Store,
1420                    },
1421                    depth_slice: None,
1422                })],
1423                depth_stencil_attachment: None,
1424                occlusion_query_set: None,
1425                timestamp_writes: None,
1426                multiview_mask: None,
1427            });
1428
1429            // 1a. Background Atmosphere
1430            p.set_pipeline(&self.background_pipeline);
1431            p.set_bind_group(0, &self.dummy_texture_bind_group, &[]);
1432            p.set_bind_group(1, &self.blur_env_bind_group_a, &[]); // Use previous frame's blur for background depth
1433            p.set_bind_group(2, &self.berserker_bind_group, &[]);
1434            p.draw(0..6, 0..1);
1435
1436            // 1b. Opaque Main Elements (non-glass, non-ui)
1437            if !self.draw_calls.is_empty() {
1438                p.set_pipeline(&self.pipeline);
1439                p.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1440                p.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1441                p.set_bind_group(1, &self.dummy_env_bind_group, &[]);
1442                p.set_bind_group(2, &self.berserker_bind_group, &[]);
1443
1444                for call in self.draw_calls.iter().filter(|c| !c.is_glass && !c.is_ui) {
1445                    let bg = if let Some(id) = call.texture_id {
1446                        self.texture_bind_groups
1447                            .get(id as usize)
1448                            .unwrap_or(&self.dummy_texture_bind_group)
1449                    } else {
1450                        &self.dummy_texture_bind_group
1451                    };
1452                    p.set_bind_group(0, bg, &[]);
1453                    p.draw_indexed(
1454                        call.index_start..call.index_start + call.index_count,
1455                        0,
1456                        0..1,
1457                    );
1458                }
1459            }
1460        }
1461
1462        // ── Pass 2: Backdrop Blur (Bifrost) ──────────────────────────────────
1463        // Capture the background into blur_texture_b
1464        {
1465            // First extract into texture_a
1466            let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1467                label: Some("Surtr Blur Extract"),
1468                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1469                    view: &self.blur_texture_a,
1470                    resolve_target: None,
1471                    ops: wgpu::Operations {
1472                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1473                        store: wgpu::StoreOp::Store,
1474                    },
1475                    depth_slice: None,
1476                })],
1477                ..Default::default()
1478            });
1479            p.set_pipeline(&self.bloom_extract_pipeline); // Use extract as a direct copy for now
1480            p.set_bind_group(0, &self.scene_texture_bind_group, &[]);
1481            p.set_bind_group(1, &self.dummy_env_bind_group, &[]);
1482            p.set_bind_group(2, &self.berserker_bind_group, &[]);
1483            p.draw(0..6, 0..1);
1484        }
1485
1486        let blur_iters: u32 = 4;
1487        for _i in 0..blur_iters {
1488            {
1489                let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1490                    label: Some("Blur H"),
1491                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1492                        view: &self.blur_texture_b,
1493                        resolve_target: None,
1494                        ops: wgpu::Operations {
1495                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1496                            store: wgpu::StoreOp::Store,
1497                        },
1498                        depth_slice: None,
1499                    })],
1500                    ..Default::default()
1501                });
1502                p.set_pipeline(&self.blur_h_pipeline);
1503                p.set_bind_group(0, &self.blur_bind_group_a, &[]);
1504                p.set_bind_group(1, &self.dummy_env_bind_group, &[]);
1505                p.set_bind_group(2, &self.berserker_bind_group, &[]);
1506                p.draw(0..6, 0..1);
1507            }
1508            {
1509                let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1510                    label: Some("Blur V"),
1511                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1512                        view: &self.blur_texture_a,
1513                        resolve_target: None,
1514                        ops: wgpu::Operations {
1515                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1516                            store: wgpu::StoreOp::Store,
1517                        },
1518                        depth_slice: None,
1519                    })],
1520                    ..Default::default()
1521                });
1522                p.set_pipeline(&self.blur_v_pipeline);
1523                p.set_bind_group(0, &self.blur_bind_group_b, &[]);
1524                p.set_bind_group(1, &self.dummy_env_bind_group, &[]);
1525                p.set_bind_group(2, &self.berserker_bind_group, &[]);
1526                p.draw(0..6, 0..1);
1527            }
1528        }
1529
1530        // ── Pass 3: Liquid Glass Elements ───────────────────────────────────
1531        {
1532            let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1533                label: Some("Surtr P3 Liquid Glass"),
1534                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1535                    view: &self.scene_texture, // RENDER OVER THE OPAQUE BACKGROUND
1536                    resolve_target: None,
1537                    ops: wgpu::Operations {
1538                        load: wgpu::LoadOp::Load,
1539                        store: wgpu::StoreOp::Store,
1540                    },
1541                    depth_slice: None,
1542                })],
1543                ..Default::default()
1544            });
1545
1546            p.set_pipeline(&self.pipeline);
1547            p.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1548            p.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1549            p.set_bind_group(1, &self.blur_env_bind_group_a, &[]); // Sample the freshly blurred backdrop
1550            p.set_bind_group(2, &self.berserker_bind_group, &[]);
1551
1552            for call in self.draw_calls.iter().filter(|c| c.is_glass) {
1553                let bg = if let Some(id) = call.texture_id {
1554                    self.texture_bind_groups
1555                        .get(id as usize)
1556                        .unwrap_or(&self.dummy_texture_bind_group)
1557                } else {
1558                    &self.dummy_texture_bind_group
1559                };
1560                p.set_bind_group(0, bg, &[]);
1561                p.draw_indexed(
1562                    call.index_start..call.index_start + call.index_count,
1563                    0,
1564                    0..1,
1565                );
1566            }
1567        }
1568
1569        // ── Pass 4: UI & Text Overlay ──────────────────────────────────────
1570        {
1571            let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1572                label: Some("Surtr P4 UI Layer"),
1573                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1574                    view: &self.scene_texture,
1575                    resolve_target: None,
1576                    ops: wgpu::Operations {
1577                        load: wgpu::LoadOp::Load,
1578                        store: wgpu::StoreOp::Store,
1579                    },
1580                    depth_slice: None,
1581                })],
1582                ..Default::default()
1583            });
1584
1585            p.set_pipeline(&self.pipeline);
1586            p.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1587            p.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1588            p.set_bind_group(1, &self.dummy_env_bind_group, &[]);
1589            p.set_bind_group(2, &self.berserker_bind_group, &[]);
1590
1591            for call in self.draw_calls.iter().filter(|c| c.is_ui) {
1592                let bg = if let Some(id) = call.texture_id {
1593                    self.texture_bind_groups
1594                        .get(id as usize)
1595                        .unwrap_or(&self.dummy_texture_bind_group)
1596                } else {
1597                    &self.dummy_texture_bind_group
1598                };
1599                p.set_bind_group(0, bg, &[]);
1600                p.draw_indexed(
1601                    call.index_start..call.index_start + call.index_count,
1602                    0,
1603                    0..1,
1604                );
1605            }
1606        }
1607
1608        // ── Pass 5: Bloom Extract (Complete Scene) ──────────────────────────
1609        {
1610            let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1611                label: Some("Surtr Bloom Extract"),
1612                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1613                    view: &self.blur_texture_a,
1614                    resolve_target: None,
1615                    ops: wgpu::Operations {
1616                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1617                        store: wgpu::StoreOp::Store,
1618                    },
1619                    depth_slice: None,
1620                })],
1621                ..Default::default()
1622            });
1623            p.set_pipeline(&self.bloom_extract_pipeline);
1624            p.set_bind_group(0, &self.scene_texture_bind_group, &[]);
1625            p.set_bind_group(1, &self.dummy_env_bind_group, &[]);
1626            p.set_bind_group(2, &self.berserker_bind_group, &[]);
1627            p.draw(0..6, 0..1);
1628        }
1629
1630        // ── Pass 6: Blur Bloom ──────────────────────────────────────────────
1631        for _ in 0..2 {
1632            {
1633                let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1634                    label: Some("Bloom Blur H"),
1635                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1636                        view: &self.blur_texture_b,
1637                        resolve_target: None,
1638                        ops: wgpu::Operations {
1639                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1640                            store: wgpu::StoreOp::Store,
1641                        },
1642                        depth_slice: None,
1643                    })],
1644                    ..Default::default()
1645                });
1646                p.set_pipeline(&self.blur_h_pipeline);
1647                p.set_bind_group(0, &self.blur_bind_group_a, &[]);
1648                p.set_bind_group(1, &self.dummy_env_bind_group, &[]);
1649                p.set_bind_group(2, &self.berserker_bind_group, &[]);
1650                p.draw(0..6, 0..1);
1651            }
1652            {
1653                let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1654                    label: Some("Bloom Blur V"),
1655                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1656                        view: &self.blur_texture_a,
1657                        resolve_target: None,
1658                        ops: wgpu::Operations {
1659                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1660                            store: wgpu::StoreOp::Store,
1661                        },
1662                        depth_slice: None,
1663                    })],
1664                    ..Default::default()
1665                });
1666                p.set_pipeline(&self.blur_v_pipeline);
1667                p.set_bind_group(0, &self.blur_bind_group_b, &[]);
1668                p.set_bind_group(1, &self.dummy_env_bind_group, &[]);
1669                p.set_bind_group(2, &self.berserker_bind_group, &[]);
1670                p.draw(0..6, 0..1);
1671            }
1672        }
1673
1674        // ── Pass 7: Final Composite ─────────────────────────────────────────
1675        {
1676            let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1677                label: Some("Surtr P7 Final Composite"),
1678                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1679                    view: &screen,
1680                    resolve_target: None,
1681                    ops: wgpu::Operations {
1682                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1683                        store: wgpu::StoreOp::Store,
1684                    },
1685                    depth_slice: None,
1686                })],
1687                ..Default::default()
1688            });
1689            p.set_pipeline(&self.composite_pipeline);
1690            p.set_bind_group(0, &self.scene_texture_bind_group, &[]); // Main Scene (Group 0 -> t_diffuse)
1691            p.set_bind_group(1, &self.blur_bind_group_a, &[]); // Bloom (Group 1 -> t_env)
1692            p.set_bind_group(2, &self.berserker_bind_group, &[]);
1693            p.draw(0..6, 0..1);
1694        }
1695
1696        self.queue.submit(Some(encoder.finish()));
1697        frame.present();
1698    }
1699}
1700
1701impl cvkg_core::Renderer for SurtrRenderer {
1702    /// fill_rect — Standard rectangle drawing method.
1703    fn fill_rect(&mut self, rect: Rect, color: [f32; 4]) {
1704        self.fill_rect_with_mode(rect, self.apply_opacity(color), 0, None);
1705    }
1706
1707    fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4]) {
1708        self.fill_rect_with_full_params(
1709            rect,
1710            self.apply_opacity(color),
1711            3,
1712            None,
1713            radius,
1714            Rect {
1715                x: 0.0,
1716                y: 0.0,
1717                width: 1.0,
1718                height: 1.0,
1719            },
1720        );
1721    }
1722
1723    fn fill_ellipse(&mut self, rect: Rect, color: [f32; 4]) {
1724        self.fill_rect_with_full_params(
1725            rect,
1726            self.apply_opacity(color),
1727            4,
1728            None,
1729            0.0,
1730            Rect {
1731                x: 0.0,
1732                y: 0.0,
1733                width: 1.0,
1734                height: 1.0,
1735            },
1736        );
1737    }
1738
1739    fn bifrost(&mut self, rect: Rect, blur: f32, _saturation: f32, opacity: f32) {
1740        // Calculate screen-space UVs for high-fidelity global refraction
1741        let screen_uv = Rect {
1742            x: rect.x / self.config.width as f32,
1743            y: rect.y / self.config.height as f32,
1744            width: rect.width / self.config.width as f32,
1745            height: rect.height / self.config.height as f32,
1746        };
1747        // Use mode 7 for high-fidelity background blur sampling
1748        // Use the blur parameter as corner radius for the glass panel
1749        self.fill_rect_with_full_params(rect, [1.0, 1.0, 1.0, opacity], 7, None, blur, screen_uv);
1750    }
1751
1752    fn stroke_rect(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
1753        let c = self.apply_opacity(color);
1754        let hw = stroke_width;
1755        // Top, bottom, left, right edge bars
1756        self.fill_rect_with_mode(
1757            Rect {
1758                x: rect.x,
1759                y: rect.y,
1760                width: rect.width,
1761                height: hw,
1762            },
1763            c,
1764            1,
1765            None,
1766        );
1767        self.fill_rect_with_mode(
1768            Rect {
1769                x: rect.x,
1770                y: rect.y + rect.height - hw,
1771                width: rect.width,
1772                height: hw,
1773            },
1774            c,
1775            1,
1776            None,
1777        );
1778        self.fill_rect_with_mode(
1779            Rect {
1780                x: rect.x,
1781                y: rect.y,
1782                width: hw,
1783                height: rect.height,
1784            },
1785            c,
1786            1,
1787            None,
1788        );
1789        self.fill_rect_with_mode(
1790            Rect {
1791                x: rect.x + rect.width - hw,
1792                y: rect.y,
1793                width: hw,
1794                height: rect.height,
1795            },
1796            c,
1797            1,
1798            None,
1799        );
1800    }
1801
1802    fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4], stroke_width: f32) {
1803        self.fill_rect_with_full_params(
1804            rect,
1805            self.apply_opacity(color),
1806            17,
1807            None,
1808            radius,
1809            Rect {
1810                x: stroke_width,
1811                y: 0.0,
1812                width: 0.0,
1813                height: 0.0,
1814            },
1815        );
1816    }
1817
1818    fn stroke_ellipse(&mut self, _rect: Rect, _color: [f32; 4], _stroke_width: f32) {
1819        // Future: Implement stroked SDFs.
1820    }
1821
1822    fn draw_linear_gradient(
1823        &mut self,
1824        rect: Rect,
1825        start_color: [f32; 4],
1826        end_color: [f32; 4],
1827        angle: f32,
1828    ) {
1829        self.fill_rect_with_full_params_and_slice(
1830            rect,
1831            self.apply_opacity(start_color),
1832            15,
1833            None,
1834            0.0,
1835            Rect {
1836                x: angle,
1837                y: 0.0,
1838                width: 1.0,
1839                height: 1.0,
1840            },
1841            end_color,
1842        );
1843    }
1844
1845    fn draw_radial_gradient(&mut self, rect: Rect, inner_color: [f32; 4], outer_color: [f32; 4]) {
1846        self.fill_rect_with_full_params_and_slice(
1847            rect,
1848            self.apply_opacity(inner_color),
1849            16,
1850            None,
1851            0.0,
1852            Rect {
1853                x: 0.0,
1854                y: 0.0,
1855                width: 1.0,
1856                height: 1.0,
1857            },
1858            outer_color,
1859        );
1860    }
1861
1862    fn draw_drop_shadow(
1863        &mut self,
1864        rect: Rect,
1865        radius: f32,
1866        color: [f32; 4],
1867        blur: f32,
1868        spread: f32,
1869    ) {
1870        let margin = blur + spread;
1871        let inflated = Rect {
1872            x: rect.x - margin,
1873            y: rect.y - margin,
1874            width: rect.width + margin * 2.0,
1875            height: rect.height + margin * 2.0,
1876        };
1877        // uv.x = total margin (for SDF offset), uv.y = blur width (for falloff)
1878        self.fill_rect_with_full_params(
1879            inflated,
1880            self.apply_opacity(color),
1881            18,
1882            None,
1883            radius,
1884            Rect {
1885                x: margin,
1886                y: blur,
1887                width: 0.0,
1888                height: 0.0,
1889            },
1890        );
1891    }
1892
1893    fn stroke_dashed_rounded_rect(
1894        &mut self,
1895        rect: Rect,
1896        radius: f32,
1897        color: [f32; 4],
1898        width: f32,
1899        dash: f32,
1900        gap: f32,
1901    ) {
1902        self.fill_rect_with_full_params(
1903            rect,
1904            self.apply_opacity(color),
1905            19,
1906            None,
1907            radius,
1908            Rect {
1909                x: width,
1910                y: dash,
1911                width: gap,
1912                height: 0.0,
1913            },
1914        );
1915    }
1916
1917    fn draw_9slice(
1918        &mut self,
1919        image_name: &str,
1920        rect: Rect,
1921        left: f32,
1922        top: f32,
1923        right: f32,
1924        bottom: f32,
1925    ) {
1926        let c = self.apply_opacity([1.0, 1.0, 1.0, 1.0]);
1927        let tid = self.get_texture_id(image_name);
1928        self.fill_rect_with_full_params(
1929            rect,
1930            c,
1931            20,
1932            tid,
1933            bottom,
1934            Rect {
1935                x: left,
1936                y: top,
1937                width: right,
1938                height: 0.0,
1939            },
1940        );
1941    }
1942
1943    fn draw_line(
1944        &mut self,
1945        x1: f32,
1946        y1: f32,
1947        x2: f32,
1948        y2: f32,
1949        color: [f32; 4],
1950        stroke_width: f32,
1951    ) {
1952        let dx = x2 - x1;
1953        let dy = y2 - y1;
1954        let len = (dx * dx + dy * dy).sqrt();
1955        if len < 0.001 {
1956            return;
1957        }
1958
1959        let _angle = dy.atan2(dx).to_degrees();
1960        let c = self.apply_opacity(color);
1961
1962        // Push an oriented quad by using the Mjolnir Slice infrastructure or
1963        // by calculating rotated vertices. For now, we use a simple rotation push.
1964        // In a future pass, we will add 'rotation' to the Vertex struct for batching.
1965        // For now, we use the centered rect logic.
1966        self.fill_rect_with_mode(
1967            Rect {
1968                x: (x1 + x2) / 2.0 - len / 2.0,
1969                y: (y1 + y2) / 2.0 - stroke_width / 2.0,
1970                width: len,
1971                height: stroke_width,
1972            },
1973            c,
1974            1, // Gungnir Mode for glowing lines
1975            None,
1976        );
1977    }
1978
1979    fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
1980        // High-Fidelity Text Forge: Layout -> Rasterize -> Atlas -> Quad
1981        let mut buffer =
1982            cosmic_text::Buffer::new(&mut self.font_system, cosmic_text::Metrics::new(size, size));
1983        // Use Basic shaping for 'SYSTEM' readouts to avoid aggressive kerning pull-in
1984        buffer.set_text(
1985            &mut self.font_system,
1986            text,
1987            &cosmic_text::Attrs::new(),
1988            cosmic_text::Shaping::Basic,
1989        );
1990        buffer.shape_until_scroll(&mut self.font_system, false);
1991
1992        let c = self.apply_opacity(color);
1993
1994        let mut glyph_idx = 0;
1995        for run in buffer.layout_runs() {
1996            for glyph in run.glyphs {
1997                // Adaptive Berserker Tracking: More space for small text, tight for headers
1998                let tracking = (2.8 - 0.22 * (size - 16.0)).max(0.6);
1999                let x_offset = x + (glyph_idx as f32 * tracking);
2000
2001                let physical_glyph = glyph.physical((x_offset, y), 1.0);
2002                let cache_key = physical_glyph.cache_key;
2003
2004                // Check cache or rasterize (Keyed by the full CacheKey to support multiple sizes)
2005                let (uv_rect, w, h) = if let Some(info) = self.text_cache.get(&cache_key) {
2006                    *info
2007                } else {
2008                    // Rasterize new glyph
2009                    if let Some(image) =
2010                        self.swash_cache.get_image(&mut self.font_system, cache_key)
2011                    {
2012                        let (gx, _gy) = self.text_atlas_pos;
2013                        let gw = image.placement.width;
2014                        let gh = image.placement.height;
2015
2016                        // Simple grid packing (Phase 2 Forge)
2017                        if gx + gw > 1024 {
2018                            self.text_atlas_pos.0 = 0;
2019                            self.text_atlas_pos.1 += 64; // Max glyph height
2020                        }
2021                        let (nx, ny) = self.text_atlas_pos;
2022
2023                        self.queue.write_texture(
2024                            wgpu::TexelCopyTextureInfo {
2025                                texture: &self.text_atlas_tex,
2026                                mip_level: 0,
2027                                origin: wgpu::Origin3d { x: nx, y: ny, z: 0 },
2028                                aspect: wgpu::TextureAspect::All,
2029                            },
2030                            &image.data,
2031                            wgpu::TexelCopyBufferLayout {
2032                                offset: 0,
2033                                bytes_per_row: Some(gw),
2034                                rows_per_image: Some(gh),
2035                            },
2036                            wgpu::Extent3d {
2037                                width: gw,
2038                                height: gh,
2039                                depth_or_array_layers: 1,
2040                            },
2041                        );
2042
2043                        let info = (
2044                            Rect {
2045                                x: nx as f32 / 1024.0,
2046                                y: ny as f32 / 1024.0,
2047                                width: gw as f32 / 1024.0,
2048                                height: gh as f32 / 1024.0,
2049                            },
2050                            gw as f32,
2051                            gh as f32,
2052                        );
2053                        self.text_cache.insert(cache_key, info);
2054                        self.text_atlas_pos.0 += gw + 2;
2055                        info
2056                    } else {
2057                        (Rect::zero(), 0.0, 0.0)
2058                    }
2059                };
2060
2061                if w > 0.0 {
2062                    let glyph_rect = Rect {
2063                        x: physical_glyph.x as f32,
2064                        y: physical_glyph.y as f32,
2065                        width: w,
2066                        height: h,
2067                    };
2068                    let tid = self.get_texture_id("__text_atlas");
2069                    self.fill_rect_with_full_params(glyph_rect, c, 6, tid, 0.0, uv_rect); // Mode 6 = Text
2070                }
2071                glyph_idx += 1;
2072            }
2073        }
2074    }
2075
2076    /// measure_text — Calculates the dimensions of a text string without rendering.
2077    /// This is essential for layout engines to perform pre-pass calculations.
2078    fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
2079        let mut buffer =
2080            cosmic_text::Buffer::new(&mut self.font_system, cosmic_text::Metrics::new(size, size));
2081        buffer.set_text(
2082            &mut self.font_system,
2083            text,
2084            &cosmic_text::Attrs::new(),
2085            cosmic_text::Shaping::Advanced,
2086        );
2087        buffer.shape_until_scroll(&mut self.font_system, false);
2088
2089        let mut width = 0.0f32;
2090        let mut height = 0.0f32;
2091
2092        for run in buffer.layout_runs() {
2093            width = width.max(run.line_w);
2094            height += size;
2095        }
2096
2097        (width, height)
2098    }
2099
2100    fn draw_texture(&mut self, texture_id: u32, rect: Rect) {
2101        self.fill_rect_with_full_params(
2102            rect,
2103            [1.0, 1.0, 1.0, 1.0],
2104            2,
2105            Some(texture_id),
2106            0.0,
2107            Rect {
2108                x: 0.0,
2109                y: 0.0,
2110                width: 1.0,
2111                height: 1.0,
2112            },
2113        );
2114    }
2115
2116    fn draw_image(&mut self, image_name: &str, rect: Rect) {
2117        let tid = self.get_texture_id(image_name);
2118        self.fill_rect_with_full_params(
2119            rect,
2120            [1.0, 1.0, 1.0, 1.0],
2121            2,
2122            tid,
2123            0.0,
2124            Rect {
2125                x: 0.0,
2126                y: 0.0,
2127                width: 1.0,
2128                height: 1.0,
2129            },
2130        );
2131    }
2132
2133    /// load_image — Proactively pushes a raw asset into the GPU texture registry.
2134    /// This method is idempotent; it checks the cache to prevent redundant allocations.
2135    fn load_image(&mut self, name: &str, data: &[u8]) {
2136        if self.texture_registry.contains_key(name) {
2137            return;
2138        }
2139        let img = image::load_from_memory(data)
2140            .expect("Failed to load image")
2141            .to_rgba8();
2142        let (width, height) = img.dimensions();
2143        let size = wgpu::Extent3d {
2144            width,
2145            height,
2146            depth_or_array_layers: 1,
2147        };
2148        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
2149            label: Some(name),
2150            size,
2151            mip_level_count: 1,
2152            sample_count: 1,
2153            dimension: wgpu::TextureDimension::D2,
2154            format: wgpu::TextureFormat::Rgba8UnormSrgb,
2155            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2156            view_formats: &[],
2157        });
2158        self.queue.write_texture(
2159            wgpu::TexelCopyTextureInfo {
2160                texture: &texture,
2161                mip_level: 0,
2162                origin: wgpu::Origin3d::ZERO,
2163                aspect: wgpu::TextureAspect::All,
2164            },
2165            &img,
2166            wgpu::TexelCopyBufferLayout {
2167                offset: 0,
2168                bytes_per_row: Some(4 * width),
2169                rows_per_image: Some(height),
2170            },
2171            size,
2172        );
2173        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
2174        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
2175            address_mode_u: wgpu::AddressMode::ClampToEdge,
2176            address_mode_v: wgpu::AddressMode::ClampToEdge,
2177            mag_filter: wgpu::FilterMode::Linear,
2178            ..Default::default()
2179        });
2180        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2181            layout: &self.texture_bind_group_layout,
2182            entries: &[
2183                wgpu::BindGroupEntry {
2184                    binding: 0,
2185                    resource: wgpu::BindingResource::TextureView(&view),
2186                },
2187                wgpu::BindGroupEntry {
2188                    binding: 1,
2189                    resource: wgpu::BindingResource::Sampler(&sampler),
2190                },
2191            ],
2192            label: Some(name),
2193        });
2194        self.texture_bind_groups.push(bind_group);
2195        let id = (self.texture_bind_groups.len() - 1) as u32;
2196        self.texture_registry.insert(name.to_string(), id);
2197    }
2198
2199    fn push_clip_rect(&mut self, rect: Rect) {
2200        self.clip_stack.push(rect);
2201    }
2202
2203    fn pop_clip_rect(&mut self) {
2204        self.clip_stack.pop();
2205    }
2206
2207    fn push_opacity(&mut self, opacity: f32) {
2208        let current = self.opacity_stack.last().copied().unwrap_or(1.0);
2209        self.opacity_stack.push(current * opacity);
2210    }
2211
2212    fn pop_opacity(&mut self) {
2213        self.opacity_stack.pop();
2214    }
2215
2216    fn set_theme(&mut self, theme: ColorTheme) {
2217        self.current_theme = theme;
2218        self.queue
2219            .write_buffer(&self.theme_buffer, 0, bytemuck::bytes_of(&theme));
2220    }
2221
2222    fn set_rage(&mut self, rage: f32) {
2223        self.current_scene.berzerker_rage = rage;
2224        // scene_buffer is updated every frame in begin_frame, so no need to write here
2225    }
2226
2227    fn trigger_shatter_event(&mut self, origin: [f32; 2], force: f32) {
2228        self.current_scene.shatter_origin = origin;
2229        self.current_scene.shatter_time = self.current_scene.time;
2230        self.current_scene.shatter_force = force;
2231    }
2232
2233    /// push_mjolnir_slice — Pushes a geometric clipping plane onto the stack.
2234    /// All subsequent draw calls will be sliced by this plane until it is popped.
2235    fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
2236        self.slice_stack.push((angle, offset));
2237    }
2238
2239    /// pop_mjolnir_slice — Removes the top-most geometric clipping plane from the stack.
2240    fn pop_mjolnir_slice(&mut self) {
2241        self.slice_stack.pop();
2242    }
2243
2244    fn mjolnir_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
2245        self.shatter_internal(rect, pieces, force, color, 8);
2246    }
2247
2248    fn mjolnir_fluid_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
2249        self.shatter_internal(rect, pieces, force, color, 11);
2250    }
2251
2252    fn draw_mjolnir_bolt(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
2253        self.recursive_bolt(from, to, 4, color);
2254    }
2255
2256    fn upload_data_texture(&mut self, id: &str, data: &[f32], width: u32, height: u32) {
2257        let size = wgpu::Extent3d {
2258            width,
2259            height,
2260            depth_or_array_layers: 1,
2261        };
2262        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
2263            label: Some(id),
2264            size,
2265            mip_level_count: 1,
2266            sample_count: 1,
2267            dimension: wgpu::TextureDimension::D2,
2268            format: wgpu::TextureFormat::R32Float,
2269            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2270            view_formats: &[],
2271        });
2272        self.queue.write_texture(
2273            wgpu::TexelCopyTextureInfo {
2274                texture: &texture,
2275                mip_level: 0,
2276                origin: wgpu::Origin3d::ZERO,
2277                aspect: wgpu::TextureAspect::All,
2278            },
2279            bytemuck::cast_slice(data),
2280            wgpu::TexelCopyBufferLayout {
2281                offset: 0,
2282                bytes_per_row: Some(4 * width),
2283                rows_per_image: Some(height),
2284            },
2285            size,
2286        );
2287        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
2288        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
2289            address_mode_u: wgpu::AddressMode::ClampToEdge,
2290            address_mode_v: wgpu::AddressMode::ClampToEdge,
2291            mag_filter: wgpu::FilterMode::Linear,
2292            ..Default::default()
2293        });
2294        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2295            layout: &self.texture_bind_group_layout,
2296            entries: &[
2297                wgpu::BindGroupEntry {
2298                    binding: 0,
2299                    resource: wgpu::BindingResource::TextureView(&view),
2300                },
2301                wgpu::BindGroupEntry {
2302                    binding: 1,
2303                    resource: wgpu::BindingResource::Sampler(&sampler),
2304                },
2305            ],
2306            label: Some(id),
2307        });
2308        self.texture_bind_groups.push(bind_group);
2309        let tid = (self.texture_bind_groups.len() - 1) as u32;
2310        self.texture_registry.insert(id.to_string(), tid);
2311    }
2312
2313    fn draw_heatmap(&mut self, texture_id: &str, rect: Rect, _palette: &str) {
2314        let tid = self.get_texture_id(texture_id);
2315        self.fill_rect_with_mode(rect, [1.0, 1.0, 1.0, 1.0], 12, tid);
2316    }
2317
2318    fn draw_mesh(&mut self, mesh: &Mesh, color: [f32; 4], transform: glam::Mat4) {
2319        let base_idx = self.vertices.len() as u32;
2320        let screen = [self.config.width as f32, self.config.height as f32];
2321
2322        for i in 0..mesh.vertices.len() {
2323            let pos = transform.transform_point3(glam::Vec3::from(mesh.vertices[i]));
2324            let norm = transform.transform_vector3(glam::Vec3::from(mesh.normals[i]));
2325
2326            self.vertices.push(Vertex {
2327                position: pos.to_array(),
2328                normal: norm.to_array(),
2329                uv: [0.0, 0.0],
2330                color,
2331                mode: 13, // Mode 13: 3D Surface
2332                radius: 0.0,
2333                slice: [0.0, 0.0, 0.0, 1.0],
2334                logical: [0.0, 0.0],
2335                size: [0.0, 0.0],
2336                screen,
2337                clip: [-10000.0, -10000.0, 20000.0, 20000.0],
2338            });
2339        }
2340
2341        for idx in &mesh.indices {
2342            self.indices.push(base_idx + idx);
2343        }
2344
2345        if self.draw_calls.is_empty() || self.current_texture_id.is_some() {
2346            self.current_texture_id = None;
2347            self.draw_calls.push(DrawCall {
2348                texture_id: None,
2349                scissor_rect: self.clip_stack.last().copied(),
2350                index_start: (self.indices.len() as u32) - (mesh.indices.len() as u32),
2351                index_count: mesh.indices.len() as u32,
2352                is_glass: false,
2353                is_ui: false,
2354            });
2355        } else {
2356            self.draw_calls.last_mut().unwrap().index_count += mesh.indices.len() as u32;
2357        }
2358    }
2359
2360    fn register_shared_element(&mut self, id: &str, rect: Rect) {
2361        self.shared_elements.insert(id.to_string(), rect);
2362    }
2363}
2364
2365impl Drop for SurtrRenderer {
2366    fn drop(&mut self) {
2367        // Ensure GPU is idle before dropping to avoid Swapchain semaphore panics
2368        let _ = self.device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None });
2369    }
2370}
2371
2372impl cvkg_core::FrameRenderer<wgpu::CommandEncoder> for SurtrRenderer {
2373    fn begin_frame(&mut self) -> wgpu::CommandEncoder {
2374        self.begin_frame()
2375    }
2376
2377    fn end_frame(&mut self, encoder: wgpu::CommandEncoder) {
2378        self.end_frame(encoder)
2379    }
2380}
2381
2382impl SurtrRenderer {
2383    /// Returns the current effective opacity (product of all stacked values).
2384    fn apply_opacity(&self, mut color: [f32; 4]) -> [f32; 4] {
2385        if let Some(&alpha) = self.opacity_stack.last() {
2386            color[3] *= alpha;
2387        }
2388        color
2389    }
2390}