1pub mod camera_follow;
43pub mod context;
44pub mod debug_draw;
46pub mod events;
47pub mod gpu_plugin;
49pub mod guide;
51pub mod jobs;
53pub mod mode;
54pub mod output;
55pub mod plugin;
56pub mod resources;
58pub mod snapshot;
59pub mod systems;
61pub mod timestep;
62
63pub use camera_follow::CameraFollow;
64pub use context::{RuntimeFrameContext, RuntimeStepContext, SimulationStepContext};
65pub use debug_draw::{DebugDraw, DebugLayer, DebugPrim};
66pub use events::RuntimeEventBus;
67pub use gpu_plugin::{GpuFrameContext, GpuPlugin, PostPaintTargets, gpu_phase};
68pub use jobs::{JobPoll, JobSender, JobSlot};
69pub use mode::SceneRuntimeMode;
70pub use output::{
71 CameraCommand, CameraFollowTarget, NodeTransformOp, RuntimeOutput, SelectionOp,
72 TransformWriteback, apply_camera_commands,
73};
74pub use plugin::{RuntimeEvent, RuntimePhase, RuntimePlugin, phase};
75pub use resources::RuntimeResources;
76pub use snapshot::{TransformSnapshot, TransformSnapshotTable};
77pub use systems::{ManipulationSystem, SelectionSystem};
78pub use timestep::{FixedStepIter, FixedTimestep};
79
80use crate::interaction::select::selection::Selection;
81use crate::scene::scene::Scene;
82
83#[allow(clippy::too_many_arguments)]
88fn run_range(
89 plugins: &mut Vec<Box<dyn RuntimePlugin>>,
90 min: i32,
91 max: i32,
92 dt: f32,
93 frame: &RuntimeFrameContext,
94 scene: &Scene,
95 writeback: &mut TransformWriteback,
96 output: &mut RuntimeOutput,
97 resources: &mut RuntimeResources,
98 timings: &mut std::collections::HashMap<&'static str, f32>,
99) {
100 for plugin in plugins.iter_mut() {
101 let p = plugin.priority();
102 if p >= min && p < max {
103 let mut ctx = RuntimeStepContext {
104 priority: p,
105 dt,
106 scene,
107 writeback,
108 output,
109 pick_hit: frame.pick_hit,
110 resources,
111 };
112 let t = std::time::Instant::now();
113 plugin.step(&mut ctx);
114 *timings.entry(plugin.type_name()).or_insert(0.0) += t.elapsed().as_secs_f32() * 1000.0;
115 }
116 }
117}
118
119#[derive(Debug, Default, Clone)]
129pub struct RuntimeStats {
130 pub step_ms: std::collections::HashMap<&'static str, f32>,
133 pub pre_prepare_ms: std::collections::HashMap<&'static str, f32>,
136 pub post_paint_ms: std::collections::HashMap<&'static str, f32>,
139}
140
141impl RuntimeStats {
142 pub fn total_step_ms(&self) -> f32 {
144 self.step_ms.values().sum()
145 }
146
147 pub fn total_pre_prepare_ms(&self) -> f32 {
149 self.pre_prepare_ms.values().sum()
150 }
151
152 pub fn total_post_paint_ms(&self) -> f32 {
154 self.post_paint_ms.values().sum()
155 }
156}
157
158#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::camera::camera::Camera;
164 use crate::interaction::input::ActionFrame;
165 use crate::interaction::select::selection::{NodeId, Selection};
166 use crate::scene::material::Material;
167 use crate::scene::scene::Scene;
168 use std::sync::{Arc, Mutex};
169
170 fn make_frame(camera: &Camera, input: &ActionFrame, dt: f32) -> RuntimeFrameContext {
171 RuntimeFrameContext {
172 dt,
173 camera: camera.clone(),
174 viewport_size: glam::Vec2::new(800.0, 600.0),
175 input: input.clone(),
176 pick_hit: None,
177 clicked: false,
178 drag_started: false,
179 dragging: false,
180 pointer_delta: glam::Vec2::ZERO,
181 cursor_viewport: None,
182 shift_held: false,
183 }
184 }
185
186 struct OrderTracker {
188 priority: i32,
189 id: u32,
190 log: Arc<Mutex<Vec<(i32, u32)>>>,
191 }
192
193 impl RuntimePlugin for OrderTracker {
194 fn priority(&self) -> i32 {
195 self.priority
196 }
197 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
198 self.log.lock().unwrap().push((self.priority, self.id));
199 }
200 }
201
202 struct CallCounter {
204 priority: i32,
205 count: Arc<Mutex<u32>>,
206 }
207
208 impl RuntimePlugin for CallCounter {
209 fn priority(&self) -> i32 {
210 self.priority
211 }
212 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
213 *self.count.lock().unwrap() += 1;
214 }
215 }
216
217 struct WritebackPlugin {
219 node_id: NodeId,
220 transform: glam::Affine3A,
221 }
222
223 impl RuntimePlugin for WritebackPlugin {
224 fn priority(&self) -> i32 {
225 phase::SIMULATE
226 }
227 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
228 ctx.writeback.set(self.node_id, self.transform);
229 }
230 }
231
232 struct DtRecorder {
234 dts: Arc<Mutex<Vec<f32>>>,
235 }
236
237 impl RuntimePlugin for DtRecorder {
238 fn priority(&self) -> i32 {
239 phase::SIMULATE
240 }
241 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
242 self.dts.lock().unwrap().push(ctx.dt);
243 }
244 }
245
246 #[test]
247 fn test_phase_execution_order() {
248 let log = Arc::new(Mutex::new(Vec::<(i32, u32)>::new()));
250 let mut runtime = ViewportRuntime::new()
251 .with_plugin(OrderTracker {
252 priority: phase::WRITEBACK,
253 id: 3,
254 log: log.clone(),
255 })
256 .with_plugin(OrderTracker {
257 priority: phase::SIMULATE,
258 id: 2,
259 log: log.clone(),
260 })
261 .with_plugin(OrderTracker {
262 priority: phase::ANIMATE,
263 id: 1,
264 log: log.clone(),
265 })
266 .with_plugin(OrderTracker {
267 priority: phase::PREPARE,
268 id: 0,
269 log: log.clone(),
270 });
271
272 let camera = Camera::default();
273 let input = ActionFrame::default();
274 let mut scene = Scene::new();
275 let mut sel = Selection::new();
276 runtime.step(
277 &mut scene,
278 &mut sel,
279 &make_frame(&camera, &input, 1.0 / 60.0),
280 );
281
282 let calls = log.lock().unwrap();
283 let priorities: Vec<i32> = calls.iter().map(|(p, _)| *p).collect();
284 for w in priorities.windows(2) {
286 assert!(
287 w[0] <= w[1],
288 "expected ascending order, got {:?}",
289 priorities
290 );
291 }
292 assert_eq!(priorities.len(), 4);
293 }
294
295 #[test]
296 fn test_registration_order_within_phase() {
297 let log = Arc::new(Mutex::new(Vec::<(i32, u32)>::new()));
299 let mut runtime = ViewportRuntime::new()
300 .with_plugin(OrderTracker {
301 priority: phase::SIMULATE,
302 id: 0,
303 log: log.clone(),
304 })
305 .with_plugin(OrderTracker {
306 priority: phase::SIMULATE,
307 id: 1,
308 log: log.clone(),
309 });
310
311 let camera = Camera::default();
312 let input = ActionFrame::default();
313 let mut scene = Scene::new();
314 let mut sel = Selection::new();
315 runtime.step(
316 &mut scene,
317 &mut sel,
318 &make_frame(&camera, &input, 1.0 / 60.0),
319 );
320
321 let calls = log.lock().unwrap();
322 let ids: Vec<u32> = calls.iter().map(|(_, id)| *id).collect();
323 assert_eq!(ids, vec![0, 1]);
324 }
325
326 #[test]
327 fn test_simulate_runs_once_without_fixed_timestep() {
328 let count = Arc::new(Mutex::new(0u32));
329 let mut runtime = ViewportRuntime::new().with_plugin(CallCounter {
330 priority: phase::SIMULATE,
331 count: count.clone(),
332 });
333
334 let camera = Camera::default();
335 let input = ActionFrame::default();
336 let mut scene = Scene::new();
337 let mut sel = Selection::new();
338 runtime.step(
339 &mut scene,
340 &mut sel,
341 &make_frame(&camera, &input, 1.0 / 60.0),
342 );
343
344 assert_eq!(*count.lock().unwrap(), 1);
345 }
346
347 #[test]
348 fn test_simulate_runs_n_times_with_fixed_timestep() {
349 let count = Arc::new(Mutex::new(0u32));
350 let hz = 60.0_f32;
351 let step_dt = 1.0 / hz;
352 let mut runtime = ViewportRuntime::new()
353 .with_fixed_timestep(FixedTimestep::new(hz))
354 .with_plugin(CallCounter {
355 priority: phase::SIMULATE,
356 count: count.clone(),
357 });
358
359 let camera = Camera::default();
360 let input = ActionFrame::default();
361 let mut scene = Scene::new();
362 let mut sel = Selection::new();
363
364 let dt = step_dt * 3.0 + 0.0001;
366 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, dt));
367
368 assert_eq!(*count.lock().unwrap(), 3);
369 }
370
371 #[test]
372 fn test_simulate_dt_equals_step_dt_with_fixed_timestep() {
373 let dts = Arc::new(Mutex::new(Vec::<f32>::new()));
374 let hz = 60.0_f32;
375 let step_dt = 1.0 / hz;
376 let mut runtime = ViewportRuntime::new()
377 .with_fixed_timestep(FixedTimestep::new(hz))
378 .with_plugin(DtRecorder { dts: dts.clone() });
379
380 let camera = Camera::default();
381 let input = ActionFrame::default();
382 let mut scene = Scene::new();
383 let mut sel = Selection::new();
384
385 let dt = step_dt * 2.0 + 0.0001;
387 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, dt));
388
389 let recorded = dts.lock().unwrap();
390 assert_eq!(recorded.len(), 2);
391 for &d in recorded.iter() {
392 assert!((d - step_dt).abs() < 1e-6, "expected {step_dt}, got {d}");
393 }
394 }
395
396 #[test]
397 fn test_writeback_flushes_to_scene() {
398 let target = glam::Affine3A::from_translation(glam::Vec3::new(3.0, 4.0, 5.0));
399 let mut scene = Scene::new();
400 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
401
402 let mut runtime = ViewportRuntime::new().with_plugin(WritebackPlugin {
403 node_id,
404 transform: target,
405 });
406
407 let camera = Camera::default();
408 let input = ActionFrame::default();
409 let mut sel = Selection::new();
410 runtime.step(
411 &mut scene,
412 &mut sel,
413 &make_frame(&camera, &input, 1.0 / 60.0),
414 );
415
416 let node = scene.node(node_id).expect("node not found");
417 let pos = node.world_transform().col(3).truncate();
418 assert!(
419 (pos - glam::Vec3::new(3.0, 4.0, 5.0)).length() < 1e-5,
420 "pos was {pos:?}"
421 );
422 }
423
424 #[test]
425 fn test_writeback_ops_in_output() {
426 let target = glam::Affine3A::from_translation(glam::Vec3::new(1.0, 0.0, 0.0));
427 let mut scene = Scene::new();
428 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
429
430 let mut runtime = ViewportRuntime::new().with_plugin(WritebackPlugin {
431 node_id,
432 transform: target,
433 });
434
435 let camera = Camera::default();
436 let input = ActionFrame::default();
437 let mut sel = Selection::new();
438 let output = runtime.step(
439 &mut scene,
440 &mut sel,
441 &make_frame(&camera, &input, 1.0 / 60.0),
442 );
443
444 assert_eq!(output.node_transform_ops.len(), 1);
445 assert_eq!(output.node_transform_ops[0].id, node_id);
446 }
447
448 #[test]
449 fn test_step_index_increments_each_simulate() {
450 let mut runtime = ViewportRuntime::new();
451 let camera = Camera::default();
452 let input = ActionFrame::default();
453 let mut scene = Scene::new();
454 let mut sel = Selection::new();
455
456 assert_eq!(runtime.step_index(), 0);
457 runtime.step(
458 &mut scene,
459 &mut sel,
460 &make_frame(&camera, &input, 1.0 / 60.0),
461 );
462 assert_eq!(runtime.step_index(), 1);
463 runtime.step(
464 &mut scene,
465 &mut sel,
466 &make_frame(&camera, &input, 1.0 / 60.0),
467 );
468 assert_eq!(runtime.step_index(), 2);
469 }
470
471 #[test]
472 fn test_step_index_increments_n_times_with_fixed_timestep() {
473 let hz = 60.0_f32;
474 let step_dt = 1.0 / hz;
475 let mut runtime = ViewportRuntime::new().with_fixed_timestep(FixedTimestep::new(hz));
476
477 let camera = Camera::default();
478 let input = ActionFrame::default();
479 let mut scene = Scene::new();
480 let mut sel = Selection::new();
481
482 let dt = step_dt * 3.0 + 0.0001;
484 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, dt));
485 assert_eq!(runtime.step_index(), 3);
486 }
487
488 #[test]
489 fn test_snapshot_updated_after_writeback() {
490 let target = glam::Affine3A::from_translation(glam::Vec3::new(7.0, 0.0, 0.0));
491 let mut scene = Scene::new();
492 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
493
494 let mut runtime = ViewportRuntime::new().with_plugin(WritebackPlugin {
495 node_id,
496 transform: target,
497 });
498
499 let camera = Camera::default();
500 let input = ActionFrame::default();
501 let mut sel = Selection::new();
502 runtime.step(
503 &mut scene,
504 &mut sel,
505 &make_frame(&camera, &input, 1.0 / 60.0),
506 );
507
508 let snap = runtime.snapshots().get(node_id).expect("snapshot missing");
509 assert!((snap.curr.translation.x - 7.0).abs() < 1e-5);
510 }
511
512 #[test]
515 fn test_post_sim_runs_after_simulate() {
516 let log = Arc::new(Mutex::new(Vec::<(i32, u32)>::new()));
517 let mut runtime = ViewportRuntime::new()
518 .with_plugin(OrderTracker {
519 priority: phase::POST_SIM,
520 id: 1,
521 log: log.clone(),
522 })
523 .with_plugin(OrderTracker {
524 priority: phase::SIMULATE,
525 id: 0,
526 log: log.clone(),
527 });
528
529 let camera = Camera::default();
530 let input = ActionFrame::default();
531 let mut scene = Scene::new();
532 let mut sel = Selection::new();
533 runtime.step(
534 &mut scene,
535 &mut sel,
536 &make_frame(&camera, &input, 1.0 / 60.0),
537 );
538
539 let calls = log.lock().unwrap();
540 let ids: Vec<u32> = calls.iter().map(|(_, id)| *id).collect();
541 assert_eq!(ids, vec![0, 1], "simulate must run before post_sim");
542 }
543
544 #[test]
545 fn test_plugin_between_bands() {
546 let log = Arc::new(Mutex::new(Vec::<(i32, u32)>::new()));
547 let mid = phase::ANIMATE + 50;
548 let mut runtime = ViewportRuntime::new()
549 .with_plugin(OrderTracker {
550 priority: phase::SIMULATE,
551 id: 2,
552 log: log.clone(),
553 })
554 .with_plugin(OrderTracker {
555 priority: mid,
556 id: 1,
557 log: log.clone(),
558 })
559 .with_plugin(OrderTracker {
560 priority: phase::ANIMATE,
561 id: 0,
562 log: log.clone(),
563 });
564
565 let camera = Camera::default();
566 let input = ActionFrame::default();
567 let mut scene = Scene::new();
568 let mut sel = Selection::new();
569 runtime.step(
570 &mut scene,
571 &mut sel,
572 &make_frame(&camera, &input, 1.0 / 60.0),
573 );
574
575 let calls = log.lock().unwrap();
576 let ids: Vec<u32> = calls.iter().map(|(_, id)| *id).collect();
577 assert_eq!(ids, vec![0, 1, 2], "plugins must execute in priority order");
578 }
579
580 struct EventRecorder {
582 added: Arc<Mutex<Vec<NodeId>>>,
583 removed: Arc<Mutex<Vec<NodeId>>>,
584 }
585
586 impl RuntimePlugin for EventRecorder {
587 fn priority(&self) -> i32 {
588 phase::PREPARE
589 }
590 fn on_event(&mut self, event: &RuntimeEvent, _ctx: &mut RuntimeStepContext<'_>) {
591 match event {
592 RuntimeEvent::NodeAdded(id) => self.added.lock().unwrap().push(*id),
593 RuntimeEvent::NodeRemoved(id) => self.removed.lock().unwrap().push(*id),
594 }
595 }
596 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {}
597 }
598
599 #[test]
600 fn test_lifecycle_events_node_added() {
601 let added = Arc::new(Mutex::new(Vec::<NodeId>::new()));
602 let removed = Arc::new(Mutex::new(Vec::<NodeId>::new()));
603 let mut runtime = ViewportRuntime::new().with_plugin(EventRecorder {
604 added: added.clone(),
605 removed: removed.clone(),
606 });
607
608 let camera = Camera::default();
609 let input = ActionFrame::default();
610 let mut scene = Scene::new();
611 let mut sel = Selection::new();
612
613 runtime.step(
615 &mut scene,
616 &mut sel,
617 &make_frame(&camera, &input, 1.0 / 60.0),
618 );
619 assert!(added.lock().unwrap().is_empty(), "no events on first step");
620
621 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
623 runtime.step(
624 &mut scene,
625 &mut sel,
626 &make_frame(&camera, &input, 1.0 / 60.0),
627 );
628
629 let a = added.lock().unwrap();
630 assert!(
631 a.contains(&node_id),
632 "expected NodeAdded event for {:?}",
633 node_id
634 );
635 assert!(removed.lock().unwrap().is_empty());
636 }
637
638 #[test]
639 fn test_lifecycle_events_node_removed() {
640 let added = Arc::new(Mutex::new(Vec::<NodeId>::new()));
641 let removed = Arc::new(Mutex::new(Vec::<NodeId>::new()));
642 let mut runtime = ViewportRuntime::new().with_plugin(EventRecorder {
643 added: added.clone(),
644 removed: removed.clone(),
645 });
646
647 let camera = Camera::default();
648 let input = ActionFrame::default();
649 let mut scene = Scene::new();
650 let mut sel = Selection::new();
651
652 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
653
654 runtime.step(
656 &mut scene,
657 &mut sel,
658 &make_frame(&camera, &input, 1.0 / 60.0),
659 );
660 added.lock().unwrap().clear();
661
662 scene.remove(node_id);
664 runtime.step(
665 &mut scene,
666 &mut sel,
667 &make_frame(&camera, &input, 1.0 / 60.0),
668 );
669
670 let r = removed.lock().unwrap();
671 assert!(
672 r.contains(&node_id),
673 "expected NodeRemoved event for {:?}",
674 node_id
675 );
676 }
677
678 #[derive(Default)]
680 struct LifecycleRecorder {
681 log: Arc<Mutex<Vec<&'static str>>>,
682 }
683
684 impl RuntimePlugin for LifecycleRecorder {
685 fn priority(&self) -> i32 {
686 phase::PREPARE
687 }
688 fn submit(&mut self, _ctx: &RuntimeStepContext<'_>) {
689 self.log.lock().unwrap().push("submit");
690 }
691 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
692 self.log.lock().unwrap().push("step");
693 }
694 fn collect(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
695 self.log.lock().unwrap().push("collect");
696 }
697 }
698
699 #[test]
700 fn test_submit_collect_called() {
701 let log = Arc::new(Mutex::new(Vec::<&'static str>::new()));
702 let mut runtime =
703 ViewportRuntime::new().with_plugin(LifecycleRecorder { log: log.clone() });
704
705 let camera = Camera::default();
706 let input = ActionFrame::default();
707 let mut scene = Scene::new();
708 let mut sel = Selection::new();
709
710 runtime.step(
711 &mut scene,
712 &mut sel,
713 &make_frame(&camera, &input, 1.0 / 60.0),
714 );
715
716 let calls = log.lock().unwrap();
717 assert!(calls.contains(&"submit"), "submit must be called");
718 assert!(calls.contains(&"step"), "step must be called");
719 assert!(calls.contains(&"collect"), "collect must be called");
720 let si = calls.iter().position(|&s| s == "submit").unwrap();
722 let st = calls.iter().position(|&s| s == "step").unwrap();
723 let co = calls.iter().position(|&s| s == "collect").unwrap();
724 assert!(si < st, "submit must come before step");
725 assert!(st < co, "step must come before collect");
726 }
727
728 #[test]
731 fn test_resource_insert_get() {
732 let mut res = RuntimeResources::new();
733 res.insert(42u32);
734 assert_eq!(res.get::<u32>(), Some(&42));
735 assert!(res.get::<u64>().is_none());
736 }
737
738 #[test]
739 fn test_resource_insert_overwrites() {
740 let mut res = RuntimeResources::new();
741 res.insert(1u32);
742 res.insert(2u32);
743 assert_eq!(res.get::<u32>(), Some(&2));
744 }
745
746 #[test]
747 fn test_resource_get_mut() {
748 let mut res = RuntimeResources::new();
749 res.insert(0u32);
750 *res.get_mut::<u32>().unwrap() = 99;
751 assert_eq!(res.get::<u32>(), Some(&99));
752 }
753
754 #[test]
755 fn test_resource_remove() {
756 let mut res = RuntimeResources::new();
757 res.insert(7u32);
758 assert!(res.contains::<u32>());
759 let val = res.remove::<u32>();
760 assert_eq!(val, Some(7));
761 assert!(!res.contains::<u32>());
762 assert!(res.remove::<u32>().is_none());
764 }
765
766 #[test]
767 fn test_resource_missing_returns_none() {
768 let res = RuntimeResources::new();
769 assert!(res.get::<u32>().is_none());
770 assert!(!res.contains::<u32>());
771 }
772
773 struct CounterWriter {
775 value: u32,
776 }
777
778 impl RuntimePlugin for CounterWriter {
779 fn priority(&self) -> i32 {
780 phase::ANIMATE
781 }
782 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
783 ctx.resources.insert(self.value);
784 }
785 }
786
787 struct CounterReader {
789 recorded: Arc<Mutex<Vec<u32>>>,
790 }
791
792 impl RuntimePlugin for CounterReader {
793 fn priority(&self) -> i32 {
794 phase::POST_SIM
795 }
796 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
797 if let Some(&v) = ctx.resources.get::<u32>() {
798 self.recorded.lock().unwrap().push(v);
799 }
800 }
801 }
802
803 #[test]
804 fn test_resource_shared_across_plugins_same_frame() {
805 let recorded = Arc::new(Mutex::new(Vec::<u32>::new()));
808 let mut runtime = ViewportRuntime::new()
809 .with_plugin(CounterWriter { value: 42 })
810 .with_plugin(CounterReader {
811 recorded: recorded.clone(),
812 });
813
814 let camera = Camera::default();
815 let input = ActionFrame::default();
816 let mut scene = Scene::new();
817 let mut sel = Selection::new();
818 runtime.step(
819 &mut scene,
820 &mut sel,
821 &make_frame(&camera, &input, 1.0 / 60.0),
822 );
823
824 let vals = recorded.lock().unwrap();
825 assert_eq!(
826 vals.as_slice(),
827 &[42],
828 "reader must see value written by writer in the same frame"
829 );
830 }
831
832 #[test]
833 fn test_resource_persists_across_frames() {
834 let mut runtime = ViewportRuntime::new().with_plugin(CounterWriter { value: 10 });
836
837 let camera = Camera::default();
838 let input = ActionFrame::default();
839 let mut scene = Scene::new();
840 let mut sel = Selection::new();
841
842 runtime.step(
843 &mut scene,
844 &mut sel,
845 &make_frame(&camera, &input, 1.0 / 60.0),
846 );
847 assert!(
848 runtime.resources().contains::<u32>(),
849 "resource must persist after step"
850 );
851 }
852
853 #[test]
854 fn test_runtime_works_without_resources() {
855 let mut runtime = ViewportRuntime::new();
857 let camera = Camera::default();
858 let input = ActionFrame::default();
859 let mut scene = Scene::new();
860 let mut sel = Selection::new();
861 let output = runtime.step(
862 &mut scene,
863 &mut sel,
864 &make_frame(&camera, &input, 1.0 / 60.0),
865 );
866 assert_eq!(
867 output
868 .events
869 .read::<crate::plugins::physics_lite::ContactEvent>()
870 .count(),
871 0
872 );
873 assert!(output.node_transform_ops.is_empty());
874 }
875
876 #[derive(Debug, PartialEq)]
880 struct GameplayEvent {
881 id: u32,
882 }
883
884 #[derive(Debug, PartialEq)]
885 struct DiagnosticsEvent {
886 frame_ms: f32,
887 }
888
889 struct GameplayEmitter {
891 count: u32,
892 }
893
894 impl RuntimePlugin for GameplayEmitter {
895 fn priority(&self) -> i32 {
896 phase::SIMULATE
897 }
898 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
899 for i in 0..self.count {
900 ctx.output.events.emit(GameplayEvent { id: i });
901 }
902 }
903 }
904
905 struct DiagnosticsEmitter;
907
908 impl RuntimePlugin for DiagnosticsEmitter {
909 fn priority(&self) -> i32 {
910 phase::POST_SIM
911 }
912 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
913 ctx.output.events.emit(DiagnosticsEvent {
914 frame_ms: ctx.dt * 1000.0,
915 });
916 }
917 }
918
919 struct GameplayReader {
921 seen: Arc<Mutex<Vec<u32>>>,
922 }
923
924 impl RuntimePlugin for GameplayReader {
925 fn priority(&self) -> i32 {
926 phase::WRITEBACK
927 }
928 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
929 for ev in ctx.output.events.read::<GameplayEvent>() {
930 self.seen.lock().unwrap().push(ev.id);
931 }
932 }
933 }
934
935 fn run_one_frame(runtime: &mut ViewportRuntime) -> RuntimeOutput {
936 let camera = Camera::default();
937 let input = ActionFrame::default();
938 let mut scene = Scene::new();
939 let mut sel = Selection::new();
940 runtime.step(
941 &mut scene,
942 &mut sel,
943 &make_frame(&camera, &input, 1.0 / 60.0),
944 )
945 }
946
947 #[test]
948 fn test_event_bus_emit_and_read() {
949 let mut bus = RuntimeEventBus::new();
950 bus.emit(GameplayEvent { id: 1 });
951 bus.emit(GameplayEvent { id: 2 });
952 let ids: Vec<u32> = bus.read::<GameplayEvent>().map(|e| e.id).collect();
953 assert_eq!(ids, vec![1, 2]);
954 }
955
956 #[test]
957 fn test_event_bus_typed_isolation() {
958 let mut bus = RuntimeEventBus::new();
960 bus.emit(GameplayEvent { id: 42 });
961 assert!(!bus.has::<DiagnosticsEvent>());
962 assert_eq!(bus.count::<DiagnosticsEvent>(), 0);
963 let diag: Vec<_> = bus.read::<DiagnosticsEvent>().collect();
964 assert!(diag.is_empty());
965 }
966
967 #[test]
968 fn test_event_bus_drain() {
969 let mut bus = RuntimeEventBus::new();
970 bus.emit(GameplayEvent { id: 7 });
971 bus.emit(GameplayEvent { id: 8 });
972 let drained = bus.drain::<GameplayEvent>();
973 assert_eq!(
974 drained,
975 vec![GameplayEvent { id: 7 }, GameplayEvent { id: 8 }]
976 );
977 assert!(bus.drain::<GameplayEvent>().is_empty());
979 assert!(!bus.has::<GameplayEvent>());
980 }
981
982 #[test]
983 fn test_event_bus_has_and_count() {
984 let mut bus = RuntimeEventBus::new();
985 assert!(!bus.has::<GameplayEvent>());
986 assert_eq!(bus.count::<GameplayEvent>(), 0);
987 bus.emit(GameplayEvent { id: 1 });
988 bus.emit(GameplayEvent { id: 2 });
989 assert!(bus.has::<GameplayEvent>());
990 assert_eq!(bus.count::<GameplayEvent>(), 2);
991 }
992
993 #[test]
994 fn test_event_bus_is_empty() {
995 let mut bus = RuntimeEventBus::new();
996 assert!(bus.is_empty());
997 bus.emit(GameplayEvent { id: 0 });
998 assert!(!bus.is_empty());
999 }
1000
1001 #[test]
1002 fn test_events_emitted_by_plugin_visible_in_output() {
1003 let mut runtime = ViewportRuntime::new().with_plugin(GameplayEmitter { count: 3 });
1004 let output = run_one_frame(&mut runtime);
1005 assert_eq!(output.events.count::<GameplayEvent>(), 3);
1006 let ids: Vec<u32> = output
1007 .events
1008 .read::<GameplayEvent>()
1009 .map(|e| e.id)
1010 .collect();
1011 assert_eq!(ids, vec![0, 1, 2]);
1012 }
1013
1014 #[test]
1015 fn test_events_typed_routing_across_plugins() {
1016 let seen = Arc::new(Mutex::new(Vec::<u32>::new()));
1019 let mut runtime = ViewportRuntime::new()
1020 .with_plugin(GameplayEmitter { count: 2 })
1021 .with_plugin(GameplayReader { seen: seen.clone() });
1022 run_one_frame(&mut runtime);
1023 let ids = seen.lock().unwrap();
1024 assert_eq!(ids.as_slice(), &[0, 1]);
1025 }
1026
1027 #[test]
1028 fn test_multiple_event_types_do_not_interfere() {
1029 let mut runtime = ViewportRuntime::new()
1031 .with_plugin(GameplayEmitter { count: 2 })
1032 .with_plugin(DiagnosticsEmitter);
1033 let output = run_one_frame(&mut runtime);
1034 assert_eq!(output.events.count::<GameplayEvent>(), 2);
1035 assert_eq!(output.events.count::<DiagnosticsEvent>(), 1);
1036 }
1037
1038 #[test]
1039 fn test_events_cleared_each_frame() {
1040 let mut runtime = ViewportRuntime::new().with_plugin(GameplayEmitter { count: 1 });
1042 run_one_frame(&mut runtime);
1043 let output2 = run_one_frame(&mut runtime);
1044 assert_eq!(output2.events.count::<GameplayEvent>(), 1);
1046 }
1047
1048 #[test]
1049 fn test_default_output_is_empty() {
1050 let mut runtime = ViewportRuntime::new();
1051 let output = run_one_frame(&mut runtime);
1052 assert!(output.selection_ops.is_empty());
1053 assert!(output.node_transform_ops.is_empty());
1054 assert!(output.events.is_empty());
1055 }
1056
1057 use crate::runtime::jobs::{JobPoll, JobSlot};
1060
1061 #[test]
1062 fn test_job_slot_empty_by_default() {
1063 let slot: JobSlot<u32> = JobSlot::empty();
1064 assert!(slot.is_empty());
1065 assert!(!slot.is_pending());
1066 assert!(!slot.is_ready());
1067 }
1068
1069 #[test]
1070 fn test_job_slot_pending_after_new() {
1071 let (slot, _sender) = JobSlot::<u32>::new();
1072 assert!(slot.is_pending());
1073 assert!(!slot.is_empty());
1074 assert!(!slot.is_ready());
1075 }
1076
1077 #[test]
1078 fn test_job_handoff_complete() {
1079 let (mut slot, sender) = JobSlot::<u32>::new();
1080 assert!(slot.is_pending());
1081 sender.complete(42);
1082 assert!(slot.is_ready());
1083 match slot.take() {
1084 JobPoll::Ready(v) => assert_eq!(v, 42),
1085 _ => panic!("expected Ready, got other variant"),
1086 }
1087 assert!(slot.is_empty());
1089 }
1090
1091 #[test]
1092 fn test_job_handoff_fail() {
1093 let (mut slot, sender) = JobSlot::<u32>::new();
1094 sender.fail("disk error");
1095 match slot.take() {
1096 JobPoll::Failed(msg) => assert_eq!(msg, "disk error"),
1097 _ => panic!("expected Failed"),
1098 }
1099 assert!(slot.is_empty());
1100 }
1101
1102 #[test]
1103 fn test_job_handoff_cancel() {
1104 let (mut slot, sender) = JobSlot::<u32>::new();
1105 sender.cancel();
1106 match slot.take() {
1107 JobPoll::Cancelled => {}
1108 _ => panic!("expected Cancelled"),
1109 }
1110 assert!(slot.is_empty());
1111 }
1112
1113 #[test]
1114 fn test_job_sender_drop_cancels() {
1115 let (mut slot, sender) = JobSlot::<u32>::new();
1116 drop(sender);
1117 match slot.take() {
1118 JobPoll::Cancelled => {}
1119 _ => panic!("expected Cancelled after sender drop"),
1120 }
1121 assert!(slot.is_empty());
1122 }
1123
1124 #[test]
1125 fn test_job_take_on_empty_returns_empty() {
1126 let mut slot: JobSlot<u32> = JobSlot::empty();
1127 match slot.take() {
1128 JobPoll::Empty => {}
1129 _ => panic!("expected Empty"),
1130 }
1131 }
1132
1133 #[test]
1134 fn test_job_take_while_pending_returns_pending() {
1135 let (mut slot, _sender) = JobSlot::<u32>::new();
1136 match slot.take() {
1137 JobPoll::Pending => {}
1138 _ => panic!("expected Pending"),
1139 }
1140 assert!(slot.is_pending());
1142 }
1143
1144 #[test]
1145 fn test_job_async_handoff_across_frames() {
1146 struct LoaderPlugin {
1148 slot: JobSlot<u32>,
1149 result: Arc<Mutex<Option<u32>>>,
1150 }
1151
1152 impl RuntimePlugin for LoaderPlugin {
1153 fn priority(&self) -> i32 {
1154 phase::PREPARE
1155 }
1156
1157 fn submit(&mut self, _ctx: &RuntimeStepContext<'_>) {
1158 if self.slot.is_empty() {
1159 let (new_slot, sender) = JobSlot::new();
1160 self.slot = new_slot;
1161 sender.complete(99);
1163 }
1164 }
1165
1166 fn collect(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
1167 if let JobPoll::Ready(v) = self.slot.take() {
1168 *self.result.lock().unwrap() = Some(v);
1169 }
1170 }
1171
1172 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {}
1173 }
1174
1175 let result = Arc::new(Mutex::new(None::<u32>));
1176 let mut runtime = ViewportRuntime::new().with_plugin(LoaderPlugin {
1177 slot: JobSlot::empty(),
1178 result: result.clone(),
1179 });
1180
1181 run_one_frame(&mut runtime);
1182 assert_eq!(*result.lock().unwrap(), Some(99));
1183 }
1184
1185 #[test]
1186 fn test_job_staged_resource_update_ordering() {
1187 struct Integrator {
1192 slot: JobSlot<u32>,
1193 }
1194
1195 impl RuntimePlugin for Integrator {
1196 fn priority(&self) -> i32 {
1197 phase::PREPARE
1198 } fn submit(&mut self, _ctx: &RuntimeStepContext<'_>) {
1201 if self.slot.is_empty() {
1202 let (s, sender) = JobSlot::new();
1203 self.slot = s;
1204 sender.complete(77);
1205 }
1206 }
1207
1208 fn collect(&mut self, ctx: &mut RuntimeStepContext<'_>) {
1209 if let JobPoll::Ready(v) = self.slot.take() {
1210 ctx.resources.insert(v);
1211 }
1212 }
1213
1214 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {}
1215 }
1216
1217 struct Reader {
1218 seen: Arc<Mutex<Option<u32>>>,
1219 }
1220
1221 impl RuntimePlugin for Reader {
1222 fn priority(&self) -> i32 {
1223 phase::POST_SIM
1224 } fn collect(&mut self, ctx: &mut RuntimeStepContext<'_>) {
1227 *self.seen.lock().unwrap() = ctx.resources.get::<u32>().copied();
1228 }
1229
1230 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {}
1231 }
1232
1233 let seen = Arc::new(Mutex::new(None::<u32>));
1234 let mut runtime = ViewportRuntime::new()
1235 .with_plugin(Integrator {
1236 slot: JobSlot::empty(),
1237 })
1238 .with_plugin(Reader { seen: seen.clone() });
1239
1240 run_one_frame(&mut runtime);
1241 assert_eq!(*seen.lock().unwrap(), Some(77));
1243 }
1244
1245 #[test]
1246 fn test_job_slot_in_resources() {
1247 let (slot, sender) = JobSlot::<u32>::new();
1249 let mut res = RuntimeResources::new();
1250 res.insert(slot);
1251 sender.complete(55);
1252 let found = res.get_mut::<JobSlot<u32>>().unwrap();
1253 match found.take() {
1254 JobPoll::Ready(v) => assert_eq!(v, 55),
1255 _ => panic!("expected Ready"),
1256 }
1257 }
1258
1259 use crate::plugins::skeleton::{
1262 Joint, JointMatrices, Pose, Skeleton, SkeletonPlugin, apply_skin,
1263 };
1264 use crate::plugins::skinning::{SkinWeights, SkinnedMeshUpdate};
1265
1266 fn two_joint_skeleton() -> Skeleton {
1267 Skeleton::new(vec![
1268 Joint {
1269 name: "root".into(),
1270 parent: None,
1271 inverse_bind: glam::Affine3A::IDENTITY,
1272 },
1273 Joint {
1274 name: "child".into(),
1275 parent: Some(0),
1276 inverse_bind: glam::Affine3A::from_translation(-glam::Vec3::Y),
1277 },
1278 ])
1279 }
1280
1281 fn single_vertex_weights(joint: u8, weight: f32) -> SkinWeights {
1282 SkinWeights {
1283 joint_indices: vec![[joint, 0, 0, 0]],
1284 joint_weights: vec![[weight, 0.0, 0.0, 0.0]],
1285 }
1286 }
1287
1288 #[test]
1289 fn test_skeleton_joint_count() {
1290 let sk = two_joint_skeleton();
1291 assert_eq!(sk.joint_count(), 2);
1292 }
1293
1294 #[test]
1295 fn test_skeleton_find_joint() {
1296 let sk = two_joint_skeleton();
1297 assert_eq!(sk.find_joint("root"), Some(0));
1298 assert_eq!(sk.find_joint("child"), Some(1));
1299 assert_eq!(sk.find_joint("missing"), None);
1300 }
1301
1302 #[test]
1303 fn test_pose_identity_transforms() {
1304 let pose = Pose::identity(3);
1305 assert_eq!(pose.joint_count(), 3);
1306 for t in &pose.local_transforms {
1307 assert_eq!(*t, glam::Affine3A::IDENTITY);
1308 }
1309 }
1310
1311 #[test]
1312 fn test_joint_matrices_single_root_identity() {
1313 let sk = Skeleton::new(vec![Joint {
1314 name: "root".into(),
1315 parent: None,
1316 inverse_bind: glam::Affine3A::IDENTITY,
1317 }]);
1318 let pose = Pose::identity(1);
1319 let mats = JointMatrices::compute(&sk, &pose);
1320 let m = mats.as_slice()[0];
1321 assert!(m.matrix3.col(0).abs_diff_eq(glam::Vec3A::X, 1e-5));
1323 assert!(m.translation.abs_diff_eq(glam::Vec3A::ZERO, 1e-5));
1324 }
1325
1326 #[test]
1327 fn test_joint_matrices_parent_child_chain() {
1328 let sk = two_joint_skeleton();
1334 let mut pose = Pose::identity(2);
1335 pose.local_transforms[0] = glam::Affine3A::from_translation(glam::Vec3::X);
1336 let mats = JointMatrices::compute(&sk, &pose);
1337 let child = mats.as_slice()[1];
1338 let expected_translation = glam::Vec3A::new(1.0, -1.0, 0.0);
1339 assert!(
1340 child.translation.abs_diff_eq(expected_translation, 1e-5),
1341 "child translation was {:?}, expected {:?}",
1342 child.translation,
1343 expected_translation,
1344 );
1345 }
1346
1347 #[test]
1348 fn test_apply_skin_identity_pose() {
1349 let sk = Skeleton::new(vec![Joint {
1350 name: "root".into(),
1351 parent: None,
1352 inverse_bind: glam::Affine3A::IDENTITY,
1353 }]);
1354 let pose = Pose::identity(1);
1355 let mats = JointMatrices::compute(&sk, &pose);
1356
1357 let positions = vec![[1.0f32, 2.0, 3.0]];
1358 let normals = vec![[0.0f32, 1.0, 0.0]];
1359 let weights = single_vertex_weights(0, 1.0);
1360 let (out_pos, out_nrm) = apply_skin(&positions, &normals, &weights, &mats);
1361
1362 assert!((out_pos[0][0] - 1.0).abs() < 1e-5);
1363 assert!((out_pos[0][1] - 2.0).abs() < 1e-5);
1364 assert!((out_pos[0][2] - 3.0).abs() < 1e-5);
1365 assert!((out_nrm[0][1] - 1.0).abs() < 1e-5);
1366 }
1367
1368 #[test]
1369 fn test_apply_skin_single_joint_full_weight() {
1370 let sk = Skeleton::new(vec![Joint {
1372 name: "root".into(),
1373 parent: None,
1374 inverse_bind: glam::Affine3A::IDENTITY,
1375 }]);
1376 let mut pose = Pose::identity(1);
1377 pose.local_transforms[0] = glam::Affine3A::from_translation(glam::Vec3::new(5.0, 0.0, 0.0));
1378 let mats = JointMatrices::compute(&sk, &pose);
1379
1380 let positions = vec![[0.0f32, 0.0, 0.0]];
1381 let normals = vec![[1.0f32, 0.0, 0.0]];
1382 let weights = single_vertex_weights(0, 1.0);
1383 let (out_pos, _) = apply_skin(&positions, &normals, &weights, &mats);
1384 assert!((out_pos[0][0] - 5.0).abs() < 1e-5);
1385 assert!(out_pos[0][1].abs() < 1e-5);
1386 }
1387
1388 #[test]
1389 fn test_apply_skin_two_joint_blend() {
1390 let sk = Skeleton::new(vec![
1393 Joint {
1394 name: "a".into(),
1395 parent: None,
1396 inverse_bind: glam::Affine3A::IDENTITY,
1397 },
1398 Joint {
1399 name: "b".into(),
1400 parent: None,
1401 inverse_bind: glam::Affine3A::IDENTITY,
1402 },
1403 ]);
1404 let mut pose = Pose::identity(2);
1405 pose.local_transforms[1] =
1406 glam::Affine3A::from_translation(glam::Vec3::new(10.0, 0.0, 0.0));
1407 let mats = JointMatrices::compute(&sk, &pose);
1408
1409 let positions = vec![[0.0f32, 0.0, 0.0]];
1410 let normals = vec![[1.0f32, 0.0, 0.0]];
1411 let weights = SkinWeights {
1412 joint_indices: vec![[0, 1, 0, 0]],
1413 joint_weights: vec![[0.5, 0.5, 0.0, 0.0]],
1414 };
1415 let (out_pos, _) = apply_skin(&positions, &normals, &weights, &mats);
1416 assert!(
1417 (out_pos[0][0] - 5.0).abs() < 1e-5,
1418 "expected 5.0, got {}",
1419 out_pos[0][0]
1420 );
1421 }
1422
1423 #[test]
1424 fn test_apply_skin_normal_renormalized() {
1425 let sk = Skeleton::new(vec![Joint {
1427 name: "root".into(),
1428 parent: None,
1429 inverse_bind: glam::Affine3A::IDENTITY,
1430 }]);
1431 let mut pose = Pose::identity(1);
1432 pose.local_transforms[0] = glam::Affine3A::from_scale(glam::Vec3::splat(2.0));
1433 let mats = JointMatrices::compute(&sk, &pose);
1434
1435 let positions = vec![[1.0f32, 0.0, 0.0]];
1436 let normals = vec![[1.0f32, 0.0, 0.0]];
1437 let weights = single_vertex_weights(0, 1.0);
1438 let (_, out_nrm) = apply_skin(&positions, &normals, &weights, &mats);
1439 let len = (out_nrm[0][0].powi(2) + out_nrm[0][1].powi(2) + out_nrm[0][2].powi(2)).sqrt();
1440 assert!((len - 1.0).abs() < 1e-5, "normal length was {len}");
1441 }
1442
1443 #[test]
1444 fn test_skeleton_plugin_writes_update_to_output() {
1445 let sk = Skeleton::new(vec![Joint {
1446 name: "root".into(),
1447 parent: None,
1448 inverse_bind: glam::Affine3A::IDENTITY,
1449 }]);
1450 let positions = vec![[0.0f32, 0.0, 0.0]];
1451 let normals = vec![[0.0f32, 1.0, 0.0]];
1452 let weights = single_vertex_weights(0, 1.0);
1453 let mesh_id = crate::resources::mesh::mesh_store::MeshId::new(0, 0);
1454
1455 let mut runtime = ViewportRuntime::new().with_plugin(SkeletonPlugin::new(
1456 sk, mesh_id, positions, normals, weights,
1457 ));
1458
1459 runtime.resources_mut().insert(Pose::identity(1));
1461
1462 let output = run_one_frame(&mut runtime);
1463 let updates: Vec<&SkinnedMeshUpdate> = output.events.read::<SkinnedMeshUpdate>().collect();
1464 assert_eq!(updates.len(), 1);
1465 assert_eq!(updates[0].mesh_id, mesh_id);
1466 }
1467
1468 #[test]
1469 fn test_skinned_mesh_updates_empty_without_pose() {
1470 let sk = Skeleton::new(vec![Joint {
1471 name: "root".into(),
1472 parent: None,
1473 inverse_bind: glam::Affine3A::IDENTITY,
1474 }]);
1475 let mesh_id = crate::resources::mesh::mesh_store::MeshId::new(0, 0);
1476 let mut runtime = ViewportRuntime::new().with_plugin(SkeletonPlugin::new(
1477 sk,
1478 mesh_id,
1479 vec![[0.0f32; 3]],
1480 vec![[0.0f32, 1.0, 0.0]],
1481 single_vertex_weights(0, 1.0),
1482 ));
1483 let output = run_one_frame(&mut runtime);
1485 assert_eq!(output.events.read::<SkinnedMeshUpdate>().count(), 0);
1486 }
1487
1488 #[test]
1489 fn test_skinned_mesh_updates_cleared_each_frame() {
1490 let sk = Skeleton::new(vec![Joint {
1491 name: "root".into(),
1492 parent: None,
1493 inverse_bind: glam::Affine3A::IDENTITY,
1494 }]);
1495 let mesh_id = crate::resources::mesh::mesh_store::MeshId::new(0, 0);
1496 let mut runtime = ViewportRuntime::new().with_plugin(SkeletonPlugin::new(
1497 sk,
1498 mesh_id,
1499 vec![[0.0f32; 3]],
1500 vec![[0.0f32, 1.0, 0.0]],
1501 single_vertex_weights(0, 1.0),
1502 ));
1503 runtime.resources_mut().insert(Pose::identity(1));
1504
1505 run_one_frame(&mut runtime);
1506 let output2 = run_one_frame(&mut runtime);
1507 assert_eq!(output2.events.read::<SkinnedMeshUpdate>().count(), 1);
1509 }
1510}
1511
1512pub struct ViewportRuntime {
1527 mode: SceneRuntimeMode,
1528 plugins: Vec<Box<dyn RuntimePlugin>>,
1529 gpu_plugins: Vec<Box<dyn GpuPlugin>>,
1530 gpu_initialized: bool,
1533 fixed_timestep: Option<FixedTimestep>,
1534 snapshots: TransformSnapshotTable,
1535 step_index: u64,
1536 selection_system: Option<SelectionSystem>,
1537 manipulation_system: Option<ManipulationSystem>,
1538 camera_follow: Option<CameraFollow>,
1539 resources: RuntimeResources,
1541 prev_node_ids: std::collections::HashSet<crate::interaction::select::selection::NodeId>,
1543 scene_initialized: bool,
1545 stats: RuntimeStats,
1547}
1548
1549impl Default for ViewportRuntime {
1550 fn default() -> Self {
1551 Self::new()
1552 }
1553}
1554
1555impl ViewportRuntime {
1556 pub fn new() -> Self {
1558 Self {
1559 mode: SceneRuntimeMode::default(),
1560 plugins: Vec::new(),
1561 gpu_plugins: Vec::new(),
1562 gpu_initialized: false,
1563 fixed_timestep: None,
1564 snapshots: TransformSnapshotTable::new(),
1565 step_index: 0,
1566 selection_system: None,
1567 manipulation_system: None,
1568 camera_follow: None,
1569 resources: RuntimeResources::new(),
1570 prev_node_ids: std::collections::HashSet::new(),
1571 scene_initialized: false,
1572 stats: RuntimeStats::default(),
1573 }
1574 }
1575
1576 pub fn last_stats(&self) -> &RuntimeStats {
1581 &self.stats
1582 }
1583
1584 pub fn with_selection_system(mut self) -> Self {
1590 self.selection_system = Some(SelectionSystem::new());
1591 self
1592 }
1593
1594 pub fn with_manipulation_system(mut self) -> Self {
1600 self.manipulation_system = Some(ManipulationSystem::new());
1601 self
1602 }
1603
1604 pub fn is_manipulating(&self) -> bool {
1608 self.manipulation_system
1609 .as_ref()
1610 .map_or(false, |m| m.is_active())
1611 }
1612
1613 pub fn manipulation_system(&self) -> Option<&ManipulationSystem> {
1615 self.manipulation_system.as_ref()
1616 }
1617
1618 pub fn with_mode(mut self, mode: SceneRuntimeMode) -> Self {
1620 self.mode = mode;
1621 self
1622 }
1623
1624 pub fn with_plugin(mut self, plugin: impl RuntimePlugin) -> Self {
1627 self.plugins.push(Box::new(plugin));
1628 self
1629 }
1630
1631 pub fn add_plugin(&mut self, plugin: impl RuntimePlugin) {
1637 self.plugins.push(Box::new(plugin));
1638 }
1639
1640 pub fn with_gpu_plugin(mut self, plugin: impl GpuPlugin) -> Self {
1649 self.gpu_plugins.push(Box::new(plugin));
1650 self.gpu_initialized = false;
1651 self
1652 }
1653
1654 pub fn add_gpu_plugin(&mut self, plugin: impl GpuPlugin) {
1659 self.gpu_plugins.push(Box::new(plugin));
1660 self.gpu_initialized = false;
1661 }
1662
1663 pub fn with_gpu_plugin_for_viewport(
1675 mut self,
1676 viewport: crate::renderer::ViewportId,
1677 plugin: impl GpuPlugin,
1678 ) -> Self {
1679 let scoped = gpu_plugin::ViewportScopedPlugin {
1680 viewport,
1681 inner: plugin,
1682 };
1683 self.gpu_plugins.push(Box::new(scoped));
1684 self.gpu_initialized = false;
1685 self
1686 }
1687
1688 pub fn notify_device_recreated(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
1697 for p in self.gpu_plugins.iter_mut() {
1698 p.on_device_recreated(device, queue);
1699 }
1700 self.gpu_initialized = false;
1701 }
1702
1703 pub fn pre_prepare(
1722 &mut self,
1723 device: &wgpu::Device,
1724 queue: &wgpu::Queue,
1725 ctx: &GpuFrameContext<'_>,
1726 ) -> Vec<wgpu::CommandBuffer> {
1727 if !self.gpu_initialized {
1728 for p in self.gpu_plugins.iter_mut() {
1729 p.init_gpu(device);
1730 }
1731 self.gpu_initialized = true;
1732 }
1733 self.gpu_plugins.sort_by_key(|p| p.priority());
1736
1737 let mut timings: std::collections::HashMap<&'static str, f32> =
1738 std::collections::HashMap::new();
1739 let mut out = Vec::new();
1740 for p in self.gpu_plugins.iter_mut() {
1741 let t = std::time::Instant::now();
1742 out.extend(p.pre_prepare(device, queue, ctx));
1743 *timings.entry(p.type_name()).or_insert(0.0) += t.elapsed().as_secs_f32() * 1000.0;
1744 }
1745 self.stats.pre_prepare_ms = timings;
1746 out
1747 }
1748
1749 pub fn post_paint(
1762 &mut self,
1763 device: &wgpu::Device,
1764 queue: &wgpu::Queue,
1765 targets: &PostPaintTargets<'_>,
1766 ctx: &GpuFrameContext<'_>,
1767 ) -> Vec<wgpu::CommandBuffer> {
1768 self.gpu_plugins.sort_by_key(|p| p.priority());
1772
1773 let mut timings: std::collections::HashMap<&'static str, f32> =
1774 std::collections::HashMap::new();
1775 let mut out = Vec::new();
1776 for p in self.gpu_plugins.iter_mut() {
1777 let t = std::time::Instant::now();
1778 out.extend(p.post_paint(device, queue, targets, ctx));
1779 *timings.entry(p.type_name()).or_insert(0.0) += t.elapsed().as_secs_f32() * 1000.0;
1780 }
1781 self.stats.post_paint_ms = timings;
1782 out
1783 }
1784
1785 pub fn with_fixed_timestep(mut self, ts: FixedTimestep) -> Self {
1791 self.fixed_timestep = Some(ts);
1792 self
1793 }
1794
1795 pub fn set_fixed_timestep(&mut self, ts: FixedTimestep) {
1800 self.fixed_timestep = Some(ts);
1801 }
1802
1803 pub fn clear_fixed_timestep(&mut self) {
1805 self.fixed_timestep = None;
1806 }
1807
1808 pub fn mode(&self) -> SceneRuntimeMode {
1810 self.mode
1811 }
1812
1813 pub fn snapshots(&self) -> &TransformSnapshotTable {
1818 &self.snapshots
1819 }
1820
1821 pub fn resources(&self) -> &RuntimeResources {
1826 &self.resources
1827 }
1828
1829 pub fn resources_mut(&mut self) -> &mut RuntimeResources {
1834 &mut self.resources
1835 }
1836
1837 pub fn alpha(&self) -> f32 {
1842 self.fixed_timestep.as_ref().map_or(1.0, |ts| ts.alpha())
1843 }
1844
1845 pub fn step_index(&self) -> u64 {
1850 self.step_index
1851 }
1852
1853 pub fn with_camera_follow(mut self, follow: CameraFollow) -> Self {
1859 self.camera_follow = Some(follow);
1860 self
1861 }
1862
1863 pub fn set_camera_follow(&mut self, follow: CameraFollow) {
1865 self.camera_follow = Some(follow);
1866 }
1867
1868 pub fn clear_camera_follow(&mut self) {
1871 self.camera_follow = None;
1872 }
1873
1874 pub fn camera_follow(&self) -> Option<&CameraFollow> {
1876 self.camera_follow.as_ref()
1877 }
1878
1879 pub fn step(
1890 &mut self,
1891 scene: &mut Scene,
1892 selection: &mut Selection,
1893 frame: &RuntimeFrameContext,
1894 ) -> RuntimeOutput {
1895 let mut output = RuntimeOutput::default();
1896 let mut writeback = TransformWriteback::default();
1897
1898 let current_ids: std::collections::HashSet<crate::interaction::select::selection::NodeId> =
1901 scene.nodes().map(|n| n.id()).collect();
1902
1903 if self.scene_initialized {
1904 let mut events: Vec<RuntimeEvent> = Vec::new();
1906 for &id in current_ids.difference(&self.prev_node_ids) {
1907 events.push(RuntimeEvent::NodeAdded(id));
1908 }
1909 for &id in self.prev_node_ids.difference(¤t_ids) {
1910 events.push(RuntimeEvent::NodeRemoved(id));
1911 }
1912
1913 if !events.is_empty() {
1914 let mut plugins = std::mem::take(&mut self.plugins);
1916 for event in &events {
1917 for plugin in plugins.iter_mut() {
1918 let mut ctx = RuntimeStepContext {
1919 priority: plugin.priority(),
1920 dt: frame.dt,
1921 scene,
1922 writeback: &mut writeback,
1923 output: &mut output,
1924 pick_hit: frame.pick_hit,
1925 resources: &mut self.resources,
1926 };
1927 plugin.on_event(event, &mut ctx);
1928 }
1929 }
1930 self.plugins = plugins;
1931 }
1932 } else {
1933 self.scene_initialized = true;
1934 }
1935 self.prev_node_ids = current_ids;
1936
1937 self.plugins.sort_by_key(|p| p.priority());
1939
1940 let mut plugins = std::mem::take(&mut self.plugins);
1942
1943 let mut step_timings: std::collections::HashMap<&'static str, f32> =
1946 std::collections::HashMap::new();
1947
1948 for plugin in plugins.iter_mut() {
1950 let ctx = RuntimeStepContext {
1951 priority: plugin.priority(),
1952 dt: frame.dt,
1953 scene,
1954 writeback: &mut writeback,
1955 output: &mut output,
1956 pick_hit: frame.pick_hit,
1957 resources: &mut self.resources,
1958 };
1959 let t = std::time::Instant::now();
1960 plugin.submit(&ctx);
1961 *step_timings.entry(plugin.type_name()).or_insert(0.0) +=
1962 t.elapsed().as_secs_f32() * 1000.0;
1963 }
1964
1965 run_range(
1969 &mut plugins,
1970 i32::MIN,
1971 phase::SELECT,
1972 frame.dt,
1973 frame,
1974 scene,
1975 &mut writeback,
1976 &mut output,
1977 &mut self.resources,
1978 &mut step_timings,
1979 );
1980
1981 if let Some(sel_sys) = &self.selection_system {
1983 sel_sys.step(frame, &mut output);
1984 }
1985 run_range(
1987 &mut plugins,
1988 phase::SELECT,
1989 phase::MANIPULATE,
1990 frame.dt,
1991 frame,
1992 scene,
1993 &mut writeback,
1994 &mut output,
1995 &mut self.resources,
1996 &mut step_timings,
1997 );
1998
1999 if self.manipulation_system.is_some() {
2001 let mut manip_sys = self.manipulation_system.take().unwrap();
2002 manip_sys.step(frame, scene, selection, &mut writeback, &mut output);
2003 self.manipulation_system = Some(manip_sys);
2004 }
2005 run_range(
2007 &mut plugins,
2008 phase::MANIPULATE,
2009 phase::SIMULATE,
2010 frame.dt,
2011 frame,
2012 scene,
2013 &mut writeback,
2014 &mut output,
2015 &mut self.resources,
2016 &mut step_timings,
2017 );
2018
2019 if self.fixed_timestep.is_some() {
2021 let mut ts = self.fixed_timestep.take().unwrap();
2022 for step_dt in ts.advance(frame.dt) {
2023 run_range(
2024 &mut plugins,
2025 phase::SIMULATE,
2026 phase::POST_SIM,
2027 step_dt,
2028 frame,
2029 scene,
2030 &mut writeback,
2031 &mut output,
2032 &mut self.resources,
2033 &mut step_timings,
2034 );
2035 self.step_index = self.step_index.wrapping_add(1);
2036 }
2037 self.fixed_timestep = Some(ts);
2038 } else {
2039 run_range(
2040 &mut plugins,
2041 phase::SIMULATE,
2042 phase::POST_SIM,
2043 frame.dt,
2044 frame,
2045 scene,
2046 &mut writeback,
2047 &mut output,
2048 &mut self.resources,
2049 &mut step_timings,
2050 );
2051 self.step_index = self.step_index.wrapping_add(1);
2052 }
2053
2054 run_range(
2056 &mut plugins,
2057 phase::POST_SIM,
2058 i32::MAX,
2059 frame.dt,
2060 frame,
2061 scene,
2062 &mut writeback,
2063 &mut output,
2064 &mut self.resources,
2065 &mut step_timings,
2066 );
2067
2068 for plugin in plugins.iter_mut() {
2070 let mut ctx = RuntimeStepContext {
2071 priority: plugin.priority(),
2072 dt: frame.dt,
2073 scene,
2074 writeback: &mut writeback,
2075 output: &mut output,
2076 pick_hit: frame.pick_hit,
2077 resources: &mut self.resources,
2078 };
2079 let t = std::time::Instant::now();
2080 plugin.collect(&mut ctx);
2081 *step_timings.entry(plugin.type_name()).or_insert(0.0) +=
2082 t.elapsed().as_secs_f32() * 1000.0;
2083 }
2084
2085 self.plugins = plugins;
2087 self.stats.step_ms = step_timings;
2088
2089 let ops = writeback.into_ops();
2091 for op in &ops {
2092 let new_local = if op.preserve_scale {
2093 let incoming = glam::Mat4::from(op.transform);
2098 let (_, rot, trans) = incoming.to_scale_rotation_translation();
2099 let existing_scale = scene
2100 .node(op.id)
2101 .map(|n| n.local_transform().to_scale_rotation_translation().0)
2102 .unwrap_or(glam::Vec3::ONE);
2103 glam::Mat4::from_scale_rotation_translation(existing_scale, rot, trans)
2104 } else {
2105 glam::Mat4::from(op.transform)
2106 };
2107 scene.set_local_transform(op.id, new_local);
2108 self.snapshots.update(op.id, op.transform);
2109 }
2110 if !ops.is_empty() {
2111 scene.update_transforms();
2112 }
2113 output.node_transform_ops = ops;
2114
2115 for op in &output.selection_ops {
2117 op.apply_to(selection);
2118 }
2119
2120 if let Some(CameraFollow::Node { id, offset, .. }) = &self.camera_follow {
2122 let id = *id;
2123 let offset = *offset;
2124 let alpha = self.alpha();
2125 let pos = self
2126 .snapshots
2127 .interpolated(id, alpha)
2128 .map(|t| glam::Vec3::from(t.translation))
2129 .or_else(|| {
2130 scene
2131 .node(id)
2132 .map(|n| n.world_transform().col(3).truncate())
2133 });
2134 if let Some(pos) = pos {
2135 output
2136 .events
2137 .emit(crate::runtime::output::CameraFollowTarget(pos + offset));
2138 }
2139 }
2140
2141 output
2142 }
2143}
2144
2145#[cfg(test)]
2146mod plugin_stats_tests {
2147 use super::*;
2148
2149 struct Probe;
2150 impl RuntimePlugin for Probe {
2151 fn priority(&self) -> i32 {
2152 phase::SIMULATE
2153 }
2154 fn type_name(&self) -> &'static str {
2155 "probe"
2156 }
2157 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
2158 let _ = (0..2000u64).fold(0u64, |a, b| a.wrapping_add(b));
2160 }
2161 }
2162
2163 #[test]
2164 fn last_stats_attributes_step_time_by_type_name() {
2165 let mut rt = ViewportRuntime::new().with_plugin(Probe);
2166 let mut scene = Scene::new();
2167 let mut sel = Selection::new();
2168 let frame = RuntimeFrameContext::default();
2169 rt.step(&mut scene, &mut sel, &frame);
2170
2171 let stats = rt.last_stats();
2172 assert!(
2173 stats.step_ms.contains_key("probe"),
2174 "expected per-plugin step timing keyed by type_name"
2175 );
2176 assert!(stats.total_step_ms() >= 0.0);
2177 }
2178}