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