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