1use crate::renderer::GpuRenderer;
3
4pub mod frame;
5pub mod shapes;
6pub mod text;
7
8use crate::renderer::material_id;
9use crate::types::*;
10use crate::vertex::*;
11use cvkg_core::LAYOUT_DIRTY;
12use cvkg_core::{ColorTheme, Mesh, Rect, RenderStateSnapshot, Renderer};
13use lyon::math::point;
14use lyon::tessellation::{BuffersBuilder, StrokeOptions, StrokeTessellator, VertexBuffers};
15use std::hash::{Hash, Hasher};
16use std::sync::atomic::Ordering;
17
18impl cvkg_core::ElapsedTime for GpuRenderer {
19 fn delta_time(&self) -> f32 {
20 self.current_scene.delta_time
21 }
22
23 fn elapsed_time(&self) -> f32 {
24 self.start_time.elapsed().as_secs_f32()
25 }
26}
27
28impl cvkg_core::RendererErrorHandler for GpuRenderer {
29 fn on_render_error(&mut self, error: &cvkg_core::CvkgError) {
30 tracing::error!("[GpuRenderer] {error}");
31 self.render_error_count += 1;
32 }
33
34 fn on_fatal_error(&mut self, error: &cvkg_core::CvkgError) {
35 tracing::error!("[GpuRenderer FATAL] {error}");
36 self.has_fatal_error = true;
37 }
38
39 fn has_error(&self) -> bool {
40 self.has_fatal_error
41 }
42}
43
44impl cvkg_core::Renderer for GpuRenderer {
45 fn is_over_budget(&self) -> bool {
46 self.frame_budget.allow_degradation
47 && self.last_frame_start.elapsed().as_secs_f32() * 1000.0 > self.frame_budget.target_ms
48 }
49
50 fn text_scale_factor(&self) -> f32 {
51 self.current_scale_factor()
52 }
53
54 fn prewarm_vram(&mut self, assets: Vec<(String, Vec<u8>)>) {
55 tracing::info!(
56 "[Surtr] Pre-warming Mega-Heim with {} assets...",
57 assets.len()
58 );
59 for (name, data) in assets {
60 self.load_image_to_heim(&name, &data);
61 }
62 }
63
64 fn fill_rect(&mut self, rect: Rect, color: [f32; 4]) {
65 self.fill_rect_with_mode(rect, self.apply_opacity(color), 0, None);
66 }
67
68 fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4]) {
69 self.fill_rect_with_full_params(
70 rect,
71 self.apply_opacity(color),
72 3,
73 None,
74 radius,
75 Rect {
76 x: 0.0,
77 y: 0.0,
78 width: 1.0,
79 height: 1.0,
80 },
81 );
82 }
83
84 fn fill_glass_rect(&mut self, rect: Rect, radius: f32, blur_radius: f32) {
91 self.fill_glass_rect_with_intensity(rect, radius, blur_radius, 1.0);
92 }
93
94 fn fill_glass_rect_with_intensity(
98 &mut self,
99 rect: Rect,
100 radius: f32,
101 blur_radius: f32,
102 glass_intensity: f32,
103 ) {
104 self.fill_glass_rect_with_tint(
106 rect,
107 radius,
108 blur_radius,
109 [1.0, 1.0, 1.0, 0.4],
110 glass_intensity,
111 );
112 }
113
114 fn fill_glass_rect_with_tint(
117 &mut self,
118 rect: Rect,
119 radius: f32,
120 blur_radius: f32,
121 tint_color: [f32; 4],
122 glass_intensity: f32,
123 ) {
124 let gi = glass_intensity.clamp(0.0, 1.0);
125 let blur_strength = (blur_radius / 25.0).clamp(0.0, 4.0) * gi;
128
129 if self.current_z != 0.0 {
131 self.portal_regions.push_back(rect);
132 }
133
134 let prev_material = self.current_draw_material;
136 self.current_draw_material = cvkg_core::DrawMaterial::Glass {
137 blur_radius: blur_strength,
138 ior_override: 0.0,
139 glass_intensity: gi,
140 };
141
142 let fill_color = [
144 tint_color[0],
145 tint_color[1],
146 tint_color[2],
147 tint_color[3] * gi,
148 ];
149
150 self.fill_rect_with_full_params(
151 rect,
152 fill_color,
153 7, None,
155 radius,
156 Rect {
157 x: 0.0,
158 y: 0.0,
159 width: 1.0,
160 height: 1.0,
161 },
162 );
163
164 self.current_draw_material = prev_material;
165 }
166
167 fn fill_glass_rect_with_pressure(
168 &mut self,
169 rect: Rect,
170 radius: f32,
171 blur_radius: f32,
172 pressure: f32,
173 ) {
174 let p = pressure.clamp(0.0, 1.0);
176 self.fill_glass_rect_with_intensity(rect, radius, blur_radius * p, p);
177 }
178
179 fn set_default_background_color(&mut self, color: [f32; 4]) {
183 self.default_background_color = color;
184 }
185
186 fn fill_squircle(&mut self, rect: Rect, n: f32, color: [f32; 4]) {
189 let prev_material = self.current_draw_material;
190 self.current_draw_material = cvkg_core::DrawMaterial::Opaque;
191 self.fill_rect_with_full_params(
192 rect,
193 self.apply_opacity(color),
194 0,
195 None,
196 rect.width.min(rect.height) * 0.22 * (n / 4.0),
197 Rect {
198 x: 0.0,
199 y: 0.0,
200 width: 1.0,
201 height: 1.0,
202 },
203 );
204 self.current_draw_material = prev_material;
205 }
206
207 fn stroke_squircle(&mut self, rect: Rect, n: f32, color: [f32; 4], stroke_width: f32) {
209 let prev_material = self.current_draw_material;
210 self.current_draw_material = cvkg_core::DrawMaterial::Opaque;
211 self.fill_rect_with_full_params(
212 rect,
213 self.apply_opacity(color),
214 material_id::SQUIRCLE_STROKE,
215 None,
216 rect.width.min(rect.height) * 0.22 * (n / 4.0),
217 Rect {
218 x: stroke_width,
219 y: 0.0,
220 width: 0.0,
221 height: 0.0,
222 },
223 );
224 self.current_draw_material = prev_material;
225 }
226
227 fn draw_focus_ring(
230 &mut self,
231 rect: Rect,
232 radius: f32,
233 offset: f32,
234 width: f32,
235 color: [f32; 4],
236 ) {
237 let ring_rect = Rect {
238 x: rect.x - offset,
239 y: rect.y - offset,
240 width: rect.width + 2.0 * offset,
241 height: rect.height + 2.0 * offset,
242 };
243 self.stroke_squircle(ring_rect, 4.0, color, width);
244 }
245
246 fn fill_ellipse(&mut self, rect: Rect, color: [f32; 4]) {
247 self.fill_rect_with_full_params(
248 rect,
249 self.apply_opacity(color),
250 4,
251 None,
252 0.0,
253 Rect {
254 x: 0.0,
255 y: 0.0,
256 width: 1.0,
257 height: 1.0,
258 },
259 );
260 }
261
262 fn draw_3d_cube(&mut self, rect: Rect, color: [f32; 4], rotation: [f32; 3]) {
263 self.fill_rect_with_full_params_and_slice(
264 rect,
265 self.apply_opacity(color),
266 material_id::MESH_3D,
267 None,
268 0.0,
269 Rect {
270 x: 0.0,
271 y: 0.0,
272 width: 1.0,
273 height: 1.0,
274 },
275 [rotation[0], rotation[1], rotation[2], 0.0],
276 [0.0, 0.0],
277 );
278 }
279
280 fn bifrost(&mut self, rect: Rect, blur: f32, _saturation: f32, opacity: f32) {
281 let logical_w = self.current_width() as f32 / self.current_scale_factor();
283 let logical_h = self.current_height() as f32 / self.current_scale_factor();
284 let screen_uv = Rect {
285 x: rect.x / logical_w,
286 y: rect.y / logical_h,
287 width: rect.width / logical_w,
288 height: rect.height / logical_h,
289 };
290 self.fill_rect_with_full_params(rect, [1.0, 1.0, 1.0, opacity], 7, None, blur, screen_uv);
293 }
294
295 fn gungnir(&mut self, rect: Rect, color: [f32; 4], radius: f32, intensity: f32) {
296 let margin = radius;
298 let glow_rect = Rect {
299 x: rect.x - margin,
300 y: rect.y - margin,
301 width: rect.width + 2.0 * margin,
302 height: rect.height + 2.0 * margin,
303 };
304 let glow_color = [color[0], color[1], color[2], intensity * 0.3];
305 self.fill_rect_with_full_params(
306 glow_rect,
307 self.apply_opacity(glow_color),
308 material_id::DROP_SHADOW,
309 None,
310 8.0,
311 Rect {
312 x: margin,
313 y: radius,
314 width: 0.0,
315 height: 0.0,
316 },
317 );
318 }
319
320 fn gungnir_soft(&mut self, rect: Rect, color: [f32; 4], radius: f32, intensity: f32) {
323 self.gungnir(rect, color, radius, intensity * 0.5);
324 }
325
326 fn mani_glow(&mut self, rect: Rect, color: [f32; 4], radius: f32) {
333 let margin = radius;
334 let glow_rect = Rect {
335 x: rect.x - margin,
336 y: rect.y - margin,
337 width: rect.width + 2.0 * margin,
338 height: rect.height + 2.0 * margin,
339 };
340 let uv_rect = Rect {
341 x: margin,
342 y: radius,
343 width: 0.0,
344 height: 0.0,
345 };
346 self.fill_rect_with_full_params(
347 glow_rect,
348 self.apply_opacity(color),
349 material_id::DROP_SHADOW,
350 None,
351 8.0,
352 uv_rect,
353 );
354 }
355
356 fn stroke_rect(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
357 let c = self.apply_opacity(color);
358 self.fill_rect_with_full_params(
360 rect,
361 c,
362 material_id::SQUIRCLE_STROKE,
363 None,
364 0.0, Rect {
366 x: stroke_width,
367 y: 0.0,
368 width: 0.0,
369 height: 0.0,
370 },
371 );
372 }
373
374 fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4], stroke_width: f32) {
375 self.fill_rect_with_full_params(
376 rect,
377 self.apply_opacity(color),
378 material_id::SQUIRCLE_STROKE,
379 None,
380 radius,
381 Rect {
382 x: stroke_width,
383 y: 0.0,
384 width: 0.0,
385 height: 0.0,
386 },
387 );
388 }
389
390 fn stroke_ellipse(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
391 let cx = rect.x + rect.width / 2.0;
393 let cy = rect.y + rect.height / 2.0;
394 let rx = rect.width / 2.0;
395 let ry = rect.height / 2.0;
396
397 let mut builder = lyon::path::Path::builder();
399 if rx > 0.0 && ry > 0.0 {
400 let segments = 64;
402 for i in 0..segments {
403 let angle = 2.0 * std::f32::consts::PI * (i as f32) / (segments as f32);
404 let x = cx + rx * angle.cos();
405 let y = cy + ry * angle.sin();
406 if i == 0 {
407 builder.begin(lyon::math::point(x, y));
408 } else {
409 builder.line_to(lyon::math::point(x, y));
410 }
411 }
412 builder.close();
413 }
414 let path = builder.build();
415 self.stroke_path(&path, color, stroke_width);
416 }
417
418 fn draw_linear_gradient(
419 &mut self,
420 rect: Rect,
421 start_color: [f32; 4],
422 end_color: [f32; 4],
423 angle: f32,
424 ) {
425 self.fill_rect_with_full_params_and_slice(
426 rect,
427 self.apply_opacity(start_color),
428 15,
429 None,
430 0.0,
431 Rect {
432 x: angle,
433 y: 0.0,
434 width: 1.0,
435 height: 1.0,
436 },
437 end_color,
438 [0.0, 0.0],
439 );
440 }
441
442 fn draw_radial_gradient(&mut self, rect: Rect, inner_color: [f32; 4], outer_color: [f32; 4]) {
443 self.fill_rect_with_full_params_and_slice(
444 rect,
445 self.apply_opacity(inner_color),
446 material_id::RADIAL_GRADIENT,
447 None,
448 0.0,
449 Rect {
450 x: 0.0,
451 y: 0.0,
452 width: 1.0,
453 height: 1.0,
454 },
455 outer_color,
456 [0.0, 0.0],
457 );
458 }
459
460 fn draw_linear_gradient_multi(&mut self, rect: Rect, stops: &[[f32; 4]], angle: f32) {
461 self.draw_gradient_multi(rect, stops, angle, false);
462 }
463
464 fn draw_radial_gradient_multi(&mut self, rect: Rect, stops: &[[f32; 4]]) {
465 self.draw_gradient_multi(rect, stops, 0.0, true);
466 }
467
468 fn draw_drop_shadow(
469 &mut self,
470 rect: Rect,
471 radius: f32,
472 color: [f32; 4],
473 blur: f32,
474 spread: f32,
475 ) {
476 let margin = blur + spread;
477 let inflated = Rect {
478 x: rect.x - margin,
479 y: rect.y - margin,
480 width: rect.width + margin * 2.0,
481 height: rect.height + margin * 2.0,
482 };
483 self.fill_rect_with_full_params_and_slice(
485 inflated,
486 self.apply_opacity(color),
487 material_id::DROP_SHADOW,
488 None,
489 radius,
490 Rect {
491 x: margin,
492 y: blur,
493 width: 0.0,
494 height: 0.0,
495 },
496 [0.0, 0.0, 0.0, 1.0],
497 [0.0, 0.0],
498 );
499 }
500
501 fn stroke_dashed_rounded_rect(
502 &mut self,
503 rect: Rect,
504 radius: f32,
505 color: [f32; 4],
506 width: f32,
507 dash: f32,
508 gap: f32,
509 ) {
510 self.fill_rect_with_full_params(
511 rect,
512 self.apply_opacity(color),
513 material_id::DASHED_STROKE,
514 None,
515 radius,
516 Rect {
517 x: width,
518 y: dash,
519 width: gap,
520 height: 0.0,
521 },
522 );
523 }
524
525 fn draw_9slice(
526 &mut self,
527 image_name: &str,
528 rect: Rect,
529 left: f32,
530 top: f32,
531 right: f32,
532 bottom: f32,
533 ) {
534 let c = self.apply_opacity([1.0, 1.0, 1.0, 1.0]);
535 let tid = self.get_texture_id(image_name);
536 self.fill_rect_with_full_params(
537 rect,
538 c,
539 20,
540 tid,
541 bottom,
542 Rect {
543 x: left,
544 y: top,
545 width: right,
546 height: 0.0,
547 },
548 );
549 }
550
551 fn draw_line(
552 &mut self,
553 x1: f32,
554 y1: f32,
555 x2: f32,
556 y2: f32,
557 color: [f32; 4],
558 stroke_width: f32,
559 ) {
560 let dx = x2 - x1;
561 let dy = y2 - y1;
562 let len_sq = dx * dx + dy * dy;
563 if len_sq < 0.000001 {
564 return;
565 }
566 let len = len_sq.sqrt();
567 let half_w = stroke_width * 0.5;
568 let nx = -dy / len * half_w;
570 let ny = dx / len * half_w;
571 let points = [
573 [x1 + nx, y1 + ny],
574 [x2 + nx, y2 + ny],
575 [x2 - nx, y2 - ny],
576 [x1 - nx, y1 - ny],
577 ];
578 self.push_oriented_quad(
579 points,
580 color,
581 1,
582 Rect {
583 x: 0.0,
584 y: 0.0,
585 width: 1.0,
586 height: 1.0,
587 },
588 );
589 }
590
591 fn draw_image(&mut self, image_name: &str, rect: Rect) {
592 if !self.image_uv_registry.contains(image_name) {
594 tracing::warn!("[Surtr] draw_image: '{}' not loaded, skipping", image_name);
595 return;
596 }
597 let tid = self
598 .get_texture_id(image_name)
599 .or_else(|| self.get_texture_id("__mega_heim"));
600 let uv_rect = self
601 .image_uv_registry
602 .get(image_name)
603 .copied()
604 .unwrap_or(Rect {
605 x: 0.0,
606 y: 0.0,
607 width: 1.0,
608 height: 1.0,
609 });
610 self.fill_rect_with_full_params(rect, [1.0, 1.0, 1.0, 1.0], 2, tid, 0.0, uv_rect);
611 }
612
613 fn shape_rich_text(
614 &mut self,
615 spans: &[cvkg_runic_text::TextSpan],
616 max_width: Option<f32>,
617 align: cvkg_runic_text::TextAlign,
618 overflow: cvkg_runic_text::TextOverflow,
619 ) -> Option<cvkg_runic_text::ShapedText> {
620 self.shape_rich_text_impl(spans, max_width, align, overflow)
621 }
622
623 fn draw_shaped_text(&mut self, shaped: &cvkg_runic_text::ShapedText, x: f32, y: f32) {
624 self.draw_shaped_text_impl(shaped, x, y);
625 }
626
627 fn draw_texture(&mut self, texture_id: u32, rect: Rect) {
628 self.fill_rect_with_full_params_and_slice(
629 rect,
630 [1.0, 1.0, 1.0, 1.0],
631 2,
632 Some(texture_id),
633 0.0,
634 Rect {
635 x: 0.0,
636 y: 0.0,
637 width: 1.0,
638 height: 1.0,
639 },
640 [0.0, 0.0, 0.0, 1.0],
641 [0.0, 0.0],
642 );
643 }
644
645 fn load_image(&mut self, name: &str, data: &[u8]) {
648 if self.image_uv_registry.contains(name) {
649 return;
650 }
651 let img_result = image::load_from_memory(data);
652 let img = match img_result {
653 Ok(img) => img.to_rgba8(),
654 Err(e) => {
655 tracing::error!("Failed to load image {}: {}", name, e);
656 image::RgbaImage::from_pixel(1, 1, image::Rgba([255, 255, 255, 255]))
657 }
658 };
659 let (width, height) = img.dimensions();
660
661 let size = wgpu::Extent3d {
662 width,
663 height,
664 depth_or_array_layers: 1,
665 };
666 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
667 label: Some(&format!("Texture Array Layer: {}", name)),
668 size,
669 mip_level_count: 1,
670 sample_count: 1,
671 dimension: wgpu::TextureDimension::D2,
672 format: wgpu::TextureFormat::Rgba8UnormSrgb,
673 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
674 view_formats: &[],
675 });
676
677 self.queue.write_texture(
678 wgpu::TexelCopyTextureInfo {
679 texture: &texture,
680 mip_level: 0,
681 origin: wgpu::Origin3d::ZERO,
682 aspect: wgpu::TextureAspect::All,
683 },
684 &img,
685 wgpu::TexelCopyBufferLayout {
686 offset: 0,
687 bytes_per_row: Some(4 * width),
688 rows_per_image: Some(height),
689 },
690 size,
691 );
692
693 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
694
695 let index = if self.texture_registry.len() < 31 {
698 (self.texture_registry.len() + 1) as u32
699 } else {
700 if let Some((old_name, old_index)) = self.texture_registry.pop_lru() {
703 self.image_uv_registry.pop(&old_name);
704 old_index
705 } else {
706 tracing::warn!("[GPU] texture registry full and no LRU entry to evict");
707 return;
708 }
709 };
710
711 if index == 0 || index as usize >= self.texture_views.len() {
713 tracing::error!(
714 "[GPU] load_image: invalid texture index {} (registry has {} entries)",
715 index,
716 self.texture_registry.len()
717 );
718 return;
719 }
720
721 self.texture_views[index as usize] = view;
722 self.image_uv_registry.put(
723 name.to_string(),
724 Rect {
725 x: 0.0,
726 y: 0.0,
727 width: 1.0,
728 height: 1.0,
729 },
730 );
731 self.texture_registry.put(name.to_string(), index);
732 self.rebuild_texture_array_bind_group();
733 }
734
735 fn push_clip_rect(&mut self, rect: Rect) {
736 self.clip_stack.push(rect);
737 }
738
739 fn pop_clip_rect(&mut self) {
740 self.clip_stack.pop();
741 }
742
743 fn current_clip_rect(&self) -> Rect {
744 self.clip_stack.last().copied().unwrap_or(Rect::new(
745 0.0,
746 0.0,
747 self.current_width() as f32,
748 self.current_height() as f32,
749 ))
750 }
751
752 fn memoize(&mut self, id: u64, data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer)) {
753 use crate::types::{DrawCall, MemoEntry};
766
767 let should_skip = self
768 .memo_cache
769 .get(&id)
770 .is_some_and(|entry| entry.hash == data_hash);
771
772 if should_skip {
773 if let Some(entry) = self.memo_cache.get(&id) {
775 let i_offset = self.indices.len() as u32;
776 let inst_offset = self.instance_data.len() as u32;
777
778 self.vertices.extend_from_slice(&entry.vertices);
779 self.indices.extend_from_slice(&entry.indices);
780 self.instance_data.extend_from_slice(&entry.instance_data);
781
782 for dc in &entry.draw_calls {
783 let mut replayed = dc.clone();
784 replayed.index_start += i_offset;
788 replayed.instance_start += inst_offset;
789 self.draw_calls.push(replayed);
790 }
791 }
792 } else {
793 let v_start = self.vertices.len();
795 let i_start = self.indices.len();
796 let inst_start = self.instance_data.len();
797 let dc_start = self.draw_calls.len();
798
799 render_fn(self);
800
801 let draw_calls: Vec<DrawCall> = self.draw_calls[dc_start..]
803 .iter()
804 .map(|dc| {
805 let mut remapped = dc.clone();
806 remapped.index_start = remapped.index_start.saturating_sub(i_start as u32);
810 remapped.instance_start =
811 remapped.instance_start.saturating_sub(inst_start as u32);
812 remapped
813 })
814 .collect();
815
816 let entry = MemoEntry {
817 hash: data_hash,
818 frame_gen: self.frame_generation,
819 vertices: self.vertices[v_start..].to_vec(),
820 indices: self.indices[i_start..].to_vec(),
821 instance_data: self.instance_data[inst_start..].to_vec(),
822 draw_calls,
823 };
824
825 self.memo_cache.insert(id, entry);
826 }
827 }
828
829 fn snapshot_render_state(&self) -> RenderStateSnapshot {
830 RenderStateSnapshot {
831 clip_depth: self.clip_stack.len() as u32,
832 opacity_depth: self.opacity_stack.len() as u32,
833 slice_depth: self.slice_stack.len() as u32,
834 shadow_depth: self.shadow_stack.len() as u32,
835 transform_depth: self.transform_stack.len() as u32,
836 vnode_depth: self.vnode_stack.len() as u32,
837 }
838 }
839
840 fn restore_render_state(&mut self, snap: RenderStateSnapshot) {
841 while self.clip_stack.len() as u32 > snap.clip_depth {
843 self.clip_stack.pop();
844 }
845 while self.opacity_stack.len() as u32 > snap.opacity_depth {
846 self.opacity_stack.pop();
847 }
848 while self.slice_stack.len() as u32 > snap.slice_depth {
849 self.slice_stack.pop();
850 }
851 while self.shadow_stack.len() as u32 > snap.shadow_depth {
852 self.shadow_stack.pop();
853 }
854 while self.transform_stack.len() as u32 > snap.transform_depth {
855 self.transform_stack.pop();
856 }
857 while self.vnode_stack.len() as u32 > snap.vnode_depth {
858 self.vnode_stack.pop();
859 }
860 }
861
862 fn push_opacity(&mut self, opacity: f32) {
863 let current = self.opacity_stack.last().copied().unwrap_or(1.0);
864 self.opacity_stack.push(current * opacity);
865 }
866
867 fn pop_opacity(&mut self) {
868 self.opacity_stack.pop();
869 }
870
871 fn push_shadow(&mut self, radius: f32, color: [f32; 4], offset: [f32; 2]) {
872 self.shadow_stack.push(ShadowState {
873 radius,
874 color,
875 _offset: offset,
876 });
877 }
878
879 fn pop_shadow(&mut self) {
880 self.shadow_stack.pop();
881 }
882
883 fn push_transform(&mut self, translation: [f32; 2], scale: [f32; 2], rotation: f32) {
884 let c = rotation.cos();
885 let sn = rotation.sin();
886 let affine = glam::Mat3::from_cols(
887 glam::Vec3::new(c * scale[0], sn * scale[0], 0.0),
888 glam::Vec3::new(-sn * scale[1], c * scale[1], 0.0),
889 glam::Vec3::new(translation[0], translation[1], 1.0),
890 );
891
892 let parent = self
893 .transform_stack
894 .last()
895 .copied()
896 .unwrap_or(glam::Mat3::IDENTITY);
897 self.transform_stack.push(parent * affine);
898 }
899
900 fn push_affine(&mut self, transform: [f32; 6]) {
901 let affine = glam::Mat3::from_cols(
902 glam::Vec3::new(transform[0], transform[1], 0.0),
903 glam::Vec3::new(transform[2], transform[3], 0.0),
904 glam::Vec3::new(transform[4], transform[5], 1.0),
905 );
906 let parent = self
907 .transform_stack
908 .last()
909 .copied()
910 .unwrap_or(glam::Mat3::IDENTITY);
911 self.transform_stack.push(parent * affine);
912 }
913
914 fn pop_transform(&mut self) {
915 self.transform_stack.pop();
916 }
917
918 fn set_theme(&mut self, theme: ColorTheme) {
919 self.current_theme = theme;
920 self.queue
921 .write_buffer(&self.theme_buffer, 0, bytemuck::bytes_of(&theme));
922 }
923
924 fn set_rage(&mut self, rage: f32) {
925 self.current_scene.berzerker_rage = rage;
926 }
928
929 fn set_fireball_pos(&mut self, pos: [f32; 2]) {
930 self.current_scene.fireball_pos = pos;
931 }
932
933 fn trigger_shatter_event(&mut self, origin: [f32; 2], force: f32) {
934 self.current_scene.shatter_origin = origin;
935 self.current_scene.shatter_time = self.current_scene.time;
936 self.current_scene.shatter_force = force;
937 }
938
939 fn set_scene_preset(&mut self, preset: u32) {
940 self.current_scene.scene_type = preset;
941 }
942
943 fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
946 self.slice_stack.push((angle, offset));
947 }
948
949 fn pop_mjolnir_slice(&mut self) {
951 self.slice_stack.pop();
952 }
953
954 fn mjolnir_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
955 self.shatter_internal(rect, pieces, force, color, 8);
956 }
957
958 fn mjolnir_fluid_shatter(&mut self, rect: Rect, pieces: u32, force: f32, color: [f32; 4]) {
959 self.shatter_internal(rect, pieces, force, color, 11);
960 }
961
962 fn draw_mjolnir_bolt(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
963 self.recursive_bolt(from, to, 4, color);
964 }
965
966 fn dispatch_particles(
967 &mut self,
968 origin: [f32; 2],
969 count: u32,
970 effect_type: &str,
971 color: [f32; 4],
972 ) {
973 use crate::types::{GpuParticle, MAX_PARTICLES};
974
975 let dt = self.current_scene.delta_time;
976 let now = std::time::Instant::now();
977
978 let (speed_range, life_range, spread_angle) = match effect_type {
980 "firework" => (100.0..300.0, 1.0..2.5, std::f32::consts::TAU),
981 "spark" => (50.0..150.0, 0.5..1.5, std::f32::consts::PI),
982 "rain" => (20.0..80.0, 1.0..3.0, std::f32::consts::FRAC_PI_4),
983 "data_stream" => (80.0..200.0, 0.8..2.0, std::f32::consts::FRAC_PI_6),
984 "bubble" => (10.0..40.0, 2.0..4.0, std::f32::consts::TAU),
985 _ => (30.0..120.0, 1.0..2.0, std::f32::consts::TAU),
986 };
987
988 let count = count.min((MAX_PARTICLES - self.particles.count as usize) as u32);
989 if count == 0 {
990 return;
991 }
992
993 let mut rng_state = (now.elapsed().as_nanos() as u64)
994 .wrapping_mul(6364136223846793005)
995 .wrapping_add(1442695040888963407);
996 let mut rand_f32 = |range: std::ops::Range<f32>| -> f32 {
997 rng_state = rng_state
998 .wrapping_mul(6364136223846793005)
999 .wrapping_add(1442695040888963407);
1000 let t = (rng_state >> 33) as f32 / (1u64 << 31) as f32;
1001 range.start + t * (range.end - range.start)
1002 };
1003
1004 for _ in 0..count {
1005 let angle = rand_f32(0.0..spread_angle);
1006 let speed = rand_f32(speed_range.clone());
1007 let life = rand_f32(life_range.clone());
1008 let vx = angle.cos() * speed;
1009 let vy = angle.sin() * speed;
1010
1011 let particle = GpuParticle {
1012 pos_vel: [origin[0], origin[1], vx, vy],
1013 color_life: [color[0], color[1], color[2], life],
1014 };
1015 self.particles.staging.push(particle);
1016 }
1017
1018 tracing::debug!(
1019 "[Surtr] dispatch_particles: {} {} particles at {:?} (staged, {} total pending)",
1020 count,
1021 effect_type,
1022 origin,
1023 self.particles.staging.len()
1024 );
1025 }
1026
1027 fn draw_hologram(&mut self, rect: Rect, hologram_id: &str, time: f32) {
1028 use std::hash::{Hash, Hasher};
1029 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1030 hologram_id.hash(&mut hasher);
1031 let id_hash = hasher.finish() as u32;
1032
1033 tracing::debug!(
1034 "[Surtr] draw_hologram: {} at {:?} t={} (hologram pipeline)",
1035 hologram_id,
1036 rect,
1037 time
1038 );
1039
1040 self.hologram_instances
1041 .push(crate::renderer::HologramInstance {
1042 rect,
1043 id_hash,
1044 time,
1045 });
1046 self.volumetric_enabled = true;
1047 }
1048
1049 fn upload_data_texture(&mut self, id: &str, data: &[f32], width: u32, height: u32) {
1050 let size = wgpu::Extent3d {
1051 width,
1052 height,
1053 depth_or_array_layers: 1,
1054 };
1055 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
1056 label: Some(id),
1057 size,
1058 mip_level_count: 1,
1059 sample_count: 1,
1060 dimension: wgpu::TextureDimension::D2,
1061 format: wgpu::TextureFormat::R32Float,
1062 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1063 view_formats: &[],
1064 });
1065 self.queue.write_texture(
1066 wgpu::TexelCopyTextureInfo {
1067 texture: &texture,
1068 mip_level: 0,
1069 origin: wgpu::Origin3d::ZERO,
1070 aspect: wgpu::TextureAspect::All,
1071 },
1072 bytemuck::cast_slice(data),
1073 wgpu::TexelCopyBufferLayout {
1074 offset: 0,
1075 bytes_per_row: Some(4 * width),
1076 rows_per_image: Some(height),
1077 },
1078 size,
1079 );
1080 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1081 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1084 layout: &self.texture_bind_group_layout,
1085 entries: &[
1086 wgpu::BindGroupEntry {
1087 binding: 0,
1088 resource: wgpu::BindingResource::TextureViewArray(&vec![&view; 32]),
1090 },
1091 wgpu::BindGroupEntry {
1092 binding: 1,
1093 resource: wgpu::BindingResource::Sampler(&self.linear_sampler),
1094 },
1095 ],
1096 label: Some(id),
1097 });
1098 self.texture_bind_groups.push(bind_group);
1099 let tid = (self.texture_bind_groups.len() - 1) as u32;
1100 self.texture_registry.put(id.to_string(), tid);
1101 }
1102
1103 fn draw_heatmap(&mut self, texture_id: &str, rect: Rect, _palette: &str) {
1104 let tid = self.get_texture_id(texture_id);
1105 self.fill_rect_with_mode(rect, [1.0, 1.0, 1.0, 1.0], 12, tid);
1106 }
1107
1108 fn draw_mesh(&mut self, mesh: &Mesh, color: [f32; 4], transform: glam::Mat4) {
1109 let base_idx = self.vertices.len() as u32;
1110
1111 for i in 0..mesh.vertices.len() {
1112 let pos = transform.transform_point3(glam::Vec3::from(mesh.vertices[i]));
1113 let norm = transform.transform_vector3(glam::Vec3::from(mesh.normals[i]));
1114
1115 self.vertices.push(Vertex {
1116 position: pos.to_array(),
1117 normal: norm.to_array(),
1118 uv: [0.0, 0.0],
1119 color,
1120 material_id: 13, radius: 0.0,
1122 slice: [0.0, 0.0, 0.0, 1.0],
1123 logical: [0.0, 0.0],
1124 size: [0.0, 0.0],
1125 clip: [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY],
1126 tex_index: 0,
1127 });
1128 }
1129
1130 for idx in &mesh.indices {
1131 self.indices.push(base_idx + idx);
1132 }
1133
1134 let (translation, scale_transform, rotation, _, _) = self.current_transform();
1135
1136 if self.draw_calls.is_empty() || self.current_texture_id.is_some() {
1137 self.current_texture_id = None;
1138
1139 self.instance_data.push(InstanceData {
1140 translation,
1141 scale: scale_transform,
1142 rotation,
1143 blur_radius: 0.0,
1144 ior_override: 0.0,
1145 glass_intensity: 1.0,
1146 });
1147 self.draw_calls.push(DrawCall {
1148 target_id: None,
1149 texture_id: None,
1150 scissor_rect: self.clip_stack.last().copied(),
1151 index_start: (self.indices.len() as u32) - (mesh.indices.len() as u32),
1152 index_count: mesh.indices.len() as u32,
1153 instance_count: 1,
1154 material: cvkg_core::DrawMaterial::Opaque,
1155 instance_start: (self.instance_data.len() - 1) as u32,
1156 draw_order: 0,
1157 });
1158 } else {
1159 self.draw_calls.last_mut().unwrap().index_count += mesh.indices.len() as u32;
1160 }
1161 }
1162
1163 fn draw_mesh_3d(
1164 &mut self,
1165 mesh: &Mesh,
1166 material: &cvkg_core::Material3D,
1167 transform: &cvkg_core::Transform3D,
1168 ) {
1169 let base_idx = self.vertices.len() as u32;
1170 let model_matrix = transform.to_matrix();
1171
1172 for i in 0..mesh.vertices.len() {
1173 let pos = model_matrix.transform_point3(glam::Vec3::from(mesh.vertices[i]));
1174 let norm = model_matrix.transform_vector3(glam::Vec3::from(mesh.normals[i]));
1175
1176 self.vertices.push(Vertex {
1177 position: [pos.x, pos.y, pos.z],
1178 normal: [norm.x, norm.y, norm.z],
1179 uv: [0.0, 0.0],
1180 color: material.base_color,
1181 material_id: 13, radius: 0.0,
1183 slice: [material.metallic, material.roughness, material.opacity, 1.0],
1184 logical: [0.0, 0.0],
1185 size: [0.0, 0.0],
1186 clip: [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY],
1187 tex_index: 0,
1188 });
1189 }
1190
1191 for idx in &mesh.indices {
1192 self.indices.push(base_idx + idx);
1193 }
1194
1195 self.instance_data.push(InstanceData {
1196 translation: [0.0, 0.0],
1197 scale: [1.0, 1.0],
1198 rotation: 0.0,
1199 blur_radius: 0.0,
1200 ior_override: 0.0,
1201 glass_intensity: 1.0,
1202 });
1203
1204 self.draw_calls.push(DrawCall {
1205 target_id: None,
1206 texture_id: None,
1207 scissor_rect: self.clip_stack.last().copied(),
1208 index_start: (self.indices.len() as u32) - (mesh.indices.len() as u32),
1209 index_count: mesh.indices.len() as u32,
1210 instance_count: 1,
1211 material: cvkg_core::DrawMaterial::Opaque,
1212 instance_start: (self.instance_data.len() - 1) as u32,
1213 draw_order: 0,
1214 });
1215 }
1216
1217 fn set_camera_3d(&mut self, camera: &cvkg_core::Camera3D) {
1218 self.current_scene.proj = camera.projection_matrix();
1219 self.current_scene.view = camera.view_matrix();
1220 }
1221
1222 fn push_transform_3d(&mut self, transform: &cvkg_core::Transform3D) {
1223 let (translation, rotation_quat, scale_glam) =
1226 transform.to_matrix().to_scale_rotation_translation();
1227 let translation = [translation.x, translation.y];
1228 let scale = [scale_glam.x, scale_glam.y];
1229 let rotation = if rotation_quat.length_squared() > 0.0 {
1230 let (axis, angle) = rotation_quat.to_axis_angle();
1231 angle * axis.z.signum() } else {
1233 0.0
1234 };
1235 self.push_transform(translation, scale, rotation);
1236 }
1237
1238 fn pop_transform_3d(&mut self) {
1239 self.pop_transform();
1241 }
1242
1243 fn render_scene_node_3d(
1249 &mut self,
1250 position: [f32; 3],
1251 rotation: [f32; 4],
1252 scale: [f32; 3],
1253 color: [f32; 4],
1254 meshes: &[Mesh],
1255 ) {
1256 let transform = cvkg_core::Transform3D {
1257 position: glam::Vec3::from(position),
1258 rotation: glam::Quat::from_xyzw(rotation[0], rotation[1], rotation[2], rotation[3]),
1259 scale: glam::Vec3::from(scale),
1260 };
1261 if meshes.is_empty() {
1263 let h = 0.5f32;
1265 let cube = Mesh {
1266 vertices: vec![
1267 [-h, -h, -h],
1268 [h, -h, -h],
1269 [h, h, -h],
1270 [-h, h, -h],
1271 [-h, -h, h],
1272 [h, -h, h],
1273 [h, h, h],
1274 [-h, h, h],
1275 ],
1276 normals: vec![
1277 [0.0, 0.0, -1.0],
1278 [0.0, 0.0, -1.0],
1279 [0.0, 0.0, -1.0],
1280 [0.0, 0.0, -1.0],
1281 [0.0, 0.0, 1.0],
1282 [0.0, 0.0, 1.0],
1283 [0.0, 0.0, 1.0],
1284 [0.0, 0.0, 1.0],
1285 [0.0, -1.0, 0.0],
1286 [0.0, -1.0, 0.0],
1287 [0.0, -1.0, 0.0],
1288 [0.0, -1.0, 0.0],
1289 [1.0, 0.0, 0.0],
1290 [1.0, 0.0, 0.0],
1291 [1.0, 0.0, 0.0],
1292 [1.0, 0.0, 0.0],
1293 [0.0, 1.0, 0.0],
1294 [0.0, 1.0, 0.0],
1295 [0.0, 1.0, 0.0],
1296 [0.0, 1.0, 0.0],
1297 [-1.0, 0.0, 0.0],
1298 [-1.0, 0.0, 0.0],
1299 [-1.0, 0.0, 0.0],
1300 [-1.0, 0.0, 0.0],
1301 ],
1302 indices: vec![
1303 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, ],
1310 };
1311 let material = cvkg_core::Material3D {
1312 base_color: color,
1313 metallic: 0.0,
1314 roughness: 0.5,
1315 emissive: [0.0, 0.0, 0.0],
1316 opacity: color[3],
1317 };
1318 self.draw_mesh_3d(&cube, &material, &transform);
1319 } else {
1320 let material = cvkg_core::Material3D {
1321 base_color: color,
1322 metallic: 0.0,
1323 roughness: 0.5,
1324 emissive: [0.0, 0.0, 0.0],
1325 opacity: color[3],
1326 };
1327 self.draw_mesh_3d(&meshes[0], &material, &transform);
1328 }
1329 }
1330
1331 fn register_shared_element(&mut self, id: &str, rect: Rect) {
1332 self.shared_elements.put(id.to_string(), rect);
1333 }
1334
1335 fn set_z_index(&mut self, z: f32) {
1336 self.current_z = z;
1337 }
1338
1339 fn set_material(&mut self, material: cvkg_core::DrawMaterial) {
1340 self.current_draw_material = material;
1341 }
1342
1343 fn current_material(&self) -> cvkg_core::DrawMaterial {
1344 self.current_draw_material
1345 }
1346
1347 fn get_z_index(&self) -> f32 {
1348 self.current_z
1349 }
1350
1351 fn request_redraw(&mut self) {
1352 self.redraw_requested = true;
1353 }
1354
1355 fn enter_portal(&mut self, z_index: i32) {
1367 self.current_z = z_index as f32;
1371 }
1372
1373 fn exit_portal(&mut self) {
1377 self.current_z = 0.0;
1378 }
1379
1380 fn push_vnode(&mut self, rect: Rect, name: &'static str) {
1381 self.vnode_stack.push((rect, name));
1382 }
1383
1384 fn pop_vnode(&mut self) {
1385 self.vnode_stack.pop();
1386 }
1387
1388 fn register_handler(
1389 &mut self,
1390 event_type: &str,
1391 handler: std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>,
1392 ) {
1393 self.event_handlers
1394 .entry(event_type.to_string())
1395 .or_insert_with(Vec::new)
1396 .push(handler);
1397 }
1398
1399 fn load_svg(&mut self, name: &str, svg_data: &[u8]) {
1400 GpuRenderer::load_svg(self, name, svg_data);
1401 }
1402
1403 fn draw_svg(&mut self, name: &str, rect: Rect) {
1404 GpuRenderer::draw_svg(self, name, rect, None, 0);
1405 }
1406 fn draw_svg_with_offset(&mut self, name: &str, rect: Rect, animation_time_offset: f32) {
1407 GpuRenderer::draw_svg_with_offset(self, name, rect, None, 0, animation_time_offset);
1408 }
1409
1410 fn draw_svg_with_order(&mut self, name: &str, rect: Rect, draw_order: i32) {
1413 GpuRenderer::draw_svg_with_order(self, name, rect, None, 0, 0.0, draw_order);
1414 }
1415
1416 fn serialize_svg(&mut self, name: &str) -> Result<String, String> {
1417 let tree = self
1418 .svg
1419 .tree_cache
1420 .get(name)
1421 .ok_or_else(|| format!("SVG '{}' not found", name))?;
1422 let config = cvkg_svg_serialize::SerializerConfig::default();
1423 let mut serializer = cvkg_svg_serialize::SvgSerializer::with_config(config);
1424 serializer
1425 .serialize(tree)
1426 .map_err(|e| format!("SVG serialization failed: {}", e))
1427 }
1428
1429 fn apply_svg_filter(
1430 &mut self,
1431 name: &str,
1432 filter_id: &str,
1433 _region: Rect,
1434 ) -> Result<String, String> {
1435 let tree = self
1436 .svg
1437 .tree_cache
1438 .get(name)
1439 .ok_or_else(|| format!("SVG '{}' not found", name))?;
1440 let _filter = Self::find_filter(tree, filter_id)
1441 .ok_or_else(|| format!("Filter '{}' not found in SVG '{}'", filter_id, name))?;
1442 let config = cvkg_svg_serialize::SerializerConfig::default();
1443 let mut serializer = cvkg_svg_serialize::SvgSerializer::with_config(config);
1444 serializer
1445 .serialize(tree)
1446 .map_err(|e| format!("SVG filter serialization failed: {}", e))
1447 }
1448
1449 fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
1450 self.measure_text_impl(text, size)
1451 }
1452
1453 fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
1454 self.draw_text_impl(text, x, y, size, color);
1455 }
1456}
1457
1458impl GpuRenderer {
1461 pub fn clear_event_handlers(&mut self) {
1464 self.event_handlers.clear();
1465 }
1466
1467 pub fn clear_text_cache(&mut self) {
1469 self.clear_text_cache_impl();
1470 }
1471
1472 pub fn get_handlers(
1474 &self,
1475 event_type: &str,
1476 ) -> Option<&Vec<std::sync::Arc<dyn Fn(cvkg_core::Event) + Send + Sync>>> {
1477 self.event_handlers.get(event_type)
1478 }
1479
1480 pub(crate) fn current_transform(&self) -> ([f32; 2], [f32; 2], f32, f32, f32) {
1484 let m = self
1487 .transform_stack
1488 .last()
1489 .copied()
1490 .unwrap_or(glam::Mat3::IDENTITY);
1491 let t = [m.z_axis.x, m.z_axis.y];
1492 let a = m.x_axis.x;
1494 let b = m.x_axis.y;
1495 let c = m.y_axis.x;
1496 let d = m.y_axis.y;
1497 let sx = (a * a + b * b).sqrt();
1498 let sy = (c * c + d * d).sqrt();
1499 let rotation = b.atan2(a);
1500 let skew_x = (a * c + b * d) / (sx * sy); (t, [sx, sy], rotation, skew_x, 0.0)
1503 }
1504
1505 pub fn stroke_path(&mut self, path: &lyon::path::Path, color: [f32; 4], stroke_width: f32) {
1506 self.stroke_path_impl(path, color, stroke_width);
1507 }
1508}