1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::component::BackgroundColorComponent;
3use crate::engine::ecs::component::OverlayComponent;
4use crate::engine::ecs::component::{
5 BackgroundComponent, BoundsComponent, ColorComponent, EmissiveComponent,
6 LightQuantizationComponent, MeshComponent, OpacityComponent, RenderableComponent,
7 RendererSettingsComponent, TransparentCutoutComponent, UVComponent,
8};
9
10use crate::engine::ecs::World;
11use crate::engine::ecs::system::System;
12use crate::engine::ecs::system::TransformSystem;
13use crate::engine::graphics::bounds::Aabb;
14use crate::engine::graphics::primitives::{CpuMeshHandle, MaterialHandle, Transform};
15use crate::engine::graphics::{GpuRenderable, VisualWorld};
16use crate::engine::graphics::{MeshUploader, RenderAssets};
17use crate::engine::user_input::InputState;
18use std::collections::{HashMap, VecDeque};
19use std::sync::atomic::{AtomicUsize, Ordering};
20
21#[derive(Debug, Default)]
30pub struct RenderableSystem {
31 renderables: Vec<ComponentId>,
32
33 pending: HashMap<ComponentId, PendingRenderable>,
36
37 pending_uv: HashMap<ComponentId, Vec<[f32; 2]>>,
41
42 uv_mesh_cache: HashMap<UvMeshCacheKey, CpuMeshHandle>,
48
49 pending_color: HashMap<ComponentId, [f32; 4]>,
53
54 pending_opacity: HashMap<ComponentId, PendingOpacity>,
58
59 pending_cutout: HashMap<ComponentId, bool>,
63
64 pending_emissive: HashMap<ComponentId, f32>,
68
69 pending_quant_steps: HashMap<ComponentId, f32>,
73
74 pending_normal_vis: Vec<(ComponentId, ComponentId, CpuMeshHandle, f32)>,
80}
81
82#[derive(Debug, Clone, Copy)]
83struct PendingOpacity {
84 opacity: f32,
85 multiple_layers: bool,
86}
87
88impl Default for PendingOpacity {
89 fn default() -> Self {
90 Self {
91 opacity: 1.0,
92 multiple_layers: false,
93 }
94 }
95}
96
97#[derive(Debug, Clone, Copy)]
98struct EffectiveRenderableStyle {
99 color: [f32; 4],
100 opacity: PendingOpacity,
101 transparent_cutout: bool,
102 background: bool,
103 background_occluded_lit: bool,
104 overlay: bool,
105}
106
107impl Default for EffectiveRenderableStyle {
108 fn default() -> Self {
109 Self {
110 color: [1.0, 1.0, 1.0, 1.0],
111 opacity: PendingOpacity::default(),
112 transparent_cutout: false,
113 background: false,
114 background_occluded_lit: false,
115 overlay: false,
116 }
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
121struct UvMeshCacheKey {
122 base_mesh: CpuMeshHandle,
123 uv_bits: [u32; 8],
128}
129
130#[derive(Debug, Clone)]
131struct PendingRenderable {
132 cpu_mesh: CpuMeshHandle,
133 material: MaterialHandle,
134 renderable_cid: ComponentId,
135 effective_style: EffectiveRenderableStyle,
136
137 mesh_key: Option<String>,
139}
140
141fn clone_mesh_with_uv_overrides(
142 render_assets: &mut RenderAssets,
143 base_mesh: CpuMeshHandle,
144 uvs: &[[f32; 2]],
145) -> Option<CpuMeshHandle> {
146 let mut mesh = render_assets.cpu_mesh(base_mesh)?.clone();
147
148 for (i, v) in mesh.vertices.iter_mut().enumerate() {
149 v.uv = uvs.get(i).copied().unwrap_or([0.0, 0.0]);
150 }
151
152 Some(render_assets.register_mesh(mesh))
153}
154
155impl RenderableSystem {
156 fn uv_clone_audit_enabled() -> bool {
157 std::env::var("CAT_DEBUG_RENDERABLE_UV_CLONES")
158 .ok()
159 .map(|s| {
160 let s = s.trim().to_ascii_lowercase();
161 s == "1" || s == "true" || s == "on" || s == "yes"
162 })
163 .unwrap_or(false)
164 }
165
166 fn material_with_emissive(material: MaterialHandle, emissive_intensity: f32) -> MaterialHandle {
167 let is_emissive = emissive_intensity > 0.0;
168 match (material, is_emissive) {
169 (MaterialHandle::TOON_MESH, true) => MaterialHandle::EMISSIVE_TOON_MESH,
170 (MaterialHandle::SKINNED_TOON_MESH, true) => MaterialHandle::SKINNED_EMISSIVE_TOON_MESH,
171 (MaterialHandle::EMISSIVE_TOON_MESH, false) => MaterialHandle::TOON_MESH,
172 (MaterialHandle::SKINNED_EMISSIVE_TOON_MESH, false) => {
173 MaterialHandle::SKINNED_TOON_MESH
174 }
175 _ => material,
176 }
177 }
178
179 fn immediate_color_child(world: &World, node: ComponentId) -> Option<[f32; 4]> {
180 world.children_of(node).iter().find_map(|&ch| {
181 world
182 .get_component_by_id_as::<ColorComponent>(ch)
183 .map(|c| c.rgba)
184 })
185 }
186
187 fn immediate_opacity_child(world: &World, node: ComponentId) -> Option<PendingOpacity> {
188 world.children_of(node).iter().find_map(|&ch| {
189 world
190 .get_component_by_id_as::<OpacityComponent>(ch)
191 .map(|o| PendingOpacity {
192 opacity: o.opacity,
193 multiple_layers: o.multiple_layers,
194 })
195 })
196 }
197
198 fn immediate_cutout_child(world: &World, node: ComponentId) -> Option<bool> {
199 world.children_of(node).iter().find_map(|&ch| {
200 world
201 .get_component_by_id_as::<TransparentCutoutComponent>(ch)
202 .map(|c| c.enabled)
203 })
204 }
205
206 fn immediate_emissive_child(world: &World, node: ComponentId) -> Option<f32> {
207 world.children_of(node).iter().find_map(|&ch| {
208 world
209 .get_component_by_id_as::<EmissiveComponent>(ch)
210 .map(|e| e.intensity.max(0.0))
211 })
212 }
213
214 fn resolve_effective_renderable_style(
215 world: &World,
216 renderable_cid: ComponentId,
217 ) -> EffectiveRenderableStyle {
218 let mut style = EffectiveRenderableStyle::default();
219 let mut color_resolved = false;
220 let mut opacity_resolved = false;
221 let mut cutout_resolved = false;
222
223 if let Some(rgba) = Self::immediate_color_child(world, renderable_cid) {
224 style.color = rgba;
225 color_resolved = true;
226 }
227
228 if let Some(opacity) = Self::immediate_opacity_child(world, renderable_cid) {
229 style.opacity = opacity;
230 opacity_resolved = true;
231 }
232
233 if let Some(enabled) = Self::immediate_cutout_child(world, renderable_cid) {
234 style.transparent_cutout = enabled;
235 cutout_resolved = true;
236 }
237
238 let mut cur = renderable_cid;
239 while let Some(parent) = world.parent_of(cur) {
240 if !color_resolved {
241 if let Some(rgba) = Self::immediate_color_child(world, parent) {
242 style.color = rgba;
243 color_resolved = true;
244 }
245 }
246
247 if !opacity_resolved {
248 if let Some(opacity) = Self::immediate_opacity_child(world, parent) {
249 style.opacity = opacity;
250 opacity_resolved = true;
251 }
252 }
253
254 if !cutout_resolved {
255 if let Some(enabled) = Self::immediate_cutout_child(world, parent) {
256 style.transparent_cutout = enabled;
257 cutout_resolved = true;
258 }
259 }
260
261 if !style.background {
262 if let Some(bg) = world.get_component_by_id_as::<BackgroundComponent>(parent) {
263 style.background = true;
264 style.background_occluded_lit = bg.occlusion_and_lighting;
265 }
266 }
267
268 if !style.overlay
269 && world
270 .get_component_by_id_as::<OverlayComponent>(parent)
271 .is_some()
272 {
273 style.overlay = true;
274 }
275
276 cur = parent;
277 }
278
279 style
280 }
281
282 fn clone_mesh_with_uv_overrides_cached(
283 &mut self,
284 render_assets: &mut RenderAssets,
285 base_mesh: CpuMeshHandle,
286 uvs: &[[f32; 2]],
287 ) -> Option<CpuMeshHandle> {
288 let vertex_count = render_assets.cpu_mesh(base_mesh)?.vertices.len();
290 if vertex_count == 4 && uvs.len() >= 4 {
291 let mut uv_bits = [0u32; 8];
292 for i in 0..4 {
293 uv_bits[i * 2] = uvs[i][0].to_bits();
294 uv_bits[i * 2 + 1] = uvs[i][1].to_bits();
295 }
296
297 let key = UvMeshCacheKey { base_mesh, uv_bits };
298 if let Some(&cached) = self.uv_mesh_cache.get(&key) {
299 return Some(cached);
300 }
301
302 let new_mesh = clone_mesh_with_uv_overrides(render_assets, base_mesh, uvs)?;
303 self.uv_mesh_cache.insert(key, new_mesh);
304 return Some(new_mesh);
305 }
306
307 clone_mesh_with_uv_overrides(render_assets, base_mesh, uvs)
309 }
310}
311
312impl RenderableSystem {
313 fn apply_pending_emissive_updates_to_registered_renderables(
314 &mut self,
315 world: &mut World,
316 visuals: &mut VisualWorld,
317 ) {
318 let keys: Vec<ComponentId> = self.pending_emissive.keys().copied().collect();
319 for renderable_cid in keys {
320 let Some(renderable_comp) =
321 world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
322 else {
323 let _ = self.pending_emissive.remove(&renderable_cid);
324 continue;
325 };
326 let Some(handle) = renderable_comp.get_handle() else {
327 continue;
328 };
329
330 let Some(emissive) = self.pending_emissive.get(&renderable_cid).copied() else {
331 continue;
332 };
333
334 let _ = visuals.update_emissive(handle, emissive);
335
336 if let Some(inst) = visuals.instance(handle) {
337 let material = Self::material_with_emissive(inst.renderable.material, emissive);
338 let _ = visuals.update_material(handle, material);
339 }
340 let _ = self.pending_emissive.remove(&renderable_cid);
341 }
342 }
343
344 fn apply_pending_quant_updates_to_registered_renderables(
345 &mut self,
346 world: &mut World,
347 visuals: &mut VisualWorld,
348 ) {
349 let keys: Vec<ComponentId> = self.pending_quant_steps.keys().copied().collect();
350 for renderable_cid in keys {
351 let Some(renderable_comp) =
352 world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
353 else {
354 let _ = self.pending_quant_steps.remove(&renderable_cid);
355 continue;
356 };
357
358 let Some(handle) = renderable_comp.get_handle() else {
359 continue;
360 };
361
362 let Some(quant_steps) = self.pending_quant_steps.get(&renderable_cid).copied() else {
363 continue;
364 };
365
366 let _ = visuals.update_quant_steps(handle, quant_steps);
367 let _ = self.pending_quant_steps.remove(&renderable_cid);
368 }
369 }
370
371 fn apply_pending_color_updates_to_registered_renderables(
372 &mut self,
373 world: &mut World,
374 visuals: &mut VisualWorld,
375 ) {
376 let color_keys: Vec<ComponentId> = self.pending_color.keys().copied().collect();
377 for renderable_cid in color_keys {
378 let Some(renderable_comp) =
379 world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
380 else {
381 let _ = self.pending_color.remove(&renderable_cid);
382 continue;
383 };
384 let Some(handle) = renderable_comp.get_handle() else {
385 continue;
387 };
388
389 let Some(color) = self.pending_color.get(&renderable_cid).copied() else {
390 continue;
391 };
392
393 let _ = visuals.update_color(handle, color);
394 let _ = self.pending_color.remove(&renderable_cid);
395 }
396 }
397
398 fn apply_pending_opacity_updates_to_registered_renderables(
399 &mut self,
400 world: &mut World,
401 visuals: &mut VisualWorld,
402 ) {
403 let keys: Vec<ComponentId> = self.pending_opacity.keys().copied().collect();
404 for renderable_cid in keys {
405 let Some(renderable_comp) =
406 world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
407 else {
408 let _ = self.pending_opacity.remove(&renderable_cid);
409 continue;
410 };
411
412 let Some(handle) = renderable_comp.get_handle() else {
413 continue;
415 };
416
417 let Some(pending) = self.pending_opacity.get(&renderable_cid).copied() else {
418 continue;
419 };
420
421 let _ = visuals.update_opacity_state(handle, pending.opacity, pending.multiple_layers);
422 let _ = self.pending_opacity.remove(&renderable_cid);
423 }
424 }
425
426 fn apply_pending_cutout_updates_to_registered_renderables(
427 &mut self,
428 world: &mut World,
429 visuals: &mut VisualWorld,
430 ) {
431 let keys: Vec<ComponentId> = self.pending_cutout.keys().copied().collect();
432 for renderable_cid in keys {
433 let Some(renderable_comp) =
434 world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
435 else {
436 let _ = self.pending_cutout.remove(&renderable_cid);
437 continue;
438 };
439
440 let Some(handle) = renderable_comp.get_handle() else {
441 continue;
443 };
444
445 let Some(enabled) = self.pending_cutout.get(&renderable_cid).copied() else {
446 continue;
447 };
448
449 let _ = visuals.update_transparent_cutout(handle, enabled);
450 let _ = self.pending_cutout.remove(&renderable_cid);
451 }
452 }
453
454 fn apply_pending_uv_updates_to_registered_renderables(
455 &mut self,
456 world: &mut World,
457 visuals: &mut VisualWorld,
458 render_assets: &mut RenderAssets,
459 uploader: &mut dyn MeshUploader,
460 ) {
461 let uv_keys: Vec<ComponentId> = self.pending_uv.keys().copied().collect();
463 for renderable_cid in uv_keys {
464 let Some(renderable_comp) =
465 world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
466 else {
467 let _ = self.pending_uv.remove(&renderable_cid);
468 continue;
469 };
470 let Some(handle) = renderable_comp.get_handle() else {
471 continue;
473 };
474
475 let base_mesh = renderable_comp.renderable.mesh;
476 let material = renderable_comp.renderable.material;
477
478 let Some(uvs) = self.pending_uv.get(&renderable_cid).cloned() else {
479 continue;
480 };
481
482 let Some(new_mesh) =
483 self.clone_mesh_with_uv_overrides_cached(render_assets, base_mesh, &uvs)
484 else {
485 continue;
486 };
487
488 let mesh = match render_assets.gpu_mesh_handle(uploader, new_mesh) {
489 Ok(h) => h,
490 Err(_err) => continue,
491 };
492
493 let Some(model) = TransformSystem::world_model(world, renderable_cid) else {
494 continue;
495 };
496 let transform = Transform {
497 model,
498 matrix_world: model,
499 ..Default::default()
500 };
501
502 let gpu_r = GpuRenderable { mesh, material };
503 let _ = visuals.update(handle, gpu_r, transform);
504
505 if let Some(renderable_comp) =
506 world.get_component_by_id_as_mut::<RenderableComponent>(renderable_cid)
507 {
508 renderable_comp.renderable.mesh = new_mesh;
509 }
510
511 let _ = self.pending_uv.remove(&renderable_cid);
512 }
513 }
514
515 pub fn register_color(
516 &mut self,
517 world: &mut World,
518 _visuals: &mut VisualWorld,
519 component: ComponentId,
520 ) {
521 let Some(color_comp) = world.get_component_by_id_as::<ColorComponent>(component) else {
522 return;
523 };
524 let mut cur = component;
526 let mut renderable_cid: Option<ComponentId> = None;
527 while let Some(parent) = world.parent_of(cur) {
528 if world
529 .get_component_by_id_as::<RenderableComponent>(parent)
530 .is_some()
531 {
532 renderable_cid = Some(parent);
533 break;
534 }
535 cur = parent;
536 }
537
538 if let Some(renderable_cid) = renderable_cid {
540 self.pending_color.insert(renderable_cid, color_comp.rgba);
541 return;
542 }
543
544 let mut q = VecDeque::new();
547
548 let start = world.parent_of(component).unwrap_or(component);
552 q.push_back(start);
553
554 while let Some(node) = q.pop_front() {
555 for &ch in world.children_of(node).iter() {
556 q.push_back(ch);
557 }
558
559 if world
560 .get_component_by_id_as::<RenderableComponent>(node)
561 .is_none()
562 {
563 continue;
564 }
565
566 if Self::immediate_color_child(world, node).is_some() {
568 continue;
569 }
570
571 self.pending_color.insert(node, color_comp.rgba);
572 }
573 }
574
575 pub fn register_opacity(
576 &mut self,
577 world: &mut World,
578 _visuals: &mut VisualWorld,
579 component: ComponentId,
580 ) {
581 let Some(opacity_comp) = world.get_component_by_id_as::<OpacityComponent>(component) else {
582 return;
583 };
584
585 let pending = PendingOpacity {
586 opacity: opacity_comp.opacity,
587 multiple_layers: opacity_comp.multiple_layers,
588 };
589
590 let mut cur = component;
592 let mut renderable_cid: Option<ComponentId> = None;
593 while let Some(parent) = world.parent_of(cur) {
594 if world
595 .get_component_by_id_as::<RenderableComponent>(parent)
596 .is_some()
597 {
598 renderable_cid = Some(parent);
599 break;
600 }
601 cur = parent;
602 }
603
604 if let Some(renderable_cid) = renderable_cid {
606 self.pending_opacity.insert(renderable_cid, pending);
607 return;
608 }
609
610 let mut q = VecDeque::new();
613 q.push_back(component);
614
615 while let Some(node) = q.pop_front() {
616 for &ch in world.children_of(node).iter() {
617 q.push_back(ch);
618 }
619
620 if world
621 .get_component_by_id_as::<RenderableComponent>(node)
622 .is_none()
623 {
624 continue;
625 }
626
627 if Self::immediate_opacity_child(world, node).is_some() {
629 continue;
630 }
631
632 self.pending_opacity.insert(node, pending);
633 }
634 }
635
636 pub fn register_transparent_cutout(
637 &mut self,
638 world: &mut World,
639 visuals: &mut VisualWorld,
640 component: ComponentId,
641 ) {
642 let Some(cutout_comp) =
643 world.get_component_by_id_as::<TransparentCutoutComponent>(component)
644 else {
645 return;
646 };
647
648 let pending = cutout_comp.enabled;
649
650 let mut cur = component;
652 let mut renderable_cid: Option<ComponentId> = None;
653 while let Some(parent) = world.parent_of(cur) {
654 if world
655 .get_component_by_id_as::<RenderableComponent>(parent)
656 .is_some()
657 {
658 renderable_cid = Some(parent);
659 break;
660 }
661 cur = parent;
662 }
663
664 if let Some(renderable_cid) = renderable_cid {
666 self.pending_cutout.insert(renderable_cid, pending);
667
668 if let Some(renderable_comp) =
670 world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
671 {
672 if let Some(handle) = renderable_comp.get_handle() {
673 let _ = visuals.update_transparent_cutout(handle, pending);
674 let _ = self.pending_cutout.remove(&renderable_cid);
675 }
676 }
677
678 return;
679 }
680
681 let mut q = VecDeque::new();
684 q.push_back(component);
685
686 while let Some(node) = q.pop_front() {
687 for &ch in world.children_of(node).iter() {
688 q.push_back(ch);
689 }
690
691 if world
692 .get_component_by_id_as::<RenderableComponent>(node)
693 .is_none()
694 {
695 continue;
696 }
697
698 if Self::immediate_cutout_child(world, node).is_some() {
700 continue;
701 }
702
703 self.pending_cutout.insert(node, pending);
704 if let Some(renderable_comp) = world.get_component_by_id_as::<RenderableComponent>(node)
705 {
706 if let Some(handle) = renderable_comp.get_handle() {
707 let _ = visuals.update_transparent_cutout(handle, pending);
708 let _ = self.pending_cutout.remove(&node);
709 }
710 }
711 }
712 }
713
714 pub fn register_light_quantization(
715 &mut self,
716 world: &mut World,
717 visuals: &mut VisualWorld,
718 component: ComponentId,
719 ) {
720 let Some(q_comp) = world.get_component_by_id_as::<LightQuantizationComponent>(component)
721 else {
722 return;
723 };
724
725 let mut cur = component;
727 let mut renderable_cid: Option<ComponentId> = None;
728 while let Some(parent) = world.parent_of(cur) {
729 if world
730 .get_component_by_id_as::<RenderableComponent>(parent)
731 .is_some()
732 {
733 renderable_cid = Some(parent);
734 break;
735 }
736 cur = parent;
737 }
738
739 let Some(renderable_cid) = renderable_cid else {
740 return;
741 };
742
743 self.pending_quant_steps
744 .insert(renderable_cid, q_comp.quant_steps);
745
746 if let Some(renderable_comp) =
748 world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
749 {
750 if let Some(handle) = renderable_comp.get_handle() {
751 let _ = visuals.update_quant_steps(handle, q_comp.quant_steps);
752 let _ = self.pending_quant_steps.remove(&renderable_cid);
753 }
754 }
755 }
756
757 pub fn register_emissive(
758 &mut self,
759 world: &mut World,
760 _visuals: &mut VisualWorld,
761 component: ComponentId,
762 ) {
763 let Some(emissive_comp) = world.get_component_by_id_as::<EmissiveComponent>(component)
764 else {
765 return;
766 };
767
768 let emissive = emissive_comp.intensity.max(0.0);
769
770 if let Some(parent) = world.parent_of(component) {
772 if world
773 .get_component_by_id_as::<RenderableComponent>(parent)
774 .is_some()
775 {
776 self.pending_emissive.insert(parent, emissive);
777 return;
778 }
779 }
780
781 let start = world.parent_of(component).unwrap_or(component);
785 let mut q = VecDeque::new();
786 q.push_back(start);
787
788 while let Some(node) = q.pop_front() {
789 for &ch in world.children_of(node).iter() {
790 q.push_back(ch);
791 }
792
793 if world
794 .get_component_by_id_as::<RenderableComponent>(node)
795 .is_none()
796 {
797 continue;
798 }
799
800 if Self::immediate_emissive_child(world, node).is_some() {
801 continue;
802 }
803
804 self.pending_emissive.insert(node, emissive);
805 }
806 }
807
808 pub fn register_uv(
809 &mut self,
810 world: &mut World,
811 _visuals: &mut VisualWorld,
812 component: ComponentId,
813 ) {
814 let Some(uv_comp) = world.get_component_by_id_as::<UVComponent>(component) else {
815 return;
816 };
817 let mut cur = component;
819 let mut renderable_cid: Option<ComponentId> = None;
820 while let Some(parent) = world.parent_of(cur) {
821 if world
822 .get_component_by_id_as::<RenderableComponent>(parent)
823 .is_some()
824 {
825 renderable_cid = Some(parent);
826 break;
827 }
828 cur = parent;
829 }
830 let Some(renderable_cid) = renderable_cid else {
831 return;
832 };
833
834 self.pending_uv.insert(renderable_cid, uv_comp.uvs.clone());
837 }
838
839 pub fn register_background_color(
840 &mut self,
841 world: &mut World,
842 visuals: &mut VisualWorld,
843 component: ComponentId,
844 ) {
845 if world
846 .get_component_by_id_as::<BackgroundColorComponent>(component)
847 .is_none()
848 {
849 return;
850 }
851
852 const DEFAULT: [f32; 4] = [0.0, 0.0, 0.0, 1.0];
853 let rgba = world
854 .children_of(component)
855 .iter()
856 .find_map(|&ch| {
857 world
858 .get_component_by_id_as::<ColorComponent>(ch)
859 .map(|c| c.rgba)
860 })
861 .unwrap_or(DEFAULT);
862
863 visuals.set_clear_color(rgba);
865 }
866
867 pub fn register_renderer_settings(
868 &mut self,
869 world: &mut World,
870 visuals: &mut VisualWorld,
871 component: ComponentId,
872 ) {
873 let Some(settings) = world.get_component_by_id_as::<RendererSettingsComponent>(component)
874 else {
875 return;
876 };
877
878 visuals.set_renderer_msaa_mode(settings.msaa_mode());
880 visuals.set_preferred_window_size(settings.window_size);
881 }
882
883 pub fn register_normal_vis(&mut self, world: &World, component: ComponentId) {
890 use crate::engine::ecs::component::{NormalVisualisationComponent, RenderableComponent};
891
892 let Some(nv) = world.get_component_by_id_as::<NormalVisualisationComponent>(component)
893 else {
894 return;
895 };
896 let thickness = nv.thickness;
897
898 let mut cur = component;
900 let mut parent_renderable: Option<(ComponentId, CpuMeshHandle)> = None;
901 while let Some(p) = world.parent_of(cur) {
902 if let Some(r) = world.get_component_by_id_as::<RenderableComponent>(p) {
903 parent_renderable = Some((p, r.renderable.base_mesh));
904 break;
905 }
906 cur = p;
907 }
908
909 let Some((renderable_id, base_mesh)) = parent_renderable else {
910 return;
911 };
912
913 self.pending_normal_vis
914 .push((component, renderable_id, base_mesh, thickness));
915 }
916
917 pub fn register_renderable(
921 &mut self,
922 world: &mut World,
923 visuals: &mut VisualWorld,
924 component: ComponentId,
925 ) {
926 if !self.renderables.iter().any(|c| *c == component) {
927 self.renderables.push(component);
928 }
929
930 self.register_renderable_from_world(world, visuals, component);
931 }
932
933 pub fn remove_renderable(
934 &mut self,
935 world: &mut World,
936 visuals: &mut VisualWorld,
937 component: ComponentId,
938 ) {
939 self.renderables.retain(|&c| c != component);
940
941 let _ = self.pending.remove(&component);
942 let _ = self.pending_uv.remove(&component);
943 let _ = self.pending_color.remove(&component);
944 let _ = self.pending_opacity.remove(&component);
945 let _ = self.pending_emissive.remove(&component);
946 let _ = self.pending_quant_steps.remove(&component);
947
948 if let Some(r) = world.get_component_by_id_as_mut::<RenderableComponent>(component) {
949 if let Some(handle) = r.handle.take() {
950 let _ = visuals.remove(handle);
951 }
952 }
953 }
954
955 pub fn register_renderable_from_world(
957 &mut self,
958 world: &mut World,
959 visuals: &mut VisualWorld,
960 component: ComponentId,
961 ) {
962 {
964 let Some(renderable_comp) =
965 world.get_component_by_id_as::<RenderableComponent>(component)
966 else {
967 return;
968 };
969 if renderable_comp.get_handle().is_some() {
970 return;
971 }
972 }
973
974 let Some(renderable_comp) = world.get_component_by_id_as::<RenderableComponent>(component)
976 else {
977 return;
978 };
979
980 let mesh_key = world
981 .children_of(component)
982 .iter()
983 .copied()
984 .find_map(|cid| {
985 world
986 .get_component_by_id_as::<MeshComponent>(cid)
987 .map(|m| m.key.clone())
988 });
989
990 self.pending.insert(
991 component,
992 PendingRenderable {
993 cpu_mesh: renderable_comp.renderable.mesh,
994 material: renderable_comp.renderable.material,
995 renderable_cid: component,
996 effective_style: Self::resolve_effective_renderable_style(world, component),
997 mesh_key,
998 },
999 );
1000
1001 let _ = visuals;
1003 }
1004
1005 pub fn flush_pending(
1008 &mut self,
1009 world: &mut World,
1010 visuals: &mut VisualWorld,
1011 render_assets: &mut RenderAssets,
1012 uploader: &mut dyn MeshUploader,
1013 queue: &mut crate::engine::ecs::CommandQueue,
1014 ) -> bool {
1015 let parse_bool_env = |name: &str| {
1016 std::env::var(name)
1017 .ok()
1018 .map(|s| {
1019 let s = s.trim().to_ascii_lowercase();
1020 s == "1" || s == "true" || s == "on" || s == "yes"
1021 })
1022 .unwrap_or(false)
1023 };
1024
1025 let debug_mesh_stats = parse_bool_env("CAT_DEBUG_RENDERABLE_MESH_STATS");
1026 let debug_mesh_stats_all = parse_bool_env("CAT_DEBUG_RENDERABLE_MESH_STATS_ALL");
1027 static MESH_STATS_LOG_COUNT: AtomicUsize = AtomicUsize::new(0);
1028 let mut inserted_any = false;
1029
1030 let keys: Vec<ComponentId> = self.pending.keys().copied().collect();
1037 for key in keys {
1038 let Some(p) = self.pending.get(&key).cloned() else {
1039 continue;
1040 };
1041 let effective_style = Self::resolve_effective_renderable_style(world, p.renderable_cid);
1042 if let Some(pending) = self.pending.get_mut(&key) {
1043 pending.effective_style = effective_style;
1044 }
1045
1046 let mut cpu_mesh = p.cpu_mesh;
1047
1048 if let Some(mesh_key) = p.mesh_key.as_deref() {
1050 let Some(imported) = render_assets.imported_mesh(mesh_key) else {
1051 continue;
1052 };
1053 cpu_mesh = imported;
1054 if let Some(pending) = self.pending.get_mut(&key) {
1055 pending.cpu_mesh = cpu_mesh;
1056 }
1057 if let Some(renderable_comp) =
1058 world.get_component_by_id_as_mut::<RenderableComponent>(p.renderable_cid)
1059 {
1060 renderable_comp.renderable.mesh = cpu_mesh;
1061 renderable_comp.renderable.base_mesh = cpu_mesh;
1062 }
1063 }
1064
1065 if let Some(uvs) = self.pending_uv.get(&p.renderable_cid).cloned() {
1066 if let Some(new_mesh) =
1067 self.clone_mesh_with_uv_overrides_cached(render_assets, cpu_mesh, &uvs)
1068 {
1069 let uv_base_mesh = cpu_mesh;
1070 if Self::uv_clone_audit_enabled() {
1071 let (verts, indices) = render_assets
1072 .cpu_mesh(new_mesh)
1073 .map(|m| (m.vertices.len(), m.indices_u32.len()))
1074 .unwrap_or((0, 0));
1075 println!(
1076 "[RenderableSystem][audit] uv_clone renderable={:?} base_mesh={:?} new_mesh={:?} verts={} indices={} repeated_work=true",
1077 p.renderable_cid, uv_base_mesh, new_mesh, verts, indices
1078 );
1079 }
1080 cpu_mesh = new_mesh;
1081 if let Some(pending) = self.pending.get_mut(&key) {
1082 pending.cpu_mesh = cpu_mesh;
1083 }
1084 if let Some(renderable_comp) =
1085 world.get_component_by_id_as_mut::<RenderableComponent>(p.renderable_cid)
1086 {
1087 renderable_comp.renderable.mesh = cpu_mesh;
1088 renderable_comp.renderable.base_mesh = uv_base_mesh;
1089 }
1090 }
1091 }
1092
1093 cache_resolved_mesh_bounds(world, render_assets, p.renderable_cid, cpu_mesh);
1094
1095 let mesh = match render_assets.gpu_mesh_handle(uploader, cpu_mesh) {
1097 Ok(h) => h,
1098 Err(_err) => continue,
1099 };
1100
1101 if debug_mesh_stats {
1102 let (vcount, icount, has_skin) = render_assets
1103 .cpu_mesh(cpu_mesh)
1104 .map(|m| {
1105 (
1106 m.vertices.len(),
1107 m.indices_u32.len(),
1108 m.joints0.is_some() && m.weights0.is_some(),
1109 )
1110 })
1111 .unwrap_or((0, 0, false));
1112
1113 let key_str = p.mesh_key.as_deref().unwrap_or("<no mesh_key>");
1114 let should_log = debug_mesh_stats_all || key_str != "<no mesh_key>" || has_skin;
1115
1116 if should_log {
1117 let limit = std::env::var("CAT_DEBUG_RENDERABLE_MESH_STATS_LIMIT")
1118 .ok()
1119 .and_then(|s| s.trim().parse::<usize>().ok())
1120 .unwrap_or(50);
1121 let n = MESH_STATS_LOG_COUNT.fetch_add(1, Ordering::Relaxed);
1122 if n < limit {
1123 println!(
1124 "[RenderableSystem] renderable={:?} material={:?} mesh_key='{}' cpu_mesh={:?} gpu_mesh={:?} verts={} indices={} skinned_attrs={}",
1125 p.renderable_cid,
1126 p.material,
1127 key_str,
1128 cpu_mesh,
1129 mesh,
1130 vcount,
1131 icount,
1132 has_skin
1133 );
1134 }
1135 }
1136 }
1137
1138 let gpu_r = GpuRenderable {
1139 mesh,
1140 material: p.material,
1141 };
1142
1143 let model = match TransformSystem::world_model(world, p.renderable_cid) {
1144 Some(m) => m,
1145 None => {
1146 self.pending.remove(&key);
1147 continue;
1148 }
1149 };
1150
1151 let transform = Transform {
1152 model,
1153 matrix_world: model,
1154 ..Default::default()
1155 };
1156
1157 let color = self
1158 .pending_color
1159 .get(&p.renderable_cid)
1160 .copied()
1161 .unwrap_or(effective_style.color);
1162
1163 let opacity = self
1164 .pending_opacity
1165 .get(&p.renderable_cid)
1166 .copied()
1167 .unwrap_or(effective_style.opacity);
1168
1169 let transparent_cutout = self
1170 .pending_cutout
1171 .get(&p.renderable_cid)
1172 .copied()
1173 .unwrap_or(effective_style.transparent_cutout);
1174
1175 let emissive = self
1176 .pending_emissive
1177 .get(&p.renderable_cid)
1178 .copied()
1179 .unwrap_or(0.0);
1180
1181 let gpu_r = GpuRenderable::new(
1182 gpu_r.mesh,
1183 Self::material_with_emissive(gpu_r.material, emissive),
1184 );
1185
1186 let quant_steps = self
1187 .pending_quant_steps
1188 .get(&p.renderable_cid)
1189 .copied()
1190 .unwrap_or_else(|| match p.material {
1191 MaterialHandle::TOON_MESH => 3.0,
1192 MaterialHandle::UNLIT_MESH => 1.0,
1193 _ => 3.0,
1194 });
1195
1196 let handle = visuals.register(
1197 p.renderable_cid,
1198 gpu_r,
1199 transform,
1200 color,
1201 opacity.opacity,
1202 opacity.multiple_layers,
1203 transparent_cutout,
1204 effective_style.background,
1205 effective_style.background_occluded_lit,
1206 effective_style.overlay,
1207 emissive,
1208 None,
1209 quant_steps,
1210 );
1211 if let Some(renderable_comp) =
1212 world.get_component_by_id_as_mut::<RenderableComponent>(p.renderable_cid)
1213 {
1214 renderable_comp.handle = Some(handle);
1215 }
1216
1217 let _ = self.pending_uv.remove(&p.renderable_cid);
1219
1220 let _ = self.pending_color.remove(&p.renderable_cid);
1222
1223 let _ = self.pending_opacity.remove(&p.renderable_cid);
1225
1226 let _ = self.pending_cutout.remove(&p.renderable_cid);
1228
1229 let _ = self.pending_emissive.remove(&p.renderable_cid);
1231
1232 let _ = self.pending_quant_steps.remove(&p.renderable_cid);
1234
1235 self.pending.remove(&key);
1237 inserted_any = true;
1238 }
1239
1240 self.apply_pending_uv_updates_to_registered_renderables(
1241 world,
1242 visuals,
1243 render_assets,
1244 uploader,
1245 );
1246 self.apply_pending_color_updates_to_registered_renderables(world, visuals);
1247 self.apply_pending_opacity_updates_to_registered_renderables(world, visuals);
1248 self.apply_pending_cutout_updates_to_registered_renderables(world, visuals);
1249 self.apply_pending_emissive_updates_to_registered_renderables(world, visuals);
1250 self.apply_pending_quant_updates_to_registered_renderables(world, visuals);
1251
1252 self.spawn_pending_normal_vis(world, render_assets, queue);
1253
1254 inserted_any
1255 }
1256
1257 fn spawn_pending_normal_vis(
1258 &mut self,
1259 world: &mut World,
1260 render_assets: &RenderAssets,
1261 queue: &mut crate::engine::ecs::CommandQueue,
1262 ) {
1263 use crate::engine::ecs::component::{
1264 ColorComponent, EmissiveComponent, NormalVisualisationComponent, RenderableComponent,
1265 TransformComponent,
1266 };
1267 use crate::engine::graphics::primitives::{CpuMeshHandle, MaterialHandle, Renderable};
1268
1269 let pending = std::mem::take(&mut self.pending_normal_vis);
1270 for (nv_id, _renderable_id, base_mesh, thickness) in pending {
1271 if let Some(nv) = world.get_component_by_id_as::<NormalVisualisationComponent>(nv_id) {
1273 if !nv.spawned_roots.is_empty() {
1274 continue;
1275 }
1276 } else {
1277 continue;
1278 }
1279
1280 let Some(cpu_mesh) = render_assets.cpu_mesh(base_mesh) else {
1281 self.pending_normal_vis
1283 .push((nv_id, _renderable_id, base_mesh, thickness));
1284 continue;
1285 };
1286
1287 let half_height = thickness * 5.0;
1288 let mut spawned_roots: Vec<ComponentId> = Vec::new();
1289
1290 for vertex in &cpu_mesh.vertices {
1291 let pos = vertex.pos;
1292 let n = vertex.normal;
1293
1294 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1296 let n = if len > 1e-6 {
1297 [n[0] / len, n[1] / len, n[2] / len]
1298 } else {
1299 [0.0, 1.0, 0.0]
1300 };
1301
1302 let cx = pos[0] + n[0] * half_height;
1304 let cy = pos[1] + n[1] * half_height;
1305 let cz = pos[2] + n[2] * half_height;
1306
1307 let quat = crate::utils::math::shortest_arc_quat([0.0, 1.0, 0.0], n);
1309
1310 let t_id = world.add_component(
1311 TransformComponent::new()
1312 .with_position(cx, cy, cz)
1313 .with_rotation_quat(quat)
1314 .with_scale(thickness, thickness * 10.0, thickness),
1315 );
1316 let r_id = world.add_component(RenderableComponent::new(Renderable::new(
1317 CpuMeshHandle::CUBE,
1318 MaterialHandle::TOON_MESH,
1319 )));
1320 let c_id = world.add_component(ColorComponent::rgba(0.0, 1.0, 1.0, 1.0));
1321 let e_id = world.add_component(EmissiveComponent::on());
1322
1323 let _ = world.add_child(nv_id, t_id);
1324 let _ = world.add_child(t_id, r_id);
1325 let _ = world.add_child(r_id, c_id);
1326 let _ = world.add_child(r_id, e_id);
1327
1328 world.init_component_tree(t_id, queue);
1329 spawned_roots.push(t_id);
1330 }
1331
1332 if let Some(nv) =
1333 world.get_component_by_id_as_mut::<NormalVisualisationComponent>(nv_id)
1334 {
1335 nv.spawned_roots = spawned_roots;
1336 }
1337 }
1338 }
1339}
1340
1341fn cache_resolved_mesh_bounds(
1342 world: &mut World,
1343 render_assets: &RenderAssets,
1344 renderable: ComponentId,
1345 mesh: CpuMeshHandle,
1346) {
1347 let Some(local) = render_assets.cpu_mesh(mesh).and_then(|cpu_mesh| {
1348 let positions: Vec<[f32; 3]> = cpu_mesh.vertices.iter().map(|vertex| vertex.pos).collect();
1349 Aabb::from_points(&positions)
1350 }) else {
1351 return;
1352 };
1353
1354 let existing_bounds = world
1355 .children_of(renderable)
1356 .iter()
1357 .copied()
1358 .find(|&child| {
1359 world
1360 .get_component_by_id_as::<BoundsComponent>(child)
1361 .is_some()
1362 });
1363 if let Some(bounds) =
1364 existing_bounds.and_then(|child| world.get_component_by_id_as_mut::<BoundsComponent>(child))
1365 {
1366 bounds.local = local;
1367 return;
1368 }
1369
1370 let bounds = world.add_component(BoundsComponent::new(local));
1371 let _ = world.add_child(renderable, bounds);
1372}
1373
1374impl System for RenderableSystem {
1375 fn tick(
1376 &mut self,
1377 _world: &mut World,
1378 _visuals: &mut VisualWorld,
1379 _input: &InputState,
1380 _dt_sec: f32,
1381 ) {
1382 }
1390}
1391
1392#[cfg(test)]
1393mod tests {
1394 use super::RenderableSystem;
1395 use crate::engine::ecs::CommandQueue;
1396 use crate::engine::ecs::World;
1397 use crate::engine::ecs::component::{
1398 BackgroundComponent, ColorComponent, EmissiveComponent, OpacityComponent, OverlayComponent,
1399 RenderableComponent, TextComponent, TransformComponent, TransparentCutoutComponent,
1400 };
1401 use crate::engine::graphics::primitives::MeshHandle;
1402 use crate::engine::graphics::{CpuMesh, MeshUploader, RenderAssets, VisualWorld};
1403
1404 #[derive(Default)]
1405 struct TestUploader {
1406 next_mesh: u32,
1407 }
1408
1409 impl MeshUploader for TestUploader {
1410 fn upload_mesh(
1411 &mut self,
1412 _mesh: &CpuMesh,
1413 ) -> Result<MeshHandle, Box<dyn std::error::Error>> {
1414 let handle = MeshHandle(self.next_mesh);
1415 self.next_mesh += 1;
1416 Ok(handle)
1417 }
1418 }
1419
1420 #[test]
1421 fn effective_style_preserves_renderable_local_and_ancestor_semantics() {
1422 let mut world = World::default();
1423
1424 let text = world.add_component(TextComponent::new("item"));
1425 let text_color = world.add_component(ColorComponent::rgba(0.2, 0.3, 0.4, 1.0));
1426 let text_opacity = world.add_component(OpacityComponent::new().with_opacity(0.4));
1427 let overlay = world.add_component(OverlayComponent::new());
1428 let background =
1429 world.add_component(BackgroundComponent::new().with_occlusion_and_lighting());
1430 let overlay_host = world.add_component(TransformComponent::new());
1431 let glyph_t = world.add_component(TransformComponent::new());
1432 let glyph_r = world.add_component(RenderableComponent::square());
1433 let local_color = world.add_component(ColorComponent::rgba(0.9, 0.8, 0.7, 1.0));
1434 let cutout = world.add_component(TransparentCutoutComponent::new());
1435
1436 let _ = world.add_child(text, text_color);
1437 let _ = world.add_child(text, text_opacity);
1438 let _ = world.add_child(text, background);
1439 let _ = world.add_child(background, overlay);
1440 let _ = world.add_child(overlay, overlay_host);
1441 let _ = world.add_child(overlay_host, glyph_t);
1442 let _ = world.add_child(glyph_t, glyph_r);
1443 let _ = world.add_child(glyph_r, local_color);
1444 let _ = world.add_child(glyph_r, cutout);
1445
1446 let style = RenderableSystem::resolve_effective_renderable_style(&world, glyph_r);
1447
1448 assert_eq!(style.color, [0.9, 0.8, 0.7, 1.0]);
1449 assert_eq!(style.opacity.opacity, 0.4);
1450 assert!(!style.opacity.multiple_layers);
1451 assert!(style.transparent_cutout);
1452 assert!(style.background);
1453 assert!(style.background_occluded_lit);
1454 assert!(style.overlay);
1455 }
1456
1457 #[test]
1458 fn effective_style_defaults_when_no_style_ancestors_exist() {
1459 let mut world = World::default();
1460
1461 let text = world.add_component(TextComponent::new("item"));
1462 let glyph_t = world.add_component(TransformComponent::new());
1463 let glyph_r = world.add_component(RenderableComponent::square());
1464
1465 let _ = world.add_child(text, glyph_t);
1466 let _ = world.add_child(glyph_t, glyph_r);
1467
1468 let style = RenderableSystem::resolve_effective_renderable_style(&world, glyph_r);
1469
1470 assert_eq!(style.color, [1.0, 1.0, 1.0, 1.0]);
1471 assert_eq!(style.opacity.opacity, 1.0);
1472 assert!(!style.opacity.multiple_layers);
1473 assert!(!style.transparent_cutout);
1474 assert!(!style.background);
1475 assert!(!style.background_occluded_lit);
1476 assert!(!style.overlay);
1477 }
1478
1479 #[test]
1480 fn flush_pending_recomputes_style_after_attach() {
1481 let mut world = World::default();
1482 let mut visuals = VisualWorld::default();
1483 let mut renderable_system = RenderableSystem::default();
1484 let mut render_assets = RenderAssets::new();
1485 let mut uploader = TestUploader::default();
1486 let mut queue = CommandQueue::new();
1487
1488 let overlay_root = world.add_component(OverlayComponent::new());
1489 let item_root = world.add_component(TransformComponent::new());
1490 let renderable_root = world.add_component(TransformComponent::new());
1491 let renderable = world.add_component(RenderableComponent::square());
1492
1493 let _ = world.add_child(item_root, renderable_root);
1494 let _ = world.add_child(renderable_root, renderable);
1495
1496 renderable_system.register_renderable_from_world(&mut world, &mut visuals, renderable);
1497
1498 let _ = world.add_child(overlay_root, item_root);
1499
1500 let inserted = renderable_system.flush_pending(
1501 &mut world,
1502 &mut visuals,
1503 &mut render_assets,
1504 &mut uploader,
1505 &mut queue,
1506 );
1507
1508 assert!(inserted);
1509 let handle = world
1510 .get_component_by_id_as::<RenderableComponent>(renderable)
1511 .and_then(|r| r.get_handle())
1512 .expect("renderable handle after flush");
1513 let instance = visuals.instance(handle).expect("visual instance");
1514 assert!(instance.overlay);
1515 }
1516
1517 #[test]
1518 fn text_style_emissive_targets_descendants_not_ancestor_renderable() {
1519 let mut world = World::default();
1520 let mut visuals = VisualWorld::default();
1521 let mut renderable = RenderableSystem::default();
1522
1523 let plane = world.add_component(RenderableComponent::square());
1524 let text = world.add_component(TextComponent::new("item"));
1525 let glyph_t = world.add_component(TransformComponent::new());
1526 let glyph_r = world.add_component(RenderableComponent::square());
1527 let emissive = world.add_component(EmissiveComponent::on());
1528
1529 let _ = world.add_child(plane, text);
1530 let _ = world.add_child(text, glyph_t);
1531 let _ = world.add_child(glyph_t, glyph_r);
1532 let _ = world.add_child(text, emissive);
1533
1534 renderable.register_emissive(&mut world, &mut visuals, emissive);
1535
1536 assert_eq!(renderable.pending_emissive.get(&plane), None);
1537 assert_eq!(renderable.pending_emissive.get(&glyph_r), Some(&1.0));
1538 }
1539}