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