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::{Rect, ColorTheme, SceneUniforms, Mesh};
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,
41    Node, NodeId, Role, Tree, TreeId, TreeUpdate,
42};
43pub use accesskit_winit::Adapter as ShieldWallAdapter;
44
45
46#[repr(C)]
47#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
48pub struct Vertex {
49    pub position: [f32; 3],
50    pub normal:   [f32; 3],
51    pub uv:       [f32; 2],
52    pub color:    [f32; 4],
53    pub mode:     u32,
54    pub radius:   f32,
55    pub slice:    [f32; 3],
56    pub logical:  [f32; 2],
57    pub size:     [f32; 2],
58    pub screen:   [f32; 2],
59    pub clip:     [f32; 4], // [x, y, width, height]
60}
61
62/// Represents a single batched GPU draw call.
63/// Batches are broken whenever the active texture or primitive mode changes.
64#[derive(Debug, Clone)]
65struct DrawCall {
66    pub texture_id: Option<u32>,
67    pub scissor_rect: Option<Rect>,
68    pub index_start: u32,
69    pub index_count: u32,
70}
71
72impl Vertex {
73    const ATTRIBUTES: [wgpu::VertexAttribute; 11] = wgpu::vertex_attr_array![
74        0 => Float32x3, // position
75        1 => Float32x3, // normal
76        2 => Float32x2, // uv
77        3 => Float32x4, // color
78        4 => Uint32,    // mode
79        5 => Float32,   // radius
80        6 => Float32x3, // slice
81        7 => Float32x2, // logical
82        8 => Float32x2, // size
83        9 => Float32x2, // screen
84        10 => Float32x4 // clip
85    ];
86
87    fn desc() -> wgpu::VertexBufferLayout<'static> {
88        wgpu::VertexBufferLayout {
89            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
90            step_mode: wgpu::VertexStepMode::Vertex,
91            attributes: &Self::ATTRIBUTES,
92        }
93    }
94}
95
96/// SurtrRenderer implements the high-performance GPU backend.
97pub struct SurtrRenderer {
98    device: Arc<wgpu::Device>,
99    queue: Arc<wgpu::Queue>,
100    surface: wgpu::Surface<'static>,
101    config: wgpu::SurfaceConfiguration,
102
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<u32, (Rect, f32, f32)>,
115    text_atlas_pos: (u32, u32),
116    
117    // Niflheim Resources
118    dummy_bind_group: wgpu::BindGroup,
119    texture_bind_group_layout: wgpu::BindGroupLayout,
120    texture_bind_groups: Vec<wgpu::BindGroup>,
121    texture_registry: std::collections::HashMap<String, u32>,
122    shared_elements: std::collections::HashMap<String, cvkg_core::Rect>,
123
124    // The Forge's Anvil (GPU Buffers)
125    vertex_buffer: wgpu::Buffer,
126    index_buffer: wgpu::Buffer,
127    vertices: Vec<Vertex>,
128    indices: Vec<u32>,
129    draw_calls: Vec<DrawCall>,
130    current_texture_id: Option<u32>,
131
132    // Opacity stack: each push multiplies into the current effective alpha.
133    opacity_stack: Vec<f32>,
134    // Clip rect stack: used for batched scissoring.
135    clip_stack: Vec<Rect>,
136    // Mjolnir Slice stack: (angle, offset)
137    slice_stack: Vec<(f32, f32)>,
138
139    // The Forge's Heart (Berserker State)
140    theme_buffer: wgpu::Buffer,
141    scene_buffer: wgpu::Buffer,
142    berserker_bind_group: wgpu::BindGroup,
143    #[allow(dead_code)]
144    berserker_bind_group_layout: wgpu::BindGroupLayout,
145    start_time: std::time::Instant,
146    current_theme: ColorTheme,
147    current_scene: SceneUniforms,
148
149    // Muspelheim Pipelines
150    pipeline: wgpu::RenderPipeline,
151    background_pipeline: wgpu::RenderPipeline,
152    bloom_extract_pipeline: wgpu::RenderPipeline,
153    blur_h_pipeline: wgpu::RenderPipeline,
154    blur_v_pipeline: wgpu::RenderPipeline,
155    composite_pipeline: wgpu::RenderPipeline,
156
157    // Muspelheim Textures & Bind Groups
158    blur_texture_a: wgpu::TextureView,
159    blur_texture_b: wgpu::TextureView,
160    blur_bind_group_a: wgpu::BindGroup,
161    blur_bind_group_b: wgpu::BindGroup,
162    scene_texture: wgpu::TextureView,
163    scene_bind_group: wgpu::BindGroup,
164    scene_texture_bind_group: wgpu::BindGroup,
165}
166
167const MAX_VERTICES: usize = 100_000;
168const MAX_INDICES: usize = 150_000;
169
170impl SurtrRenderer {
171    /// Forge a new SurtrRenderer from a winit window.
172    pub async fn forge(window: Arc<winit::window::Window>) -> Self {
173        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
174            backends: wgpu::Backends::all(),
175            flags: wgpu::InstanceFlags::default(),
176            backend_options: wgpu::BackendOptions::default(),
177            display: None,
178            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
179        });
180        
181        let surface = instance.create_surface(window.clone()).expect("Failed to create surface");
182        
183        let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
184            power_preference: wgpu::PowerPreference::HighPerformance,
185            compatible_surface: Some(&surface),
186            force_fallback_adapter: false,
187        }).await.expect("Failed to find a suitable GPU for Surtr");
188
189        let (device, queue) = adapter.request_device(
190            &wgpu::DeviceDescriptor {
191                label: Some("Surtr Forge"),
192                required_features: wgpu::Features::empty(),
193                required_limits: wgpu::Limits::default(),
194                memory_hints: wgpu::MemoryHints::default(),
195                experimental_features: wgpu::ExperimentalFeatures::disabled(),
196                trace: wgpu::Trace::Off,
197            },
198        ).await.expect("Failed to create Surtr device");
199
200        let device = Arc::new(device);
201        let queue = Arc::new(queue);
202        
203        let size = window.inner_size();
204        let surface_caps = surface.get_capabilities(&adapter);
205        let surface_format = surface_caps.formats.iter()
206            .find(|f| f.is_srgb())
207            .copied()
208            .unwrap_or(surface_caps.formats[0]);
209            
210        let config = wgpu::SurfaceConfiguration {
211            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
212            format: surface_format,
213            width: size.width,
214            height: size.height,
215            present_mode: wgpu::PresentMode::Fifo,
216            alpha_mode: surface_caps.alpha_modes[0],
217            view_formats: vec![],
218            desired_maximum_frame_latency: 2,
219        };
220        surface.configure(&device, &config);
221
222        // Load the Muspelheim Shaders
223        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
224            label: Some("Muspelheim Main Shader"),
225            source: wgpu::ShaderSource::Wgsl(include_str!("shaders.wgsl").into()),
226        });
227
228        // Niflheim Bind Group Layout (for textures/samplers)
229        let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
230            entries: &[
231                wgpu::BindGroupLayoutEntry {
232                    binding: 0,
233                    visibility: wgpu::ShaderStages::FRAGMENT,
234                    ty: wgpu::BindingType::Texture {
235                        multisampled: false,
236                        view_dimension: wgpu::TextureViewDimension::D2,
237                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
238                    },
239                    count: None,
240                },
241                wgpu::BindGroupLayoutEntry {
242                    binding: 1,
243                    visibility: wgpu::ShaderStages::FRAGMENT,
244                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
245                    count: None,
246                },
247            ],
248            label: Some("Niflheim Texture Bind Group Layout"),
249        });
250
251        // Environment Bind Group Layout (for blurred background / Bifrost)
252        // Environment Bind Group Layout (for blurred background / Bifrost)
253        let env_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
254            entries: &[
255                wgpu::BindGroupLayoutEntry {
256                    binding: 0,
257                    visibility: wgpu::ShaderStages::FRAGMENT,
258                    ty: wgpu::BindingType::Texture {
259                        multisampled: false,
260                        view_dimension: wgpu::TextureViewDimension::D2,
261                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
262                    },
263                    count: None,
264                },
265                wgpu::BindGroupLayoutEntry {
266                    binding: 1,
267                    visibility: wgpu::ShaderStages::FRAGMENT,
268                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
269                    count: None,
270                },
271            ],
272            label: Some("Surtr Environment Bind Group Layout"),
273        });
274
275        let berserker_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
276            entries: &[
277                wgpu::BindGroupLayoutEntry {
278                    binding: 0,
279                    visibility: wgpu::ShaderStages::FRAGMENT,
280                    ty: wgpu::BindingType::Buffer {
281                        ty: wgpu::BufferBindingType::Uniform,
282                        has_dynamic_offset: false,
283                        min_binding_size: None,
284                    },
285                    count: None,
286                },
287                wgpu::BindGroupLayoutEntry {
288                    binding: 1,
289                    visibility: wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::VERTEX,
290                    ty: wgpu::BindingType::Buffer {
291                        ty: wgpu::BufferBindingType::Uniform,
292                        has_dynamic_offset: false,
293                        min_binding_size: None,
294                    },
295                    count: None,
296                },
297            ],
298            label: Some("Surtr Berserker Bind Group Layout"),
299        });
300
301        // Pipeline setup
302        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
303            label: Some("Surtr Main Pipeline Layout"),
304            bind_group_layouts: &[Some(&texture_bind_group_layout), Some(&env_bind_group_layout), Some(&berserker_bind_group_layout)],
305            immediate_size: 0,
306        });
307
308        // Specialized layout for post-processing (Bloom Extract, Blur) which only need Group 0 + Globals
309        let post_process_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
310            label: Some("Muspelheim Post Process Layout"),
311            bind_group_layouts: &[
312                Some(&texture_bind_group_layout),
313                Some(&texture_bind_group_layout),
314                Some(&berserker_bind_group_layout),
315            ],
316            immediate_size: 0,
317        });
318
319        // Specialized layout for composite (Blur + Scene)
320        let composite_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
321            label: Some("Muspelheim Composite Layout"),
322            bind_group_layouts: &[
323                Some(&texture_bind_group_layout),
324                Some(&texture_bind_group_layout),
325                Some(&berserker_bind_group_layout),
326            ],
327            immediate_size: 0,
328        });
329
330        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
331            label: Some("Surtr Main Pipeline"),
332            layout: Some(&pipeline_layout),
333            vertex: wgpu::VertexState {
334                module: &shader,
335                entry_point: Some("vs_main"),
336                buffers: &[Vertex::desc()],
337                compilation_options: wgpu::PipelineCompilationOptions::default(),
338            },
339            fragment: Some(wgpu::FragmentState {
340                module: &shader,
341                entry_point: Some("fs_main"),
342                targets: &[Some(wgpu::ColorTargetState {
343                    format: config.format,
344                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
345                    write_mask: wgpu::ColorWrites::ALL,
346                })],
347                compilation_options: wgpu::PipelineCompilationOptions::default(),
348            }),
349            primitive: wgpu::PrimitiveState::default(),
350            depth_stencil: None,
351            multisample: wgpu::MultisampleState::default(),
352            multiview_mask: None,
353            cache: None,
354        });
355
356        let background_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
357            label: Some("Surtr Background Pipeline"),
358            layout: Some(&pipeline_layout),
359            vertex: wgpu::VertexState {
360                module: &shader,
361                entry_point: Some("vs_fullscreen"),
362                buffers: &[],
363                compilation_options: wgpu::PipelineCompilationOptions::default(),
364            },
365            fragment: Some(wgpu::FragmentState {
366                module: &shader,
367                entry_point: Some("fs_background"),
368                targets: &[Some(wgpu::ColorTargetState {
369                    format: config.format,
370                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
371                    write_mask: wgpu::ColorWrites::ALL,
372                })],
373                compilation_options: wgpu::PipelineCompilationOptions::default(),
374            }),
375            primitive: wgpu::PrimitiveState::default(),
376            depth_stencil: None,
377            multisample: wgpu::MultisampleState::default(),
378            multiview_mask: None,
379            cache: None,
380        });
381
382        // Muspelheim Bloom Extract Pipeline
383        let bloom_extract_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
384            label: Some("Muspelheim Bloom Extract"),
385            layout: Some(&post_process_layout),
386            vertex: wgpu::VertexState {
387                module: &shader,
388                entry_point: Some("vs_fullscreen"),
389                buffers: &[],
390                compilation_options: wgpu::PipelineCompilationOptions::default(),
391            },
392            fragment: Some(wgpu::FragmentState {
393                module: &shader,
394                entry_point: Some("fs_bloom_extract"),
395                targets: &[Some(wgpu::ColorTargetState {
396                    format: config.format,
397                    blend: None,
398                    write_mask: wgpu::ColorWrites::ALL,
399                })],
400                compilation_options: wgpu::PipelineCompilationOptions::default(),
401            }),
402            primitive: wgpu::PrimitiveState::default(),
403            depth_stencil: None,
404            multisample: wgpu::MultisampleState::default(),
405            multiview_mask: None,
406            cache: None,
407        });
408
409        // Muspelheim Blur Pipelines (H and V)
410        let blur_h_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
411            label: Some("Muspelheim Horizontal Blur"),
412            layout: Some(&post_process_layout),
413            vertex: wgpu::VertexState {
414                module: &shader,
415                entry_point: Some("vs_fullscreen"),
416                buffers: &[],
417                compilation_options: wgpu::PipelineCompilationOptions::default(),
418            },
419            fragment: Some(wgpu::FragmentState {
420                module: &shader,
421                entry_point: Some("fs_blur_h"),
422                targets: &[Some(wgpu::ColorTargetState {
423                    format: config.format,
424                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
425                    write_mask: wgpu::ColorWrites::ALL,
426                })],
427                compilation_options: wgpu::PipelineCompilationOptions::default(),
428            }),
429            primitive: wgpu::PrimitiveState::default(),
430            depth_stencil: None,
431            multisample: wgpu::MultisampleState::default(),
432            multiview_mask: None,
433            cache: None,
434        });
435
436        let blur_v_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
437            label: Some("Muspelheim Vertical Blur"),
438            layout: Some(&post_process_layout),
439            vertex: wgpu::VertexState {
440                module: &shader,
441                entry_point: Some("vs_fullscreen"),
442                buffers: &[],
443                compilation_options: wgpu::PipelineCompilationOptions::default(),
444            },
445            fragment: Some(wgpu::FragmentState {
446                module: &shader,
447                entry_point: Some("fs_blur_v"),
448                targets: &[Some(wgpu::ColorTargetState {
449                    format: config.format,
450                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
451                    write_mask: wgpu::ColorWrites::ALL,
452                })],
453                compilation_options: wgpu::PipelineCompilationOptions::default(),
454            }),
455            primitive: wgpu::PrimitiveState::default(),
456            depth_stencil: None,
457            multisample: wgpu::MultisampleState::default(),
458            multiview_mask: None,
459            cache: None,
460        });
461
462        // Muspelheim Composite Pipeline (additive blend onto screen)
463        let composite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
464            label: Some("Muspelheim Composite"),
465            layout: Some(&composite_layout),
466            vertex: wgpu::VertexState {
467                module: &shader,
468                entry_point: Some("vs_fullscreen"),
469                buffers: &[],
470                compilation_options: wgpu::PipelineCompilationOptions::default(),
471            },
472            fragment: Some(wgpu::FragmentState {
473                module: &shader,
474                entry_point: Some("fs_composite"),
475                targets: &[Some(wgpu::ColorTargetState {
476                    format: config.format,
477                    // Additive blend: src + dst — glow lights up the scene
478                    blend: Some(wgpu::BlendState {
479                        color: wgpu::BlendComponent {
480                            src_factor: wgpu::BlendFactor::One,
481                            dst_factor: wgpu::BlendFactor::One,
482                            operation: wgpu::BlendOperation::Add,
483                        },
484                        alpha: wgpu::BlendComponent {
485                            src_factor: wgpu::BlendFactor::One,
486                            dst_factor: wgpu::BlendFactor::One,
487                            operation: wgpu::BlendOperation::Add,
488                        },
489                    }),
490                    write_mask: wgpu::ColorWrites::ALL,
491                })],
492                compilation_options: wgpu::PipelineCompilationOptions::default(),
493            }),
494            primitive: wgpu::PrimitiveState::default(),
495            depth_stencil: None,
496            multisample: wgpu::MultisampleState::default(),
497            multiview_mask: None,
498            cache: None,
499        });
500
501        // Muspelheim Intermediate Textures
502        let blur_tex_desc = wgpu::TextureDescriptor {
503            label: Some("Muspelheim Intermediate"),
504            size: wgpu::Extent3d {
505                width: config.width,
506                height: config.height,
507                depth_or_array_layers: 1,
508            },
509            mip_level_count: 1,
510            sample_count: 1,
511            dimension: wgpu::TextureDimension::D2,
512            format: config.format,
513            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
514            view_formats: &[],
515        };
516        let blur_texture_a_obj = device.create_texture(&blur_tex_desc);
517        let blur_texture_b_obj = device.create_texture(&blur_tex_desc);
518        let blur_texture_a = blur_texture_a_obj.create_view(&wgpu::TextureViewDescriptor::default());
519        let blur_texture_b = blur_texture_b_obj.create_view(&wgpu::TextureViewDescriptor::default());
520
521        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
522            address_mode_u: wgpu::AddressMode::ClampToEdge,
523            address_mode_v: wgpu::AddressMode::ClampToEdge,
524            mag_filter: wgpu::FilterMode::Linear,
525            ..Default::default()
526        });
527
528        let blur_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
529            layout: &texture_bind_group_layout,
530            entries: &[
531                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&blur_texture_a) },
532                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
533            ],
534            label: Some("Blur Bind Group A"),
535        });
536
537        let blur_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
538            layout: &texture_bind_group_layout,
539            entries: &[
540                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&blur_texture_b) },
541                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
542            ],
543            label: Some("Blur Bind Group B"),
544        });
545
546        // Forge the Scene Capture Texture
547        let scene_texture_obj = device.create_texture(&blur_tex_desc);
548        let scene_texture = scene_texture_obj.create_view(&wgpu::TextureViewDescriptor::default());
549        let scene_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
550            layout: &env_bind_group_layout,
551            entries: &[
552                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&scene_texture) },
553                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
554            ],
555            label: Some("Scene Capture Bind Group"),
556        });
557
558        // Forge the Text Atlas (1024x1024 Alpha-only for speed)
559        let text_atlas_tex = device.create_texture(&wgpu::TextureDescriptor {
560            label: Some("Surtr Text Atlas"),
561            size: wgpu::Extent3d { width: 1024, height: 1024, depth_or_array_layers: 1 },
562            mip_level_count: 1,
563            sample_count: 1,
564            dimension: wgpu::TextureDimension::D2,
565            format: wgpu::TextureFormat::R8Unorm,
566            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
567            view_formats: &[],
568        });
569        let text_atlas = text_atlas_tex.create_view(&wgpu::TextureViewDescriptor::default());
570        let text_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
571            address_mode_u: wgpu::AddressMode::ClampToEdge,
572            address_mode_v: wgpu::AddressMode::ClampToEdge,
573            mag_filter: wgpu::FilterMode::Linear,
574            ..Default::default()
575        });
576
577        // Forge the Niflheim Dummy Texture (1x1 White)
578        let dummy_size = wgpu::Extent3d {
579            width: 1,
580            height: 1,
581            depth_or_array_layers: 1,
582        };
583        let dummy_texture = device.create_texture(&wgpu::TextureDescriptor {
584            label: Some("Niflheim Dummy Texture"),
585            size: dummy_size,
586            mip_level_count: 1,
587            sample_count: 1,
588            dimension: wgpu::TextureDimension::D2,
589            format: wgpu::TextureFormat::Rgba8UnormSrgb,
590            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
591            view_formats: &[],
592        });
593        queue.write_texture(
594            wgpu::TexelCopyTextureInfo {
595                texture: &dummy_texture,
596                mip_level: 0,
597                origin: wgpu::Origin3d::ZERO,
598                aspect: wgpu::TextureAspect::All,
599            },
600            &[255, 255, 255, 255],
601            wgpu::TexelCopyBufferLayout {
602                offset: 0,
603                bytes_per_row: Some(4),
604                rows_per_image: Some(1),
605            },
606            dummy_size,
607        );
608
609        let dummy_view = dummy_texture.create_view(&wgpu::TextureViewDescriptor::default());
610        let dummy_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
611            address_mode_u: wgpu::AddressMode::ClampToEdge,
612            address_mode_v: wgpu::AddressMode::ClampToEdge,
613            address_mode_w: wgpu::AddressMode::ClampToEdge,
614            mag_filter: wgpu::FilterMode::Linear,
615            min_filter: wgpu::FilterMode::Nearest,
616            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
617            ..Default::default()
618        });
619
620        let dummy_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
621            layout: &texture_bind_group_layout,
622            entries: &[
623                wgpu::BindGroupEntry {
624                    binding: 0,
625                    resource: wgpu::BindingResource::TextureView(&dummy_view),
626                },
627                wgpu::BindGroupEntry {
628                    binding: 1,
629                    resource: wgpu::BindingResource::Sampler(&dummy_sampler),
630                },
631            ],
632            label: Some("Niflheim Dummy Bind Group"),
633        });
634
635        let mut texture_registry = std::collections::HashMap::new();
636        let mut texture_bind_groups = Vec::new();
637
638        let text_atlas_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
639            layout: &texture_bind_group_layout,
640            entries: &[
641                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&text_atlas) },
642                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&text_sampler) },
643            ],
644            label: Some("Text Atlas Bind Group"),
645        });
646        texture_registry.insert("__text_atlas".to_string(), texture_bind_groups.len() as u32);
647        texture_bind_groups.push(text_atlas_bg);
648
649        // Forge the Anvil (Buffers)
650        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
651            label: Some("Surtr Vertex Anvil"),
652            size: (MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64,
653            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
654            mapped_at_creation: false,
655        });
656
657        let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
658            label: Some("Surtr Index Anvil"),
659            size: (MAX_INDICES * std::mem::size_of::<u16>()) as u64,
660            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
661            mapped_at_creation: false,
662        });
663
664
665        // Register atlas
666
667        // Texture registry and bind groups already initialized above.
668
669        // Forge the Heart (Berserker Uniforms)
670        let current_theme = ColorTheme::default();
671        use wgpu::util::DeviceExt;
672        let theme_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
673            label: Some("Surtr Theme Buffer"),
674            contents: bytemuck::bytes_of(&current_theme),
675            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
676        });
677
678        let current_scene = SceneUniforms::new(config.width as f32, config.height as f32);
679        let scene_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
680            label: Some("Surtr Scene Buffer"),
681            contents: bytemuck::bytes_of(&current_scene),
682            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
683        });
684
685        let berserker_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
686            layout: &berserker_bind_group_layout,
687            entries: &[
688                wgpu::BindGroupEntry {
689                    binding: 0,
690                    resource: theme_buffer.as_entire_binding(),
691                },
692                wgpu::BindGroupEntry {
693                    binding: 1,
694                    resource: scene_buffer.as_entire_binding(),
695                },
696            ],
697            label: Some("Surtr Berserker Bind Group"),
698        });
699
700        let scene_texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
701            layout: &texture_bind_group_layout,
702            entries: &[
703                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&scene_texture) },
704                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
705            ],
706            label: Some("Scene Texture Bind Group (Group 0)"),
707        });
708
709        Self {
710            device,
711            queue,
712            surface,
713            config,
714            pipeline,
715            bloom_extract_pipeline,
716            blur_h_pipeline,
717            blur_v_pipeline,
718            composite_pipeline,
719            blur_texture_a,
720            blur_texture_b,
721            blur_bind_group_a,
722            blur_bind_group_b,
723            scene_texture,
724            scene_bind_group,
725            scene_texture_bind_group,
726            font_system: cosmic_text::FontSystem::new(),
727            swash_cache: cosmic_text::SwashCache::new(),
728            text_atlas_tex,
729            text_atlas_view: text_atlas,
730            text_sampler,
731            text_cache: std::collections::HashMap::new(),
732            text_atlas_pos: (0, 0),
733            dummy_bind_group,
734            texture_bind_group_layout,
735            texture_bind_groups,
736            texture_registry,
737            shared_elements: std::collections::HashMap::new(),
738            vertex_buffer,
739            index_buffer,
740            vertices: Vec::with_capacity(MAX_VERTICES),
741            indices: Vec::with_capacity(MAX_INDICES),
742            draw_calls: Vec::new(),
743            current_texture_id: None,
744            opacity_stack: vec![1.0],
745            clip_stack: Vec::new(),
746            slice_stack: Vec::new(),
747            theme_buffer,
748            scene_buffer,
749            berserker_bind_group,
750            berserker_bind_group_layout,
751            start_time: std::time::Instant::now(),
752            current_theme,
753            current_scene,
754            background_pipeline,
755        }
756    }
757
758    pub fn resize(&mut self, width: u32, height: u32) {
759        if width > 0 && height > 0 {
760            self.config.width = width;
761            self.config.height = height;
762            self.surface.configure(&self.device, &self.config);
763            
764            // Re-create Muspelheim textures
765            let texture_desc = wgpu::TextureDescriptor {
766                label: Some("Surtr Scene Texture"),
767                size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
768                mip_level_count: 1,
769                sample_count: 1,
770                dimension: wgpu::TextureDimension::D2,
771                format: self.config.format,
772                usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
773                view_formats: &[],
774            };
775            
776            let scene_tex = self.device.create_texture(&texture_desc);
777            self.scene_texture = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
778            
779            let blur_tex_a = self.device.create_texture(&texture_desc);
780            self.blur_texture_a = blur_tex_a.create_view(&wgpu::TextureViewDescriptor::default());
781            
782            let blur_tex_b = self.device.create_texture(&texture_desc);
783            self.blur_texture_b = blur_tex_b.create_view(&wgpu::TextureViewDescriptor::default());
784
785            // Re-create bind groups (using existing layouts)
786            let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
787                address_mode_u: wgpu::AddressMode::ClampToEdge,
788                address_mode_v: wgpu::AddressMode::ClampToEdge,
789                mag_filter: wgpu::FilterMode::Linear,
790                min_filter: wgpu::FilterMode::Linear,
791                ..Default::default()
792            });
793
794            // For scene_bind_group, we need the env_bind_group_layout.
795            // Since it's not stored, we re-create it exactly as in forge.
796            let env_layout = self.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
797                entries: &[
798                    wgpu::BindGroupLayoutEntry {
799                        binding: 0,
800                        visibility: wgpu::ShaderStages::FRAGMENT,
801                        ty: wgpu::BindingType::Texture {
802                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
803                            view_dimension: wgpu::TextureViewDimension::D2,
804                            multisampled: false,
805                        },
806                        count: None,
807                    },
808                    wgpu::BindGroupLayoutEntry {
809                        binding: 1,
810                        visibility: wgpu::ShaderStages::FRAGMENT,
811                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
812                        count: None,
813                    },
814                ],
815                label: Some("Surtr Env Layout Resize"),
816            });
817
818            self.scene_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
819                layout: &env_layout,
820                entries: &[
821                    wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&self.scene_texture) },
822                    wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
823                ],
824                label: Some("Surtr Scene Bind Group Resize"),
825            });
826
827            self.blur_bind_group_a = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
828                layout: &self.texture_bind_group_layout,
829                entries: &[
830                    wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&self.blur_texture_a) },
831                    wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
832                ],
833                label: Some("Surtr Blur Bind Group A Resize"),
834            });
835
836            self.blur_bind_group_b = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
837                layout: &self.texture_bind_group_layout,
838                entries: &[
839                    wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&self.blur_texture_b) },
840                    wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
841                ],
842                label: Some("Surtr Blur Bind Group B Resize"),
843            });
844
845            self.scene_texture_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
846                layout: &self.texture_bind_group_layout,
847                entries: &[
848                    wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&self.scene_texture) },
849                    wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
850                ],
851                label: Some("Scene Texture Bind Group Resize"),
852            });
853            
854            self.current_scene.resolution = [width as f32, height as f32];
855        }
856    }
857
858    /// begin_frame — Strike the flaming sword to begin a new GPU frame.
859    pub fn begin_frame(&mut self) -> wgpu::CommandEncoder {
860        self.vertices.clear();
861        self.indices.clear();
862        self.draw_calls.clear();
863        self.shared_elements.clear(); // Clear registry for the new frame
864        self.current_texture_id = None;
865
866        let time = self.start_time.elapsed().as_secs_f32();
867        let dt = time - self.current_scene.time;
868        self.current_scene.time = time;
869        self.current_scene.delta_time = dt;
870        
871        self.queue.write_buffer(&self.scene_buffer, 0, bytemuck::bytes_of(&self.current_scene));
872
873        self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
874            label: Some("Surtr's Flaming Sword"),
875        })
876    }
877
878    /// Reset the internal clock (for interactive effects)
879    pub fn reset_time(&mut self) {
880        self.start_time = std::time::Instant::now();
881    }
882
883    fn shatter_internal(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4], mode: u32) {
884        // High-Fidelity Variable Particle Density
885        let count = (pieces as f32).sqrt().ceil() as u32;
886        let dw = rect.width / count as f32;
887        let dh = rect.height / count as f32;
888
889        let c = self.apply_opacity(color);
890
891        for y in 0..count {
892            for x in 0..count {
893                let shard_rect = Rect {
894                    x: rect.x + x as f32 * dw,
895                    y: rect.y + y as f32 * dh,
896                    width: dw,
897                    height: dh,
898                };
899                
900                let uv = Rect {
901                    x: x as f32 / count as f32,
902                    y: y as f32 / count as f32,
903                    width: 1.0 / count as f32,
904                    height: 1.0 / count as f32,
905                };
906
907                self.fill_rect_with_full_params(
908                    shard_rect, 
909                    c, 
910                    mode, 
911                    None, 
912                    force, 
913                    uv
914                );
915            }
916        }
917    }
918
919    fn recursive_bolt(&mut self, from: [f32; 2], to: [f32; 2], depth: u32, color: [f32; 4]) {
920        if depth == 0 {
921            self.draw_lightning_segment(from, to, color);
922            return;
923        }
924
925        let mid_x = (from[0] + to[0]) * 0.5;
926        let mid_y = (from[1] + to[1]) * 0.5;
927        
928        let dx = to[0] - from[0];
929        let dy = to[1] - from[1];
930        let len = (dx * dx + dy * dy).sqrt();
931        
932        // Perpendicular offset for jaggedness
933        let offset_scale = len * 0.15;
934        let seed = (from[0] * 12.9898 + from[1] * 78.233 + (depth as f32) * 37.11).sin().fract();
935        let offset_x = -dy / len * (seed - 0.5) * offset_scale;
936        let offset_y = dx / len * (seed - 0.5) * offset_scale;
937        
938        let mid = [mid_x + offset_x, mid_y + offset_y];
939        
940        self.recursive_bolt(from, mid, depth - 1, color);
941        self.recursive_bolt(mid, to, depth - 1, color);
942        
943        // 20% chance of a secondary branch
944        if depth > 2 && seed > 0.8 {
945            let branch_to = [
946                mid[0] + offset_x * 2.0 + (seed * 100.0).sin() * 50.0,
947                mid[1] + offset_y * 2.0 + (seed * 100.0).cos() * 50.0
948            ];
949            self.recursive_bolt(mid, branch_to, depth - 2, color);
950        }
951    }
952
953    fn draw_lightning_segment(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
954        let dx = to[0] - from[0];
955        let dy = to[1] - from[1];
956        let len = (dx * dx + dy * dy).sqrt();
957        if len < 0.001 { return; }
958        
959        let glow_width = 32.0; 
960        let core_width = 4.0;
961        let c = self.apply_opacity(color);
962        
963        // 1. Render Volumetric Glow (Cyan)
964        let gnx = -dy / len * glow_width * 0.5;
965        let gny = dx / len * glow_width * 0.5;
966        let gp1 = [from[0] + gnx, from[1] + gny];
967        let gp2 = [to[0] + gnx, to[1] + gny];
968        let gp3 = [to[0] - gnx, to[1] - gny];
969        let gp4 = [from[0] - gnx, from[1] - gny];
970        self.push_oriented_quad([gp1, gp2, gp3, gp4], c, 9, Rect { x: 0.0, y: 0.0, width: 1.0, height: 1.0 });
971
972        // 2. Render Blinding Core (White)
973        let cnx = -dy / len * core_width * 0.5;
974        let cny = dx / len * core_width * 0.5;
975        let cp1 = [from[0] + cnx, from[1] + cny];
976        let cp2 = [to[0] + cnx, to[1] + cny];
977        let cp3 = [to[0] - cnx, to[1] - cny];
978        let cp4 = [from[0] - cnx, from[1] - cny];
979        self.push_oriented_quad([cp1, cp2, cp3, cp4], [1.0, 1.0, 1.0, c[3]], 0, Rect { x: 0.0, y: 0.0, width: 1.0, height: 1.0 });
980    }
981
982    fn push_oriented_quad(&mut self, points: [[f32; 2]; 4], color: [f32; 4], mode: u32, uv_rect: Rect) {
983        let scissor = self.clip_stack.last().copied();
984        let texture_id = None; // Oriented quads like lightning don't use textures yet
985        
986        if self.draw_calls.is_empty() || self.current_texture_id != texture_id || self.draw_calls.last().unwrap().scissor_rect != scissor {
987            self.current_texture_id = texture_id;
988            self.draw_calls.push(DrawCall {
989                texture_id,
990                scissor_rect: scissor,
991                index_start: self.indices.len() as u32,
992                index_count: 0,
993            });
994        }
995
996        let base_idx = self.vertices.len() as u32;
997
998        let uvs = [
999            [uv_rect.x, uv_rect.y],
1000            [uv_rect.x + uv_rect.width, uv_rect.y],
1001            [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
1002            [uv_rect.x, uv_rect.y + uv_rect.height],
1003        ];
1004
1005        let screen = [self.config.width as f32, self.config.height as f32];
1006
1007        let origin = points[0];
1008        let normal = [0.0, 0.0, 1.0];
1009        for i in 0..4 {
1010            let px = points[i][0];
1011            let py = points[i][1];
1012            
1013            self.vertices.push(Vertex {
1014                position: [px, py, 0.0],
1015                normal,
1016                uv: uvs[i],
1017                color,
1018                mode,
1019                radius: 0.0,
1020                slice: [0.0, 0.0, 0.0],
1021                logical: origin,
1022                size: [0.0, 0.0], 
1023                screen,
1024                clip: [-10000.0, -10000.0, 20000.0, 20000.0],
1025            });
1026        }
1027
1028        if let Some(call) = self.draw_calls.last_mut() {
1029            call.index_count += 6;
1030        }
1031    }
1032
1033    fn get_texture_id(&self, name: &str) -> Option<u32> {
1034        self.texture_registry.get(name).copied()
1035    }
1036
1037    fn fill_rect_with_mode(&mut self, rect: Rect, color: [f32; 4], mode: u32, texture_id: Option<u32>) {
1038        self.fill_rect_with_full_params(rect, color, mode, texture_id, 0.0, Rect { x: 0.0, y: 0.0, width: 1.0, height: 1.0 });
1039    }
1040
1041    fn fill_rect_with_full_params(&mut self, rect: Rect, color: [f32; 4], mode: u32, texture_id: Option<u32>, radius: f32, uv_rect: Rect) {
1042        let scissor = self.clip_stack.last().copied();
1043        
1044        // Batching: check if we need to start a new DrawCall
1045        let needs_new_call = self.draw_calls.is_empty() 
1046            || self.current_texture_id != texture_id
1047            || self.draw_calls.last().unwrap().scissor_rect != scissor;
1048
1049        if needs_new_call {
1050            self.current_texture_id = texture_id;
1051            self.draw_calls.push(DrawCall {
1052                texture_id,
1053                scissor_rect: scissor,
1054                index_start: self.indices.len() as u32,
1055                index_count: 0,
1056            });
1057        }
1058
1059        let base_idx = self.vertices.len() as u32;
1060        let x1 = rect.x;
1061        let y1 = rect.y;
1062        let x2 = rect.x + rect.width;
1063        let y2 = rect.y + rect.height;
1064        let z = 0.0;
1065        let normal = [0.0, 0.0, 1.0];
1066        let slice = self.slice_stack.last().copied().map(|(a, o)| [a, o, 1.0]).unwrap_or([0.0, 0.0, 0.0]);
1067        let screen = [self.config.width as f32, self.config.height as f32];
1068        let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect { x: -10000.0, y: -10000.0, width: 20000.0, height: 20000.0 });
1069        let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
1070
1071        self.vertices.push(Vertex { 
1072            position: [x1, y1, z], normal, uv: [uv_rect.x, uv_rect.y], color, mode, radius, 
1073            slice, logical: [rect.x, rect.y], size: [rect.width, rect.height], screen, clip
1074        });
1075        self.vertices.push(Vertex { 
1076            position: [x2, y1, z], normal, uv: [uv_rect.x + uv_rect.width, uv_rect.y], color, mode, radius, 
1077            slice, logical: [rect.x, rect.y], size: [rect.width, rect.height], screen, clip
1078        });
1079        self.vertices.push(Vertex { 
1080            position: [x2, y2, z], normal, uv: [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height], color, mode, radius, 
1081            slice, logical: [rect.x, rect.y], size: [rect.width, rect.height], screen, clip
1082        });
1083        self.vertices.push(Vertex { 
1084            position: [x1, y2, z], normal, uv: [uv_rect.x, uv_rect.y + uv_rect.height], color, mode, radius, 
1085            slice, logical: [rect.x, rect.y], size: [rect.width, rect.height], screen, clip
1086        });
1087
1088        self.indices.extend_from_slice(&[
1089            base_idx, base_idx + 3, base_idx + 2,
1090            base_idx, base_idx + 2, base_idx + 1,
1091        ]);
1092
1093        if let Some(call) = self.draw_calls.last_mut() {
1094            call.index_count += 6;
1095        }
1096    }
1097
1098    /// end_frame — Quench the blade by submitting the full Muspelheim multi-pass effect.
1099    pub fn end_frame(&mut self, mut encoder: wgpu::CommandEncoder) {
1100        self.queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&self.vertices));
1101        self.queue.write_buffer(&self.index_buffer, 0, bytemuck::cast_slice(&self.indices));
1102
1103        let frame = match self.surface.get_current_texture() {
1104            wgpu::CurrentSurfaceTexture::Success(t) => t,
1105            wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
1106            _ => {
1107                 self.surface.configure(&self.device, &self.config);
1108                 return;
1109            }
1110        };
1111        let screen = frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
1112
1113        // ── Pass 1: Base scene → intermediate ─────────────────────────────────────
1114        {
1115            let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1116                label: Some("Surtr P1 Base Scene"),
1117                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1118                    view: &self.scene_texture, // Render to intermediate texture
1119                    resolve_target: None,
1120                    ops: wgpu::Operations {
1121                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1122                        store: wgpu::StoreOp::Store,
1123                    },
1124                    depth_slice: None,
1125                })],
1126                depth_stencil_attachment: None,
1127                occlusion_query_set: None,
1128                timestamp_writes: None,
1129                multiview_mask: None,
1130            });
1131            // ── Background Atmosphere Pass ──────────────────────────────────────
1132            p.set_pipeline(&self.background_pipeline);
1133            p.set_bind_group(0, &self.dummy_bind_group, &[]);
1134            p.set_bind_group(1, &self.blur_bind_group_a, &[]); // Use previous frame's blur instead of active target
1135            p.set_bind_group(2, &self.berserker_bind_group, &[]);
1136            p.draw(0..6, 0..1);
1137
1138            // ── Main Scene Pass ──────────────────────────────────────────────────
1139            if !self.draw_calls.is_empty() {
1140                p.set_pipeline(&self.pipeline);
1141                p.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1142                p.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1143                
1144                p.set_bind_group(1, &self.blur_bind_group_a, &[]);
1145                p.set_bind_group(2, &self.berserker_bind_group, &[]);
1146                
1147                for call in &self.draw_calls {
1148                    let bg = if let Some(id) = call.texture_id {
1149                        self.texture_bind_groups.get(id as usize).unwrap_or(&self.dummy_bind_group)
1150                    } else {
1151                        &self.dummy_bind_group
1152                    };
1153                    p.set_bind_group(0, bg, &[]);
1154                    if let Some(s) = call.scissor_rect {
1155                        p.set_scissor_rect(
1156                            s.x.max(0.0) as u32,
1157                            s.y.max(0.0) as u32,
1158                            s.width.max(1.0) as u32,
1159                            s.height.max(1.0) as u32
1160                        );
1161                    } else {
1162                        p.set_scissor_rect(0, 0, self.config.width, self.config.height);
1163                    }
1164                    p.draw_indexed(call.index_start..call.index_start + call.index_count, 0, 0..1);
1165                }
1166            }
1167        }
1168
1169        // ── Pass 2: Bloom extract  scene → tex_a ────────────────────────────
1170        {
1171            let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1172                label: Some("Surtr P2 Bloom Src"),
1173                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1174                    view: &self.blur_texture_a,
1175                    resolve_target: None,
1176                    ops: wgpu::Operations {
1177                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1178                        store: wgpu::StoreOp::Store,
1179                    },
1180                    depth_slice: None,
1181                })],
1182                depth_stencil_attachment: None,
1183                occlusion_query_set: None,
1184                timestamp_writes: None,
1185                multiview_mask: None,
1186            });
1187            p.set_pipeline(&self.bloom_extract_pipeline);
1188            p.set_bind_group(0, &self.scene_texture_bind_group, &[]); 
1189            p.set_bind_group(1, &self.dummy_bind_group, &[]); 
1190            p.set_bind_group(2, &self.berserker_bind_group, &[]);
1191            p.draw(0..6, 0..1);
1192        }
1193
1194        // ── Passes 3–6+: Ping-pong Gaussian blur ──────────────────────────
1195        let blur_iters: u32 = 6;
1196        for i in 0..blur_iters {
1197            {
1198                let label = format!("Surtr Blur H iter {}", i);
1199                let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1200                    label: Some(&label),
1201                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1202                        view: &self.blur_texture_b,
1203                        resolve_target: None,
1204                        ops: wgpu::Operations {
1205                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1206                            store: wgpu::StoreOp::Store,
1207                        },
1208                        depth_slice: None,
1209                    })],
1210                    depth_stencil_attachment: None,
1211                    occlusion_query_set: None,
1212                    timestamp_writes: None,
1213                    multiview_mask: None,
1214                });
1215                p.set_pipeline(&self.blur_h_pipeline);
1216                p.set_bind_group(0, &self.blur_bind_group_a, &[]);
1217                p.set_bind_group(1, &self.dummy_bind_group, &[]);
1218                p.set_bind_group(2, &self.berserker_bind_group, &[]);
1219                p.draw(0..6, 0..1);
1220            }
1221            {
1222                let label = format!("Surtr Blur V iter {}", i);
1223                let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1224                    label: Some(&label),
1225                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1226                        view: &self.blur_texture_a,
1227                        resolve_target: None,
1228                        ops: wgpu::Operations {
1229                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1230                            store: wgpu::StoreOp::Store,
1231                        },
1232                        depth_slice: None,
1233                    })],
1234                    depth_stencil_attachment: None,
1235                    occlusion_query_set: None,
1236                    timestamp_writes: None,
1237                    multiview_mask: None,
1238                });
1239                p.set_pipeline(&self.blur_v_pipeline);
1240                p.set_bind_group(0, &self.blur_bind_group_b, &[]);
1241                p.set_bind_group(1, &self.dummy_bind_group, &[]);
1242                p.set_bind_group(2, &self.berserker_bind_group, &[]);
1243                p.draw(0..6, 0..1);
1244            }
1245        }
1246
1247        // ── Pass 7: Uber-Composite (Scene + Bloom + Effects) → Screen ────────
1248        {
1249            let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1250                label: Some("Surtr P7 Uber-Composite"),
1251                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1252                    view: &screen,
1253                    resolve_target: None,
1254                    ops: wgpu::Operations {
1255                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1256                        store: wgpu::StoreOp::Store,
1257                    },
1258                    depth_slice: None,
1259                })],
1260                depth_stencil_attachment: None,
1261                occlusion_query_set: None,
1262                timestamp_writes: None,
1263                multiview_mask: None,
1264            });
1265            p.set_pipeline(&self.composite_pipeline);
1266            p.set_bind_group(0, &self.blur_bind_group_a, &[]);
1267            p.set_bind_group(1, &self.scene_bind_group, &[]);
1268            p.set_bind_group(2, &self.berserker_bind_group, &[]);
1269            p.draw(0..6, 0..1);
1270        }
1271
1272        self.queue.submit(Some(encoder.finish()));
1273        frame.present();
1274    }
1275}
1276
1277impl cvkg_core::Renderer for SurtrRenderer {
1278    fn fill_rect(&mut self, rect: Rect, color: [f32; 4]) {
1279        self.fill_rect_with_mode(rect, self.apply_opacity(color), 0, None);
1280    }
1281
1282    fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4]) {
1283        self.fill_rect_with_full_params(rect, self.apply_opacity(color), 3, None, radius, Rect { x: 0.0, y: 0.0, width: 1.0, height: 1.0 });
1284    }
1285
1286    fn fill_ellipse(&mut self, rect: Rect, color: [f32; 4]) {
1287        self.fill_rect_with_full_params(rect, self.apply_opacity(color), 4, None, 0.0, Rect { x: 0.0, y: 0.0, width: 1.0, height: 1.0 });
1288    }
1289
1290    fn stroke_rect(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
1291        let c = self.apply_opacity(color);
1292        let hw = stroke_width;
1293        // Top, bottom, left, right edge bars
1294        self.fill_rect_with_mode(Rect { x: rect.x, y: rect.y, width: rect.width, height: hw }, c, 1, None);
1295        self.fill_rect_with_mode(Rect { x: rect.x, y: rect.y + rect.height - hw, width: rect.width, height: hw }, c, 1, None);
1296        self.fill_rect_with_mode(Rect { x: rect.x, y: rect.y, width: hw, height: rect.height }, c, 1, None);
1297        self.fill_rect_with_mode(Rect { x: rect.x + rect.width - hw, y: rect.y, width: hw, height: rect.height }, c, 1, None);
1298    }
1299
1300    fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4], stroke_width: f32) {
1301        self.fill_rect_with_full_params(rect, self.apply_opacity(color), 17, None, radius, Rect { x: stroke_width, y: 0.0, width: 0.0, height: 0.0 });
1302    }
1303
1304    fn stroke_ellipse(&mut self, _rect: Rect, _color: [f32; 4], _stroke_width: f32) {
1305        // Future: Implement stroked SDFs.
1306    }
1307
1308    fn draw_linear_gradient(&mut self, rect: Rect, start_color: [f32; 4], _end_color: [f32; 4], angle: f32) {
1309        self.fill_rect_with_full_params(rect, self.apply_opacity(start_color), 15, None, 0.0, Rect { x: angle, y: 0.0, width: 0.0, height: 0.0 });
1310    }
1311
1312    fn draw_radial_gradient(&mut self, rect: Rect, inner_color: [f32; 4], _outer_color: [f32; 4]) {
1313        self.fill_rect_with_full_params(rect, self.apply_opacity(inner_color), 16, None, 0.0, Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 });
1314    }
1315
1316    fn draw_drop_shadow(&mut self, rect: Rect, radius: f32, color: [f32; 4], blur: f32, spread: f32) {
1317        self.fill_rect_with_full_params(rect, self.apply_opacity(color), 18, None, radius, Rect { x: blur, y: spread, width: 0.0, height: 0.0 });
1318    }
1319
1320    fn stroke_dashed_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4], width: f32, dash: f32, gap: f32) {
1321        self.fill_rect_with_full_params(rect, self.apply_opacity(color), 19, None, radius, Rect { x: width, y: dash, width: gap, height: 0.0 });
1322    }
1323
1324    fn draw_9slice(&mut self, image_name: &str, rect: Rect, left: f32, top: f32, right: f32, bottom: f32) {
1325        let c = self.apply_opacity([1.0, 1.0, 1.0, 1.0]);
1326        let tid = self.get_texture_id(image_name);
1327        self.fill_rect_with_full_params(rect, c, 20, tid, bottom, Rect { x: left, y: top, width: right, height: 0.0 });
1328    }
1329
1330    fn draw_line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: [f32; 4], stroke_width: f32) {
1331        let dx = x2 - x1;
1332        let dy = y2 - y1;
1333        let len = (dx * dx + dy * dy).sqrt();
1334        if len < 0.001 { return; }
1335        
1336        let _angle = dy.atan2(dx).to_degrees();
1337        let c = self.apply_opacity(color);
1338        
1339        // Push an oriented quad by using the Mjolnir Slice infrastructure or 
1340        // by calculating rotated vertices. For now, we use a simple rotation push.
1341        // In a future pass, we will add 'rotation' to the Vertex struct for batching.
1342        // For now, we use the centered rect logic.
1343        self.fill_rect_with_mode(
1344            Rect { x: (x1 + x2) / 2.0 - len / 2.0, y: (y1 + y2) / 2.0 - stroke_width / 2.0, width: len, height: stroke_width },
1345            c,
1346            1, // Gungnir Mode for glowing lines
1347            None
1348        );
1349    }
1350
1351    fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
1352        // High-Fidelity Text Forge: Layout -> Rasterize -> Atlas -> Quad
1353        let mut buffer = cosmic_text::Buffer::new(&mut self.font_system, cosmic_text::Metrics::new(size, size));
1354        buffer.set_text(&mut self.font_system, text, &cosmic_text::Attrs::new(), cosmic_text::Shaping::Advanced);
1355        buffer.shape_until_scroll(&mut self.font_system, false);
1356
1357        let c = self.apply_opacity(color);
1358        
1359        for run in buffer.layout_runs() {
1360            for glyph in run.glyphs {
1361                // Top-Left Anchor: Add the run's line_y (baseline) to the input y
1362                let physical_glyph = glyph.physical((x, y), 1.0);
1363                let cache_key = physical_glyph.cache_key;
1364                
1365                // Check cache or rasterize
1366                let (uv_rect, w, h) = if let Some(info) = self.text_cache.get(&(cache_key.glyph_id as u32)) {
1367                    *info
1368                } else {
1369                    // Rasterize new glyph
1370                    if let Some(image) = self.swash_cache.get_image(&mut self.font_system, cache_key) {
1371                        let (gx, _gy) = self.text_atlas_pos;
1372                        let gw = image.placement.width;
1373                        let gh = image.placement.height;
1374                        
1375                        // Simple grid packing (Phase 2 Forge)
1376                        if gx + gw > 1024 {
1377                            self.text_atlas_pos.0 = 0;
1378                            self.text_atlas_pos.1 += 64; // Max glyph height
1379                        }
1380                        let (nx, ny) = self.text_atlas_pos;
1381                        
1382                        self.queue.write_texture(
1383                            wgpu::TexelCopyTextureInfo {
1384                                texture: &self.text_atlas_tex,
1385                                mip_level: 0,
1386                                origin: wgpu::Origin3d { x: nx, y: ny, z: 0 },
1387                                aspect: wgpu::TextureAspect::All,
1388                            },
1389                            &image.data,
1390                            wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(gw), rows_per_image: Some(gh) },
1391                            wgpu::Extent3d { width: gw, height: gh, depth_or_array_layers: 1 },
1392                        );
1393                        
1394                        let info = (Rect { x: nx as f32 / 1024.0, y: ny as f32 / 1024.0, width: gw as f32 / 1024.0, height: gh as f32 / 1024.0 }, gw as f32, gh as f32);
1395                        self.text_cache.insert(cache_key.glyph_id as u32, info);
1396                        self.text_atlas_pos.0 += gw + 2;
1397                        info
1398                    } else {
1399                        (Rect::zero(), 0.0, 0.0)
1400                    }
1401                };
1402                
1403                if w > 0.0 {
1404                    let glyph_rect = Rect {
1405                        x: physical_glyph.x as f32,
1406                        y: physical_glyph.y as f32,
1407                        width: w,
1408                        height: h,
1409                    };
1410                    let tid = self.get_texture_id("__text_atlas");
1411                    self.fill_rect_with_full_params(glyph_rect, c, 6, tid, 0.0, uv_rect); // Mode 6 = Text
1412                }
1413            }
1414        }
1415    }
1416
1417    fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
1418        let mut buffer = cosmic_text::Buffer::new(&mut self.font_system, cosmic_text::Metrics::new(size, size));
1419        buffer.set_text(&mut self.font_system, text, &cosmic_text::Attrs::new(), cosmic_text::Shaping::Advanced);
1420        buffer.shape_until_scroll(&mut self.font_system, false);
1421        
1422        let mut width = 0.0f32;
1423        let mut height = 0.0f32;
1424        
1425        for run in buffer.layout_runs() {
1426            width = width.max(run.line_w);
1427            height += size; 
1428        }
1429        
1430        (width, height)
1431    }
1432
1433    fn draw_texture(&mut self, texture_id: u32, rect: Rect) {
1434        self.fill_rect_with_full_params(rect, [1.0, 1.0, 1.0, 1.0], 2, Some(texture_id), 0.0, Rect { x: 0.0, y: 0.0, width: 1.0, height: 1.0 });
1435    }
1436
1437    fn draw_image(&mut self, image_name: &str, rect: Rect) {
1438        let tid = self.get_texture_id(image_name);
1439        self.fill_rect_with_full_params(rect, [1.0, 1.0, 1.0, 1.0], 2, tid, 0.0, Rect { x: 0.0, y: 0.0, width: 1.0, height: 1.0 });
1440    }
1441
1442    fn load_image(&mut self, name: &str, data: &[u8]) {
1443        let img = image::load_from_memory(data).expect("Failed to load image").to_rgba8();
1444        let (width, height) = img.dimensions();
1445        let size = wgpu::Extent3d { width, height, depth_or_array_layers: 1 };
1446        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
1447            label: Some(name),
1448            size,
1449            mip_level_count: 1,
1450            sample_count: 1,
1451            dimension: wgpu::TextureDimension::D2,
1452            format: wgpu::TextureFormat::Rgba8UnormSrgb,
1453            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1454            view_formats: &[],
1455        });
1456        self.queue.write_texture(
1457            wgpu::TexelCopyTextureInfo {
1458                texture: &texture,
1459                mip_level: 0,
1460                origin: wgpu::Origin3d::ZERO,
1461                aspect: wgpu::TextureAspect::All,
1462            },
1463            &img,
1464            wgpu::TexelCopyBufferLayout {
1465                offset: 0,
1466                bytes_per_row: Some(4 * width),
1467                rows_per_image: Some(height),
1468            },
1469            size,
1470        );
1471        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1472        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
1473            address_mode_u: wgpu::AddressMode::ClampToEdge,
1474            address_mode_v: wgpu::AddressMode::ClampToEdge,
1475            mag_filter: wgpu::FilterMode::Linear,
1476            ..Default::default()
1477        });
1478        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1479            layout: &self.texture_bind_group_layout,
1480            entries: &[
1481                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) },
1482                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
1483            ],
1484            label: Some(name),
1485        });
1486        self.texture_bind_groups.push(bind_group);
1487        let id = (self.texture_bind_groups.len() - 1) as u32;
1488        self.texture_registry.insert(name.to_string(), id);
1489    }
1490
1491    fn push_clip_rect(&mut self, rect: Rect) {
1492        self.clip_stack.push(rect);
1493    }
1494
1495    fn pop_clip_rect(&mut self) {
1496        self.clip_stack.pop();
1497    }
1498
1499    fn push_opacity(&mut self, opacity: f32) {
1500        let current = self.opacity_stack.last().copied().unwrap_or(1.0);
1501        self.opacity_stack.push(current * opacity);
1502    }
1503
1504    fn pop_opacity(&mut self) {
1505        self.opacity_stack.pop();
1506    }
1507
1508    fn set_theme(&mut self, theme: ColorTheme) {
1509        self.current_theme = theme;
1510        self.queue.write_buffer(&self.theme_buffer, 0, bytemuck::bytes_of(&theme));
1511    }
1512
1513    fn set_rage(&mut self, rage: f32) {
1514        self.current_scene.berzerker_rage = rage;
1515        // scene_buffer is updated every frame in begin_frame, so no need to write here
1516    }
1517
1518    fn trigger_shatter_event(&mut self, origin: [f32; 2], force: f32) {
1519        self.current_scene.shatter_origin = origin;
1520        self.current_scene.shatter_time = self.current_scene.time;
1521        self.current_scene.shatter_force = force;
1522    }
1523
1524    fn bifrost(&mut self, rect: Rect, _blur: f32, _saturation: f32, opacity: f32) {
1525        // Use mode 7 for high-fidelity background blur sampling
1526        self.fill_rect_with_mode(rect, [1.0, 1.0, 1.0, opacity], 7, None);
1527    }
1528
1529    fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
1530        self.slice_stack.push((angle, offset));
1531    }
1532
1533    fn pop_mjolnir_slice(&mut self) {
1534        self.slice_stack.pop();
1535    }
1536
1537    fn mjolnir_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
1538        self.shatter_internal(rect, pieces, force, color, 8);
1539    }
1540
1541    fn mjolnir_fluid_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
1542        self.shatter_internal(rect, pieces, force, color, 11);
1543    }
1544
1545    fn draw_mjolnir_bolt(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
1546        self.recursive_bolt(from, to, 4, color);
1547    }
1548
1549    fn upload_data_texture(&mut self, id: &str, data: &[f32], width: u32, height: u32) {
1550        let size = wgpu::Extent3d { width, height, depth_or_array_layers: 1 };
1551        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
1552            label: Some(id),
1553            size,
1554            mip_level_count: 1,
1555            sample_count: 1,
1556            dimension: wgpu::TextureDimension::D2,
1557            format: wgpu::TextureFormat::R32Float,
1558            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1559            view_formats: &[],
1560        });
1561        self.queue.write_texture(
1562            wgpu::TexelCopyTextureInfo {
1563                texture: &texture,
1564                mip_level: 0,
1565                origin: wgpu::Origin3d::ZERO,
1566                aspect: wgpu::TextureAspect::All,
1567            },
1568            bytemuck::cast_slice(data),
1569            wgpu::TexelCopyBufferLayout {
1570                offset: 0,
1571                bytes_per_row: Some(4 * width),
1572                rows_per_image: Some(height),
1573            },
1574            size,
1575        );
1576        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1577        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
1578            address_mode_u: wgpu::AddressMode::ClampToEdge,
1579            address_mode_v: wgpu::AddressMode::ClampToEdge,
1580            mag_filter: wgpu::FilterMode::Linear,
1581            ..Default::default()
1582        });
1583        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1584            layout: &self.texture_bind_group_layout,
1585            entries: &[
1586                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) },
1587                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
1588            ],
1589            label: Some(id),
1590        });
1591        self.texture_bind_groups.push(bind_group);
1592        let tid = (self.texture_bind_groups.len() - 1) as u32;
1593        self.texture_registry.insert(id.to_string(), tid);
1594    }
1595
1596    fn draw_heatmap(&mut self, texture_id: &str, rect: Rect, _palette: &str) {
1597        let tid = self.get_texture_id(texture_id);
1598        self.fill_rect_with_mode(rect, [1.0, 1.0, 1.0, 1.0], 12, tid);
1599    }
1600
1601    fn draw_mesh(&mut self, mesh: &Mesh, color: [f32; 4], transform: glam::Mat4) {
1602        let base_idx = self.vertices.len() as u32;
1603        let screen = [self.config.width as f32, self.config.height as f32];
1604        
1605        for i in 0..mesh.vertices.len() {
1606            let pos = transform.transform_point3(glam::Vec3::from(mesh.vertices[i]));
1607            let norm = transform.transform_vector3(glam::Vec3::from(mesh.normals[i]));
1608            
1609            self.vertices.push(Vertex {
1610                position: pos.to_array(),
1611                normal: norm.to_array(),
1612                uv: [0.0, 0.0],
1613                color,
1614                mode: 13, // Mode 13: 3D Surface
1615                radius: 0.0,
1616                slice: [0.0, 0.0, 0.0],
1617                logical: [0.0, 0.0],
1618                size: [0.0, 0.0],
1619                screen,
1620                clip: [-10000.0, -10000.0, 20000.0, 20000.0],
1621            });
1622        }
1623        
1624        for idx in &mesh.indices {
1625            self.indices.push(base_idx + idx);
1626        }
1627        
1628        if self.draw_calls.is_empty() || self.current_texture_id.is_some() {
1629             self.current_texture_id = None;
1630             self.draw_calls.push(DrawCall {
1631                 texture_id: None,
1632                 scissor_rect: self.clip_stack.last().copied(),
1633                 index_start: (self.indices.len() as u32) - (mesh.indices.len() as u32),
1634                 index_count: mesh.indices.len() as u32,
1635             });
1636        } else {
1637             self.draw_calls.last_mut().unwrap().index_count += mesh.indices.len() as u32;
1638        }
1639    }
1640
1641    fn register_shared_element(&mut self, id: &str, rect: Rect) {
1642        self.shared_elements.insert(id.to_string(), rect);
1643    }
1644}
1645
1646
1647impl cvkg_core::FrameRenderer<wgpu::CommandEncoder> for SurtrRenderer {
1648    fn begin_frame(&mut self) -> wgpu::CommandEncoder {
1649        self.begin_frame()
1650    }
1651
1652    fn end_frame(&mut self, encoder: wgpu::CommandEncoder) {
1653        self.end_frame(encoder)
1654    }
1655}
1656
1657impl SurtrRenderer {
1658    /// Returns the current effective opacity (product of all stacked values).
1659    fn apply_opacity(&self, mut color: [f32; 4]) -> [f32; 4] {
1660        if let Some(&alpha) = self.opacity_stack.last() {
1661            color[3] *= alpha;
1662        }
1663        color
1664    }
1665}