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)
592 }
593
594 fn shape_rich_text(
595 &mut self,
596 spans: &[cvkg_runic_text::TextSpan],
597 max_width: Option<f32>,
598 align: cvkg_runic_text::TextAlign,
599 overflow: cvkg_runic_text::TextOverflow,
600 ) -> Option<cvkg_runic_text::ShapedText> {
601 let sf = self.current_scale_factor();
602 let mut scaled_spans = spans.to_vec();
603 for span in &mut scaled_spans {
604 span.style.font_size *= sf;
605 if span.style.fallback_families.is_empty() {
606 span.style.fallback_families = vec![
607 "SF Pro".to_string(),
608 "Inter".to_string(),
609 "Helvetica Neue".to_string(),
610 "Helvetica".to_string(),
611 "Arial".to_string(),
612 "sans-serif".to_string(),
613 ];
614 }
615 }
616 let scaled_max_width = max_width.map(|w| w * sf);
617 self.text_engine
618 .shape_layout(&scaled_spans, scaled_max_width, align, overflow)
619 .ok()
620 }
621
622 fn draw_shaped_text(&mut self, shaped: &cvkg_runic_text::ShapedText, x: f32, y: f32) {
623 for glyph in &shaped.glyphs {
624 let byte_idx = shaped
625 .grapheme_boundaries
626 .get(glyph.cluster as usize)
627 .copied()
628 .unwrap_or(0);
629 let mut span_color = [1.0, 1.0, 1.0, 1.0];
630 for span in &shaped.spans {
631 if byte_idx >= span.byte_offset && byte_idx < span.byte_offset + span.text.len() {
632 span_color = [
633 span.style.color[0] as f32 / 255.0,
634 span.style.color[1] as f32 / 255.0,
635 span.style.color[2] as f32 / 255.0,
636 span.style.color[3] as f32 / 255.0,
637 ];
638 break;
639 }
640 }
641 let c = self.apply_opacity(span_color);
642
643 let cache_key = glyph.cache_key;
644 let (uv_rect, w, h, x_off, y_off) = if let Some(info) = self.text_cache.get(&cache_key)
645 {
646 *info
647 } else {
648 if let Some(image) = self.text_engine.rasterize(cache_key) {
649 let glyph_id = image.glyph_id;
650 let data_len = image.data.len();
651 let gw = image.width;
652 let gh = image.height;
653 let x_offset = image.x_offset;
654 let y_offset = image.y_offset;
655 let (rgba_data, gw, gh) = glyph_image_to_rgba(image);
656 if gw == 0 || gh == 0 {
657 continue;
658 }
659 if rgba_data.is_empty() {
660 log::warn!(
661 "Glyph rasterizer returned unsupported pixel format for glyph {} ({} bytes, {}x{}), skipping",
662 glyph_id,
663 data_len,
664 gw,
665 gh
666 );
667 continue;
668 }
669
670 let pack_res = self.heim_packer.pack(gw, gh);
671 let (nx, ny) = if let Some(pos) = pack_res {
672 pos
673 } else {
674 self.reclaim_vram();
675 match self.heim_packer.pack(gw, gh) {
676 Some(pos) => pos,
677 None => {
678 log::error!(
679 "Glyph heim critically full after reclaim: cannot pack {}x{} glyph, skipping",
680 gw,
681 gh
682 );
683 continue; }
685 }
686 };
687 let sample = &rgba_data[0..std::cmp::min(rgba_data.len(), 20)];
689 log::info!("Rasterized glyph {}, gw: {}, gh: {}, data len: {}, first 20 bytes: {:?}", glyph_id, gw, gh, rgba_data.len(), sample);
690
691 self.queue.write_texture(
692 wgpu::TexelCopyTextureInfo {
693 texture: &self.mega_heim_tex,
694 mip_level: 0,
695 origin: wgpu::Origin3d { x: nx, y: ny, z: 0 },
696 aspect: wgpu::TextureAspect::All,
697 },
698 &rgba_data,
699 wgpu::TexelCopyBufferLayout {
700 offset: 0,
701 bytes_per_row: Some(gw * 4),
702 rows_per_image: Some(gh),
703 },
704 wgpu::Extent3d {
705 width: gw,
706 height: gh,
707 depth_or_array_layers: 1,
708 },
709 );
710
711 let tex_w = self.mega_heim_tex.width() as f32;
712 let tex_h = self.mega_heim_tex.height() as f32;
713 let info = (
714 Rect {
715 x: nx as f32 / tex_w,
716 y: ny as f32 / tex_h,
717 width: gw as f32 / tex_w,
718 height: gh as f32 / tex_h,
719 },
720 gw as f32,
721 gh as f32,
722 x_offset,
723 y_offset,
724 );
725 self.text_cache.put(cache_key, info);
726 info
727 } else {
728 (Rect::zero(), 0.0, 0.0, 0.0, 0.0)
729 }
730 };
731
732 if w > 0.0 {
733 let sf = self.current_scale_factor();
734 let baseline_y = y + shaped.ascent / sf;
738 let glyph_rect = Rect {
739 x: x + (glyph.x + x_off) / sf,
740 y: baseline_y + (glyph.y - y_off) / sf,
741 width: w / sf,
742 height: h / sf,
743 };
744 let tid = self.get_texture_id("__mega_heim");
745 let slice = self
746 .slice_stack
747 .last()
748 .copied()
749 .map(|(a, o)| [a, o, 1.0, 1.0])
750 .unwrap_or([0.0, 0.0, 0.0, 1.0]);
751 self.fill_rect_with_full_params_and_slice(
752 glyph_rect,
753 c,
754 6,
755 tid,
756 0.0,
757 uv_rect,
758 slice,
759 [glyph.glyph_index as f32, glyph.time_offset],
760 );
761 }
762 }
763 }
764
765 fn draw_texture(&mut self, texture_id: u32, rect: Rect) {
766 self.fill_rect_with_full_params_and_slice(
767 rect,
768 [1.0, 1.0, 1.0, 1.0],
769 2,
770 Some(texture_id),
771 0.0,
772 Rect {
773 x: 0.0,
774 y: 0.0,
775 width: 1.0,
776 height: 1.0,
777 },
778 [0.0, 0.0, 0.0, 1.0],
779 [0.0, 0.0],
780 );
781 }
782
783 fn load_image(&mut self, name: &str, data: &[u8]) {
786 if self.image_uv_registry.contains(name) {
787 return;
788 }
789 let img_result = image::load_from_memory(data);
790 let img = match img_result {
791 Ok(img) => img.to_rgba8(),
792 Err(e) => {
793 log::error!("Failed to load image {}: {}", name, e);
794 image::RgbaImage::from_pixel(1, 1, image::Rgba([255, 255, 255, 255]))
795 }
796 };
797 let (width, height) = img.dimensions();
798
799 let size = wgpu::Extent3d {
800 width,
801 height,
802 depth_or_array_layers: 1,
803 };
804 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
805 label: Some(&format!("Texture Array Layer: {}", name)),
806 size,
807 mip_level_count: 1,
808 sample_count: 1,
809 dimension: wgpu::TextureDimension::D2,
810 format: wgpu::TextureFormat::Rgba8UnormSrgb,
811 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
812 view_formats: &[],
813 });
814
815 self.queue.write_texture(
816 wgpu::TexelCopyTextureInfo {
817 texture: &texture,
818 mip_level: 0,
819 origin: wgpu::Origin3d::ZERO,
820 aspect: wgpu::TextureAspect::All,
821 },
822 &img,
823 wgpu::TexelCopyBufferLayout {
824 offset: 0,
825 bytes_per_row: Some(4 * width),
826 rows_per_image: Some(height),
827 },
828 size,
829 );
830
831 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
832
833 let index = if self.texture_registry.len() < 255 {
835 (self.texture_registry.len() + 1) as u32
836 } else {
837 if let Some((old_name, old_index)) = self.texture_registry.pop_lru() {
839 self.image_uv_registry.pop(&old_name);
840 old_index
841 } else {
842 1 }
844 };
845
846 self.texture_views[index as usize] = view;
847 self.image_uv_registry.put(
848 name.to_string(),
849 Rect {
850 x: 0.0,
851 y: 0.0,
852 width: 1.0,
853 height: 1.0,
854 },
855 );
856 self.texture_registry.put(name.to_string(), index);
857 self.rebuild_texture_array_bind_group();
858 }
859
860 fn push_clip_rect(&mut self, rect: Rect) {
861 self.clip_stack.push(rect);
862 }
863
864 fn pop_clip_rect(&mut self) {
865 self.clip_stack.pop();
866 }
867
868 fn current_clip_rect(&self) -> Rect {
869 self.clip_stack.last().copied().unwrap_or(Rect::new(
870 0.0,
871 0.0,
872 self.current_width() as f32,
873 self.current_height() as f32,
874 ))
875 }
876
877 fn memoize(&mut self, id: u64, data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer)) {
878 let should_skip = self.memo_cache.get(&id) == Some(&data_hash);
881
882 if !should_skip {
883 self.memo_cache.insert(id, data_hash);
885 render_fn(self);
886 }
887 }
889
890 fn push_opacity(&mut self, opacity: f32) {
891 let current = self.opacity_stack.last().copied().unwrap_or(1.0);
892 self.opacity_stack.push(current * opacity);
893 }
894
895 fn pop_opacity(&mut self) {
896 self.opacity_stack.pop();
897 }
898
899 fn push_shadow(&mut self, radius: f32, color: [f32; 4], offset: [f32; 2]) {
900 self.shadow_stack.push(ShadowState {
901 radius,
902 color,
903 _offset: offset,
904 });
905 }
906
907 fn pop_shadow(&mut self) {
908 self.shadow_stack.pop();
909 }
910
911 fn push_transform(&mut self, translation: [f32; 2], scale: [f32; 2], rotation: f32) {
912 let c = rotation.cos();
913 let sn = rotation.sin();
914 let affine = glam::Mat3::from_cols(
915 glam::Vec3::new(c * scale[0], sn * scale[0], 0.0),
916 glam::Vec3::new(-sn * scale[1], c * scale[1], 0.0),
917 glam::Vec3::new(translation[0], translation[1], 1.0),
918 );
919
920 let parent = self
921 .transform_stack
922 .last()
923 .copied()
924 .unwrap_or(glam::Mat3::IDENTITY);
925 self.transform_stack.push(parent * affine);
926 }
927
928 fn push_affine(&mut self, transform: [f32; 6]) {
929 let affine = glam::Mat3::from_cols(
930 glam::Vec3::new(transform[0], transform[1], 0.0),
931 glam::Vec3::new(transform[2], transform[3], 0.0),
932 glam::Vec3::new(transform[4], transform[5], 1.0),
933 );
934 let parent = self
935 .transform_stack
936 .last()
937 .copied()
938 .unwrap_or(glam::Mat3::IDENTITY);
939 self.transform_stack.push(parent * affine);
940 }
941
942 fn pop_transform(&mut self) {
943 self.transform_stack.pop();
944 }
945
946 fn set_theme(&mut self, theme: ColorTheme) {
947 self.current_theme = theme;
948 self.queue
949 .write_buffer(&self.theme_buffer, 0, bytemuck::bytes_of(&theme));
950 }
951
952 fn set_rage(&mut self, rage: f32) {
953 self.current_scene.berzerker_rage = rage;
954 }
956
957 fn trigger_shatter_event(&mut self, origin: [f32; 2], force: f32) {
958 self.current_scene.shatter_origin = origin;
959 self.current_scene.shatter_time = self.current_scene.time;
960 self.current_scene.shatter_force = force;
961 }
962
963 fn set_scene_preset(&mut self, preset: u32) {
964 self.current_scene.scene_type = preset;
965 }
966
967 fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
970 self.slice_stack.push((angle, offset));
971 }
972
973 fn pop_mjolnir_slice(&mut self) {
975 self.slice_stack.pop();
976 }
977
978 fn mjolnir_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
979 self.shatter_internal(rect, pieces, force, color, 8);
980 }
981
982 fn mjolnir_fluid_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
983 self.shatter_internal(rect, pieces, force, color, 11);
984 }
985
986 fn draw_mjolnir_bolt(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
987 self.recursive_bolt(from, to, 4, color);
988 }
989
990 fn dispatch_particles(
991 &mut self,
992 origin: [f32; 2],
993 count: u32,
994 effect_type: &str,
995 _color: [f32; 4],
996 ) {
997 log::info!(
998 "[Surtr] Dispatching {} {} particles at {:?}",
999 count,
1000 effect_type,
1001 origin
1002 );
1003 }
1005
1006 fn draw_hologram(&mut self, rect: Rect, hologram_id: &str, time: f32) {
1007 log::info!(
1008 "[Surtr] Drawing hologram {} at {:?} (t={})",
1009 hologram_id,
1010 rect,
1011 time
1012 );
1013 self.stroke_rect(rect, [0.0, 1.0, 1.0, 0.5], 2.0);
1016 }
1017
1018 fn upload_data_texture(&mut self, id: &str, data: &[f32], width: u32, height: u32) {
1019 let size = wgpu::Extent3d {
1020 width,
1021 height,
1022 depth_or_array_layers: 1,
1023 };
1024 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
1025 label: Some(id),
1026 size,
1027 mip_level_count: 1,
1028 sample_count: 1,
1029 dimension: wgpu::TextureDimension::D2,
1030 format: wgpu::TextureFormat::R32Float,
1031 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1032 view_formats: &[],
1033 });
1034 self.queue.write_texture(
1035 wgpu::TexelCopyTextureInfo {
1036 texture: &texture,
1037 mip_level: 0,
1038 origin: wgpu::Origin3d::ZERO,
1039 aspect: wgpu::TextureAspect::All,
1040 },
1041 bytemuck::cast_slice(data),
1042 wgpu::TexelCopyBufferLayout {
1043 offset: 0,
1044 bytes_per_row: Some(4 * width),
1045 rows_per_image: Some(height),
1046 },
1047 size,
1048 );
1049 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1050 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
1051 address_mode_u: wgpu::AddressMode::ClampToEdge,
1052 address_mode_v: wgpu::AddressMode::ClampToEdge,
1053 mag_filter: wgpu::FilterMode::Linear,
1054 ..Default::default()
1055 });
1056 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1057 layout: &self.texture_bind_group_layout,
1058 entries: &[
1059 wgpu::BindGroupEntry {
1060 binding: 0,
1061 resource: wgpu::BindingResource::TextureViewArray(&vec![&view; 256]),
1062 },
1063 wgpu::BindGroupEntry {
1064 binding: 1,
1065 resource: wgpu::BindingResource::Sampler(&sampler),
1066 },
1067 ],
1068 label: Some(id),
1069 });
1070 self.texture_bind_groups.push(bind_group);
1071 let tid = (self.texture_bind_groups.len() - 1) as u32;
1072 self.texture_registry.put(id.to_string(), tid);
1073 }
1074
1075 fn draw_heatmap(&mut self, texture_id: &str, rect: Rect, _palette: &str) {
1076 let tid = self.get_texture_id(texture_id);
1077 self.fill_rect_with_mode(rect, [1.0, 1.0, 1.0, 1.0], 12, tid);
1078 }
1079
1080 fn draw_mesh(&mut self, mesh: &Mesh, color: [f32; 4], transform: glam::Mat4) {
1081 let base_idx = self.vertices.len() as u32;
1082 let screen = [self.current_width() as f32, self.current_height() as f32];
1083
1084 for i in 0..mesh.vertices.len() {
1085 let pos = transform.transform_point3(glam::Vec3::from(mesh.vertices[i]));
1086 let norm = transform.transform_vector3(glam::Vec3::from(mesh.normals[i]));
1087
1088 self.vertices.push(Vertex {
1089 position: pos.to_array(),
1090 normal: norm.to_array(),
1091 uv: [0.0, 0.0],
1092 color,
1093 material_id: 13, radius: 0.0,
1095 slice: [0.0, 0.0, 0.0, 1.0],
1096 logical: [0.0, 0.0],
1097 size: [0.0, 0.0],
1098 clip: [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY],
1099 tex_index: 0,
1100 });
1101 }
1102
1103 for idx in &mesh.indices {
1104 self.indices.push(base_idx + idx);
1105 }
1106
1107 let (translation, scale_transform, rotation, _, _) = self.current_transform();
1108
1109 if self.draw_calls.is_empty() || self.current_texture_id.is_some() {
1110 self.current_texture_id = None;
1111
1112 self.instance_data.push(InstanceData {
1113 translation,
1114 scale: scale_transform,
1115 rotation,
1116 blur_radius: 0.0,
1117 ior_override: 0.0,
1118 });
1119 self.draw_calls.push(DrawCall {
1120 target_id: None,
1121 texture_id: None,
1122 scissor_rect: self.clip_stack.last().copied(),
1123 index_start: (self.indices.len() as u32) - (mesh.indices.len() as u32),
1124 index_count: mesh.indices.len() as u32,
1125 material: cvkg_core::DrawMaterial::Opaque,
1126 instance_start: (self.instance_data.len() - 1) as u32,
1127 });
1128 } else {
1129 self.draw_calls.last_mut().unwrap().index_count += mesh.indices.len() as u32;
1130 }
1131 }
1132
1133 fn draw_mesh_3d(
1134 &mut self,
1135 mesh: &Mesh,
1136 material: &cvkg_core::Material3D,
1137 transform: &cvkg_core::Transform3D,
1138 ) {
1139 let base_idx = self.vertices.len() as u32;
1140 let screen = [self.current_width() as f32, self.current_height() as f32];
1141 let model_matrix = transform.to_matrix();
1142
1143 for i in 0..mesh.vertices.len() {
1144 let pos = model_matrix.transform_point3(glam::Vec3::from(mesh.vertices[i]));
1145 let norm = model_matrix.transform_vector3(glam::Vec3::from(mesh.normals[i]));
1146
1147 self.vertices.push(Vertex {
1148 position: [pos.x, pos.y, pos.z],
1149 normal: [norm.x, norm.y, norm.z],
1150 uv: [0.0, 0.0],
1151 color: material.base_color,
1152 material_id: 13, radius: 0.0,
1154 slice: [material.metallic, material.roughness, material.opacity, 1.0],
1155 logical: [0.0, 0.0],
1156 size: [0.0, 0.0],
1157 clip: [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY],
1158 tex_index: 0,
1159 });
1160 }
1161
1162 for idx in &mesh.indices {
1163 self.indices.push(base_idx + idx);
1164 }
1165
1166 self.instance_data.push(InstanceData {
1167 translation: [0.0, 0.0],
1168 scale: [1.0, 1.0],
1169 rotation: 0.0,
1170 blur_radius: 0.0,
1171 ior_override: 0.0,
1172 });
1173
1174 self.draw_calls.push(DrawCall {
1175 target_id: None,
1176 texture_id: None,
1177 scissor_rect: self.clip_stack.last().copied(),
1178 index_start: (self.indices.len() as u32) - (mesh.indices.len() as u32),
1179 index_count: mesh.indices.len() as u32,
1180 material: cvkg_core::DrawMaterial::Opaque,
1181 instance_start: (self.instance_data.len() - 1) as u32,
1182 });
1183 }
1184
1185 fn set_camera_3d(&mut self, camera: &cvkg_core::Camera3D) {
1186 self.current_scene.proj = camera.projection_matrix();
1187 self.current_scene.view = camera.view_matrix();
1188 }
1189
1190 fn push_transform_3d(&mut self, transform: &cvkg_core::Transform3D) {
1191 let (translation, rotation_quat, scale_glam) =
1194 transform.to_matrix().to_scale_rotation_translation();
1195 let translation = [translation.x, translation.y];
1196 let scale = [scale_glam.x, scale_glam.y];
1197 let rotation = if rotation_quat.length_squared() > 0.0 {
1198 let (axis, angle) = rotation_quat.to_axis_angle();
1199 angle * axis.z.signum() } else {
1201 0.0
1202 };
1203 self.push_transform(translation, scale, rotation);
1204 }
1205
1206 fn pop_transform_3d(&mut self) {
1207 self.pop_transform();
1209 }
1210
1211 fn render_scene_node_3d(
1212 &mut self,
1213 position: [f32; 3],
1214 rotation: [f32; 4],
1215 scale: [f32; 3],
1216 color: [f32; 4],
1217 meshes: &[Mesh],
1218 ) {
1219 let transform = cvkg_core::Transform3D {
1220 position: glam::Vec3::from(position),
1221 rotation: glam::Quat::from_xyzw(rotation[0], rotation[1], rotation[2], rotation[3]),
1222 scale: glam::Vec3::from(scale),
1223 };
1224 if meshes.is_empty() {
1226 let h = 0.5f32;
1228 let cube = Mesh {
1229 vertices: vec![
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 [-h, h, h],
1238 ],
1239 normals: vec![
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, 0.0, 1.0],
1248 [0.0, -1.0, 0.0],
1249 [0.0, -1.0, 0.0],
1250 [0.0, -1.0, 0.0],
1251 [0.0, -1.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 [1.0, 0.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 [0.0, 1.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 [-1.0, 0.0, 0.0],
1264 ],
1265 indices: vec![
1266 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, ],
1273 };
1274 let material = cvkg_core::Material3D::unlit(color);
1275 self.draw_mesh_3d(&cube, &material, &transform);
1276 } else {
1277 let material = cvkg_core::Material3D::unlit(color);
1278 self.draw_mesh_3d(&meshes[0], &material, &transform);
1279 }
1280 }
1281
1282 fn register_shared_element(&mut self, id: &str, rect: Rect) {
1283 self.shared_elements.put(id.to_string(), rect);
1284 }
1285
1286 fn set_z_index(&mut self, z: f32) {
1287 self.current_z = z;
1288 }
1289
1290 fn set_material(&mut self, material: cvkg_core::DrawMaterial) {
1291 self.current_draw_material = material;
1292 }
1293
1294 fn current_material(&self) -> cvkg_core::DrawMaterial {
1295 self.current_draw_material
1296 }
1297
1298 fn get_z_index(&self) -> f32 {
1299 self.current_z
1300 }
1301
1302 fn request_redraw(&mut self) {
1303 self.redraw_requested = true;
1304 }
1305
1306 fn enter_portal(&mut self, z_index: i32) {
1318 self.current_z = z_index as f32;
1322 }
1323
1324 fn exit_portal(&mut self) {
1328 self.current_z = 0.0;
1329 }
1330
1331 fn push_vnode(&mut self, rect: Rect, name: &'static str) {
1332 self.vnode_stack.push((rect, name));
1333 }
1334
1335 fn pop_vnode(&mut self) {
1336 self.vnode_stack.pop();
1337 }
1338
1339 fn register_handler(
1340 &mut self,
1341 event_type: &str,
1342 handler: std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>,
1343 ) {
1344 self.event_handlers
1345 .entry(event_type.to_string())
1346 .or_insert_with(Vec::new)
1347 .push(handler);
1348 }
1349
1350 fn load_svg(&mut self, name: &str, svg_data: &[u8]) {
1351 SurtrRenderer::load_svg(self, name, svg_data);
1352 }
1353
1354 fn draw_svg(&mut self, name: &str, rect: Rect) {
1355 SurtrRenderer::draw_svg(self, name, rect, None, 1);
1356 }
1357
1358 fn serialize_svg(&mut self, name: &str) -> Result<String, String> {
1359 let tree = self
1360 .svg_trees
1361 .get(name)
1362 .ok_or_else(|| format!("SVG '{}' not found", name))?;
1363 let config = cvkg_svg_serialize::SerializerConfig::default();
1364 let mut serializer = cvkg_svg_serialize::SvgSerializer::with_config(config);
1365 serializer
1366 .serialize(tree)
1367 .map_err(|e| format!("SVG serialization failed: {}", e))
1368 }
1369
1370 fn apply_svg_filter(
1371 &mut self,
1372 name: &str,
1373 filter_id: &str,
1374 _region: Rect,
1375 ) -> Result<String, String> {
1376 let tree = self
1377 .svg_trees
1378 .get(name)
1379 .ok_or_else(|| format!("SVG '{}' not found", name))?;
1380 let _filter = Self::find_filter(tree, filter_id)
1381 .ok_or_else(|| format!("Filter '{}' not found in SVG '{}'", filter_id, name))?;
1382 let config = cvkg_svg_serialize::SerializerConfig::default();
1383 let mut serializer = cvkg_svg_serialize::SvgSerializer::with_config(config);
1384 serializer
1385 .serialize(tree)
1386 .map_err(|e| format!("SVG filter serialization failed: {}", e))
1387 }
1388}
1389
1390impl SurtrRenderer {
1393 pub fn clear_event_handlers(&mut self) {
1396 self.event_handlers.clear();
1397 }
1398
1399 pub fn get_handlers(
1401 &self,
1402 event_type: &str,
1403 ) -> Option<&Vec<std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>>> {
1404 self.event_handlers.get(event_type)
1405 }
1406
1407 pub(crate) fn current_transform(&self) -> ([f32; 2], [f32; 2], f32, f32, f32) {
1411 let m = self
1414 .transform_stack
1415 .last()
1416 .copied()
1417 .unwrap_or(glam::Mat3::IDENTITY);
1418 let t = [m.z_axis.x, m.z_axis.y];
1419 let a = m.x_axis.x;
1421 let b = m.x_axis.y;
1422 let c = m.y_axis.x;
1423 let d = m.y_axis.y;
1424 let sx = (a * a + b * b).sqrt();
1425 let sy = (c * c + d * d).sqrt();
1426 let rotation = b.atan2(a);
1427 let skew_x = (a * c + b * d) / (sx * sy); (t, [sx, sy], rotation, skew_x, 0.0)
1430 }
1431
1432 pub fn stroke_path(&mut self, path: &lyon::path::Path, color: [f32; 4], stroke_width: f32) {
1433 let c = self.apply_opacity(color);
1434 let mut tessellator = StrokeTessellator::new();
1435 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
1436 let base_vertex_idx = self.vertices.len() as u32;
1437 let base_index_idx = self.indices.len() as u32;
1438
1439 let (translation, scale, rotation, _, _) = self.current_transform();
1440 let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
1441 x: -10000.0,
1442 y: -10000.0,
1443 width: 20000.0,
1444 height: 20000.0,
1445 });
1446 let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
1447
1448 let result = tessellator.tessellate_path(
1449 path,
1450 &StrokeOptions::default().with_line_width(stroke_width),
1451 &mut BuffersBuilder::new(
1452 &mut buffers,
1453 CustomStrokeVertexConstructor { color: c, clip, path_length: 1.0 },
1454 ),
1455 );
1456 if let Err(e) = result {
1457 log::warn!("Failed to tessellate stroke path: {:?}", e);
1458 return;
1459 }
1460
1461 self.vertices.extend(buffers.vertices);
1462 for idx in &buffers.indices {
1463 self.indices.push(base_vertex_idx + *idx);
1464 }
1465
1466 let material = self.current_material();
1467 let tid = self.get_texture_id("__mega_heim");
1468
1469 let last_call = self.draw_calls.last();
1470 let needs_new_call = self.draw_calls.is_empty()
1471 || self.current_texture_id != tid
1472 || last_call.unwrap().scissor_rect != self.clip_stack.last().copied()
1473 || last_call.unwrap().material != material;
1474
1475 if needs_new_call {
1476 self.current_texture_id = tid;
1477
1478 self.instance_data.push(InstanceData {
1479 translation,
1480 scale,
1481 rotation,
1482 blur_radius: 0.0,
1483 ior_override: 0.0,
1484 });
1485 self.draw_calls.push(DrawCall {
1486 target_id: None,
1487 texture_id: tid,
1488 scissor_rect: self.clip_stack.last().copied(),
1489 index_start: base_index_idx,
1490 index_count: buffers.indices.len() as u32,
1491 material,
1492 instance_start: (self.instance_data.len() - 1) as u32,
1493 });
1494 } else if let Some(call) = self.draw_calls.last_mut() {
1495 call.index_count += buffers.indices.len() as u32;
1496 }
1497 }
1498}
1499
1500impl cvkg_core::FrameRenderer<wgpu::CommandEncoder> for SurtrRenderer {
1501 fn begin_frame(&mut self) -> wgpu::CommandEncoder {
1502 cvkg_core::begin_render_phase();
1503 let id = self
1504 .current_window
1505 .expect("No target window set for frame. Call set_target_window first.");
1506 self.begin_frame(id)
1507 }
1508
1509 fn render_frame(&mut self) {
1510 if LAYOUT_DIRTY.swap(false, Ordering::AcqRel) {
1513 if let Some(window_id) = self.current_window {
1514 if let Some(surface_ctx) = self.surfaces.get(&window_id) {
1515 let w = surface_ctx.config.width as f32;
1516 let h = surface_ctx.config.height as f32;
1517 let border_rect = cvkg_core::Rect {
1518 x: 0.0,
1519 y: 0.0,
1520 width: w,
1521 height: h,
1522 };
1523 self.stroke_rect(border_rect, [1.0, 0.0, 0.0, 1.0], 10.0);
1525 }
1526 }
1527 }
1528
1529 let req_v_size = (self.vertices.len() * std::mem::size_of::<Vertex>()) as u64;
1531 let mut cur_v_size = self.vertex_buffer.size();
1532 let max_v_size = (MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64 * 4;
1533
1534 if req_v_size > cur_v_size {
1535 while cur_v_size < req_v_size && cur_v_size < max_v_size {
1536 cur_v_size *= 2;
1537 }
1538 if req_v_size > max_v_size {
1539 log::error!("Exceeded dynamic vertex buffer max capacity! Capping geometry.");
1540 self.vertices
1541 .truncate((max_v_size / std::mem::size_of::<Vertex>() as u64) as usize);
1542 cur_v_size = max_v_size;
1543 }
1544 log::info!("Growing vertex buffer to {} bytes", cur_v_size);
1545 self.vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
1546 label: Some("Vertex Buffer (Grown)"),
1547 size: cur_v_size,
1548 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1549 mapped_at_creation: false,
1550 });
1551 }
1552
1553 let req_i_size = (self.indices.len() * std::mem::size_of::<u32>()) as u64;
1554 let mut cur_i_size = self.index_buffer.size();
1555 let max_i_size = (MAX_INDICES * std::mem::size_of::<u32>()) as u64 * 4;
1556
1557 if req_i_size > cur_i_size {
1558 while cur_i_size < req_i_size && cur_i_size < max_i_size {
1559 cur_i_size *= 2;
1560 }
1561 if req_i_size > max_i_size {
1562 log::error!("Exceeded dynamic index buffer max capacity! Capping geometry.");
1563 self.indices
1564 .truncate((max_i_size / std::mem::size_of::<u32>() as u64) as usize);
1565 cur_i_size = max_i_size;
1566 }
1567 log::info!("Growing index buffer to {} bytes", cur_i_size);
1568 self.index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
1569 label: Some("Index Buffer (Grown)"),
1570 size: cur_i_size,
1571 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1572 mapped_at_creation: false,
1573 });
1574 }
1575
1576 let mut staging_encoder =
1578 self.device
1579 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1580 label: Some("Surtr Staging Encoder"),
1581 });
1582
1583 let mut has_writes = false;
1584
1585 if !self.vertices.is_empty() {
1586 let v_bytes = bytemuck::cast_slice(&self.vertices);
1587 self.staging_belt
1588 .write_buffer(
1589 &mut staging_encoder,
1590 &self.vertex_buffer,
1591 0,
1592 wgpu::BufferSize::new(v_bytes.len() as u64).unwrap(),
1593 )
1594 .copy_from_slice(v_bytes);
1595 has_writes = true;
1596 }
1597
1598 if !self.indices.is_empty() {
1599 let i_bytes = bytemuck::cast_slice(&self.indices);
1600 self.staging_belt
1601 .write_buffer(
1602 &mut staging_encoder,
1603 &self.index_buffer,
1604 0,
1605 wgpu::BufferSize::new(i_bytes.len() as u64).unwrap(),
1606 )
1607 .copy_from_slice(i_bytes);
1608 has_writes = true;
1609 }
1610
1611 if !self.instance_data.is_empty() {
1612 let inst_bytes = bytemuck::cast_slice(&self.instance_data);
1613 self.staging_belt
1614 .write_buffer(
1615 &mut staging_encoder,
1616 &self.instance_buffer,
1617 0,
1618 wgpu::BufferSize::new(inst_bytes.len() as u64).unwrap(),
1619 )
1620 .copy_from_slice(inst_bytes);
1621 has_writes = true;
1622 }
1623
1624 if has_writes {
1625 self.staging_belt.finish();
1626 self.staging_command_buffers.push(staging_encoder.finish());
1627 }
1628
1629 self.current_scene.time = self.start_time.elapsed().as_secs_f32();
1631 self.queue.write_buffer(
1632 &self.scene_buffer,
1633 0,
1634 bytemuck::bytes_of(&self.current_scene),
1635 );
1636 self.queue.write_buffer(
1637 &self.theme_buffer,
1638 0,
1639 bytemuck::bytes_of(&self.current_theme),
1640 );
1641
1642 self.telemetry.draw_calls = self.draw_calls.len() as u32;
1644 self.telemetry.vertices = self.vertices.len() as u32;
1645 }
1646
1647 fn end_frame(&mut self, encoder: wgpu::CommandEncoder) {
1648 SurtrRenderer::end_frame(self, encoder);
1650 cvkg_core::end_render_phase();
1651 }
1652}
1653
1654fn glyph_image_to_rgba(image: cvkg_runic_text::GlyphImage) -> (Vec<u8>, u32, u32) {
1655 let width = image.width;
1656 let height = image.height;
1657 let pixels = width.saturating_mul(height) as usize;
1658
1659 if pixels == 0 || image.data.is_empty() {
1660 return (Vec::new(), width, height);
1661 }
1662
1663 let (bytes_per_pixel, remainder) = (image.data.len() / pixels, image.data.len() % pixels);
1664 if remainder != 0 {
1665 log::warn!(
1666 "Glyph rasterizer returned {} bytes for {}x{} glyph; expected whole pixels ({} bytes per pixel)",
1667 image.data.len(),
1668 width,
1669 height,
1670 bytes_per_pixel
1671 );
1672 return (Vec::new(), width, height);
1673 }
1674
1675 let rgba_data = match bytes_per_pixel {
1676 1 => {
1677 let mut data = Vec::with_capacity(pixels * 4);
1678 for alpha in &image.data {
1679 data.push(255);
1680 data.push(255);
1681 data.push(255);
1682 data.push(*alpha);
1683 }
1684 data
1685 }
1686 3 => {
1687 let mut data = Vec::with_capacity(pixels * 4);
1688 for rgb in image.data.chunks_exact(3) {
1689 let alpha = rgb.iter().copied().max().unwrap_or(0);
1690 data.push(255);
1691 data.push(255);
1692 data.push(255);
1693 data.push(alpha);
1694 }
1695 data
1696 }
1697 4 => {
1698 let mut data = image.data;
1699 for chunk in data.chunks_exact_mut(4) {
1700 if chunk[3] == 0 && (chunk[0] > 0 || chunk[1] > 0 || chunk[2] > 0) {
1703 chunk[3] = chunk[0].max(chunk[1]).max(chunk[2]);
1704 }
1705 }
1706 data
1707 }
1708 _ => {
1709 log::warn!(
1710 "Glyph rasterizer returned unsupported {} bytes per pixel for {}x{} glyph ({} bytes total)",
1711 bytes_per_pixel,
1712 width,
1713 height,
1714 image.data.len()
1715 );
1716 Vec::new()
1717 }
1718 };
1719
1720 (rgba_data, width, height)
1721}
1722
1723#[cfg(test)]
1724mod tests {
1725 use super::glyph_image_to_rgba;
1726
1727 #[test]
1728 fn glyph_image_to_rgba_keeps_rgba_color_data() {
1729 let image = cvkg_runic_text::GlyphImage {
1730 glyph_id: 1,
1731 width: 2,
1732 height: 1,
1733 data: vec![1, 2, 3, 4, 5, 6, 7, 8],
1734 x_offset: 0.0,
1735 y_offset: 0.0,
1736 cache_key: 42,
1737 };
1738
1739 assert_eq!(
1740 glyph_image_to_rgba(image),
1741 (vec![1, 2, 3, 4, 5, 6, 7, 8], 2, 1)
1742 );
1743 }
1744
1745 #[test]
1746 fn glyph_image_to_rgba_expands_grayscale_alpha() {
1747 let image = cvkg_runic_text::GlyphImage {
1748 glyph_id: 1,
1749 width: 3,
1750 height: 1,
1751 data: vec![0, 128, 255],
1752 x_offset: 0.0,
1753 y_offset: 0.0,
1754 cache_key: 42,
1755 };
1756
1757 assert_eq!(
1758 glyph_image_to_rgba(image),
1759 (
1760 vec![255, 255, 255, 0, 255, 255, 255, 128, 255, 255, 255, 255],
1761 3,
1762 1
1763 )
1764 );
1765 }
1766
1767 #[test]
1768 fn glyph_image_to_rgba_collapses_subpixel_rgb_to_alpha() {
1769 let image = cvkg_runic_text::GlyphImage {
1770 glyph_id: 1,
1771 width: 2,
1772 height: 1,
1773 data: vec![0, 128, 255, 255, 0, 64],
1774 x_offset: 0.0,
1775 y_offset: 0.0,
1776 cache_key: 42,
1777 };
1778
1779 assert_eq!(
1780 glyph_image_to_rgba(image),
1781 (vec![255, 255, 255, 255, 255, 255, 255, 255], 2, 1)
1782 );
1783 }
1784}