Skip to main content

cvkg_render_gpu/
api.rs

1//! Bridging the internal renderer to `cvkg-core` traits.
2use cvkg_core::{Mesh, Rect, Renderer, ColorTheme};
3use crate::renderer::SurtrRenderer;
4use crate::types::*;
5use crate::vertex::*;
6use bytemuck;
7use std::sync::atomic::Ordering;
8use cvkg_core::LAYOUT_DIRTY;
9use lyon::tessellation::{
10    BuffersBuilder, StrokeOptions,
11    StrokeTessellator, VertexBuffers,
12};
13use lyon::math::point;
14
15
16impl cvkg_core::ElapsedTime for SurtrRenderer {
17    fn delta_time(&self) -> f32 {
18        self.current_scene.delta_time
19    }
20
21    fn elapsed_time(&self) -> f32 {
22        self.start_time.elapsed().as_secs_f32()
23    }
24}
25
26impl cvkg_core::Renderer for SurtrRenderer {
27    fn is_over_budget(&self) -> bool {
28        self.frame_budget.allow_degradation
29        && self.last_frame_start.elapsed().as_secs_f32() * 1000.0 > self.frame_budget.target_ms
30    }
31
32    /// fill_rect — Standard rectangle drawing method.
33    fn prewarm_vram(&mut self, assets: Vec<(String, Vec<u8>)>) {
34        log::info!(
35            "[Surtr] Pre-warming Mega-Heim with {} assets...",
36            assets.len()
37        );
38        for (name, data) in assets {
39            self.load_image_to_heim(&name, &data);
40        }
41    }
42
43    fn fill_rect(&mut self, rect: Rect, color: [f32; 4]) {
44        self.fill_rect_with_mode(rect, self.apply_opacity(color), 0, None);
45    }
46
47    fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4]) {
48        self.fill_rect_with_full_params(
49            rect,
50            self.apply_opacity(color),
51                                        3,
52                                        None,
53                                        radius,
54                                        Rect {
55                                            x: 0.0,
56                                            y: 0.0,
57                                            width: 1.0,
58                                            height: 1.0,
59                                        },
60        );
61    }
62
63    fn fill_ellipse(&mut self, rect: Rect, color: [f32; 4]) {
64        self.fill_rect_with_full_params(
65            rect,
66            self.apply_opacity(color),
67                                        4,
68                                        None,
69                                        0.0,
70                                        Rect {
71                                            x: 0.0,
72                                            y: 0.0,
73                                            width: 1.0,
74                                            height: 1.0,
75                                        },
76        );
77    }
78
79    fn draw_3d_cube(&mut self, rect: Rect, color: [f32; 4], rotation: [f32; 3]) {
80        self.fill_rect_with_full_params_and_slice(
81            rect,
82            self.apply_opacity(color),
83                                                  21,
84                                                  None,
85                                                  0.0,
86                                                  Rect {
87                                                      x: 0.0,
88                                                      y: 0.0,
89                                                      width: 1.0,
90                                                      height: 1.0,
91                                                  },
92                                                  [rotation[0], rotation[1], rotation[2], 0.0],
93        );
94    }
95
96    fn bifrost(&mut self, rect: Rect, blur: f32, _saturation: f32, opacity: f32) {
97        // Calculate screen-space UVs for high-fidelity global refraction
98        let screen_uv = Rect {
99            x: rect.x / self.current_width() as f32,
100            y: rect.y / self.current_height() as f32,
101            width: rect.width / self.current_width() as f32,
102            height: rect.height / self.current_height() as f32,
103        };
104        // Use mode 7 for high-fidelity background blur sampling
105        // Use the blur parameter as corner radius for the glass panel
106        self.fill_rect_with_full_params(rect, [1.0, 1.0, 1.0, opacity], 7, None, blur, screen_uv);
107    }
108
109    fn gungnir(&mut self, rect: Rect, color: [f32; 4], radius: f32, intensity: f32) {
110        // Create neon glow effect using additive blending
111        // This renders a glowing aura around the element
112        let center_x = rect.x + rect.width * 0.5;
113        let center_y = rect.y + rect.height * 0.5;
114        let max_dim = rect.width.max(rect.height) * 0.5 + radius;
115
116        // Draw expanding glow layers
117        for i in 0..8 {
118            let alpha = intensity / (i as f32 + 1.0) * 0.3;
119            let glow_color = [color[0], color[1], color[2], alpha];
120            self.fill_rect_with_mode(
121                Rect {
122                    x: center_x - max_dim - i as f32 * 2.0,
123                    y: center_y - max_dim - i as f32 * 2.0,
124                    width: max_dim * 2.0 + i as f32 * 4.0,
125                    height: max_dim * 2.0 + i as f32 * 4.0,
126                },
127                glow_color,
128                8, // Mode for additive blending
129                None,
130            );
131        }
132    }
133
134    /// Renders a dynamic glowing hover boundary field around a hit target.
135    ///
136    /// # Contract
137    /// Expands the bounding box of the visual target by `radius` to establish
138    /// a continuous proximity glow. Uses blending mode 18 (GPU drop shadow/glow)
139    /// to rasterize the glow with specialized radius-to-margin uv coordinate mappings.
140    fn mani_glow(&mut self, rect: Rect, color: [f32; 4], radius: f32) {
141        let margin = radius;
142        let glow_rect = Rect {
143            x: rect.x - margin,
144            y: rect.y - margin,
145            width: rect.width + 2.0 * margin,
146            height: rect.height + 2.0 * margin,
147        };
148        let uv_rect = Rect {
149            x: margin,
150            y: radius,
151            width: 0.0,
152            height: 0.0,
153        };
154        self.fill_rect_with_full_params(
155            glow_rect,
156            self.apply_opacity(color),
157                                        18,
158                                        None,
159                                        8.0,
160                                        uv_rect,
161        );
162    }
163
164    fn stroke_rect(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
165        let c = self.apply_opacity(color);
166        let hw = stroke_width;
167        // Top, bottom, left, right edge bars
168        self.fill_rect_with_mode(
169            Rect {
170                x: rect.x,
171                y: rect.y,
172                width: rect.width,
173                height: hw,
174            },
175            c,
176            1,
177            None,
178        );
179        self.fill_rect_with_mode(
180            Rect {
181                x: rect.x,
182                y: rect.y + rect.height - hw,
183                width: rect.width,
184                height: hw,
185            },
186            c,
187            1,
188            None,
189        );
190        self.fill_rect_with_mode(
191            Rect {
192                x: rect.x,
193                y: rect.y,
194                width: hw,
195                height: rect.height,
196            },
197            c,
198            1,
199            None,
200        );
201        self.fill_rect_with_mode(
202            Rect {
203                x: rect.x + rect.width - hw,
204                y: rect.y,
205                width: hw,
206                height: rect.height,
207            },
208            c,
209            1,
210            None,
211        );
212    }
213
214    fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4], stroke_width: f32) {
215        self.fill_rect_with_full_params(
216            rect,
217            self.apply_opacity(color),
218                                        17,
219                                        None,
220                                        radius,
221                                        Rect {
222                                            x: stroke_width,
223                                            y: 0.0,
224                                            width: 0.0,
225                                            height: 0.0,
226                                        },
227        );
228    }
229
230    fn stroke_ellipse(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
231        // Tessellate an ellipse stroke using Lyon's StrokeTessellator.
232        let cx = rect.x + rect.width / 2.0;
233        let cy = rect.y + rect.height / 2.0;
234        let rx = rect.width / 2.0;
235        let ry = rect.height / 2.0;
236
237        // Build an ellipse path using Lyon
238        let mut builder = lyon::path::Path::builder();
239        if rx > 0.0 && ry > 0.0 {
240            // Approximate ellipse with 64 segments
241            let segments = 64;
242            for i in 0..segments {
243                let angle = 2.0 * std::f32::consts::PI * (i as f32) / (segments as f32);
244                let x = cx + rx * angle.cos();
245                let y = cy + ry * angle.sin();
246                if i == 0 {
247                    builder.begin(lyon::math::point(x, y));
248                } else {
249                    builder.line_to(lyon::math::point(x, y));
250                }
251            }
252            builder.close();
253        }
254        let path = builder.build();
255        self.stroke_path(&path, color, stroke_width);
256    }
257
258    fn draw_linear_gradient(
259        &mut self,
260        rect: Rect,
261        start_color: [f32; 4],
262        end_color: [f32; 4],
263        angle: f32,
264    ) {
265        self.fill_rect_with_full_params_and_slice(
266            rect,
267            self.apply_opacity(start_color),
268                                                  15,
269                                                  None,
270                                                  0.0,
271                                                  Rect {
272                                                      x: angle,
273                                                      y: 0.0,
274                                                      width: 1.0,
275                                                      height: 1.0,
276                                                  },
277                                                  end_color,
278        );
279    }
280
281    fn draw_radial_gradient(&mut self, rect: Rect, inner_color: [f32; 4], outer_color: [f32; 4]) {
282        self.fill_rect_with_full_params_and_slice(
283            rect,
284            self.apply_opacity(inner_color),
285                                                  16,
286                                                  None,
287                                                  0.0,
288                                                  Rect {
289                                                      x: 0.0,
290                                                      y: 0.0,
291                                                      width: 1.0,
292                                                      height: 1.0,
293                                                  },
294                                                  outer_color,
295        );
296    }
297
298    fn draw_drop_shadow(
299        &mut self,
300        rect: Rect,
301        radius: f32,
302        color: [f32; 4],
303        blur: f32,
304        spread: f32,
305    ) {
306        let margin = blur + spread;
307        let inflated = Rect {
308            x: rect.x - margin,
309            y: rect.y - margin,
310            width: rect.width + margin * 2.0,
311            height: rect.height + margin * 2.0,
312        };
313        // uv.x = total margin (for SDF offset), uv.y = blur width (for falloff)
314        self.fill_rect_with_full_params(
315            inflated,
316            self.apply_opacity(color),
317                                        18,
318                                        None,
319                                        radius,
320                                        Rect {
321                                            x: margin,
322                                            y: blur,
323                                            width: 0.0,
324                                            height: 0.0,
325                                        },
326        );
327    }
328
329    fn stroke_dashed_rounded_rect(
330        &mut self,
331        rect: Rect,
332        radius: f32,
333        color: [f32; 4],
334        width: f32,
335        dash: f32,
336        gap: f32,
337    ) {
338        self.fill_rect_with_full_params(
339            rect,
340            self.apply_opacity(color),
341                                        19,
342                                        None,
343                                        radius,
344                                        Rect {
345                                            x: width,
346                                            y: dash,
347                                            width: gap,
348                                            height: 0.0,
349                                        },
350        );
351    }
352
353    fn draw_9slice(
354        &mut self,
355        image_name: &str,
356        rect: Rect,
357        left: f32,
358        top: f32,
359        right: f32,
360        bottom: f32,
361    ) {
362        let c = self.apply_opacity([1.0, 1.0, 1.0, 1.0]);
363        let tid = self.get_texture_id(image_name);
364        self.fill_rect_with_full_params(
365            rect,
366            c,
367            20,
368            tid,
369            bottom,
370            Rect {
371                x: left,
372                y: top,
373                width: right,
374                height: 0.0,
375            },
376        );
377    }
378
379    fn draw_line(
380        &mut self,
381        x1: f32,
382        y1: f32,
383        x2: f32,
384        y2: f32,
385        color: [f32; 4],
386        stroke_width: f32,
387    ) {
388        let dx = x2 - x1;
389        let dy = y2 - y1;
390        let len = (dx * dx + dy * dy).sqrt();
391        if len < 0.001 {
392            return;
393        }
394
395        // Create a proper line path using Lyon for correct tessellation
396        // The stroke_path function will apply the current transform, which handles rotation
397        let mut builder = lyon::path::Path::builder();
398        builder.begin(point(x1, y1));
399        builder.line_to(point(x2, y2));
400        builder.close();
401        let path = builder.build();
402
403        self.stroke_path(&path, color, stroke_width);
404    }
405
406    fn draw_image(&mut self, image_name: &str, rect: Rect) {
407        // Guard: skip if image not loaded — avoids rendering garbage from uninitialized atlas regions
408        if !self.image_uv_registry.contains(image_name) {
409            log::warn!("[Surtr] draw_image: '{}' not loaded, skipping", image_name);
410            return;
411        }
412        let tid = self
413        .get_texture_id(image_name)
414        .or_else(|| self.get_texture_id("__mega_heim"));
415        let uv_rect = self
416        .image_uv_registry
417        .get(image_name)
418        .copied()
419        .unwrap_or(Rect {
420            x: 0.0,
421            y: 0.0,
422            width: 1.0,
423            height: 1.0,
424        });
425        self.fill_rect_with_full_params(rect, [1.0, 1.0, 1.0, 1.0], 2, tid, 0.0, uv_rect);
426    }
427
428    fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
429        // High-DPI: Shape and rasterize at the physical scale factor for maximum sharpness.
430        let scaled_size = size * self.current_scale_factor();
431        let shaped = self.shape_text_with_stack(text, scaled_size);
432        let c = self.apply_opacity(color);
433
434        for glyph in shaped.glyphs {
435            let cache_key = glyph.cache_key;
436
437            let (uv_rect, w, h, x_off, y_off) = if let Some(info) = self.text_cache.get(&cache_key) {
438                *info
439            } else {
440                if let Some(image) = self.text_engine.rasterize(cache_key) {
441                    let gw = image.width;
442                    let gh = image.height;
443                    let x_offset = image.x_offset;
444                    let y_offset = image.y_offset;
445
446                    let pack_res = self.heim_packer.pack(gw, gh);
447                    let (nx, ny) = if let Some(pos) = pack_res {
448                        pos
449                    } else {
450                        // RECLAIM & RETRY: Heim is full, quench the forge and try again.
451                        self.reclaim_vram();
452                        match self.heim_packer.pack(gw, gh) {
453                            Some(pos) => pos,
454                            None => {
455                                log::error!(
456                                    "Glyph heim critically full after reclaim: cannot pack {}x{} glyph for '{}', skipping",
457                                    gw, gh, text
458                                );
459                                continue; // Skip this glyph rather than corrupting atlas origin
460                            }
461                        }
462                    };
463
464                    let mut rgba_data = Vec::with_capacity((gw * gh * 4) as usize);
465                    for alpha in &image.data {
466                        rgba_data.push(255);
467                        rgba_data.push(255);
468                        rgba_data.push(255);
469                        rgba_data.push(*alpha);
470                    }
471
472                    self.queue.write_texture(
473                        wgpu::TexelCopyTextureInfo {
474                            texture: &self.mega_heim_tex,
475                            mip_level: 0,
476                            origin: wgpu::Origin3d { x: nx, y: ny, z: 0 },
477                            aspect: wgpu::TextureAspect::All,
478                        },
479                        &rgba_data,
480                        wgpu::TexelCopyBufferLayout {
481                            offset: 0,
482                            bytes_per_row: Some(gw * 4),
483                            rows_per_image: Some(gh),
484                        },
485                        wgpu::Extent3d {
486                            width: gw,
487                            height: gh,
488                            depth_or_array_layers: 1,
489                        },
490                    );
491
492                    let info = (
493                        Rect {
494                            x: nx as f32 / 4096.0,
495                            y: ny as f32 / 4096.0,
496                            width: gw as f32 / 4096.0,
497                            height: gh as f32 / 4096.0,
498                        },
499                        gw as f32,
500                        gh as f32,
501                        x_offset,
502                        y_offset,
503                    );
504                    self.text_cache.put(cache_key, info);
505                    info
506                } else {
507                    (Rect::zero(), 0.0, 0.0, 0.0, 0.0)
508                }
509            };
510
511            if w > 0.0 {
512                // Position glyph relative to baseline.
513                // glyph.x/y are in physical pixels, baseline-relative.
514                // shaped.ascent gives the baseline offset from the text origin (y).
515                let baseline_y = y + shaped.ascent / self.current_scale_factor();
516                let glyph_rect = Rect {
517                    x: x + (glyph.x + x_off) / self.current_scale_factor(),
518                    y: baseline_y + (glyph.y - y_off) / self.current_scale_factor(),
519                    width: w / self.current_scale_factor(),
520                    height: h / self.current_scale_factor(),
521                };
522                let tid = self.get_texture_id("__mega_heim");
523                self.fill_rect_with_full_params(glyph_rect, c, 6, tid, 0.0, uv_rect);
524            }
525        }
526    }
527
528    /// measure_text — Calculates the dimensions of a text string without rendering.
529    fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
530        let sf = self.current_scale_factor();
531        let shaped = self.shape_text_with_stack(text, size * sf);
532        // Convert physical pixels back to logical units
533        (shaped.width / sf, shaped.height / sf)
534    }
535
536    fn shape_rich_text(
537        &mut self,
538        spans: &[cvkg_runic_text::TextSpan],
539        max_width: Option<f32>,
540        align: cvkg_runic_text::TextAlign,
541        overflow: cvkg_runic_text::TextOverflow,
542    ) -> Option<cvkg_runic_text::ShapedText> {
543        let sf = self.current_scale_factor();
544        let mut scaled_spans = spans.to_vec();
545        for span in &mut scaled_spans {
546            span.style.font_size *= sf;
547            if span.style.fallback_families.is_empty() {
548                span.style.fallback_families = vec![
549                    "SF Pro".to_string(),
550                    "Inter".to_string(),
551                    "Helvetica Neue".to_string(),
552                    "Helvetica".to_string(),
553                    "Arial".to_string(),
554                    "sans-serif".to_string(),
555                ];
556            }
557        }
558        let scaled_max_width = max_width.map(|w| w * sf);
559        self.text_engine
560        .shape_layout(&scaled_spans, scaled_max_width, align, overflow)
561        .ok()
562    }
563
564    fn draw_shaped_text(&mut self, shaped: &cvkg_runic_text::ShapedText, x: f32, y: f32) {
565        for glyph in &shaped.glyphs {
566            let byte_idx = shaped
567            .grapheme_boundaries
568            .get(glyph.cluster as usize)
569            .copied()
570            .unwrap_or(0);
571            let mut span_color = [1.0, 1.0, 1.0, 1.0];
572            for span in &shaped.spans {
573                if byte_idx >= span.byte_offset && byte_idx < span.byte_offset + span.text.len() {
574                    span_color = [
575                        span.style.color[0] as f32 / 255.0,
576                        span.style.color[1] as f32 / 255.0,
577                        span.style.color[2] as f32 / 255.0,
578                        span.style.color[3] as f32 / 255.0,
579                    ];
580                    break;
581                }
582            }
583            let c = self.apply_opacity(span_color);
584
585            let cache_key = glyph.cache_key;
586            let (uv_rect, w, h, x_off, y_off) = if let Some(info) = self.text_cache.get(&cache_key) {
587                *info
588            } else {
589                if let Some(image) = self.text_engine.rasterize(cache_key) {
590                    let gw = image.width;
591                    let gh = image.height;
592                    let x_offset = image.x_offset;
593                    let y_offset = image.y_offset;
594
595                    let pack_res = self.heim_packer.pack(gw, gh);
596                    let (nx, ny) = if let Some(pos) = pack_res {
597                        pos
598                    } else {
599                        self.reclaim_vram();
600                        match self.heim_packer.pack(gw, gh) {
601                            Some(pos) => pos,
602                            None => {
603                                log::error!(
604                                    "Glyph heim critically full after reclaim: cannot pack {}x{} glyph, skipping",
605                                    gw, gh
606                                );
607                                continue; // Skip this glyph rather than corrupting atlas origin
608                            }
609                        }
610                    };
611
612                    let mut rgba_data = Vec::with_capacity((gw * gh * 4) as usize);
613                    for alpha in &image.data {
614                        rgba_data.push(255);
615                        rgba_data.push(255);
616                        rgba_data.push(255);
617                        rgba_data.push(*alpha);
618                    }
619
620                    self.queue.write_texture(
621                        wgpu::TexelCopyTextureInfo {
622                            texture: &self.mega_heim_tex,
623                            mip_level: 0,
624                            origin: wgpu::Origin3d { x: nx, y: ny, z: 0 },
625                            aspect: wgpu::TextureAspect::All,
626                        },
627                        &rgba_data,
628                        wgpu::TexelCopyBufferLayout {
629                            offset: 0,
630                            bytes_per_row: Some(gw * 4),
631                            rows_per_image: Some(gh),
632                        },
633                        wgpu::Extent3d {
634                            width: gw,
635                            height: gh,
636                            depth_or_array_layers: 1,
637                        },
638                    );
639
640                    let info = (
641                        Rect {
642                            x: nx as f32 / 4096.0,
643                            y: ny as f32 / 4096.0,
644                            width: gw as f32 / 4096.0,
645                            height: gh as f32 / 4096.0,
646                        },
647                        gw as f32,
648                        gh as f32,
649                        x_offset,
650                        y_offset,
651                    );
652                    self.text_cache.put(cache_key, info);
653                    info
654                } else {
655                    (Rect::zero(), 0.0, 0.0, 0.0, 0.0)
656                }
657            };
658
659            if w > 0.0 {
660                let sf = self.current_scale_factor();
661                // Position glyph relative to baseline.
662                // glyph.x/y are in physical pixels, baseline-relative.
663                // shaped.ascent gives the baseline offset from the text origin (y).
664                let baseline_y = y + shaped.ascent / sf;
665                let glyph_rect = Rect {
666                    x: x + (glyph.x + x_off) / sf,
667                    y: baseline_y + (glyph.y - y_off) / sf,
668                    width: w / sf,
669                    height: h / sf,
670                };
671                let tid = self.get_texture_id("__mega_heim");
672                self.fill_rect_with_full_params(glyph_rect, c, 6, tid, 0.0, uv_rect);
673            }
674        }
675    }
676
677    fn draw_texture(&mut self, texture_id: u32, rect: Rect) {
678        self.fill_rect_with_full_params(
679            rect,
680            [1.0, 1.0, 1.0, 1.0],
681            2,
682            Some(texture_id),
683                                        0.0,
684                                        Rect {
685                                            x: 0.0,
686                                            y: 0.0,
687                                            width: 1.0,
688                                            height: 1.0,
689                                        },
690        );
691    }
692
693    /// load_image — Proactively pushes a raw asset into the Mega-Heim.
694    /// load_image — Proactively pushes a raw asset into the Texture Array.
695    fn load_image(&mut self, name: &str, data: &[u8]) {
696        if self.image_uv_registry.contains(name) {
697            return;
698        }
699        let img_result = image::load_from_memory(data);
700        let img = match img_result {
701            Ok(img) => img.to_rgba8(),
702            Err(e) => {
703                log::error!("Failed to load image {}: {}", name, e);
704                image::RgbaImage::from_pixel(1, 1, image::Rgba([255, 255, 255, 255]))
705            }
706        };
707        let (width, height) = img.dimensions();
708
709        let size = wgpu::Extent3d {
710            width,
711            height,
712            depth_or_array_layers: 1,
713        };
714        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
715            label: Some(&format!("Texture Array Layer: {}", name)),
716                                                 size,
717                                                 mip_level_count: 1,
718                                                 sample_count: 1,
719                                                 dimension: wgpu::TextureDimension::D2,
720                                                 format: wgpu::TextureFormat::Rgba8UnormSrgb,
721                                                     usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
722                                                     view_formats: &[],
723        });
724
725        self.queue.write_texture(
726            wgpu::TexelCopyTextureInfo {
727                texture: &texture,
728                mip_level: 0,
729                origin: wgpu::Origin3d::ZERO,
730                aspect: wgpu::TextureAspect::All,
731            },
732            &img,
733            wgpu::TexelCopyBufferLayout {
734                offset: 0,
735                bytes_per_row: Some(4 * width),
736                                 rows_per_image: Some(height),
737            },
738            size,
739        );
740
741        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
742
743        // Slot allocation (Skip index 0 which is the dummy/atlas)
744        let index = if self.texture_registry.len() < 255 {
745            (self.texture_registry.len() + 1) as u32
746        } else {
747            // Evict the least recently used texture
748            if let Some((old_name, old_index)) = self.texture_registry.pop_lru() {
749                self.image_uv_registry.pop(&old_name);
750                old_index
751            } else {
752                1 // Fallback
753            }
754        };
755
756        self.texture_views[index as usize] = view;
757        self.image_uv_registry.put(
758            name.to_string(),
759                                   Rect {
760                                       x: 0.0,
761                                       y: 0.0,
762                                       width: 1.0,
763                                       height: 1.0,
764                                   },
765        );
766        self.texture_registry.put(name.to_string(), index);
767        self.rebuild_texture_array_bind_group();
768    }
769
770    fn push_clip_rect(&mut self, rect: Rect) {
771        self.clip_stack.push(rect);
772    }
773
774    fn pop_clip_rect(&mut self) {
775        self.clip_stack.pop();
776    }
777
778    fn current_clip_rect(&self) -> Rect {
779        self.clip_stack.last().copied().unwrap_or(Rect::new(
780            0.0,
781            0.0,
782            self.current_width() as f32,
783                                                            self.current_height() as f32,
784        ))
785    }
786
787    fn memoize(&mut self, id: u64, data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer)) {
788        // Check if we've already rendered this content with the same hash this frame
789        // The cache stores the last-seen hash for each ID
790        let should_skip = self.memo_cache.get(&id) == Some(&data_hash);
791
792        if !should_skip {
793            // Update cache with current hash
794            self.memo_cache.insert(id, data_hash);
795            render_fn(self);
796        }
797        // If should_skip is true, we skip rendering as the content hasn't changed
798    }
799
800    fn push_opacity(&mut self, opacity: f32) {
801        let current = self.opacity_stack.last().copied().unwrap_or(1.0);
802        self.opacity_stack.push(current * opacity);
803    }
804
805    fn pop_opacity(&mut self) {
806        self.opacity_stack.pop();
807    }
808
809    fn push_shadow(&mut self, radius: f32, color: [f32; 4], offset: [f32; 2]) {
810        self.shadow_stack.push(ShadowState {
811            radius,
812            color,
813            _offset: offset,
814        });
815    }
816
817    fn pop_shadow(&mut self) {
818        self.shadow_stack.pop();
819    }
820
821    fn push_transform(&mut self, translation: [f32; 2], scale: [f32; 2], rotation: f32) {
822        let c = rotation.cos();
823        let sn = rotation.sin();
824        let affine = glam::Mat3::from_cols(
825            glam::Vec3::new(c * scale[0], sn * scale[0], 0.0),
826                                           glam::Vec3::new(-sn * scale[1], c * scale[1], 0.0),
827                                           glam::Vec3::new(translation[0], translation[1], 1.0),
828        );
829
830        let parent = self
831        .transform_stack
832        .last()
833        .copied()
834        .unwrap_or(glam::Mat3::IDENTITY);
835        self.transform_stack.push(parent * affine);
836    }
837
838    fn push_affine(&mut self, transform: [f32; 6]) {
839        let affine = glam::Mat3::from_cols(
840            glam::Vec3::new(transform[0], transform[1], 0.0),
841                                           glam::Vec3::new(transform[2], transform[3], 0.0),
842                                           glam::Vec3::new(transform[4], transform[5], 1.0),
843        );
844        let parent = self
845        .transform_stack
846        .last()
847        .copied()
848        .unwrap_or(glam::Mat3::IDENTITY);
849        self.transform_stack.push(parent * affine);
850    }
851
852    fn pop_transform(&mut self) {
853        self.transform_stack.pop();
854    }
855
856    fn set_theme(&mut self, theme: ColorTheme) {
857        self.current_theme = theme;
858        self.queue
859        .write_buffer(&self.theme_buffer, 0, bytemuck::bytes_of(&theme));
860    }
861
862    fn set_rage(&mut self, rage: f32) {
863        self.current_scene.berzerker_rage = rage;
864        // scene_buffer is updated every frame in begin_frame, so no need to write here
865    }
866
867    fn trigger_shatter_event(&mut self, origin: [f32; 2], force: f32) {
868        self.current_scene.shatter_origin = origin;
869        self.current_scene.shatter_time = self.current_scene.time;
870        self.current_scene.shatter_force = force;
871    }
872
873    fn set_scene_preset(&mut self, preset: u32) {
874        self.current_scene.scene_type = preset;
875    }
876
877    /// push_mjolnir_slice — Pushes a geometric clipping plane onto the stack.
878    /// All subsequent draw calls will be sliced by this plane until it is popped.
879    fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
880        self.slice_stack.push((angle, offset));
881    }
882
883    /// pop_mjolnir_slice — Removes the top-most geometric clipping plane from the stack.
884    fn pop_mjolnir_slice(&mut self) {
885        self.slice_stack.pop();
886    }
887
888    fn mjolnir_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
889        self.shatter_internal(rect, pieces, force, color, 8);
890    }
891
892    fn mjolnir_fluid_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
893        self.shatter_internal(rect, pieces, force, color, 11);
894    }
895
896    fn draw_mjolnir_bolt(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
897        self.recursive_bolt(from, to, 4, color);
898    }
899
900    fn upload_data_texture(&mut self, id: &str, data: &[f32], width: u32, height: u32) {
901        let size = wgpu::Extent3d {
902            width,
903            height,
904            depth_or_array_layers: 1,
905        };
906        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
907            label: Some(id),
908                                                 size,
909                                                 mip_level_count: 1,
910                                                 sample_count: 1,
911                                                 dimension: wgpu::TextureDimension::D2,
912                                                 format: wgpu::TextureFormat::R32Float,
913                                                     usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
914                                                     view_formats: &[],
915        });
916        self.queue.write_texture(
917            wgpu::TexelCopyTextureInfo {
918                texture: &texture,
919                mip_level: 0,
920                origin: wgpu::Origin3d::ZERO,
921                aspect: wgpu::TextureAspect::All,
922            },
923            bytemuck::cast_slice(data),
924                                 wgpu::TexelCopyBufferLayout {
925                                     offset: 0,
926                                     bytes_per_row: Some(4 * width),
927                                 rows_per_image: Some(height),
928                                 },
929                                 size,
930        );
931        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
932        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
933            address_mode_u: wgpu::AddressMode::ClampToEdge,
934            address_mode_v: wgpu::AddressMode::ClampToEdge,
935            mag_filter: wgpu::FilterMode::Linear,
936            ..Default::default()
937        });
938        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
939            layout: &self.texture_bind_group_layout,
940            entries: &[
941                wgpu::BindGroupEntry {
942                    binding: 0,
943                    resource: wgpu::BindingResource::TextureViewArray(&vec![&view; 256]),
944                },
945                wgpu::BindGroupEntry {
946                    binding: 1,
947                    resource: wgpu::BindingResource::Sampler(&sampler),
948                },
949            ],
950            label: Some(id),
951        });
952        self.texture_bind_groups.push(bind_group);
953        let tid = (self.texture_bind_groups.len() - 1) as u32;
954        self.texture_registry.put(id.to_string(), tid);
955    }
956
957    fn draw_heatmap(&mut self, texture_id: &str, rect: Rect, _palette: &str) {
958        let tid = self.get_texture_id(texture_id);
959        self.fill_rect_with_mode(rect, [1.0, 1.0, 1.0, 1.0], 12, tid);
960    }
961
962    fn draw_mesh(&mut self, mesh: &Mesh, color: [f32; 4], transform: glam::Mat4) {
963        let base_idx = self.vertices.len() as u32;
964        let screen = [self.current_width() as f32, self.current_height() as f32];
965
966        for i in 0..mesh.vertices.len() {
967            let pos = transform.transform_point3(glam::Vec3::from(mesh.vertices[i]));
968            let norm = transform.transform_vector3(glam::Vec3::from(mesh.normals[i]));
969
970            let (translation, scale_transform, rotation, _, _) = self.current_transform();
971            self.vertices.push(Vertex {
972                position: pos.to_array(),
973                               normal: norm.to_array(),
974                               uv: [0.0, 0.0],
975                               color,
976                               material_id: 13, // Material 13: 3D Surface
977                               radius: 0.0,
978                               slice: [0.0, 0.0, 0.0, 1.0],
979                               logical: [0.0, 0.0],
980                               size: [0.0, 0.0],
981                               screen,
982                               clip: [-10000.0, -10000.0, 20000.0, 20000.0],
983                               translation,
984                               scale: scale_transform,
985                               rotation,
986                               tex_index: 0,
987            });
988        }
989
990        for idx in &mesh.indices {
991            self.indices.push(base_idx + idx);
992        }
993
994        if self.draw_calls.is_empty() || self.current_texture_id.is_some() {
995            self.current_texture_id = None;
996            self.draw_calls.push(DrawCall {
997                texture_id: None,
998                scissor_rect: self.clip_stack.last().copied(),
999                                 index_start: (self.indices.len() as u32) - (mesh.indices.len() as u32),
1000                                 index_count: mesh.indices.len() as u32,
1001                                 material: cvkg_core::DrawMaterial::Opaque,
1002            });
1003        } else {
1004            self.draw_calls.last_mut().unwrap().index_count += mesh.indices.len() as u32;
1005        }
1006    }
1007
1008    fn draw_mesh_3d(&mut self, mesh: &Mesh, material: &cvkg_core::Material3D, transform: &cvkg_core::Transform3D) {
1009        let base_idx = self.vertices.len() as u32;
1010        let screen = [self.current_width() as f32, self.current_height() as f32];
1011        let model_matrix = transform.to_matrix();
1012
1013        for i in 0..mesh.vertices.len() {
1014            let pos = model_matrix.transform_point3(glam::Vec3::from(mesh.vertices[i]));
1015            let norm = model_matrix.transform_vector3(glam::Vec3::from(mesh.normals[i]));
1016
1017            self.vertices.push(Vertex {
1018                position: [pos.x, pos.y, pos.z],
1019                normal: [norm.x, norm.y, norm.z],
1020                uv: [0.0, 0.0],
1021                color: material.base_color,
1022                material_id: 13, // Material 13: 3D Surface
1023                radius: 0.0,
1024                slice: [material.metallic, material.roughness, material.opacity, 1.0],
1025                logical: [0.0, 0.0],
1026                size: [0.0, 0.0],
1027                screen,
1028                clip: [-10000.0, -10000.0, 20000.0, 20000.0],
1029                translation: [0.0, 0.0],
1030                scale: [1.0, 1.0],
1031                rotation: 0.0,
1032                tex_index: 0,
1033            });
1034        }
1035
1036        for idx in &mesh.indices {
1037            self.indices.push(base_idx + idx);
1038        }
1039
1040        self.draw_calls.push(DrawCall {
1041            texture_id: None,
1042            scissor_rect: self.clip_stack.last().copied(),
1043                             index_start: (self.indices.len() as u32) - (mesh.indices.len() as u32),
1044                             index_count: mesh.indices.len() as u32,
1045                             material: cvkg_core::DrawMaterial::Opaque,
1046        });
1047    }
1048
1049    fn set_camera_3d(&mut self, camera: &cvkg_core::Camera3D) {
1050        self.current_scene.proj = camera.projection_matrix();
1051        self.current_scene.view = camera.view_matrix();
1052    }
1053
1054    fn push_transform_3d(&mut self, transform: &cvkg_core::Transform3D) {
1055        // Push a 2D-compatible transform for the existing pipeline
1056        // Use proper matrix decomposition to extract scale correctly (handles rotated matrices)
1057        let (translation, rotation_quat, scale_glam) = transform.to_matrix().to_scale_rotation_translation();
1058        let translation = [translation.x, translation.y];
1059        let scale = [scale_glam.x, scale_glam.y];
1060        let rotation = if rotation_quat.length_squared() > 0.0 {
1061            let (axis, angle) = rotation_quat.to_axis_angle();
1062            angle * axis.z.signum() // Radians (preserving Z-axis direction)
1063        } else {
1064            0.0
1065        };
1066        self.push_transform(translation, scale, rotation);
1067    }
1068
1069    fn pop_transform_3d(&mut self) {
1070        // Only pop the single transform that was pushed - no double pop
1071        self.pop_transform();
1072    }
1073
1074    fn render_scene_node_3d(
1075        &mut self,
1076        position: [f32; 3],
1077        rotation: [f32; 4],
1078        scale: [f32; 3],
1079        color: [f32; 4],
1080        meshes: &[Mesh],
1081    ) {
1082        let transform = cvkg_core::Transform3D {
1083            position: glam::Vec3::from(position),
1084            rotation: glam::Quat::from_xyzw(rotation[0], rotation[1], rotation[2], rotation[3]),
1085            scale: glam::Vec3::from(scale),
1086        };
1087        // Use provided mesh or generate a default unit cube
1088        if meshes.is_empty() {
1089            // Generate a unit cube mesh on the stack
1090            let h = 0.5f32;
1091            let cube = Mesh {
1092                vertices: vec![
1093                    [-h, -h, -h], [h, -h, -h], [h, h, -h], [-h, h, -h],
1094                    [-h, -h, h], [h, -h, h], [h, h, h], [-h, h, h],
1095                ],
1096                normals: vec![
1097                    [0.0, 0.0, -1.0], [0.0, 0.0, -1.0], [0.0, 0.0, -1.0], [0.0, 0.0, -1.0],
1098                    [0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0],
1099                    [0.0, -1.0, 0.0], [0.0, -1.0, 0.0], [0.0, -1.0, 0.0], [0.0, -1.0, 0.0],
1100                    [1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0],
1101                    [0.0, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 1.0, 0.0],
1102                    [-1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [-1.0, 0.0, 0.0],
1103                ],
1104                indices: vec![
1105                    0, 1, 2, 0, 2, 3,       // front
1106                    5, 4, 7, 5, 7, 6,       // back
1107                    4, 0, 3, 4, 3, 7,       // left
1108                    1, 5, 6, 1, 6, 2,       // right
1109                    3, 2, 6, 3, 6, 7,       // top
1110                    4, 5, 1, 4, 1, 0,       // bottom
1111                ],
1112            };
1113            let material = cvkg_core::Material3D::unlit(color);
1114            self.draw_mesh_3d(&cube, &material, &transform);
1115        } else {
1116            let material = cvkg_core::Material3D::unlit(color);
1117            self.draw_mesh_3d(&meshes[0], &material, &transform);
1118        }
1119    }
1120
1121    fn register_shared_element(&mut self, id: &str, rect: Rect) {
1122        self.shared_elements.put(id.to_string(), rect);
1123    }
1124
1125    fn set_z_index(&mut self, z: f32) {
1126        self.current_z = z;
1127    }
1128
1129    fn set_material(&mut self, material: cvkg_core::DrawMaterial) {
1130        self.current_draw_material = material;
1131    }
1132
1133    fn current_material(&self) -> cvkg_core::DrawMaterial {
1134        self.current_draw_material
1135    }
1136
1137    fn get_z_index(&self) -> f32 {
1138        self.current_z
1139    }
1140
1141    fn request_redraw(&mut self) {
1142        self.redraw_requested = true;
1143    }
1144
1145    fn push_vnode(&mut self, rect: Rect, name: &'static str) {
1146        self.vnode_stack.push((rect, name));
1147    }
1148
1149    fn pop_vnode(&mut self) {
1150        self.vnode_stack.pop();
1151    }
1152
1153    fn register_handler(
1154        &mut self,
1155        event_type: &str,
1156        handler: std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>,
1157    ) {
1158        self.event_handlers
1159        .entry(event_type.to_string())
1160        .or_insert_with(Vec::new)
1161        .push(handler);
1162    }
1163
1164    fn serialize_svg(&mut self, name: &str) -> Result<String, String> {
1165        let tree = self
1166        .svg_trees
1167        .get(name)
1168        .ok_or_else(|| format!("SVG '{}' not found", name))?;
1169        let config = cvkg_svg_serialize::SerializerConfig::default();
1170        let mut serializer = cvkg_svg_serialize::SvgSerializer::with_config(config);
1171        serializer
1172        .serialize(tree)
1173        .map_err(|e| format!("SVG serialization failed: {}", e))
1174    }
1175
1176    fn apply_svg_filter(
1177        &mut self,
1178        name: &str,
1179        filter_id: &str,
1180        _region: Rect,
1181    ) -> Result<String, String> {
1182        let tree = self
1183        .svg_trees
1184        .get(name)
1185        .ok_or_else(|| format!("SVG '{}' not found", name))?;
1186        let _filter = Self::find_filter(tree, filter_id)
1187        .ok_or_else(|| format!("Filter '{}' not found in SVG '{}'", filter_id, name))?;
1188        let config = cvkg_svg_serialize::SerializerConfig::default();
1189        let mut serializer = cvkg_svg_serialize::SvgSerializer::with_config(config);
1190        serializer
1191        .serialize(tree)
1192        .map_err(|e| format!("SVG filter serialization failed: {}", e))
1193    }
1194}
1195
1196// ── Inherent methods on SurtrRenderer (not part of the Renderer trait) ──
1197
1198impl SurtrRenderer {
1199    /// Clear all registered event handlers. Call at the start of each frame
1200    /// before re-rendering the component tree.
1201    pub fn clear_event_handlers(&mut self) {
1202        self.event_handlers.clear();
1203    }
1204
1205    /// Get all registered event handlers for a specific event type.
1206    pub fn get_handlers(
1207        &self,
1208        event_type: &str,
1209    ) -> Option<&Vec<std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>>> {
1210        self.event_handlers.get(event_type)
1211    }
1212
1213    /// Compute per-vertex transform values from the current matrix.
1214    /// Extracts translation, scale, rotation, and skew from the affine matrix
1215    /// so the existing vertex shader fields still work correctly.
1216    pub(crate) fn current_transform(&self) -> ([f32; 2], [f32; 2], f32, f32, f32) {
1217        // Returns (translation, scale, rotation, skew_x, skew_y)
1218        let m = self
1219        .transform_stack
1220        .last()
1221        .copied()
1222        .unwrap_or(glam::Mat3::IDENTITY);
1223        let t = [m.z_axis.x, m.z_axis.y];
1224        // Extract scale and rotation from the 2x2 submatrix
1225        let a = m.x_axis.x;
1226        let b = m.x_axis.y;
1227        let c = m.y_axis.x;
1228        let d = m.y_axis.y;
1229        let sx = (a * a + b * b).sqrt();
1230        let sy = (c * c + d * d).sqrt();
1231        let rotation = b.atan2(a);
1232        // Skew: the angle between the basis vectors minus 90 degrees
1233        let skew_x = (a * c + b * d) / (sx * sy); // sin(skew)
1234        (t, [sx, sy], rotation, skew_x, 0.0)
1235    }
1236
1237    pub fn stroke_path(&mut self, path: &lyon::path::Path, color: [f32; 4], stroke_width: f32) {
1238        let c = self.apply_opacity(color);
1239        let mut tessellator = StrokeTessellator::new();
1240        let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
1241        let base_vertex_idx = self.vertices.len() as u32;
1242        let base_index_idx = self.indices.len() as u32;
1243
1244        let (translation, scale, rotation, _, _) = self.current_transform();
1245        let screen = [self.current_width() as f32, self.current_height() as f32];
1246        let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
1247            x: -10000.0,
1248            y: -10000.0,
1249            width: 20000.0,
1250            height: 20000.0,
1251        });
1252        let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
1253
1254        let result = tessellator
1255        .tessellate_path(
1256            path,
1257            &StrokeOptions::default().with_line_width(stroke_width),
1258                         &mut BuffersBuilder::new(
1259                             &mut buffers,
1260                             CustomStrokeVertexConstructor {
1261                                 color: c,
1262                                 translation,
1263                                 scale,
1264                                 rotation,
1265                                 screen,
1266                                 clip,
1267                             },
1268                         ),
1269        );
1270        if let Err(e) = result {
1271            log::warn!("Failed to tessellate stroke path: {:?}", e);
1272            return;
1273        }
1274
1275        self.vertices.extend(buffers.vertices);
1276        for idx in &buffers.indices {
1277            self.indices.push(base_vertex_idx + *idx);
1278        }
1279
1280        let material = self.current_material();
1281        let tid = self.get_texture_id("__mega_heim");
1282
1283        let last_call = self.draw_calls.last();
1284        let needs_new_call = self.draw_calls.is_empty()
1285        || self.current_texture_id != tid
1286        || last_call.unwrap().scissor_rect != self.clip_stack.last().copied()
1287        || last_call.unwrap().material != material;
1288
1289        if needs_new_call {
1290            self.current_texture_id = tid;
1291            self.draw_calls.push(DrawCall {
1292                texture_id: tid,
1293                scissor_rect: self.clip_stack.last().copied(),
1294                                 index_start: base_index_idx,
1295                                 index_count: buffers.indices.len() as u32,
1296                                 material,
1297            });
1298        } else if let Some(call) = self.draw_calls.last_mut() {
1299            call.index_count += buffers.indices.len() as u32;
1300        }
1301    }
1302}
1303
1304impl cvkg_core::FrameRenderer<wgpu::CommandEncoder> for SurtrRenderer {
1305    fn begin_frame(&mut self) -> wgpu::CommandEncoder {
1306        cvkg_core::begin_render_phase();
1307        let id = self
1308        .current_window
1309        .expect("No target window set for frame. Call set_target_window first.");
1310        self.begin_frame(id)
1311    }
1312
1313    fn render_frame(&mut self) {
1314        // Visual Lint: If layout was dirtied during the render phase (layout thrashing),
1315        // draw a 10px red border as a warning flash.
1316        if LAYOUT_DIRTY.swap(false, Ordering::AcqRel)
1317            && let Some(window_id) = self.current_window
1318            && let Some(surface_ctx) = self.surfaces.get(&window_id)
1319            {
1320                let w = surface_ctx.config.width as f32;
1321                let h = surface_ctx.config.height as f32;
1322                let border_rect = cvkg_core::Rect {
1323                    x: 0.0,
1324                    y: 0.0,
1325                    width: w,
1326                    height: h,
1327                };
1328                // Draw a thick red border to signal layout-thrashing
1329                self.stroke_rect(border_rect, [1.0, 0.0, 0.0, 1.0], 10.0);
1330            }
1331
1332            // Dynamic Buffer Growth (Up to 4x capacity)
1333            let req_v_size = (self.vertices.len() * std::mem::size_of::<Vertex>()) as u64;
1334            let mut cur_v_size = self.vertex_buffer.size();
1335            let max_v_size = (MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64 * 4;
1336
1337            if req_v_size > cur_v_size {
1338                while cur_v_size < req_v_size && cur_v_size < max_v_size {
1339                    cur_v_size *= 2;
1340                }
1341                if req_v_size > max_v_size {
1342                    log::error!("Exceeded dynamic vertex buffer max capacity! Capping geometry.");
1343                    self.vertices
1344                    .truncate((max_v_size / std::mem::size_of::<Vertex>() as u64) as usize);
1345                    cur_v_size = max_v_size;
1346                }
1347                log::info!("Growing vertex buffer to {} bytes", cur_v_size);
1348                self.vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
1349                    label: Some("Vertex Buffer (Grown)"),
1350                                                               size: cur_v_size,
1351                                                               usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1352                                                               mapped_at_creation: false,
1353                });
1354            }
1355
1356            let req_i_size = (self.indices.len() * std::mem::size_of::<u32>()) as u64;
1357            let mut cur_i_size = self.index_buffer.size();
1358            let max_i_size = (MAX_INDICES * std::mem::size_of::<u32>()) as u64 * 4;
1359
1360            if req_i_size > cur_i_size {
1361                while cur_i_size < req_i_size && cur_i_size < max_i_size {
1362                    cur_i_size *= 2;
1363                }
1364                if req_i_size > max_i_size {
1365                    log::error!("Exceeded dynamic index buffer max capacity! Capping geometry.");
1366                    self.indices
1367                    .truncate((max_i_size / std::mem::size_of::<u32>() as u64) as usize);
1368                    cur_i_size = max_i_size;
1369                }
1370                log::info!("Growing index buffer to {} bytes", cur_i_size);
1371                self.index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
1372                    label: Some("Index Buffer (Grown)"),
1373                                                              size: cur_i_size,
1374                                                              usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1375                                                              mapped_at_creation: false,
1376                });
1377            }
1378
1379            // Forge Submission: Sync all geometry to GPU using StagingBelt with a dedicated encoder
1380            let mut staging_encoder =
1381            self.device
1382            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1383                label: Some("Surtr Staging Encoder"),
1384            });
1385
1386            let mut has_writes = false;
1387
1388            if !self.vertices.is_empty() {
1389                let v_bytes = bytemuck::cast_slice(&self.vertices);
1390                self.staging_belt
1391                .write_buffer(
1392                    &mut staging_encoder,
1393                    &self.vertex_buffer,
1394                    0,
1395                    wgpu::BufferSize::new(v_bytes.len() as u64).unwrap(),
1396                )
1397                .copy_from_slice(v_bytes);
1398                has_writes = true;
1399            }
1400
1401            if !self.indices.is_empty() {
1402                let i_bytes = bytemuck::cast_slice(&self.indices);
1403                self.staging_belt
1404                .write_buffer(
1405                    &mut staging_encoder,
1406                    &self.index_buffer,
1407                    0,
1408                    wgpu::BufferSize::new(i_bytes.len() as u64).unwrap(),
1409                )
1410                .copy_from_slice(i_bytes);
1411                has_writes = true;
1412            }
1413
1414            if has_writes {
1415                self.staging_belt.finish();
1416                self.staging_command_buffers.push(staging_encoder.finish());
1417            }
1418
1419            // Update Time & Uniforms (Direct write is fine for small uniforms)
1420            self.current_scene.time = self.start_time.elapsed().as_secs_f32();
1421            self.queue.write_buffer(
1422                &self.scene_buffer,
1423                0,
1424                bytemuck::bytes_of(&self.current_scene),
1425            );
1426            self.queue.write_buffer(
1427                &self.theme_buffer,
1428                0,
1429                bytemuck::bytes_of(&self.current_theme),
1430            );
1431    }
1432
1433    fn end_frame(&mut self, encoder: wgpu::CommandEncoder) {
1434        Self::end_frame(self, encoder);
1435        cvkg_core::end_render_phase();
1436    }
1437}