1use crate::renderer::SurtrRenderer;
3use crate::renderer::material_id;
4use crate::types::*;
5use crate::vertex::*;
6use cvkg_core::LAYOUT_DIRTY;
7use cvkg_core::{ColorTheme, Mesh, Rect, RenderStateSnapshot, Renderer};
8use lyon::math::point;
9use lyon::tessellation::{BuffersBuilder, StrokeOptions, StrokeTessellator, VertexBuffers};
10use std::hash::{Hash, Hasher};
11use std::sync::atomic::Ordering;
12
13impl cvkg_core::ElapsedTime for SurtrRenderer {
14 fn delta_time(&self) -> f32 {
15 self.current_scene.delta_time
16 }
17
18 fn elapsed_time(&self) -> f32 {
19 self.start_time.elapsed().as_secs_f32()
20 }
21}
22
23impl cvkg_core::Renderer for SurtrRenderer {
24 fn is_over_budget(&self) -> bool {
25 self.frame_budget.allow_degradation
26 && self.last_frame_start.elapsed().as_secs_f32() * 1000.0 > self.frame_budget.target_ms
27 }
28
29 fn text_scale_factor(&self) -> f32 {
30 self.current_scale_factor()
31 }
32
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_glass_rect(&mut self, rect: Rect, radius: f32, blur_radius: f32) {
70 self.fill_glass_rect_with_intensity(rect, radius, blur_radius, 1.0);
71 }
72
73 fn fill_glass_rect_with_intensity(&mut self, rect: Rect, radius: f32, blur_radius: f32, glass_intensity: f32) {
77 self.fill_glass_rect_with_tint(rect, radius, blur_radius, [1.0, 1.0, 1.0, 0.4], glass_intensity);
79 }
80
81 fn fill_glass_rect_with_tint(&mut self, rect: Rect, radius: f32, blur_radius: f32, tint_color: [f32; 4], glass_intensity: f32) {
84 let gi = glass_intensity.clamp(0.0, 1.0);
85 let blur_strength = (blur_radius / 25.0).clamp(0.0, 4.0) * gi;
88
89 if self.current_z != 0.0 {
91 self.portal_regions.push_back(rect);
92 }
93
94 let prev_material = self.current_draw_material;
96 self.current_draw_material = cvkg_core::DrawMaterial::Glass {
97 blur_radius: blur_strength,
98 ior_override: 0.0,
99 glass_intensity: gi,
100 };
101
102 let fill_color = [
104 tint_color[0],
105 tint_color[1],
106 tint_color[2],
107 tint_color[3] * gi,
108 ];
109
110 self.fill_rect_with_full_params(
111 rect,
112 fill_color,
113 7, None,
115 radius,
116 Rect {
117 x: 0.0,
118 y: 0.0,
119 width: 1.0,
120 height: 1.0,
121 },
122 );
123
124 self.current_draw_material = prev_material;
125 }
126
127 fn fill_glass_rect_with_pressure(&mut self, rect: Rect, radius: f32, blur_radius: f32, pressure: f32) {
128 let p = pressure.clamp(0.0, 1.0);
130 self.fill_glass_rect_with_intensity(rect, radius, blur_radius * p, p);
131 }
132
133 fn set_default_background_color(&mut self, color: [f32; 4]) {
137 self.default_background_color = color;
138 }
139
140 fn fill_squircle(&mut self, rect: Rect, n: f32, color: [f32; 4]) {
143 let prev_material = self.current_draw_material;
144 self.current_draw_material = cvkg_core::DrawMaterial::Opaque;
145 self.fill_rect_with_full_params(
146 rect,
147 self.apply_opacity(color),
148 0,
149 None,
150 rect.width.min(rect.height) * 0.22 * (n / 4.0),
151 Rect { x: 0.0, y: 0.0, width: 1.0, height: 1.0 },
152 );
153 self.current_draw_material = prev_material;
154 }
155
156 fn stroke_squircle(&mut self, rect: Rect, n: f32, color: [f32; 4], stroke_width: f32) {
158 let prev_material = self.current_draw_material;
159 self.current_draw_material = cvkg_core::DrawMaterial::Opaque;
160 self.fill_rect_with_full_params(
161 rect,
162 self.apply_opacity(color),
163 material_id::SQUIRCLE_STROKE,
164 None,
165 rect.width.min(rect.height) * 0.22 * (n / 4.0),
166 Rect { x: stroke_width, y: 0.0, width: 0.0, height: 0.0 },
167 );
168 self.current_draw_material = prev_material;
169 }
170
171 fn draw_focus_ring(&mut self, rect: Rect, radius: f32, offset: f32, width: f32, color: [f32; 4]) {
174 let ring_rect = Rect {
175 x: rect.x - offset,
176 y: rect.y - offset,
177 width: rect.width + 2.0 * offset,
178 height: rect.height + 2.0 * offset,
179 };
180 self.stroke_squircle(ring_rect, 4.0, color, width);
181 }
182
183 fn fill_ellipse(&mut self, rect: Rect, color: [f32; 4]) {
184 self.fill_rect_with_full_params(
185 rect,
186 self.apply_opacity(color),
187 4,
188 None,
189 0.0,
190 Rect {
191 x: 0.0,
192 y: 0.0,
193 width: 1.0,
194 height: 1.0,
195 },
196 );
197 }
198
199 fn draw_3d_cube(&mut self, rect: Rect, color: [f32; 4], rotation: [f32; 3]) {
200 self.fill_rect_with_full_params_and_slice(
201 rect,
202 self.apply_opacity(color),
203 material_id::MESH_3D,
204 None,
205 0.0,
206 Rect {
207 x: 0.0,
208 y: 0.0,
209 width: 1.0,
210 height: 1.0,
211 },
212 [rotation[0], rotation[1], rotation[2], 0.0],
213 [0.0, 0.0],
214 );
215 }
216
217 fn bifrost(&mut self, rect: Rect, blur: f32, _saturation: f32, opacity: f32) {
218 let logical_w = self.current_width() as f32 / self.current_scale_factor();
220 let logical_h = self.current_height() as f32 / self.current_scale_factor();
221 let screen_uv = Rect {
222 x: rect.x / logical_w,
223 y: rect.y / logical_h,
224 width: rect.width / logical_w,
225 height: rect.height / logical_h,
226 };
227 self.fill_rect_with_full_params(rect, [1.0, 1.0, 1.0, opacity], 7, None, blur, screen_uv);
230 }
231
232 fn gungnir(&mut self, rect: Rect, color: [f32; 4], radius: f32, intensity: f32) {
233 let margin = radius;
235 let glow_rect = Rect {
236 x: rect.x - margin,
237 y: rect.y - margin,
238 width: rect.width + 2.0 * margin,
239 height: rect.height + 2.0 * margin,
240 };
241 let glow_color = [color[0], color[1], color[2], intensity * 0.3];
242 self.fill_rect_with_full_params(
243 glow_rect,
244 self.apply_opacity(glow_color),
245 material_id::DROP_SHADOW,
246 None,
247 8.0,
248 Rect {
249 x: margin,
250 y: radius,
251 width: 0.0,
252 height: 0.0,
253 },
254 );
255 }
256
257 fn gungnir_soft(&mut self, rect: Rect, color: [f32; 4], radius: f32, intensity: f32) {
260 self.gungnir(rect, color, radius, intensity * 0.5);
261 }
262
263 fn mani_glow(&mut self, rect: Rect, color: [f32; 4], radius: f32) {
270 let margin = radius;
271 let glow_rect = Rect {
272 x: rect.x - margin,
273 y: rect.y - margin,
274 width: rect.width + 2.0 * margin,
275 height: rect.height + 2.0 * margin,
276 };
277 let uv_rect = Rect {
278 x: margin,
279 y: radius,
280 width: 0.0,
281 height: 0.0,
282 };
283 self.fill_rect_with_full_params(
284 glow_rect,
285 self.apply_opacity(color),
286 material_id::DROP_SHADOW,
287 None,
288 8.0,
289 uv_rect,
290 );
291 }
292
293 fn stroke_rect(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
294 let c = self.apply_opacity(color);
295 self.fill_rect_with_full_params(
297 rect,
298 c,
299 material_id::SQUIRCLE_STROKE,
300 None,
301 0.0, Rect {
303 x: stroke_width,
304 y: 0.0,
305 width: 0.0,
306 height: 0.0,
307 },
308 );
309 }
310
311 fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4], stroke_width: f32) {
312 self.fill_rect_with_full_params(
313 rect,
314 self.apply_opacity(color),
315 material_id::SQUIRCLE_STROKE,
316 None,
317 radius,
318 Rect {
319 x: stroke_width,
320 y: 0.0,
321 width: 0.0,
322 height: 0.0,
323 },
324 );
325 }
326
327 fn stroke_ellipse(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
328 let cx = rect.x + rect.width / 2.0;
330 let cy = rect.y + rect.height / 2.0;
331 let rx = rect.width / 2.0;
332 let ry = rect.height / 2.0;
333
334 let mut builder = lyon::path::Path::builder();
336 if rx > 0.0 && ry > 0.0 {
337 let segments = 64;
339 for i in 0..segments {
340 let angle = 2.0 * std::f32::consts::PI * (i as f32) / (segments as f32);
341 let x = cx + rx * angle.cos();
342 let y = cy + ry * angle.sin();
343 if i == 0 {
344 builder.begin(lyon::math::point(x, y));
345 } else {
346 builder.line_to(lyon::math::point(x, y));
347 }
348 }
349 builder.close();
350 }
351 let path = builder.build();
352 self.stroke_path(&path, color, stroke_width);
353 }
354
355 fn draw_linear_gradient(
356 &mut self,
357 rect: Rect,
358 start_color: [f32; 4],
359 end_color: [f32; 4],
360 angle: f32,
361 ) {
362 self.fill_rect_with_full_params_and_slice(
363 rect,
364 self.apply_opacity(start_color),
365 15,
366 None,
367 0.0,
368 Rect {
369 x: angle,
370 y: 0.0,
371 width: 1.0,
372 height: 1.0,
373 },
374 end_color,
375 [0.0, 0.0],
376 );
377 }
378
379 fn draw_radial_gradient(&mut self, rect: Rect, inner_color: [f32; 4], outer_color: [f32; 4]) {
380 self.fill_rect_with_full_params_and_slice(
381 rect,
382 self.apply_opacity(inner_color),
383 material_id::RADIAL_GRADIENT,
384 None,
385 0.0,
386 Rect {
387 x: 0.0,
388 y: 0.0,
389 width: 1.0,
390 height: 1.0,
391 },
392 outer_color,
393 [0.0, 0.0],
394 );
395 }
396
397 fn draw_drop_shadow(
398 &mut self,
399 rect: Rect,
400 radius: f32,
401 color: [f32; 4],
402 blur: f32,
403 spread: f32,
404 ) {
405 let margin = blur + spread;
406 let inflated = Rect {
407 x: rect.x - margin,
408 y: rect.y - margin,
409 width: rect.width + margin * 2.0,
410 height: rect.height + margin * 2.0,
411 };
412 self.fill_rect_with_full_params(
414 inflated,
415 self.apply_opacity(color),
416 material_id::DROP_SHADOW,
417 None,
418 radius,
419 Rect {
420 x: margin,
421 y: blur,
422 width: 0.0,
423 height: 0.0,
424 },
425 );
426 }
427
428 fn stroke_dashed_rounded_rect(
429 &mut self,
430 rect: Rect,
431 radius: f32,
432 color: [f32; 4],
433 width: f32,
434 dash: f32,
435 gap: f32,
436 ) {
437 self.fill_rect_with_full_params(
438 rect,
439 self.apply_opacity(color),
440 material_id::DASHED_STROKE,
441 None,
442 radius,
443 Rect {
444 x: width,
445 y: dash,
446 width: gap,
447 height: 0.0,
448 },
449 );
450 }
451
452 fn draw_9slice(
453 &mut self,
454 image_name: &str,
455 rect: Rect,
456 left: f32,
457 top: f32,
458 right: f32,
459 bottom: f32,
460 ) {
461 let c = self.apply_opacity([1.0, 1.0, 1.0, 1.0]);
462 let tid = self.get_texture_id(image_name);
463 self.fill_rect_with_full_params(
464 rect,
465 c,
466 20,
467 tid,
468 bottom,
469 Rect {
470 x: left,
471 y: top,
472 width: right,
473 height: 0.0,
474 },
475 );
476 }
477
478 fn draw_line(
479 &mut self,
480 x1: f32,
481 y1: f32,
482 x2: f32,
483 y2: f32,
484 color: [f32; 4],
485 stroke_width: f32,
486 ) {
487 let dx = x2 - x1;
488 let dy = y2 - y1;
489 let len_sq = dx * dx + dy * dy;
490 if len_sq < 0.000001 {
491 return;
492 }
493 let len = len_sq.sqrt();
494 let half_w = stroke_width * 0.5;
495 let nx = -dy / len * half_w;
497 let ny = dx / len * half_w;
498 let points = [
500 [x1 + nx, y1 + ny],
501 [x2 + nx, y2 + ny],
502 [x2 - nx, y2 - ny],
503 [x1 - nx, y1 - ny],
504 ];
505 self.push_oriented_quad(points, color, 1, Rect { x: 0.0, y: 0.0, width: 1.0, height: 1.0 });
506 }
507
508 fn draw_image(&mut self, image_name: &str, rect: Rect) {
509 if !self.image_uv_registry.contains(image_name) {
511 log::warn!("[Surtr] draw_image: '{}' not loaded, skipping", image_name);
512 return;
513 }
514 let tid = self
515 .get_texture_id(image_name)
516 .or_else(|| self.get_texture_id("__mega_heim"));
517 let uv_rect = self
518 .image_uv_registry
519 .get(image_name)
520 .copied()
521 .unwrap_or(Rect {
522 x: 0.0,
523 y: 0.0,
524 width: 1.0,
525 height: 1.0,
526 });
527 self.fill_rect_with_full_params(rect, [1.0, 1.0, 1.0, 1.0], 2, tid, 0.0, uv_rect);
528 }
529
530
531
532 fn shape_rich_text(
533 &mut self,
534 spans: &[cvkg_runic_text::TextSpan],
535 max_width: Option<f32>,
536 align: cvkg_runic_text::TextAlign,
537 overflow: cvkg_runic_text::TextOverflow,
538 ) -> Option<cvkg_runic_text::ShapedText> {
539 let sf = self.current_scale_factor();
540 let mut scaled_spans = spans.to_vec();
541 for span in &mut scaled_spans {
542 span.style.font_size *= sf;
543 if span.style.fallback_families.is_empty() {
544 span.style.fallback_families = vec![
545 "SF Pro".to_string(),
546 "Inter".to_string(),
547 "Helvetica Neue".to_string(),
548 "Helvetica".to_string(),
549 "Arial".to_string(),
550 "sans-serif".to_string(),
551 ];
552 }
553 }
554 let scaled_max_width = max_width.map(|w| w * sf);
555 self.text.engine
556 .shape_layout(&scaled_spans, scaled_max_width, align, overflow)
557 .ok()
558 }
559
560 fn draw_shaped_text(&mut self, shaped: &cvkg_runic_text::ShapedText, x: f32, y: f32) {
561 for glyph in &shaped.glyphs {
562 let byte_idx = shaped
563 .grapheme_boundaries
564 .get(glyph.cluster as usize)
565 .copied()
566 .unwrap_or(0);
567 let mut span_color = [1.0, 1.0, 1.0, 1.0];
568 for span in &shaped.spans {
569 if byte_idx >= span.byte_offset && byte_idx < span.byte_offset + span.text.len() {
570 span_color = [
571 span.style.color[0] as f32 / 255.0,
572 span.style.color[1] as f32 / 255.0,
573 span.style.color[2] as f32 / 255.0,
574 span.style.color[3] as f32 / 255.0,
575 ];
576 break;
577 }
578 }
579 let c = self.apply_opacity(span_color);
580
581 let cache_key = glyph.cache_key;
582 let (uv_rect, w, h, x_off, y_off) = if let Some(info) = self.text.glyph_cache.get(&cache_key)
583 {
584 *info
585 } else {
586 if let Some(image) = self.text.engine.rasterize(cache_key) {
587 let glyph_id = image.glyph_id;
588 let data_len = image.data.len();
589 let gw = image.width;
590 let gh = image.height;
591 let x_offset = image.x_offset;
592 let y_offset = image.y_offset;
593 let (rgba_data, gw, gh) = glyph_image_to_rgba(image);
594 if gw == 0 || gh == 0 {
595 let info = (Rect::zero(), 0.0, 0.0, 0.0, 0.0);
596 self.text.glyph_cache.put(cache_key, info);
597 continue;
598 }
599 if rgba_data.is_empty() {
600 log::warn!(
601 "Glyph rasterizer returned unsupported pixel format for glyph {} ({} bytes, {}x{}), skipping",
602 glyph_id,
603 data_len,
604 gw,
605 gh
606 );
607 continue;
608 }
609
610 let pack_res = self.heim_packer.pack(gw, gh);
611 let (nx, ny) = if let Some(pos) = pack_res {
612 pos
613 } else {
614 self.reclaim_vram();
615 match self.heim_packer.pack(gw, gh) {
616 Some(pos) => pos,
617 None => {
618 log::error!(
619 "Glyph heim critically full after reclaim: cannot pack {}x{} glyph, skipping",
620 gw,
621 gh
622 );
623 continue; }
625 }
626 };
627
628 self.queue.write_texture(
629 wgpu::TexelCopyTextureInfo {
630 texture: &self.mega_heim_tex,
631 mip_level: 0,
632 origin: wgpu::Origin3d { x: nx, y: ny, z: 0 },
633 aspect: wgpu::TextureAspect::All,
634 },
635 &rgba_data,
636 wgpu::TexelCopyBufferLayout {
637 offset: 0,
638 bytes_per_row: Some(gw * 4),
639 rows_per_image: Some(gh),
640 },
641 wgpu::Extent3d {
642 width: gw,
643 height: gh,
644 depth_or_array_layers: 1,
645 },
646 );
647
648 let tex_w = self.mega_heim_tex.width() as f32;
649 let tex_h = self.mega_heim_tex.height() as f32;
650 let info = (
651 Rect {
652 x: nx as f32 / tex_w,
653 y: ny as f32 / tex_h,
654 width: gw as f32 / tex_w,
655 height: gh as f32 / tex_h,
656 },
657 gw as f32,
658 gh as f32,
659 x_offset,
660 y_offset,
661 );
662 self.text.glyph_cache.put(cache_key, info);
663 info
664 } else {
665 (Rect::zero(), 0.0, 0.0, 0.0, 0.0)
666 }
667 };
668
669 if w > 0.0 {
670 let sf = self.current_scale_factor();
671 let baseline_y = y + shaped.ascent / sf;
675 let glyph_rect = Rect {
676 x: x + (glyph.x + x_off) / sf,
677 y: baseline_y + (glyph.y - y_off) / sf,
678 width: w / sf,
679 height: h / sf,
680 };
681 let tid = self.get_texture_id("__mega_heim");
682 let slice = self
683 .slice_stack
684 .last()
685 .copied()
686 .map(|(a, o)| [a, o, 1.0, 1.0])
687 .unwrap_or([0.0, 0.0, 0.0, 1.0]);
688 self.fill_rect_with_full_params_and_slice(
689 glyph_rect,
690 c,
691 6,
692 tid,
693 0.0,
694 uv_rect,
695 slice,
696 [glyph.glyph_index as f32, glyph.time_offset],
697 );
698 }
699 }
700 }
701
702 fn draw_texture(&mut self, texture_id: u32, rect: Rect) {
703 self.fill_rect_with_full_params_and_slice(
704 rect,
705 [1.0, 1.0, 1.0, 1.0],
706 2,
707 Some(texture_id),
708 0.0,
709 Rect {
710 x: 0.0,
711 y: 0.0,
712 width: 1.0,
713 height: 1.0,
714 },
715 [0.0, 0.0, 0.0, 1.0],
716 [0.0, 0.0],
717 );
718 }
719
720 fn load_image(&mut self, name: &str, data: &[u8]) {
723 if self.image_uv_registry.contains(name) {
724 return;
725 }
726 let img_result = image::load_from_memory(data);
727 let img = match img_result {
728 Ok(img) => img.to_rgba8(),
729 Err(e) => {
730 log::error!("Failed to load image {}: {}", name, e);
731 image::RgbaImage::from_pixel(1, 1, image::Rgba([255, 255, 255, 255]))
732 }
733 };
734 let (width, height) = img.dimensions();
735
736 let size = wgpu::Extent3d {
737 width,
738 height,
739 depth_or_array_layers: 1,
740 };
741 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
742 label: Some(&format!("Texture Array Layer: {}", name)),
743 size,
744 mip_level_count: 1,
745 sample_count: 1,
746 dimension: wgpu::TextureDimension::D2,
747 format: wgpu::TextureFormat::Rgba8UnormSrgb,
748 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
749 view_formats: &[],
750 });
751
752 self.queue.write_texture(
753 wgpu::TexelCopyTextureInfo {
754 texture: &texture,
755 mip_level: 0,
756 origin: wgpu::Origin3d::ZERO,
757 aspect: wgpu::TextureAspect::All,
758 },
759 &img,
760 wgpu::TexelCopyBufferLayout {
761 offset: 0,
762 bytes_per_row: Some(4 * width),
763 rows_per_image: Some(height),
764 },
765 size,
766 );
767
768 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
769
770 let index = if self.texture_registry.len() < 31 {
773 (self.texture_registry.len() + 1) as u32
774 } else {
775 if let Some((old_name, old_index)) = self.texture_registry.pop_lru() {
778 self.image_uv_registry.pop(&old_name);
779 old_index
780 } else {
781 log::warn!("[GPU] texture registry full and no LRU entry to evict");
782 return;
783 }
784 };
785
786 if index == 0 || index as usize >= self.texture_views.len() {
788 log::error!("[GPU] load_image: invalid texture index {} (registry has {} entries)", index, self.texture_registry.len());
789 return;
790 }
791
792 self.texture_views[index as usize] = view;
793 self.image_uv_registry.put(
794 name.to_string(),
795 Rect {
796 x: 0.0,
797 y: 0.0,
798 width: 1.0,
799 height: 1.0,
800 },
801 );
802 self.texture_registry.put(name.to_string(), index);
803 self.rebuild_texture_array_bind_group();
804 }
805
806 fn push_clip_rect(&mut self, rect: Rect) {
807 self.clip_stack.push(rect);
808 }
809
810 fn pop_clip_rect(&mut self) {
811 self.clip_stack.pop();
812 }
813
814 fn current_clip_rect(&self) -> Rect {
815 self.clip_stack.last().copied().unwrap_or(Rect::new(
816 0.0,
817 0.0,
818 self.current_width() as f32,
819 self.current_height() as f32,
820 ))
821 }
822
823 fn memoize(&mut self, id: u64, data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer)) {
824 use crate::types::{DrawCall, MemoEntry};
837
838 let should_skip = self
839 .memo_cache
840 .get(&id)
841 .map_or(false, |entry| entry.hash == data_hash);
842
843 if should_skip {
844 if let Some(entry) = self.memo_cache.get(&id) {
846 let i_offset = self.indices.len() as u32;
847 let inst_offset = self.instance_data.len() as u32;
848
849 self.vertices.extend_from_slice(&entry.vertices);
850 self.indices.extend_from_slice(&entry.indices);
851 self.instance_data
852 .extend_from_slice(&entry.instance_data);
853
854 for dc in &entry.draw_calls {
855 let mut replayed = dc.clone();
856 replayed.index_start += i_offset;
860 replayed.instance_start += inst_offset;
861 self.draw_calls.push(replayed);
862 }
863 }
864 } else {
865 let v_start = self.vertices.len();
867 let i_start = self.indices.len();
868 let inst_start = self.instance_data.len();
869 let dc_start = self.draw_calls.len();
870
871 render_fn(self);
872
873 let draw_calls: Vec<DrawCall> = self.draw_calls[dc_start..]
875 .iter()
876 .map(|dc| {
877 let mut remapped = dc.clone();
878 remapped.index_start = remapped
882 .index_start
883 .saturating_sub(i_start as u32);
884 remapped.instance_start = remapped
885 .instance_start
886 .saturating_sub(inst_start as u32);
887 remapped
888 })
889 .collect();
890
891 let entry = MemoEntry {
892 hash: data_hash,
893 frame_gen: self.frame_generation,
894 vertices: self.vertices[v_start..].to_vec(),
895 indices: self.indices[i_start..].to_vec(),
896 instance_data: self.instance_data[inst_start..].to_vec(),
897 draw_calls,
898 };
899
900 self.memo_cache.insert(id, entry);
901 }
902 }
903
904 fn snapshot_render_state(&self) -> RenderStateSnapshot {
905 RenderStateSnapshot {
906 clip_depth: self.clip_stack.len() as u32,
907 opacity_depth: self.opacity_stack.len() as u32,
908 slice_depth: self.slice_stack.len() as u32,
909 shadow_depth: self.shadow_stack.len() as u32,
910 transform_depth: self.transform_stack.len() as u32,
911 vnode_depth: self.vnode_stack.len() as u32,
912 }
913 }
914
915 fn restore_render_state(&mut self, snap: RenderStateSnapshot) {
916 while self.clip_stack.len() as u32 > snap.clip_depth {
918 self.clip_stack.pop();
919 }
920 while self.opacity_stack.len() as u32 > snap.opacity_depth {
921 self.opacity_stack.pop();
922 }
923 while self.slice_stack.len() as u32 > snap.slice_depth {
924 self.slice_stack.pop();
925 }
926 while self.shadow_stack.len() as u32 > snap.shadow_depth {
927 self.shadow_stack.pop();
928 }
929 while self.transform_stack.len() as u32 > snap.transform_depth {
930 self.transform_stack.pop();
931 }
932 while self.vnode_stack.len() as u32 > snap.vnode_depth {
933 self.vnode_stack.pop();
934 }
935 }
936
937 fn push_opacity(&mut self, opacity: f32) {
938 let current = self.opacity_stack.last().copied().unwrap_or(1.0);
939 self.opacity_stack.push(current * opacity);
940 }
941
942 fn pop_opacity(&mut self) {
943 self.opacity_stack.pop();
944 }
945
946 fn push_shadow(&mut self, radius: f32, color: [f32; 4], offset: [f32; 2]) {
947 self.shadow_stack.push(ShadowState {
948 radius,
949 color,
950 _offset: offset,
951 });
952 }
953
954 fn pop_shadow(&mut self) {
955 self.shadow_stack.pop();
956 }
957
958 fn push_transform(&mut self, translation: [f32; 2], scale: [f32; 2], rotation: f32) {
959 let c = rotation.cos();
960 let sn = rotation.sin();
961 let affine = glam::Mat3::from_cols(
962 glam::Vec3::new(c * scale[0], sn * scale[0], 0.0),
963 glam::Vec3::new(-sn * scale[1], c * scale[1], 0.0),
964 glam::Vec3::new(translation[0], translation[1], 1.0),
965 );
966
967 let parent = self
968 .transform_stack
969 .last()
970 .copied()
971 .unwrap_or(glam::Mat3::IDENTITY);
972 self.transform_stack.push(parent * affine);
973 }
974
975 fn push_affine(&mut self, transform: [f32; 6]) {
976 let affine = glam::Mat3::from_cols(
977 glam::Vec3::new(transform[0], transform[1], 0.0),
978 glam::Vec3::new(transform[2], transform[3], 0.0),
979 glam::Vec3::new(transform[4], transform[5], 1.0),
980 );
981 let parent = self
982 .transform_stack
983 .last()
984 .copied()
985 .unwrap_or(glam::Mat3::IDENTITY);
986 self.transform_stack.push(parent * affine);
987 }
988
989 fn pop_transform(&mut self) {
990 self.transform_stack.pop();
991 }
992
993 fn set_theme(&mut self, theme: ColorTheme) {
994 self.current_theme = theme;
995 self.queue
996 .write_buffer(&self.theme_buffer, 0, bytemuck::bytes_of(&theme));
997 }
998
999 fn set_rage(&mut self, rage: f32) {
1000 self.current_scene.berzerker_rage = rage;
1001 }
1003
1004 fn set_fireball_pos(&mut self, pos: [f32; 2]) {
1005 self.current_scene.fireball_pos = pos;
1006 }
1007
1008 fn trigger_shatter_event(&mut self, origin: [f32; 2], force: f32) {
1009 self.current_scene.shatter_origin = origin;
1010 self.current_scene.shatter_time = self.current_scene.time;
1011 self.current_scene.shatter_force = force;
1012 }
1013
1014 fn set_scene_preset(&mut self, preset: u32) {
1015 self.current_scene.scene_type = preset;
1016 }
1017
1018 fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
1021 self.slice_stack.push((angle, offset));
1022 }
1023
1024 fn pop_mjolnir_slice(&mut self) {
1026 self.slice_stack.pop();
1027 }
1028
1029 fn mjolnir_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
1030 self.shatter_internal(rect, pieces, force, color, 8);
1031 }
1032
1033 fn mjolnir_fluid_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
1034 self.shatter_internal(rect, pieces, force, color, 11);
1035 }
1036
1037 fn draw_mjolnir_bolt(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
1038 self.recursive_bolt(from, to, 4, color);
1039 }
1040
1041 fn dispatch_particles(
1042 &mut self,
1043 origin: [f32; 2],
1044 count: u32,
1045 effect_type: &str,
1046 color: [f32; 4],
1047 ) {
1048 use crate::types::{GpuParticle, MAX_PARTICLES};
1049
1050 let dt = self.current_scene.delta_time;
1051 let now = std::time::Instant::now();
1052
1053 let (speed_range, life_range, spread_angle) = match effect_type {
1055 "firework" => (100.0..300.0, 1.0..2.5, std::f32::consts::TAU),
1056 "spark" => (50.0..150.0, 0.5..1.5, std::f32::consts::PI),
1057 "rain" => (20.0..80.0, 1.0..3.0, std::f32::consts::FRAC_PI_4),
1058 "data_stream" => (80.0..200.0, 0.8..2.0, std::f32::consts::FRAC_PI_6),
1059 "bubble" => (10.0..40.0, 2.0..4.0, std::f32::consts::TAU),
1060 _ => (30.0..120.0, 1.0..2.0, std::f32::consts::TAU),
1061 };
1062
1063 let count = count.min((MAX_PARTICLES - self.particles.count as usize) as u32);
1064 if count == 0 {
1065 return;
1066 }
1067
1068 let mut rng_state = (now.elapsed().as_nanos() as u64).wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1069 let mut rand_f32 = |range: std::ops::Range<f32>| -> f32 {
1070 rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1071 let t = (rng_state >> 33) as f32 / (1u64 << 31) as f32;
1072 range.start + t * (range.end - range.start)
1073 };
1074
1075 for _ in 0..count {
1076 let angle = rand_f32(0.0..spread_angle);
1077 let speed = rand_f32(speed_range.clone());
1078 let life = rand_f32(life_range.clone());
1079 let vx = angle.cos() * speed;
1080 let vy = angle.sin() * speed;
1081
1082 let particle = GpuParticle {
1083 pos_vel: [origin[0], origin[1], vx, vy],
1084 color_life: [color[0], color[1], color[2], life],
1085 };
1086 self.particles.staging.push(particle);
1087 }
1088
1089 log::debug!(
1090 "[Surtr] dispatch_particles: {} {} particles at {:?} (staged, {} total pending)",
1091 count,
1092 effect_type,
1093 origin,
1094 self.particles.staging.len()
1095 );
1096 }
1097
1098 fn draw_hologram(&mut self, rect: Rect, hologram_id: &str, time: f32) {
1099 use std::hash::{Hash, Hasher};
1100 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1101 hologram_id.hash(&mut hasher);
1102 let id_hash = hasher.finish() as u32;
1103
1104 log::debug!(
1105 "[Surtr] draw_hologram: {} at {:?} t={} (hologram pipeline)",
1106 hologram_id,
1107 rect,
1108 time
1109 );
1110
1111 self.hologram_instances.push(crate::renderer::HologramInstance {
1112 rect,
1113 id_hash,
1114 time,
1115 });
1116 self.volumetric_enabled = true;
1117 }
1118
1119 fn upload_data_texture(&mut self, id: &str, data: &[f32], width: u32, height: u32) {
1120 let size = wgpu::Extent3d {
1121 width,
1122 height,
1123 depth_or_array_layers: 1,
1124 };
1125 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
1126 label: Some(id),
1127 size,
1128 mip_level_count: 1,
1129 sample_count: 1,
1130 dimension: wgpu::TextureDimension::D2,
1131 format: wgpu::TextureFormat::R32Float,
1132 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1133 view_formats: &[],
1134 });
1135 self.queue.write_texture(
1136 wgpu::TexelCopyTextureInfo {
1137 texture: &texture,
1138 mip_level: 0,
1139 origin: wgpu::Origin3d::ZERO,
1140 aspect: wgpu::TextureAspect::All,
1141 },
1142 bytemuck::cast_slice(data),
1143 wgpu::TexelCopyBufferLayout {
1144 offset: 0,
1145 bytes_per_row: Some(4 * width),
1146 rows_per_image: Some(height),
1147 },
1148 size,
1149 );
1150 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1151 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1154 layout: &self.texture_bind_group_layout,
1155 entries: &[
1156 wgpu::BindGroupEntry {
1157 binding: 0,
1158 resource: wgpu::BindingResource::TextureViewArray(&vec![&view; 32]),
1160 },
1161 wgpu::BindGroupEntry {
1162 binding: 1,
1163 resource: wgpu::BindingResource::Sampler(&self.linear_sampler),
1164 },
1165 ],
1166 label: Some(id),
1167 });
1168 self.texture_bind_groups.push(bind_group);
1169 let tid = (self.texture_bind_groups.len() - 1) as u32;
1170 self.texture_registry.put(id.to_string(), tid);
1171 }
1172
1173 fn draw_heatmap(&mut self, texture_id: &str, rect: Rect, _palette: &str) {
1174 let tid = self.get_texture_id(texture_id);
1175 self.fill_rect_with_mode(rect, [1.0, 1.0, 1.0, 1.0], 12, tid);
1176 }
1177
1178 fn draw_mesh(&mut self, mesh: &Mesh, color: [f32; 4], transform: glam::Mat4) {
1179 let base_idx = self.vertices.len() as u32;
1180
1181 for i in 0..mesh.vertices.len() {
1182 let pos = transform.transform_point3(glam::Vec3::from(mesh.vertices[i]));
1183 let norm = transform.transform_vector3(glam::Vec3::from(mesh.normals[i]));
1184
1185 self.vertices.push(Vertex {
1186 position: pos.to_array(),
1187 normal: norm.to_array(),
1188 uv: [0.0, 0.0],
1189 color,
1190 material_id: 13, radius: 0.0,
1192 slice: [0.0, 0.0, 0.0, 1.0],
1193 logical: [0.0, 0.0],
1194 size: [0.0, 0.0],
1195 clip: [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY],
1196 tex_index: 0,
1197 });
1198 }
1199
1200 for idx in &mesh.indices {
1201 self.indices.push(base_idx + idx);
1202 }
1203
1204 let (translation, scale_transform, rotation, _, _) = self.current_transform();
1205
1206 if self.draw_calls.is_empty() || self.current_texture_id.is_some() {
1207 self.current_texture_id = None;
1208
1209 self.instance_data.push(InstanceData {
1210 translation,
1211 scale: scale_transform,
1212 rotation,
1213 blur_radius: 0.0,
1214 ior_override: 0.0,
1215 glass_intensity: 1.0,
1216 });
1217 self.draw_calls.push(DrawCall {
1218 target_id: None,
1219 texture_id: None,
1220 scissor_rect: self.clip_stack.last().copied(),
1221 index_start: (self.indices.len() as u32) - (mesh.indices.len() as u32),
1222 index_count: mesh.indices.len() as u32,
1223 instance_count: 1,
1224 material: cvkg_core::DrawMaterial::Opaque,
1225 instance_start: (self.instance_data.len() - 1) as u32,
1226 draw_order: 0,
1227 });
1228 } else {
1229 self.draw_calls.last_mut().unwrap().index_count += mesh.indices.len() as u32;
1230 }
1231 }
1232
1233 fn draw_mesh_3d(
1234 &mut self,
1235 mesh: &Mesh,
1236 material: &cvkg_core::Material3D,
1237 transform: &cvkg_core::Transform3D,
1238 ) {
1239 let base_idx = self.vertices.len() as u32;
1240 let model_matrix = transform.to_matrix();
1241
1242 for i in 0..mesh.vertices.len() {
1243 let pos = model_matrix.transform_point3(glam::Vec3::from(mesh.vertices[i]));
1244 let norm = model_matrix.transform_vector3(glam::Vec3::from(mesh.normals[i]));
1245
1246 self.vertices.push(Vertex {
1247 position: [pos.x, pos.y, pos.z],
1248 normal: [norm.x, norm.y, norm.z],
1249 uv: [0.0, 0.0],
1250 color: material.base_color,
1251 material_id: 13, radius: 0.0,
1253 slice: [material.metallic, material.roughness, material.opacity, 1.0],
1254 logical: [0.0, 0.0],
1255 size: [0.0, 0.0],
1256 clip: [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY],
1257 tex_index: 0,
1258 });
1259 }
1260
1261 for idx in &mesh.indices {
1262 self.indices.push(base_idx + idx);
1263 }
1264
1265 self.instance_data.push(InstanceData {
1266 translation: [0.0, 0.0],
1267 scale: [1.0, 1.0],
1268 rotation: 0.0,
1269 blur_radius: 0.0,
1270 ior_override: 0.0,
1271 glass_intensity: 1.0,
1272 });
1273
1274 self.draw_calls.push(DrawCall {
1275 target_id: None,
1276 texture_id: None,
1277 scissor_rect: self.clip_stack.last().copied(),
1278 index_start: (self.indices.len() as u32) - (mesh.indices.len() as u32),
1279 index_count: mesh.indices.len() as u32,
1280 instance_count: 1,
1281 material: cvkg_core::DrawMaterial::Opaque,
1282 instance_start: (self.instance_data.len() - 1) as u32,
1283 draw_order: 0,
1284 });
1285 }
1286
1287 fn set_camera_3d(&mut self, camera: &cvkg_core::Camera3D) {
1288 self.current_scene.proj = camera.projection_matrix();
1289 self.current_scene.view = camera.view_matrix();
1290 }
1291
1292 fn push_transform_3d(&mut self, transform: &cvkg_core::Transform3D) {
1293 let (translation, rotation_quat, scale_glam) =
1296 transform.to_matrix().to_scale_rotation_translation();
1297 let translation = [translation.x, translation.y];
1298 let scale = [scale_glam.x, scale_glam.y];
1299 let rotation = if rotation_quat.length_squared() > 0.0 {
1300 let (axis, angle) = rotation_quat.to_axis_angle();
1301 angle * axis.z.signum() } else {
1303 0.0
1304 };
1305 self.push_transform(translation, scale, rotation);
1306 }
1307
1308 fn pop_transform_3d(&mut self) {
1309 self.pop_transform();
1311 }
1312
1313 fn render_scene_node_3d(
1319 &mut self,
1320 position: [f32; 3],
1321 rotation: [f32; 4],
1322 scale: [f32; 3],
1323 color: [f32; 4],
1324 meshes: &[Mesh],
1325 ) {
1326 let transform = cvkg_core::Transform3D {
1327 position: glam::Vec3::from(position),
1328 rotation: glam::Quat::from_xyzw(rotation[0], rotation[1], rotation[2], rotation[3]),
1329 scale: glam::Vec3::from(scale),
1330 };
1331 if meshes.is_empty() {
1333 let h = 0.5f32;
1335 let cube = Mesh {
1336 vertices: vec![
1337 [-h, -h, -h],
1338 [h, -h, -h],
1339 [h, h, -h],
1340 [-h, h, -h],
1341 [-h, -h, h],
1342 [h, -h, h],
1343 [h, h, h],
1344 [-h, h, h],
1345 ],
1346 normals: vec![
1347 [0.0, 0.0, -1.0],
1348 [0.0, 0.0, -1.0],
1349 [0.0, 0.0, -1.0],
1350 [0.0, 0.0, -1.0],
1351 [0.0, 0.0, 1.0],
1352 [0.0, 0.0, 1.0],
1353 [0.0, 0.0, 1.0],
1354 [0.0, 0.0, 1.0],
1355 [0.0, -1.0, 0.0],
1356 [0.0, -1.0, 0.0],
1357 [0.0, -1.0, 0.0],
1358 [0.0, -1.0, 0.0],
1359 [1.0, 0.0, 0.0],
1360 [1.0, 0.0, 0.0],
1361 [1.0, 0.0, 0.0],
1362 [1.0, 0.0, 0.0],
1363 [0.0, 1.0, 0.0],
1364 [0.0, 1.0, 0.0],
1365 [0.0, 1.0, 0.0],
1366 [0.0, 1.0, 0.0],
1367 [-1.0, 0.0, 0.0],
1368 [-1.0, 0.0, 0.0],
1369 [-1.0, 0.0, 0.0],
1370 [-1.0, 0.0, 0.0],
1371 ],
1372 indices: vec![
1373 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, ],
1380 };
1381 let material = cvkg_core::Material3D {
1382 base_color: color,
1383 metallic: 0.0,
1384 roughness: 0.5,
1385 emissive: [0.0, 0.0, 0.0],
1386 opacity: color[3],
1387 };
1388 self.draw_mesh_3d(&cube, &material, &transform);
1389 } else {
1390 let material = cvkg_core::Material3D {
1391 base_color: color,
1392 metallic: 0.0,
1393 roughness: 0.5,
1394 emissive: [0.0, 0.0, 0.0],
1395 opacity: color[3],
1396 };
1397 self.draw_mesh_3d(&meshes[0], &material, &transform);
1398 }
1399 }
1400
1401 fn register_shared_element(&mut self, id: &str, rect: Rect) {
1402 self.shared_elements.put(id.to_string(), rect);
1403 }
1404
1405 fn set_z_index(&mut self, z: f32) {
1406 self.current_z = z;
1407 }
1408
1409 fn set_material(&mut self, material: cvkg_core::DrawMaterial) {
1410 self.current_draw_material = material;
1411 }
1412
1413 fn current_material(&self) -> cvkg_core::DrawMaterial {
1414 self.current_draw_material
1415 }
1416
1417 fn get_z_index(&self) -> f32 {
1418 self.current_z
1419 }
1420
1421 fn request_redraw(&mut self) {
1422 self.redraw_requested = true;
1423 }
1424
1425 fn enter_portal(&mut self, z_index: i32) {
1437 self.current_z = z_index as f32;
1441 }
1442
1443 fn exit_portal(&mut self) {
1447 self.current_z = 0.0;
1448 }
1449
1450 fn push_vnode(&mut self, rect: Rect, name: &'static str) {
1451 self.vnode_stack.push((rect, name));
1452 }
1453
1454 fn pop_vnode(&mut self) {
1455 self.vnode_stack.pop();
1456 }
1457
1458 fn register_handler(
1459 &mut self,
1460 event_type: &str,
1461 handler: std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>,
1462 ) {
1463 self.event_handlers
1464 .entry(event_type.to_string())
1465 .or_insert_with(Vec::new)
1466 .push(handler);
1467 }
1468
1469 fn load_svg(&mut self, name: &str, svg_data: &[u8]) {
1470 SurtrRenderer::load_svg(self, name, svg_data);
1471 }
1472
1473 fn draw_svg(&mut self, name: &str, rect: Rect) {
1474 SurtrRenderer::draw_svg(self, name, rect, None, 0);
1475 }
1476 fn draw_svg_with_offset(&mut self, name: &str, rect: Rect, animation_time_offset: f32) {
1477 SurtrRenderer::draw_svg_with_offset(self, name, rect, None, 0, animation_time_offset);
1478 }
1479
1480 fn draw_svg_with_order(&mut self, name: &str, rect: Rect, draw_order: i32) {
1483 SurtrRenderer::draw_svg_with_order(self, name, rect, None, 0, 0.0, draw_order);
1484 }
1485
1486 fn serialize_svg(&mut self, name: &str) -> Result<String, String> {
1487 let tree = self
1488 .svg
1489 .tree_cache
1490 .get(name)
1491 .ok_or_else(|| format!("SVG '{}' not found", name))?;
1492 let config = cvkg_svg_serialize::SerializerConfig::default();
1493 let mut serializer = cvkg_svg_serialize::SvgSerializer::with_config(config);
1494 serializer
1495 .serialize(tree)
1496 .map_err(|e| format!("SVG serialization failed: {}", e))
1497 }
1498
1499 fn apply_svg_filter(
1500 &mut self,
1501 name: &str,
1502 filter_id: &str,
1503 _region: Rect,
1504 ) -> Result<String, String> {
1505 let tree = self
1506 .svg
1507 .tree_cache
1508 .get(name)
1509 .ok_or_else(|| format!("SVG '{}' not found", name))?;
1510 let _filter = Self::find_filter(tree, filter_id)
1511 .ok_or_else(|| format!("Filter '{}' not found in SVG '{}'", filter_id, name))?;
1512 let config = cvkg_svg_serialize::SerializerConfig::default();
1513 let mut serializer = cvkg_svg_serialize::SvgSerializer::with_config(config);
1514 serializer
1515 .serialize(tree)
1516 .map_err(|e| format!("SVG filter serialization failed: {}", e))
1517 }
1518
1519 fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
1522 let cache_key = (text.to_string(), (size * 100.0) as u32);
1523 if let Some(shaped) = self.text.shaped_cache.get(&cache_key) {
1524 return (shaped.width, shaped.height);
1525 }
1526 let style = cvkg_runic_text::TextStyle::new("Inter", size);
1528 let spans = [cvkg_runic_text::TextSpan::new(text, style)];
1529 if let Some(shaped) = self.shape_rich_text(
1530 &spans,
1531 None,
1532 cvkg_runic_text::TextAlign::Start,
1533 cvkg_runic_text::TextOverflow::Visible,
1534 ) {
1535 let shaped = std::sync::Arc::new(shaped);
1536 let result = (shaped.width, shaped.height);
1537 self.text.shaped_cache.put(cache_key, shaped);
1538 result
1539 } else {
1540 (0.0, 0.0)
1541 }
1542 }
1543
1544 fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
1549 let cache_key = (text.to_string(), (size * 100.0) as u32);
1550 let r = (color[0] * 255.0).clamp(0.0, 255.0) as u8;
1551 let g = (color[1] * 255.0).clamp(0.0, 255.0) as u8;
1552 let b = (color[2] * 255.0).clamp(0.0, 255.0) as u8;
1553 let a = (color[3] * 255.0).clamp(0.0, 255.0) as u8;
1554 let cached = self.text.shaped_cache.get(&cache_key).cloned();
1556 if let Some(shaped) = cached {
1557 let color_matches = shaped.spans.first()
1559 .map(|s| s.style.color == [r, g, b, a])
1560 .unwrap_or(false);
1561 if color_matches {
1562 self.draw_shaped_text(&shaped, x, y);
1563 return;
1564 }
1565 let mut shaped = (*shaped).clone();
1568 for span in &mut shaped.spans {
1569 span.style.color = [r, g, b, a];
1570 }
1571 self.draw_shaped_text(&shaped, x, y);
1572 return;
1573 }
1574 let mut style = cvkg_runic_text::TextStyle::new("Inter", size);
1576 style.color = [r, g, b, a];
1577 let spans = [cvkg_runic_text::TextSpan::new(text, style)];
1578 if let Some(shaped) = self.shape_rich_text(
1579 &spans,
1580 None,
1581 cvkg_runic_text::TextAlign::Start,
1582 cvkg_runic_text::TextOverflow::Visible,
1583 ) {
1584 let shaped = std::sync::Arc::new(shaped);
1585 self.draw_shaped_text(&shaped, x, y);
1586 self.text.shaped_cache.put(cache_key, shaped);
1587 }
1588 }
1589}
1590
1591impl SurtrRenderer {
1594 pub fn clear_event_handlers(&mut self) {
1597 self.event_handlers.clear();
1598 }
1599
1600 pub fn clear_text_cache(&mut self) {
1602 self.text.shaped_cache.clear();
1603 }
1604
1605 pub fn get_handlers(
1607 &self,
1608 event_type: &str,
1609 ) -> Option<&Vec<std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>>> {
1610 self.event_handlers.get(event_type)
1611 }
1612
1613 pub(crate) fn current_transform(&self) -> ([f32; 2], [f32; 2], f32, f32, f32) {
1617 let m = self
1620 .transform_stack
1621 .last()
1622 .copied()
1623 .unwrap_or(glam::Mat3::IDENTITY);
1624 let t = [m.z_axis.x, m.z_axis.y];
1625 let a = m.x_axis.x;
1627 let b = m.x_axis.y;
1628 let c = m.y_axis.x;
1629 let d = m.y_axis.y;
1630 let sx = (a * a + b * b).sqrt();
1631 let sy = (c * c + d * d).sqrt();
1632 let rotation = b.atan2(a);
1633 let skew_x = (a * c + b * d) / (sx * sy); (t, [sx, sy], rotation, skew_x, 0.0)
1636 }
1637
1638 pub fn stroke_path(&mut self, path: &lyon::path::Path, color: [f32; 4], stroke_width: f32) {
1639 let c = self.apply_opacity(color);
1640 let base_vertex_idx = self.vertices.len() as u32;
1641 let base_index_idx = self.indices.len() as u32;
1642 let path_hash = {
1647 let mut h = std::collections::hash_map::DefaultHasher::new();
1648 let num_elements = path.iter().count();
1650 std::hash::Hash::hash(&num_elements, &mut h);
1651 std::hash::Hash::hash(&stroke_width.to_bits(), &mut h);
1653 h.finish()
1654 };
1655
1656 let (vert_count, idx_count) = match self.path_geometry_cache.get(&path_hash) {
1658 Some((cached_verts, cached_indices)) => {
1659 self.vertices.extend_from_slice(cached_verts);
1661 for idx in cached_indices {
1662 self.indices.push(base_vertex_idx + *idx);
1663 }
1664 (cached_verts.len(), cached_indices.len())
1665 }
1666 None => {
1667 let mut tessellator = StrokeTessellator::new();
1669 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
1670 let result = tessellator.tessellate_path(
1671 path,
1672 &StrokeOptions::default().with_line_width(stroke_width),
1673 &mut BuffersBuilder::new(
1674 &mut buffers,
1675 CustomStrokeVertexConstructor {
1676 color: c,
1677 clip: [0.0, 0.0, 0.0, 0.0],
1678 path_length: 1.0,
1679 },
1680 ),
1681 );
1682 if let Err(e) = result {
1683 log::warn!("Failed to tessellate stroke path: {:?}", e);
1684 return;
1685 }
1686 let vert_count = buffers.vertices.len();
1687 let idx_count = buffers.indices.len();
1688 let cached_verts = buffers.vertices.clone();
1690 let cached_indices = buffers.indices.clone();
1691 self.path_geometry_cache
1692 .put(path_hash, (cached_verts, cached_indices));
1693 self.vertices.extend(buffers.vertices);
1695 for idx in &buffers.indices {
1696 self.indices.push(base_vertex_idx + *idx);
1697 }
1698 (vert_count, idx_count)
1699 }
1700 };
1701
1702 let material = self.current_material();
1703 let tid = self.get_texture_id("__mega_heim");
1704
1705 if self.draw_calls.last().is_none()
1706 || self.current_texture_id != tid
1707 || self.draw_calls.last().unwrap().scissor_rect != self.clip_stack.last().copied()
1708 || self.draw_calls.last().unwrap().material != material
1709 {
1710 self.current_texture_id = tid;
1711 let (translation, scale, rotation, _, _) = self.current_transform();
1712 self.instance_data.push(InstanceData {
1713 translation,
1714 scale,
1715 rotation,
1716 blur_radius: 0.0,
1717 ior_override: 0.0,
1718 glass_intensity: 1.0,
1719 });
1720 self.draw_calls.push(DrawCall {
1721 target_id: None,
1722 texture_id: tid,
1723 scissor_rect: self.clip_stack.last().copied(),
1724 index_start: base_index_idx,
1725 index_count: idx_count as u32,
1726 instance_count: 1,
1727 material,
1728 instance_start: (self.instance_data.len() - 1) as u32,
1729 draw_order: 0,
1730 });
1731 } else {
1732 if let Some(last) = self.draw_calls.last_mut() {
1734 last.index_count += idx_count as u32;
1735 }
1736 }
1737 }
1738}
1739
1740impl cvkg_core::FrameRenderer<wgpu::CommandEncoder> for SurtrRenderer {
1741 fn begin_frame(&mut self) -> wgpu::CommandEncoder {
1742 cvkg_core::begin_render_phase();
1743 self.frame_rendered = false;
1744 self.app_drew_background = false;
1745 let id = self
1746 .current_window
1747 .expect("No target window set for frame. Call set_target_window first.");
1748 self.begin_frame(id)
1749 }
1750
1751 fn render_frame(&mut self) {
1752 if LAYOUT_DIRTY.swap(false, Ordering::AcqRel) {
1755 if let Some(window_id) = self.current_window {
1756 if let Some(surface_ctx) = self.surfaces.get(&window_id) {
1757 let w = surface_ctx.config.width as f32;
1758 let h = surface_ctx.config.height as f32;
1759 let border_rect = cvkg_core::Rect {
1760 x: 0.0,
1761 y: 0.0,
1762 width: w,
1763 height: h,
1764 };
1765 self.stroke_rect(border_rect, [1.0, 0.0, 0.0, 1.0], 10.0);
1767 }
1768 }
1769 }
1770
1771 let max_v_capacity = MAX_VERTICES * 4;
1777 let grown = self.geometry_buffers.grow_vertex_buffer(
1778 &self.device,
1779 self.vertices.len(),
1780 max_v_capacity,
1781 );
1782 if grown {
1783 log::info!("Grew vertex buffer to fit {} vertices", self.vertices.len());
1784 }
1785 if self.vertices.len() > max_v_capacity {
1786 log::error!("Exceeded dynamic vertex buffer max capacity! Capping geometry.");
1787 self.vertices.truncate(max_v_capacity);
1788 }
1789
1790 let max_i_capacity = MAX_INDICES * 4;
1791 let grown = self.geometry_buffers.grow_index_buffer(
1792 &self.device,
1793 self.indices.len(),
1794 max_i_capacity,
1795 );
1796 if grown {
1797 log::info!("Grew index buffer to fit {} indices", self.indices.len());
1798 }
1799 if self.indices.len() > max_i_capacity {
1800 log::error!("Exceeded dynamic index buffer max capacity! Capping geometry.");
1801 self.indices.truncate(max_i_capacity);
1802 }
1803
1804 let mut staging_encoder =
1806 self.device
1807 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1808 label: Some("Surtr Staging Encoder"),
1809 });
1810
1811 let mut has_writes = false;
1812
1813 if !self.vertices.is_empty() {
1814 let v_bytes = bytemuck::cast_slice(&self.vertices);
1815 self.staging_belt
1816 .write_buffer(
1817 &mut staging_encoder,
1818 &self.geometry_buffers.vertex_buffer,
1819 0,
1820 wgpu::BufferSize::new(v_bytes.len() as u64).unwrap(),
1821 )
1822 .copy_from_slice(v_bytes);
1823 has_writes = true;
1824 }
1825
1826 if !self.indices.is_empty() {
1827 let i_bytes = bytemuck::cast_slice(&self.indices);
1828 self.staging_belt
1829 .write_buffer(
1830 &mut staging_encoder,
1831 &self.geometry_buffers.index_buffer,
1832 0,
1833 wgpu::BufferSize::new(i_bytes.len() as u64).unwrap(),
1834 )
1835 .copy_from_slice(i_bytes);
1836 has_writes = true;
1837 }
1838
1839 if !self.instance_data.is_empty() {
1840 let inst_bytes = bytemuck::cast_slice(&self.instance_data);
1841 self.staging_belt
1842 .write_buffer(
1843 &mut staging_encoder,
1844 &self.geometry_buffers.instance_buffer,
1845 0,
1846 wgpu::BufferSize::new(inst_bytes.len() as u64).unwrap(),
1847 )
1848 .copy_from_slice(inst_bytes);
1849 has_writes = true;
1850 }
1851
1852 if has_writes {
1853 self.staging_belt.finish();
1854 self.staging_command_buffers.push(staging_encoder.finish());
1855 }
1856
1857 self.current_scene.time = self.start_time.elapsed().as_secs_f32();
1859 self.queue.write_buffer(
1860 &self.scene_buffer,
1861 0,
1862 bytemuck::bytes_of(&self.current_scene),
1863 );
1864 self.queue.write_buffer(
1865 &self.theme_buffer,
1866 0,
1867 bytemuck::bytes_of(&self.current_theme),
1868 );
1869
1870 self.telemetry.draw_calls = self.draw_calls.len() as u32;
1872 self.telemetry.vertices = self.vertices.len() as u32;
1873 self.frame_rendered = true;
1874
1875 log::debug!(
1877 "[Perf] draw_calls={} vertices={} instances={} staging_cmds={}",
1878 self.draw_calls.len(),
1879 self.vertices.len(),
1880 self.instance_data.len(),
1881 self.staging_command_buffers.len()
1882 );
1883 }
1884
1885 fn end_frame(&mut self, encoder: wgpu::CommandEncoder) {
1886 SurtrRenderer::end_frame(self, encoder);
1888 cvkg_core::end_render_phase();
1889 }
1890}
1891
1892fn glyph_image_to_rgba(image: cvkg_runic_text::GlyphImage) -> (Vec<u8>, u32, u32) {
1893 let width = image.width;
1894 let height = image.height;
1895 let pixels = width.saturating_mul(height) as usize;
1896
1897 if pixels == 0 || image.data.is_empty() {
1898 return (Vec::new(), width, height);
1899 }
1900
1901 let (bytes_per_pixel, remainder) = (image.data.len() / pixels, image.data.len() % pixels);
1902 if remainder != 0 {
1903 log::warn!(
1904 "Glyph rasterizer returned {} bytes for {}x{} glyph; expected whole pixels ({} bytes per pixel)",
1905 image.data.len(),
1906 width,
1907 height,
1908 bytes_per_pixel
1909 );
1910 return (Vec::new(), width, height);
1911 }
1912
1913 let rgba_data = match bytes_per_pixel {
1914 1 => {
1915 let mut data = Vec::with_capacity(pixels * 4);
1916 for alpha in &image.data {
1917 data.push(255);
1918 data.push(255);
1919 data.push(255);
1920 data.push(*alpha);
1921 }
1922 data
1923 }
1924 3 => {
1925 let mut data = Vec::with_capacity(pixels * 4);
1926 for rgb in image.data.chunks_exact(3) {
1927 let alpha = rgb.iter().copied().max().unwrap_or(0);
1928 data.push(255);
1929 data.push(255);
1930 data.push(255);
1931 data.push(alpha);
1932 }
1933 data
1934 }
1935 4 => {
1936 let mut data = image.data;
1937 for chunk in data.chunks_exact_mut(4) {
1938 if chunk[3] == 0 && (chunk[0] > 0 || chunk[1] > 0 || chunk[2] > 0) {
1941 chunk[3] = chunk[0].max(chunk[1]).max(chunk[2]);
1942 }
1943 }
1944 data
1945 }
1946 _ => {
1947 log::warn!(
1948 "Glyph rasterizer returned unsupported {} bytes per pixel for {}x{} glyph ({} bytes total)",
1949 bytes_per_pixel,
1950 width,
1951 height,
1952 image.data.len()
1953 );
1954 Vec::new()
1955 }
1956 };
1957
1958 (rgba_data, width, height)
1959}
1960
1961#[cfg(test)]
1962mod tests {
1963 use super::glyph_image_to_rgba;
1964
1965 #[test]
1966 fn glyph_image_to_rgba_keeps_rgba_color_data() {
1967 let image = cvkg_runic_text::GlyphImage {
1968 glyph_id: 1,
1969 width: 2,
1970 height: 1,
1971 data: vec![1, 2, 3, 4, 5, 6, 7, 8],
1972 x_offset: 0.0,
1973 y_offset: 0.0,
1974 cache_key: 42,
1975 };
1976
1977 assert_eq!(
1978 glyph_image_to_rgba(image),
1979 (vec![1, 2, 3, 4, 5, 6, 7, 8], 2, 1)
1980 );
1981 }
1982
1983 #[test]
1984 fn glyph_image_to_rgba_expands_grayscale_alpha() {
1985 let image = cvkg_runic_text::GlyphImage {
1986 glyph_id: 1,
1987 width: 3,
1988 height: 1,
1989 data: vec![0, 128, 255],
1990 x_offset: 0.0,
1991 y_offset: 0.0,
1992 cache_key: 42,
1993 };
1994
1995 assert_eq!(
1996 glyph_image_to_rgba(image),
1997 (
1998 vec![255, 255, 255, 0, 255, 255, 255, 128, 255, 255, 255, 255],
1999 3,
2000 1
2001 )
2002 );
2003 }
2004
2005 #[test]
2006 fn glyph_image_to_rgba_collapses_subpixel_rgb_to_alpha() {
2007 let image = cvkg_runic_text::GlyphImage {
2008 glyph_id: 1,
2009 width: 2,
2010 height: 1,
2011 data: vec![0, 128, 255, 255, 0, 64],
2012 x_offset: 0.0,
2013 y_offset: 0.0,
2014 cache_key: 42,
2015 };
2016
2017 assert_eq!(
2018 glyph_image_to_rgba(image),
2019 (vec![255, 255, 255, 255, 255, 255, 255, 255], 2, 1)
2020 );
2021 }
2022}