1use crate::renderer::SurtrRenderer;
3use crate::types::*;
4use crate::vertex::*;
5use cvkg_core::LAYOUT_DIRTY;
6use cvkg_core::{ColorTheme, Mesh, Rect, Renderer};
7use lyon::math::point;
8use lyon::tessellation::{BuffersBuilder, StrokeOptions, StrokeTessellator, VertexBuffers};
9use std::sync::atomic::Ordering;
10
11impl cvkg_core::ElapsedTime for SurtrRenderer {
12 fn delta_time(&self) -> f32 {
13 self.current_scene.delta_time
14 }
15
16 fn elapsed_time(&self) -> f32 {
17 self.start_time.elapsed().as_secs_f32()
18 }
19}
20
21impl cvkg_core::Renderer for SurtrRenderer {
22 fn is_over_budget(&self) -> bool {
23 self.frame_budget.allow_degradation
24 && self.last_frame_start.elapsed().as_secs_f32() * 1000.0 > self.frame_budget.target_ms
25 }
26
27 fn prewarm_vram(&mut self, assets: Vec<(String, Vec<u8>)>) {
28 log::info!(
29 "[Surtr] Pre-warming Mega-Heim with {} assets...",
30 assets.len()
31 );
32 for (name, data) in assets {
33 self.load_image_to_heim(&name, &data);
34 }
35 }
36
37 fn fill_rect(&mut self, rect: Rect, color: [f32; 4]) {
38 self.fill_rect_with_mode(rect, self.apply_opacity(color), 0, None);
39 }
40
41 fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4]) {
42 self.fill_rect_with_full_params(
43 rect,
44 self.apply_opacity(color),
45 3,
46 None,
47 radius,
48 Rect {
49 x: 0.0,
50 y: 0.0,
51 width: 1.0,
52 height: 1.0,
53 },
54 );
55 }
56
57 fn fill_glass_rect(&mut self, rect: Rect, radius: f32, blur_radius: f32) {
63 let blur_strength = (blur_radius / 100.0).clamp(0.0, 4.0);
66
67 if self.current_z != 0.0 {
70 self.portal_regions.push_back(rect);
71 }
72
73 let prev_material = self.current_draw_material;
79 self.current_draw_material = cvkg_core::DrawMaterial::Glass {
80 blur_radius,
81 ior_override: 0.0,
82 };
83
84 self.fill_rect_with_full_params(
85 rect,
86 [1.0, 1.0, 1.0, 0.4], 7, None,
89 radius,
90 Rect {
91 x: 0.0,
92 y: 0.0,
93 width: 1.0,
94 height: 1.0,
95 },
96 );
97
98 self.current_draw_material = prev_material;
99 }
100
101 fn fill_ellipse(&mut self, rect: Rect, color: [f32; 4]) {
102 self.fill_rect_with_full_params(
103 rect,
104 self.apply_opacity(color),
105 4,
106 None,
107 0.0,
108 Rect {
109 x: 0.0,
110 y: 0.0,
111 width: 1.0,
112 height: 1.0,
113 },
114 );
115 }
116
117 fn draw_3d_cube(&mut self, rect: Rect, color: [f32; 4], rotation: [f32; 3]) {
118 self.fill_rect_with_full_params_and_slice(
119 rect,
120 self.apply_opacity(color),
121 21,
122 None,
123 0.0,
124 Rect {
125 x: 0.0,
126 y: 0.0,
127 width: 1.0,
128 height: 1.0,
129 },
130 [rotation[0], rotation[1], rotation[2], 0.0],
131 [0.0, 0.0],
132 );
133 }
134
135 fn bifrost(&mut self, rect: Rect, blur: f32, _saturation: f32, opacity: f32) {
136 let logical_w = self.current_width() as f32 / self.current_scale_factor();
138 let logical_h = self.current_height() as f32 / self.current_scale_factor();
139 let screen_uv = Rect {
140 x: rect.x / logical_w,
141 y: rect.y / logical_h,
142 width: rect.width / logical_w,
143 height: rect.height / logical_h,
144 };
145 self.fill_rect_with_full_params(rect, [1.0, 1.0, 1.0, opacity], 7, None, blur, screen_uv);
148 }
149
150 fn gungnir(&mut self, rect: Rect, color: [f32; 4], radius: f32, intensity: f32) {
151 let center_x = rect.x + rect.width * 0.5;
154 let center_y = rect.y + rect.height * 0.5;
155 let max_dim = rect.width.max(rect.height) * 0.5 + radius;
156
157 for i in 0..8 {
159 let alpha = intensity / (i as f32 + 1.0) * 0.3;
160 let glow_color = [color[0], color[1], color[2], alpha];
161 self.fill_rect_with_mode(
162 Rect {
163 x: center_x - max_dim - i as f32 * 2.0,
164 y: center_y - max_dim - i as f32 * 2.0,
165 width: max_dim * 2.0 + i as f32 * 4.0,
166 height: max_dim * 2.0 + i as f32 * 4.0,
167 },
168 glow_color,
169 8, None,
171 );
172 }
173 }
174
175 fn mani_glow(&mut self, rect: Rect, color: [f32; 4], radius: f32) {
182 let margin = radius;
183 let glow_rect = Rect {
184 x: rect.x - margin,
185 y: rect.y - margin,
186 width: rect.width + 2.0 * margin,
187 height: rect.height + 2.0 * margin,
188 };
189 let uv_rect = Rect {
190 x: margin,
191 y: radius,
192 width: 0.0,
193 height: 0.0,
194 };
195 self.fill_rect_with_full_params(
196 glow_rect,
197 self.apply_opacity(color),
198 18,
199 None,
200 8.0,
201 uv_rect,
202 );
203 }
204
205 fn stroke_rect(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
206 let c = self.apply_opacity(color);
207 let hw = stroke_width;
208 self.fill_rect_with_mode(
210 Rect {
211 x: rect.x,
212 y: rect.y,
213 width: rect.width,
214 height: hw,
215 },
216 c,
217 1,
218 None,
219 );
220 self.fill_rect_with_mode(
221 Rect {
222 x: rect.x,
223 y: rect.y + rect.height - hw,
224 width: rect.width,
225 height: hw,
226 },
227 c,
228 1,
229 None,
230 );
231 self.fill_rect_with_mode(
232 Rect {
233 x: rect.x,
234 y: rect.y,
235 width: hw,
236 height: rect.height,
237 },
238 c,
239 1,
240 None,
241 );
242 self.fill_rect_with_mode(
243 Rect {
244 x: rect.x + rect.width - hw,
245 y: rect.y,
246 width: hw,
247 height: rect.height,
248 },
249 c,
250 1,
251 None,
252 );
253 }
254
255 fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4], stroke_width: f32) {
256 self.fill_rect_with_full_params(
257 rect,
258 self.apply_opacity(color),
259 17,
260 None,
261 radius,
262 Rect {
263 x: stroke_width,
264 y: 0.0,
265 width: 0.0,
266 height: 0.0,
267 },
268 );
269 }
270
271 fn stroke_ellipse(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
272 let cx = rect.x + rect.width / 2.0;
274 let cy = rect.y + rect.height / 2.0;
275 let rx = rect.width / 2.0;
276 let ry = rect.height / 2.0;
277
278 let mut builder = lyon::path::Path::builder();
280 if rx > 0.0 && ry > 0.0 {
281 let segments = 64;
283 for i in 0..segments {
284 let angle = 2.0 * std::f32::consts::PI * (i as f32) / (segments as f32);
285 let x = cx + rx * angle.cos();
286 let y = cy + ry * angle.sin();
287 if i == 0 {
288 builder.begin(lyon::math::point(x, y));
289 } else {
290 builder.line_to(lyon::math::point(x, y));
291 }
292 }
293 builder.close();
294 }
295 let path = builder.build();
296 self.stroke_path(&path, color, stroke_width);
297 }
298
299 fn draw_linear_gradient(
300 &mut self,
301 rect: Rect,
302 start_color: [f32; 4],
303 end_color: [f32; 4],
304 angle: f32,
305 ) {
306 self.fill_rect_with_full_params_and_slice(
307 rect,
308 self.apply_opacity(start_color),
309 15,
310 None,
311 0.0,
312 Rect {
313 x: angle,
314 y: 0.0,
315 width: 1.0,
316 height: 1.0,
317 },
318 end_color,
319 [0.0, 0.0],
320 );
321 }
322
323 fn draw_radial_gradient(&mut self, rect: Rect, inner_color: [f32; 4], outer_color: [f32; 4]) {
324 self.fill_rect_with_full_params_and_slice(
325 rect,
326 self.apply_opacity(inner_color),
327 16,
328 None,
329 0.0,
330 Rect {
331 x: 0.0,
332 y: 0.0,
333 width: 1.0,
334 height: 1.0,
335 },
336 outer_color,
337 [0.0, 0.0],
338 );
339 }
340
341 fn draw_drop_shadow(
342 &mut self,
343 rect: Rect,
344 radius: f32,
345 color: [f32; 4],
346 blur: f32,
347 spread: f32,
348 ) {
349 let margin = blur + spread;
350 let inflated = Rect {
351 x: rect.x - margin,
352 y: rect.y - margin,
353 width: rect.width + margin * 2.0,
354 height: rect.height + margin * 2.0,
355 };
356 self.fill_rect_with_full_params(
358 inflated,
359 self.apply_opacity(color),
360 18,
361 None,
362 radius,
363 Rect {
364 x: margin,
365 y: blur,
366 width: 0.0,
367 height: 0.0,
368 },
369 );
370 }
371
372 fn stroke_dashed_rounded_rect(
373 &mut self,
374 rect: Rect,
375 radius: f32,
376 color: [f32; 4],
377 width: f32,
378 dash: f32,
379 gap: f32,
380 ) {
381 self.fill_rect_with_full_params(
382 rect,
383 self.apply_opacity(color),
384 19,
385 None,
386 radius,
387 Rect {
388 x: width,
389 y: dash,
390 width: gap,
391 height: 0.0,
392 },
393 );
394 }
395
396 fn draw_9slice(
397 &mut self,
398 image_name: &str,
399 rect: Rect,
400 left: f32,
401 top: f32,
402 right: f32,
403 bottom: f32,
404 ) {
405 let c = self.apply_opacity([1.0, 1.0, 1.0, 1.0]);
406 let tid = self.get_texture_id(image_name);
407 self.fill_rect_with_full_params(
408 rect,
409 c,
410 20,
411 tid,
412 bottom,
413 Rect {
414 x: left,
415 y: top,
416 width: right,
417 height: 0.0,
418 },
419 );
420 }
421
422 fn draw_line(
423 &mut self,
424 x1: f32,
425 y1: f32,
426 x2: f32,
427 y2: f32,
428 color: [f32; 4],
429 stroke_width: f32,
430 ) {
431 let dx = x2 - x1;
432 let dy = y2 - y1;
433 let len = (dx * dx + dy * dy).sqrt();
434 if len < 0.001 {
435 return;
436 }
437
438 let mut builder = lyon::path::Path::builder();
441 builder.begin(point(x1, y1));
442 builder.line_to(point(x2, y2));
443 builder.close();
444 let path = builder.build();
445
446 self.stroke_path(&path, color, stroke_width);
447 }
448
449 fn draw_image(&mut self, image_name: &str, rect: Rect) {
450 if !self.image_uv_registry.contains(image_name) {
452 log::warn!("[Surtr] draw_image: '{}' not loaded, skipping", image_name);
453 return;
454 }
455 let tid = self
456 .get_texture_id(image_name)
457 .or_else(|| self.get_texture_id("__mega_heim"));
458 let uv_rect = self
459 .image_uv_registry
460 .get(image_name)
461 .copied()
462 .unwrap_or(Rect {
463 x: 0.0,
464 y: 0.0,
465 width: 1.0,
466 height: 1.0,
467 });
468 self.fill_rect_with_full_params(rect, [1.0, 1.0, 1.0, 1.0], 2, tid, 0.0, uv_rect);
469 }
470
471 fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
472 let scaled_size = size * self.current_scale_factor();
474 let shaped = self.shape_text_with_stack(text, scaled_size);
475 let c = self.apply_opacity(color);
476
477 for glyph in shaped.glyphs {
478 let cache_key = glyph.cache_key;
479
480 let (uv_rect, w, h, x_off, y_off) = if let Some(info) = self.text_cache.get(&cache_key)
481 {
482 *info
483 } else {
484 if let Some(image) = self.text_engine.rasterize(cache_key) {
485 let glyph_id = image.glyph_id;
486 let data_len = image.data.len();
487 let gw = image.width;
488 let gh = image.height;
489 let x_offset = image.x_offset;
490 let y_offset = image.y_offset;
491 let (rgba_data, gw, gh) = glyph_image_to_rgba(image);
492 if gw == 0 || gh == 0 {
493 continue;
494 }
495 if rgba_data.is_empty() {
496 log::warn!(
497 "Glyph rasterizer returned unsupported pixel format for glyph {} ({} bytes, {}x{}), skipping",
498 glyph_id,
499 data_len,
500 gw,
501 gh
502 );
503 continue;
504 }
505
506 let pack_res = self.heim_packer.pack(gw, gh);
507 let (nx, ny) = if let Some(pos) = pack_res {
508 pos
509 } else {
510 self.reclaim_vram();
512 match self.heim_packer.pack(gw, gh) {
513 Some(pos) => pos,
514 None => {
515 log::error!(
516 "Glyph heim critically full after reclaim: cannot pack {}x{} glyph for '{}', skipping",
517 gw,
518 gh,
519 text
520 );
521 continue; }
523 }
524 };
525
526 log::info!("Rasterized glyph {}, gw: {}, gh: {}, data len: {}, first 20 bytes: {:?}", glyph_id, gw, gh, rgba_data.len(), &rgba_data[0..std::cmp::min(rgba_data.len(), 20)]);
527
528 self.queue.write_texture(
529 wgpu::TexelCopyTextureInfo {
530 texture: &self.mega_heim_tex,
531 mip_level: 0,
532 origin: wgpu::Origin3d { x: nx, y: ny, z: 0 },
533 aspect: wgpu::TextureAspect::All,
534 },
535 &rgba_data,
536 wgpu::TexelCopyBufferLayout {
537 offset: 0,
538 bytes_per_row: Some(gw * 4),
539 rows_per_image: Some(gh),
540 },
541 wgpu::Extent3d {
542 width: gw,
543 height: gh,
544 depth_or_array_layers: 1,
545 },
546 );
547
548 let tex_w = self.mega_heim_tex.width() as f32;
549 let tex_h = self.mega_heim_tex.height() as f32;
550 let info = (
551 Rect {
552 x: nx as f32 / tex_w,
553 y: ny as f32 / tex_h,
554 width: gw as f32 / tex_w,
555 height: gh as f32 / tex_h,
556 },
557 gw as f32,
558 gh as f32,
559 x_offset,
560 y_offset,
561 );
562 self.text_cache.put(cache_key, info);
563 info
564 } else {
565 (Rect::zero(), 0.0, 0.0, 0.0, 0.0)
566 }
567 };
568
569 if w > 0.0 {
570 let baseline_y = y + shaped.ascent / self.current_scale_factor();
574 let glyph_rect = Rect {
575 x: x + (glyph.x + x_off) / self.current_scale_factor(),
576 y: baseline_y + (glyph.y - y_off) / self.current_scale_factor(),
577 width: w / self.current_scale_factor(),
578 height: h / self.current_scale_factor(),
579 };
580 let tid = self.get_texture_id("__mega_heim");
581 self.fill_rect_with_full_params(glyph_rect, c, 6, tid, 0.0, uv_rect);
582 }
583 }
584 }
585
586 fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
588 let sf = self.current_scale_factor();
589 let shaped = self.shape_text_with_stack(text, size * sf);
590 (shaped.width / sf, shaped.height / sf)
591 }
592
593 fn shape_rich_text(
594 &mut self,
595 spans: &[cvkg_runic_text::TextSpan],
596 max_width: Option<f32>,
597 align: cvkg_runic_text::TextAlign,
598 overflow: cvkg_runic_text::TextOverflow,
599 ) -> Option<cvkg_runic_text::ShapedText> {
600 let sf = self.current_scale_factor();
601 let mut scaled_spans = spans.to_vec();
602 for span in &mut scaled_spans {
603 span.style.font_size *= sf;
604 if span.style.fallback_families.is_empty() {
605 span.style.fallback_families = vec![
606 "SF Pro".to_string(),
607 "Inter".to_string(),
608 "Helvetica Neue".to_string(),
609 "Helvetica".to_string(),
610 "Arial".to_string(),
611 "sans-serif".to_string(),
612 ];
613 }
614 }
615 let scaled_max_width = max_width.map(|w| w * sf);
616 self.text_engine
617 .shape_layout(&scaled_spans, scaled_max_width, align, overflow)
618 .ok()
619 }
620
621 fn draw_shaped_text(&mut self, shaped: &cvkg_runic_text::ShapedText, x: f32, y: f32) {
622 for glyph in &shaped.glyphs {
623 let byte_idx = shaped
624 .grapheme_boundaries
625 .get(glyph.cluster as usize)
626 .copied()
627 .unwrap_or(0);
628 let mut span_color = [1.0, 1.0, 1.0, 1.0];
629 for span in &shaped.spans {
630 if byte_idx >= span.byte_offset && byte_idx < span.byte_offset + span.text.len() {
631 span_color = [
632 span.style.color[0] as f32 / 255.0,
633 span.style.color[1] as f32 / 255.0,
634 span.style.color[2] as f32 / 255.0,
635 span.style.color[3] as f32 / 255.0,
636 ];
637 break;
638 }
639 }
640 let c = self.apply_opacity(span_color);
641
642 let cache_key = glyph.cache_key;
643 let (uv_rect, w, h, x_off, y_off) = if let Some(info) = self.text_cache.get(&cache_key)
644 {
645 *info
646 } else {
647 if let Some(image) = self.text_engine.rasterize(cache_key) {
648 let glyph_id = image.glyph_id;
649 let data_len = image.data.len();
650 let gw = image.width;
651 let gh = image.height;
652 let x_offset = image.x_offset;
653 let y_offset = image.y_offset;
654 let (rgba_data, gw, gh) = glyph_image_to_rgba(image);
655 if gw == 0 || gh == 0 {
656 continue;
657 }
658 if rgba_data.is_empty() {
659 log::warn!(
660 "Glyph rasterizer returned unsupported pixel format for glyph {} ({} bytes, {}x{}), skipping",
661 glyph_id,
662 data_len,
663 gw,
664 gh
665 );
666 continue;
667 }
668
669 let pack_res = self.heim_packer.pack(gw, gh);
670 let (nx, ny) = if let Some(pos) = pack_res {
671 pos
672 } else {
673 self.reclaim_vram();
674 match self.heim_packer.pack(gw, gh) {
675 Some(pos) => pos,
676 None => {
677 log::error!(
678 "Glyph heim critically full after reclaim: cannot pack {}x{} glyph, skipping",
679 gw,
680 gh
681 );
682 continue; }
684 }
685 };
686 let sample = &rgba_data[0..std::cmp::min(rgba_data.len(), 20)];
688 log::info!("Rasterized glyph {}, gw: {}, gh: {}, data len: {}, first 20 bytes: {:?}", glyph_id, gw, gh, rgba_data.len(), sample);
689
690 self.queue.write_texture(
691 wgpu::TexelCopyTextureInfo {
692 texture: &self.mega_heim_tex,
693 mip_level: 0,
694 origin: wgpu::Origin3d { x: nx, y: ny, z: 0 },
695 aspect: wgpu::TextureAspect::All,
696 },
697 &rgba_data,
698 wgpu::TexelCopyBufferLayout {
699 offset: 0,
700 bytes_per_row: Some(gw * 4),
701 rows_per_image: Some(gh),
702 },
703 wgpu::Extent3d {
704 width: gw,
705 height: gh,
706 depth_or_array_layers: 1,
707 },
708 );
709
710 let tex_w = self.mega_heim_tex.width() as f32;
711 let tex_h = self.mega_heim_tex.height() as f32;
712 let info = (
713 Rect {
714 x: nx as f32 / tex_w,
715 y: ny as f32 / tex_h,
716 width: gw as f32 / tex_w,
717 height: gh as f32 / tex_h,
718 },
719 gw as f32,
720 gh as f32,
721 x_offset,
722 y_offset,
723 );
724 self.text_cache.put(cache_key, info);
725 info
726 } else {
727 (Rect::zero(), 0.0, 0.0, 0.0, 0.0)
728 }
729 };
730
731 if w > 0.0 {
732 let sf = self.current_scale_factor();
733 let baseline_y = y + shaped.ascent / sf;
737 let glyph_rect = Rect {
738 x: x + (glyph.x + x_off) / sf,
739 y: baseline_y + (glyph.y - y_off) / sf,
740 width: w / sf,
741 height: h / sf,
742 };
743 let tid = self.get_texture_id("__mega_heim");
744 let slice = self
745 .slice_stack
746 .last()
747 .copied()
748 .map(|(a, o)| [a, o, 1.0, 1.0])
749 .unwrap_or([0.0, 0.0, 0.0, 1.0]);
750 self.fill_rect_with_full_params_and_slice(
751 glyph_rect,
752 c,
753 6,
754 tid,
755 0.0,
756 uv_rect,
757 slice,
758 [glyph.glyph_index as f32, glyph.time_offset],
759 );
760 }
761 }
762 }
763
764 fn draw_texture(&mut self, texture_id: u32, rect: Rect) {
765 self.fill_rect_with_full_params_and_slice(
766 rect,
767 [1.0, 1.0, 1.0, 1.0],
768 2,
769 Some(texture_id),
770 0.0,
771 Rect {
772 x: 0.0,
773 y: 0.0,
774 width: 1.0,
775 height: 1.0,
776 },
777 [0.0, 0.0, 0.0, 1.0],
778 [0.0, 0.0],
779 );
780 }
781
782 fn load_image(&mut self, name: &str, data: &[u8]) {
785 if self.image_uv_registry.contains(name) {
786 return;
787 }
788 let img_result = image::load_from_memory(data);
789 let img = match img_result {
790 Ok(img) => img.to_rgba8(),
791 Err(e) => {
792 log::error!("Failed to load image {}: {}", name, e);
793 image::RgbaImage::from_pixel(1, 1, image::Rgba([255, 255, 255, 255]))
794 }
795 };
796 let (width, height) = img.dimensions();
797
798 let size = wgpu::Extent3d {
799 width,
800 height,
801 depth_or_array_layers: 1,
802 };
803 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
804 label: Some(&format!("Texture Array Layer: {}", name)),
805 size,
806 mip_level_count: 1,
807 sample_count: 1,
808 dimension: wgpu::TextureDimension::D2,
809 format: wgpu::TextureFormat::Rgba8UnormSrgb,
810 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
811 view_formats: &[],
812 });
813
814 self.queue.write_texture(
815 wgpu::TexelCopyTextureInfo {
816 texture: &texture,
817 mip_level: 0,
818 origin: wgpu::Origin3d::ZERO,
819 aspect: wgpu::TextureAspect::All,
820 },
821 &img,
822 wgpu::TexelCopyBufferLayout {
823 offset: 0,
824 bytes_per_row: Some(4 * width),
825 rows_per_image: Some(height),
826 },
827 size,
828 );
829
830 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
831
832 let index = if self.texture_registry.len() < 255 {
834 (self.texture_registry.len() + 1) as u32
835 } else {
836 if let Some((old_name, old_index)) = self.texture_registry.pop_lru() {
838 self.image_uv_registry.pop(&old_name);
839 old_index
840 } else {
841 1 }
843 };
844
845 self.texture_views[index as usize] = view;
846 self.image_uv_registry.put(
847 name.to_string(),
848 Rect {
849 x: 0.0,
850 y: 0.0,
851 width: 1.0,
852 height: 1.0,
853 },
854 );
855 self.texture_registry.put(name.to_string(), index);
856 self.rebuild_texture_array_bind_group();
857 }
858
859 fn push_clip_rect(&mut self, rect: Rect) {
860 self.clip_stack.push(rect);
861 }
862
863 fn pop_clip_rect(&mut self) {
864 self.clip_stack.pop();
865 }
866
867 fn current_clip_rect(&self) -> Rect {
868 self.clip_stack.last().copied().unwrap_or(Rect::new(
869 0.0,
870 0.0,
871 self.current_width() as f32,
872 self.current_height() as f32,
873 ))
874 }
875
876 fn memoize(&mut self, id: u64, data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer)) {
877 let should_skip = self.memo_cache.get(&id) == Some(&data_hash);
880
881 if !should_skip {
882 self.memo_cache.insert(id, data_hash);
884 render_fn(self);
885 }
886 }
888
889 fn push_opacity(&mut self, opacity: f32) {
890 let current = self.opacity_stack.last().copied().unwrap_or(1.0);
891 self.opacity_stack.push(current * opacity);
892 }
893
894 fn pop_opacity(&mut self) {
895 self.opacity_stack.pop();
896 }
897
898 fn push_shadow(&mut self, radius: f32, color: [f32; 4], offset: [f32; 2]) {
899 self.shadow_stack.push(ShadowState {
900 radius,
901 color,
902 _offset: offset,
903 });
904 }
905
906 fn pop_shadow(&mut self) {
907 self.shadow_stack.pop();
908 }
909
910 fn push_transform(&mut self, translation: [f32; 2], scale: [f32; 2], rotation: f32) {
911 let c = rotation.cos();
912 let sn = rotation.sin();
913 let affine = glam::Mat3::from_cols(
914 glam::Vec3::new(c * scale[0], sn * scale[0], 0.0),
915 glam::Vec3::new(-sn * scale[1], c * scale[1], 0.0),
916 glam::Vec3::new(translation[0], translation[1], 1.0),
917 );
918
919 let parent = self
920 .transform_stack
921 .last()
922 .copied()
923 .unwrap_or(glam::Mat3::IDENTITY);
924 self.transform_stack.push(parent * affine);
925 }
926
927 fn push_affine(&mut self, transform: [f32; 6]) {
928 let affine = glam::Mat3::from_cols(
929 glam::Vec3::new(transform[0], transform[1], 0.0),
930 glam::Vec3::new(transform[2], transform[3], 0.0),
931 glam::Vec3::new(transform[4], transform[5], 1.0),
932 );
933 let parent = self
934 .transform_stack
935 .last()
936 .copied()
937 .unwrap_or(glam::Mat3::IDENTITY);
938 self.transform_stack.push(parent * affine);
939 }
940
941 fn pop_transform(&mut self) {
942 self.transform_stack.pop();
943 }
944
945 fn set_theme(&mut self, theme: ColorTheme) {
946 self.current_theme = theme;
947 self.queue
948 .write_buffer(&self.theme_buffer, 0, bytemuck::bytes_of(&theme));
949 }
950
951 fn set_rage(&mut self, rage: f32) {
952 self.current_scene.berzerker_rage = rage;
953 }
955
956 fn trigger_shatter_event(&mut self, origin: [f32; 2], force: f32) {
957 self.current_scene.shatter_origin = origin;
958 self.current_scene.shatter_time = self.current_scene.time;
959 self.current_scene.shatter_force = force;
960 }
961
962 fn set_scene_preset(&mut self, preset: u32) {
963 self.current_scene.scene_type = preset;
964 }
965
966 fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
969 self.slice_stack.push((angle, offset));
970 }
971
972 fn pop_mjolnir_slice(&mut self) {
974 self.slice_stack.pop();
975 }
976
977 fn mjolnir_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
978 self.shatter_internal(rect, pieces, force, color, 8);
979 }
980
981 fn mjolnir_fluid_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
982 self.shatter_internal(rect, pieces, force, color, 11);
983 }
984
985 fn draw_mjolnir_bolt(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
986 self.recursive_bolt(from, to, 4, color);
987 }
988
989 fn dispatch_particles(
990 &mut self,
991 origin: [f32; 2],
992 count: u32,
993 effect_type: &str,
994 _color: [f32; 4],
995 ) {
996 log::info!(
997 "[Surtr] Dispatching {} {} particles at {:?}",
998 count,
999 effect_type,
1000 origin
1001 );
1002 }
1004
1005 fn draw_hologram(&mut self, rect: Rect, hologram_id: &str, time: f32) {
1006 log::info!(
1007 "[Surtr] Drawing hologram {} at {:?} (t={})",
1008 hologram_id,
1009 rect,
1010 time
1011 );
1012 self.stroke_rect(rect, [0.0, 1.0, 1.0, 0.5], 2.0);
1015 }
1016
1017 fn upload_data_texture(&mut self, id: &str, data: &[f32], width: u32, height: u32) {
1018 let size = wgpu::Extent3d {
1019 width,
1020 height,
1021 depth_or_array_layers: 1,
1022 };
1023 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
1024 label: Some(id),
1025 size,
1026 mip_level_count: 1,
1027 sample_count: 1,
1028 dimension: wgpu::TextureDimension::D2,
1029 format: wgpu::TextureFormat::R32Float,
1030 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1031 view_formats: &[],
1032 });
1033 self.queue.write_texture(
1034 wgpu::TexelCopyTextureInfo {
1035 texture: &texture,
1036 mip_level: 0,
1037 origin: wgpu::Origin3d::ZERO,
1038 aspect: wgpu::TextureAspect::All,
1039 },
1040 bytemuck::cast_slice(data),
1041 wgpu::TexelCopyBufferLayout {
1042 offset: 0,
1043 bytes_per_row: Some(4 * width),
1044 rows_per_image: Some(height),
1045 },
1046 size,
1047 );
1048 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1049 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
1050 address_mode_u: wgpu::AddressMode::ClampToEdge,
1051 address_mode_v: wgpu::AddressMode::ClampToEdge,
1052 mag_filter: wgpu::FilterMode::Linear,
1053 ..Default::default()
1054 });
1055 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1056 layout: &self.texture_bind_group_layout,
1057 entries: &[
1058 wgpu::BindGroupEntry {
1059 binding: 0,
1060 resource: wgpu::BindingResource::TextureViewArray(&vec![&view; 256]),
1061 },
1062 wgpu::BindGroupEntry {
1063 binding: 1,
1064 resource: wgpu::BindingResource::Sampler(&sampler),
1065 },
1066 ],
1067 label: Some(id),
1068 });
1069 self.texture_bind_groups.push(bind_group);
1070 let tid = (self.texture_bind_groups.len() - 1) as u32;
1071 self.texture_registry.put(id.to_string(), tid);
1072 }
1073
1074 fn draw_heatmap(&mut self, texture_id: &str, rect: Rect, _palette: &str) {
1075 let tid = self.get_texture_id(texture_id);
1076 self.fill_rect_with_mode(rect, [1.0, 1.0, 1.0, 1.0], 12, tid);
1077 }
1078
1079 fn draw_mesh(&mut self, mesh: &Mesh, color: [f32; 4], transform: glam::Mat4) {
1080 let base_idx = self.vertices.len() as u32;
1081 let screen = [self.current_width() as f32, self.current_height() as f32];
1082
1083 for i in 0..mesh.vertices.len() {
1084 let pos = transform.transform_point3(glam::Vec3::from(mesh.vertices[i]));
1085 let norm = transform.transform_vector3(glam::Vec3::from(mesh.normals[i]));
1086
1087 self.vertices.push(Vertex {
1088 position: pos.to_array(),
1089 normal: norm.to_array(),
1090 uv: [0.0, 0.0],
1091 color,
1092 material_id: 13, radius: 0.0,
1094 slice: [0.0, 0.0, 0.0, 1.0],
1095 logical: [0.0, 0.0],
1096 size: [0.0, 0.0],
1097 clip: [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY],
1098 tex_index: 0,
1099 });
1100 }
1101
1102 for idx in &mesh.indices {
1103 self.indices.push(base_idx + idx);
1104 }
1105
1106 let (translation, scale_transform, rotation, _, _) = self.current_transform();
1107
1108 if self.draw_calls.is_empty() || self.current_texture_id.is_some() {
1109 self.current_texture_id = None;
1110
1111 self.instance_data.push(InstanceData {
1112 translation,
1113 scale: scale_transform,
1114 rotation,
1115 blur_radius: 0.0,
1116 ior_override: 0.0,
1117 });
1118 self.draw_calls.push(DrawCall {
1119 target_id: None,
1120 texture_id: None,
1121 scissor_rect: self.clip_stack.last().copied(),
1122 index_start: (self.indices.len() as u32) - (mesh.indices.len() as u32),
1123 index_count: mesh.indices.len() as u32,
1124 material: cvkg_core::DrawMaterial::Opaque,
1125 instance_start: (self.instance_data.len() - 1) as u32,
1126 });
1127 } else {
1128 self.draw_calls.last_mut().unwrap().index_count += mesh.indices.len() as u32;
1129 }
1130 }
1131
1132 fn draw_mesh_3d(
1133 &mut self,
1134 mesh: &Mesh,
1135 material: &cvkg_core::Material3D,
1136 transform: &cvkg_core::Transform3D,
1137 ) {
1138 let base_idx = self.vertices.len() as u32;
1139 let screen = [self.current_width() as f32, self.current_height() as f32];
1140 let model_matrix = transform.to_matrix();
1141
1142 for i in 0..mesh.vertices.len() {
1143 let pos = model_matrix.transform_point3(glam::Vec3::from(mesh.vertices[i]));
1144 let norm = model_matrix.transform_vector3(glam::Vec3::from(mesh.normals[i]));
1145
1146 self.vertices.push(Vertex {
1147 position: [pos.x, pos.y, pos.z],
1148 normal: [norm.x, norm.y, norm.z],
1149 uv: [0.0, 0.0],
1150 color: material.base_color,
1151 material_id: 13, radius: 0.0,
1153 slice: [material.metallic, material.roughness, material.opacity, 1.0],
1154 logical: [0.0, 0.0],
1155 size: [0.0, 0.0],
1156 clip: [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY],
1157 tex_index: 0,
1158 });
1159 }
1160
1161 for idx in &mesh.indices {
1162 self.indices.push(base_idx + idx);
1163 }
1164
1165 self.instance_data.push(InstanceData {
1166 translation: [0.0, 0.0],
1167 scale: [1.0, 1.0],
1168 rotation: 0.0,
1169 blur_radius: 0.0,
1170 ior_override: 0.0,
1171 });
1172
1173 self.draw_calls.push(DrawCall {
1174 target_id: None,
1175 texture_id: None,
1176 scissor_rect: self.clip_stack.last().copied(),
1177 index_start: (self.indices.len() as u32) - (mesh.indices.len() as u32),
1178 index_count: mesh.indices.len() as u32,
1179 material: cvkg_core::DrawMaterial::Opaque,
1180 instance_start: (self.instance_data.len() - 1) as u32,
1181 });
1182 }
1183
1184 fn set_camera_3d(&mut self, camera: &cvkg_core::Camera3D) {
1185 self.current_scene.proj = camera.projection_matrix();
1186 self.current_scene.view = camera.view_matrix();
1187 }
1188
1189 fn push_transform_3d(&mut self, transform: &cvkg_core::Transform3D) {
1190 let (translation, rotation_quat, scale_glam) =
1193 transform.to_matrix().to_scale_rotation_translation();
1194 let translation = [translation.x, translation.y];
1195 let scale = [scale_glam.x, scale_glam.y];
1196 let rotation = if rotation_quat.length_squared() > 0.0 {
1197 let (axis, angle) = rotation_quat.to_axis_angle();
1198 angle * axis.z.signum() } else {
1200 0.0
1201 };
1202 self.push_transform(translation, scale, rotation);
1203 }
1204
1205 fn pop_transform_3d(&mut self) {
1206 self.pop_transform();
1208 }
1209
1210 fn render_scene_node_3d(
1211 &mut self,
1212 position: [f32; 3],
1213 rotation: [f32; 4],
1214 scale: [f32; 3],
1215 color: [f32; 4],
1216 meshes: &[Mesh],
1217 ) {
1218 let transform = cvkg_core::Transform3D {
1219 position: glam::Vec3::from(position),
1220 rotation: glam::Quat::from_xyzw(rotation[0], rotation[1], rotation[2], rotation[3]),
1221 scale: glam::Vec3::from(scale),
1222 };
1223 if meshes.is_empty() {
1225 let h = 0.5f32;
1227 let cube = Mesh {
1228 vertices: vec![
1229 [-h, -h, -h],
1230 [h, -h, -h],
1231 [h, h, -h],
1232 [-h, h, -h],
1233 [-h, -h, h],
1234 [h, -h, h],
1235 [h, h, h],
1236 [-h, h, h],
1237 ],
1238 normals: vec![
1239 [0.0, 0.0, -1.0],
1240 [0.0, 0.0, -1.0],
1241 [0.0, 0.0, -1.0],
1242 [0.0, 0.0, -1.0],
1243 [0.0, 0.0, 1.0],
1244 [0.0, 0.0, 1.0],
1245 [0.0, 0.0, 1.0],
1246 [0.0, 0.0, 1.0],
1247 [0.0, -1.0, 0.0],
1248 [0.0, -1.0, 0.0],
1249 [0.0, -1.0, 0.0],
1250 [0.0, -1.0, 0.0],
1251 [1.0, 0.0, 0.0],
1252 [1.0, 0.0, 0.0],
1253 [1.0, 0.0, 0.0],
1254 [1.0, 0.0, 0.0],
1255 [0.0, 1.0, 0.0],
1256 [0.0, 1.0, 0.0],
1257 [0.0, 1.0, 0.0],
1258 [0.0, 1.0, 0.0],
1259 [-1.0, 0.0, 0.0],
1260 [-1.0, 0.0, 0.0],
1261 [-1.0, 0.0, 0.0],
1262 [-1.0, 0.0, 0.0],
1263 ],
1264 indices: vec![
1265 0, 1, 2, 0, 2, 3, 5, 4, 7, 5, 7, 6, 4, 0, 3, 4, 3, 7, 1, 5, 6, 1, 6, 2, 3, 2, 6, 3, 6, 7, 4, 5, 1, 4, 1, 0, ],
1272 };
1273 let material = cvkg_core::Material3D::unlit(color);
1274 self.draw_mesh_3d(&cube, &material, &transform);
1275 } else {
1276 let material = cvkg_core::Material3D::unlit(color);
1277 self.draw_mesh_3d(&meshes[0], &material, &transform);
1278 }
1279 }
1280
1281 fn register_shared_element(&mut self, id: &str, rect: Rect) {
1282 self.shared_elements.put(id.to_string(), rect);
1283 }
1284
1285 fn set_z_index(&mut self, z: f32) {
1286 self.current_z = z;
1287 }
1288
1289 fn set_material(&mut self, material: cvkg_core::DrawMaterial) {
1290 self.current_draw_material = material;
1291 }
1292
1293 fn current_material(&self) -> cvkg_core::DrawMaterial {
1294 self.current_draw_material
1295 }
1296
1297 fn get_z_index(&self) -> f32 {
1298 self.current_z
1299 }
1300
1301 fn request_redraw(&mut self) {
1302 self.redraw_requested = true;
1303 }
1304
1305 fn enter_portal(&mut self, z_index: i32) {
1317 self.current_z = z_index as f32;
1321 }
1322
1323 fn exit_portal(&mut self) {
1327 self.current_z = 0.0;
1328 }
1329
1330 fn push_vnode(&mut self, rect: Rect, name: &'static str) {
1331 self.vnode_stack.push((rect, name));
1332 }
1333
1334 fn pop_vnode(&mut self) {
1335 self.vnode_stack.pop();
1336 }
1337
1338 fn register_handler(
1339 &mut self,
1340 event_type: &str,
1341 handler: std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>,
1342 ) {
1343 self.event_handlers
1344 .entry(event_type.to_string())
1345 .or_insert_with(Vec::new)
1346 .push(handler);
1347 }
1348
1349 fn load_svg(&mut self, name: &str, svg_data: &[u8]) {
1350 SurtrRenderer::load_svg(self, name, svg_data);
1351 }
1352
1353 fn draw_svg(&mut self, name: &str, rect: Rect) {
1354 SurtrRenderer::draw_svg(self, name, rect, None, 0);
1355 }
1356 fn draw_svg_with_offset(&mut self, name: &str, rect: Rect, animation_time_offset: f32) {
1357 SurtrRenderer::draw_svg_with_offset(self, name, rect, None, 0, animation_time_offset);
1358 }
1359
1360 fn serialize_svg(&mut self, name: &str) -> Result<String, String> {
1361 let tree = self
1362 .svg_trees
1363 .get(name)
1364 .ok_or_else(|| format!("SVG '{}' not found", name))?;
1365 let config = cvkg_svg_serialize::SerializerConfig::default();
1366 let mut serializer = cvkg_svg_serialize::SvgSerializer::with_config(config);
1367 serializer
1368 .serialize(tree)
1369 .map_err(|e| format!("SVG serialization failed: {}", e))
1370 }
1371
1372 fn apply_svg_filter(
1373 &mut self,
1374 name: &str,
1375 filter_id: &str,
1376 _region: Rect,
1377 ) -> Result<String, String> {
1378 let tree = self
1379 .svg_trees
1380 .get(name)
1381 .ok_or_else(|| format!("SVG '{}' not found", name))?;
1382 let _filter = Self::find_filter(tree, filter_id)
1383 .ok_or_else(|| format!("Filter '{}' not found in SVG '{}'", filter_id, name))?;
1384 let config = cvkg_svg_serialize::SerializerConfig::default();
1385 let mut serializer = cvkg_svg_serialize::SvgSerializer::with_config(config);
1386 serializer
1387 .serialize(tree)
1388 .map_err(|e| format!("SVG filter serialization failed: {}", e))
1389 }
1390}
1391
1392impl SurtrRenderer {
1395 pub fn clear_event_handlers(&mut self) {
1398 self.event_handlers.clear();
1399 }
1400
1401 pub fn get_handlers(
1403 &self,
1404 event_type: &str,
1405 ) -> Option<&Vec<std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>>> {
1406 self.event_handlers.get(event_type)
1407 }
1408
1409 pub(crate) fn current_transform(&self) -> ([f32; 2], [f32; 2], f32, f32, f32) {
1413 let m = self
1416 .transform_stack
1417 .last()
1418 .copied()
1419 .unwrap_or(glam::Mat3::IDENTITY);
1420 let t = [m.z_axis.x, m.z_axis.y];
1421 let a = m.x_axis.x;
1423 let b = m.x_axis.y;
1424 let c = m.y_axis.x;
1425 let d = m.y_axis.y;
1426 let sx = (a * a + b * b).sqrt();
1427 let sy = (c * c + d * d).sqrt();
1428 let rotation = b.atan2(a);
1429 let skew_x = (a * c + b * d) / (sx * sy); (t, [sx, sy], rotation, skew_x, 0.0)
1432 }
1433
1434 pub fn stroke_path(&mut self, path: &lyon::path::Path, color: [f32; 4], stroke_width: f32) {
1435 let c = self.apply_opacity(color);
1436 let mut tessellator = StrokeTessellator::new();
1437 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
1438 let base_vertex_idx = self.vertices.len() as u32;
1439 let base_index_idx = self.indices.len() as u32;
1440
1441 let (translation, scale, rotation, _, _) = self.current_transform();
1442 let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
1443 x: -10000.0,
1444 y: -10000.0,
1445 width: 20000.0,
1446 height: 20000.0,
1447 });
1448 let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
1449
1450 let result = tessellator.tessellate_path(
1451 path,
1452 &StrokeOptions::default().with_line_width(stroke_width),
1453 &mut BuffersBuilder::new(
1454 &mut buffers,
1455 CustomStrokeVertexConstructor { color: c, clip, path_length: 1.0 },
1456 ),
1457 );
1458 if let Err(e) = result {
1459 log::warn!("Failed to tessellate stroke path: {:?}", e);
1460 return;
1461 }
1462
1463 self.vertices.extend(buffers.vertices);
1464 for idx in &buffers.indices {
1465 self.indices.push(base_vertex_idx + *idx);
1466 }
1467
1468 let material = self.current_material();
1469 let tid = self.get_texture_id("__mega_heim");
1470
1471 let last_call = self.draw_calls.last();
1472 let needs_new_call = self.draw_calls.is_empty()
1473 || self.current_texture_id != tid
1474 || last_call.unwrap().scissor_rect != self.clip_stack.last().copied()
1475 || last_call.unwrap().material != material;
1476
1477 if needs_new_call {
1478 self.current_texture_id = tid;
1479
1480 self.instance_data.push(InstanceData {
1481 translation,
1482 scale,
1483 rotation,
1484 blur_radius: 0.0,
1485 ior_override: 0.0,
1486 });
1487 self.draw_calls.push(DrawCall {
1488 target_id: None,
1489 texture_id: tid,
1490 scissor_rect: self.clip_stack.last().copied(),
1491 index_start: base_index_idx,
1492 index_count: buffers.indices.len() as u32,
1493 material,
1494 instance_start: (self.instance_data.len() - 1) as u32,
1495 });
1496 } else if let Some(call) = self.draw_calls.last_mut() {
1497 call.index_count += buffers.indices.len() as u32;
1498 }
1499 }
1500}
1501
1502impl cvkg_core::FrameRenderer<wgpu::CommandEncoder> for SurtrRenderer {
1503 fn begin_frame(&mut self) -> wgpu::CommandEncoder {
1504 cvkg_core::begin_render_phase();
1505 let id = self
1506 .current_window
1507 .expect("No target window set for frame. Call set_target_window first.");
1508 self.begin_frame(id)
1509 }
1510
1511 fn render_frame(&mut self) {
1512 if LAYOUT_DIRTY.swap(false, Ordering::AcqRel) {
1515 if let Some(window_id) = self.current_window {
1516 if let Some(surface_ctx) = self.surfaces.get(&window_id) {
1517 let w = surface_ctx.config.width as f32;
1518 let h = surface_ctx.config.height as f32;
1519 let border_rect = cvkg_core::Rect {
1520 x: 0.0,
1521 y: 0.0,
1522 width: w,
1523 height: h,
1524 };
1525 self.stroke_rect(border_rect, [1.0, 0.0, 0.0, 1.0], 10.0);
1527 }
1528 }
1529 }
1530
1531 let req_v_size = (self.vertices.len() * std::mem::size_of::<Vertex>()) as u64;
1533 let mut cur_v_size = self.vertex_buffer.size();
1534 let max_v_size = (MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64 * 4;
1535
1536 if req_v_size > cur_v_size {
1537 while cur_v_size < req_v_size && cur_v_size < max_v_size {
1538 cur_v_size *= 2;
1539 }
1540 if req_v_size > max_v_size {
1541 log::error!("Exceeded dynamic vertex buffer max capacity! Capping geometry.");
1542 self.vertices
1543 .truncate((max_v_size / std::mem::size_of::<Vertex>() as u64) as usize);
1544 cur_v_size = max_v_size;
1545 }
1546 log::info!("Growing vertex buffer to {} bytes", cur_v_size);
1547 self.vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
1548 label: Some("Vertex Buffer (Grown)"),
1549 size: cur_v_size,
1550 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1551 mapped_at_creation: false,
1552 });
1553 }
1554
1555 let req_i_size = (self.indices.len() * std::mem::size_of::<u32>()) as u64;
1556 let mut cur_i_size = self.index_buffer.size();
1557 let max_i_size = (MAX_INDICES * std::mem::size_of::<u32>()) as u64 * 4;
1558
1559 if req_i_size > cur_i_size {
1560 while cur_i_size < req_i_size && cur_i_size < max_i_size {
1561 cur_i_size *= 2;
1562 }
1563 if req_i_size > max_i_size {
1564 log::error!("Exceeded dynamic index buffer max capacity! Capping geometry.");
1565 self.indices
1566 .truncate((max_i_size / std::mem::size_of::<u32>() as u64) as usize);
1567 cur_i_size = max_i_size;
1568 }
1569 log::info!("Growing index buffer to {} bytes", cur_i_size);
1570 self.index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
1571 label: Some("Index Buffer (Grown)"),
1572 size: cur_i_size,
1573 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1574 mapped_at_creation: false,
1575 });
1576 }
1577
1578 let mut staging_encoder =
1580 self.device
1581 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1582 label: Some("Surtr Staging Encoder"),
1583 });
1584
1585 let mut has_writes = false;
1586
1587 if !self.vertices.is_empty() {
1588 let v_bytes = bytemuck::cast_slice(&self.vertices);
1589 self.staging_belt
1590 .write_buffer(
1591 &mut staging_encoder,
1592 &self.vertex_buffer,
1593 0,
1594 wgpu::BufferSize::new(v_bytes.len() as u64).unwrap(),
1595 )
1596 .copy_from_slice(v_bytes);
1597 has_writes = true;
1598 }
1599
1600 if !self.indices.is_empty() {
1601 let i_bytes = bytemuck::cast_slice(&self.indices);
1602 self.staging_belt
1603 .write_buffer(
1604 &mut staging_encoder,
1605 &self.index_buffer,
1606 0,
1607 wgpu::BufferSize::new(i_bytes.len() as u64).unwrap(),
1608 )
1609 .copy_from_slice(i_bytes);
1610 has_writes = true;
1611 }
1612
1613 if !self.instance_data.is_empty() {
1614 let inst_bytes = bytemuck::cast_slice(&self.instance_data);
1615 self.staging_belt
1616 .write_buffer(
1617 &mut staging_encoder,
1618 &self.instance_buffer,
1619 0,
1620 wgpu::BufferSize::new(inst_bytes.len() as u64).unwrap(),
1621 )
1622 .copy_from_slice(inst_bytes);
1623 has_writes = true;
1624 }
1625
1626 if has_writes {
1627 self.staging_belt.finish();
1628 self.staging_command_buffers.push(staging_encoder.finish());
1629 }
1630
1631 self.current_scene.time = self.start_time.elapsed().as_secs_f32();
1633 self.queue.write_buffer(
1634 &self.scene_buffer,
1635 0,
1636 bytemuck::bytes_of(&self.current_scene),
1637 );
1638 self.queue.write_buffer(
1639 &self.theme_buffer,
1640 0,
1641 bytemuck::bytes_of(&self.current_theme),
1642 );
1643
1644 self.telemetry.draw_calls = self.draw_calls.len() as u32;
1646 self.telemetry.vertices = self.vertices.len() as u32;
1647 }
1648
1649 fn end_frame(&mut self, encoder: wgpu::CommandEncoder) {
1650 SurtrRenderer::end_frame(self, encoder);
1652 cvkg_core::end_render_phase();
1653 }
1654}
1655
1656fn glyph_image_to_rgba(image: cvkg_runic_text::GlyphImage) -> (Vec<u8>, u32, u32) {
1657 let width = image.width;
1658 let height = image.height;
1659 let pixels = width.saturating_mul(height) as usize;
1660
1661 if pixels == 0 || image.data.is_empty() {
1662 return (Vec::new(), width, height);
1663 }
1664
1665 let (bytes_per_pixel, remainder) = (image.data.len() / pixels, image.data.len() % pixels);
1666 if remainder != 0 {
1667 log::warn!(
1668 "Glyph rasterizer returned {} bytes for {}x{} glyph; expected whole pixels ({} bytes per pixel)",
1669 image.data.len(),
1670 width,
1671 height,
1672 bytes_per_pixel
1673 );
1674 return (Vec::new(), width, height);
1675 }
1676
1677 let rgba_data = match bytes_per_pixel {
1678 1 => {
1679 let mut data = Vec::with_capacity(pixels * 4);
1680 for alpha in &image.data {
1681 data.push(255);
1682 data.push(255);
1683 data.push(255);
1684 data.push(*alpha);
1685 }
1686 data
1687 }
1688 3 => {
1689 let mut data = Vec::with_capacity(pixels * 4);
1690 for rgb in image.data.chunks_exact(3) {
1691 let alpha = rgb.iter().copied().max().unwrap_or(0);
1692 data.push(255);
1693 data.push(255);
1694 data.push(255);
1695 data.push(alpha);
1696 }
1697 data
1698 }
1699 4 => {
1700 let mut data = image.data;
1701 for chunk in data.chunks_exact_mut(4) {
1702 if chunk[3] == 0 && (chunk[0] > 0 || chunk[1] > 0 || chunk[2] > 0) {
1705 chunk[3] = chunk[0].max(chunk[1]).max(chunk[2]);
1706 }
1707 }
1708 data
1709 }
1710 _ => {
1711 log::warn!(
1712 "Glyph rasterizer returned unsupported {} bytes per pixel for {}x{} glyph ({} bytes total)",
1713 bytes_per_pixel,
1714 width,
1715 height,
1716 image.data.len()
1717 );
1718 Vec::new()
1719 }
1720 };
1721
1722 (rgba_data, width, height)
1723}
1724
1725#[cfg(test)]
1726mod tests {
1727 use super::glyph_image_to_rgba;
1728
1729 #[test]
1730 fn glyph_image_to_rgba_keeps_rgba_color_data() {
1731 let image = cvkg_runic_text::GlyphImage {
1732 glyph_id: 1,
1733 width: 2,
1734 height: 1,
1735 data: vec![1, 2, 3, 4, 5, 6, 7, 8],
1736 x_offset: 0.0,
1737 y_offset: 0.0,
1738 cache_key: 42,
1739 };
1740
1741 assert_eq!(
1742 glyph_image_to_rgba(image),
1743 (vec![1, 2, 3, 4, 5, 6, 7, 8], 2, 1)
1744 );
1745 }
1746
1747 #[test]
1748 fn glyph_image_to_rgba_expands_grayscale_alpha() {
1749 let image = cvkg_runic_text::GlyphImage {
1750 glyph_id: 1,
1751 width: 3,
1752 height: 1,
1753 data: vec![0, 128, 255],
1754 x_offset: 0.0,
1755 y_offset: 0.0,
1756 cache_key: 42,
1757 };
1758
1759 assert_eq!(
1760 glyph_image_to_rgba(image),
1761 (
1762 vec![255, 255, 255, 0, 255, 255, 255, 128, 255, 255, 255, 255],
1763 3,
1764 1
1765 )
1766 );
1767 }
1768
1769 #[test]
1770 fn glyph_image_to_rgba_collapses_subpixel_rgb_to_alpha() {
1771 let image = cvkg_runic_text::GlyphImage {
1772 glyph_id: 1,
1773 width: 2,
1774 height: 1,
1775 data: vec![0, 128, 255, 255, 0, 64],
1776 x_offset: 0.0,
1777 y_offset: 0.0,
1778 cache_key: 42,
1779 };
1780
1781 assert_eq!(
1782 glyph_image_to_rgba(image),
1783 (vec![255, 255, 255, 255, 255, 255, 255, 255], 2, 1)
1784 );
1785 }
1786}