Skip to main content

viewport_lib/runtime/
mod.rs

1//! Scene runtime: per-frame orchestration layer between [`Scene`] and [`ViewportRenderer`].
2//!
3//! [`ViewportRuntime`] runs registered plugins in a defined priority order each frame,
4//! drives a fixed timestep accumulator for physics, and flushes accumulated transform
5//! writes back to the scene. It does not own the scene, selection, or GPU resources.
6//!
7//! # Usage
8//!
9//! ```rust,ignore
10//! use viewport_lib::runtime::{
11//!     FixedTimestep, RuntimeFrameContext, RuntimePhase, RuntimePlugin,
12//!     RuntimeStepContext, SceneRuntimeMode, ViewportRuntime,
13//! };
14//!
15//! // Build the runtime once at startup.
16//! let mut runtime = ViewportRuntime::new()
17//!     .with_mode(SceneRuntimeMode::Simulation)
18//!     .with_fixed_timestep(FixedTimestep::new(60.0))
19//!     .with_plugin(MyPhysicsPlugin::new());
20//!
21//! // Each frame:
22//! let mut frame_ctx = RuntimeFrameContext::default();
23//! frame_ctx.dt = wall_dt;
24//! frame_ctx.camera = camera.clone();
25//! frame_ctx.viewport_size = glam::Vec2::new(width, height);
26//! frame_ctx.input = action_frame.clone();
27//! let output = runtime.step(&mut scene, &mut selection, &frame_ctx);
28//!
29//! // Handle contact events in game logic:
30//! for event in &output.contact_events { /* ... */ }
31//!
32//! // Render with interpolated transforms between fixed steps:
33//! let alpha = runtime.alpha();
34//! if let Some(t) = runtime.snapshots().interpolated(node_id, alpha) {
35//!     // use t instead of the node's scene transform
36//! }
37//! ```
38//!
39//! Existing `prepare` / `paint_to` call sites need no changes. `ViewportRuntime`
40//! is purely additive and does not affect [`ViewportRenderer`](crate::ViewportRenderer).
41
42pub mod camera_follow;
43pub mod context;
44/// Debug draw accumulator for runtime plugins.
45pub mod debug_draw;
46pub mod events;
47/// GPU plugin trait and lifecycle hooks.
48pub mod gpu_plugin;
49/// Plugin authoring guide.
50pub mod guide;
51/// Async job handoff types for runtime plugins.
52pub mod jobs;
53pub mod mode;
54pub mod output;
55pub mod plugin;
56/// Built-in animation, constraint, and physics plugins.
57pub mod plugins;
58pub mod resources;
59pub mod snapshot;
60/// Built-in interaction systems: SelectionSystem and ManipulationSystem.
61pub mod systems;
62pub mod timestep;
63
64pub use camera_follow::CameraFollow;
65pub use context::{RuntimeFrameContext, RuntimeStepContext, SimulationStepContext};
66pub use debug_draw::{DebugDraw, DebugLayer, DebugPrim};
67pub use events::RuntimeEventBus;
68pub use gpu_plugin::{gpu_phase, GpuFrameContext, GpuPlugin, PostPaintTargets};
69pub use jobs::{JobPoll, JobSender, JobSlot};
70pub use mode::SceneRuntimeMode;
71pub use output::{CameraCommand, ContactEvent, NodeTransformOp, RuntimeOutput, SelectionOp, SkinnedMeshUpdate, SkinnedPoseUpdate, TransformWriteback};
72pub use plugin::{phase, RuntimeEvent, RuntimePhase, RuntimePlugin};
73pub use plugins::{
74    AnimationClip, AnimationPlugin, AnimationTrack, Channel, ClipPlayerPlugin, Constraint,
75    ConstraintPlugin, Interpolation, Joint, JointMatrices, Keyframe, PhysicsBody,
76    PhysicsLitePlugin, Pose, Sampler, Skeleton, SkeletonPlugin, SkinnedActor, SkinnedActorPart,
77    SkinnedActorPlugin, SkinningPath, Track, TrackValue, TrackValues, apply_skin,
78};
79pub use resources::RuntimeResources;
80pub use snapshot::{TransformSnapshot, TransformSnapshotTable};
81pub use systems::{ManipulationSystem, SelectionSystem};
82pub use timestep::{FixedStepIter, FixedTimestep};
83
84use crate::interaction::selection::Selection;
85use crate::scene::scene::Scene;
86
87// ---- free function ----------------------------------------------------------
88
89/// Run all plugins whose priority is in `[min, max)`, in the order they appear
90/// in the slice.
91fn run_range(
92    plugins: &mut Vec<Box<dyn RuntimePlugin>>,
93    min: i32,
94    max: i32,
95    dt: f32,
96    frame: &RuntimeFrameContext,
97    scene: &Scene,
98    writeback: &mut TransformWriteback,
99    output: &mut RuntimeOutput,
100    resources: &mut RuntimeResources,
101) {
102    for plugin in plugins.iter_mut() {
103        let p = plugin.priority();
104        if p >= min && p < max {
105            let mut ctx = RuntimeStepContext {
106                priority: p,
107                dt,
108                scene,
109                writeback,
110                output,
111                pick_hit: frame.pick_hit,
112                resources,
113            };
114            plugin.step(&mut ctx);
115        }
116    }
117}
118
119// ---- tests ------------------------------------------------------------------
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::camera::camera::Camera;
125    use crate::interaction::input::ActionFrame;
126    use crate::interaction::selection::{NodeId, Selection};
127    use crate::scene::material::Material;
128    use crate::scene::scene::Scene;
129    use std::sync::{Arc, Mutex};
130
131    fn make_frame(camera: &Camera, input: &ActionFrame, dt: f32) -> RuntimeFrameContext {
132        RuntimeFrameContext {
133            dt,
134            camera: camera.clone(),
135            viewport_size: glam::Vec2::new(800.0, 600.0),
136            input: input.clone(),
137            pick_hit: None,
138            clicked: false,
139            drag_started: false,
140            dragging: false,
141            pointer_delta: glam::Vec2::ZERO,
142            cursor_viewport: None,
143            shift_held: false,
144        }
145    }
146
147    // Records (priority, id) each time step() is called.
148    struct OrderTracker {
149        priority: i32,
150        id: u32,
151        log: Arc<Mutex<Vec<(i32, u32)>>>,
152    }
153
154    impl RuntimePlugin for OrderTracker {
155        fn priority(&self) -> i32 {
156            self.priority
157        }
158        fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
159            self.log.lock().unwrap().push((self.priority, self.id));
160        }
161    }
162
163    // Counts total step() calls.
164    struct CallCounter {
165        priority: i32,
166        count: Arc<Mutex<u32>>,
167    }
168
169    impl RuntimePlugin for CallCounter {
170        fn priority(&self) -> i32 {
171            self.priority
172        }
173        fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
174            *self.count.lock().unwrap() += 1;
175        }
176    }
177
178    // Writes a fixed transform for one node each step.
179    struct WritebackPlugin {
180        node_id: NodeId,
181        transform: glam::Affine3A,
182    }
183
184    impl RuntimePlugin for WritebackPlugin {
185        fn priority(&self) -> i32 {
186            phase::SIMULATE
187        }
188        fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
189            ctx.writeback.set(self.node_id, self.transform);
190        }
191    }
192
193    // Records the dt value passed to each Simulate step.
194    struct DtRecorder {
195        dts: Arc<Mutex<Vec<f32>>>,
196    }
197
198    impl RuntimePlugin for DtRecorder {
199        fn priority(&self) -> i32 {
200            phase::SIMULATE
201        }
202        fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
203            self.dts.lock().unwrap().push(ctx.dt);
204        }
205    }
206
207    #[test]
208    fn test_phase_execution_order() {
209        // Register plugins in a scrambled order; verify they execute in priority order.
210        let log = Arc::new(Mutex::new(Vec::<(i32, u32)>::new()));
211        let mut runtime = ViewportRuntime::new()
212            .with_plugin(OrderTracker { priority: phase::WRITEBACK, id: 3, log: log.clone() })
213            .with_plugin(OrderTracker { priority: phase::SIMULATE,  id: 2, log: log.clone() })
214            .with_plugin(OrderTracker { priority: phase::ANIMATE,   id: 1, log: log.clone() })
215            .with_plugin(OrderTracker { priority: phase::PREPARE,   id: 0, log: log.clone() });
216
217        let camera = Camera::default();
218        let input = ActionFrame::default();
219        let mut scene = Scene::new();
220        let mut sel = Selection::new();
221        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
222
223        let calls = log.lock().unwrap();
224        let priorities: Vec<i32> = calls.iter().map(|(p, _)| *p).collect();
225        // Verify priorities are in ascending order.
226        for w in priorities.windows(2) {
227            assert!(w[0] <= w[1], "expected ascending order, got {:?}", priorities);
228        }
229        assert_eq!(priorities.len(), 4);
230    }
231
232    #[test]
233    fn test_registration_order_within_phase() {
234        // Two plugins at the same priority must execute in registration order.
235        let log = Arc::new(Mutex::new(Vec::<(i32, u32)>::new()));
236        let mut runtime = ViewportRuntime::new()
237            .with_plugin(OrderTracker { priority: phase::SIMULATE, id: 0, log: log.clone() })
238            .with_plugin(OrderTracker { priority: phase::SIMULATE, id: 1, log: log.clone() });
239
240        let camera = Camera::default();
241        let input = ActionFrame::default();
242        let mut scene = Scene::new();
243        let mut sel = Selection::new();
244        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
245
246        let calls = log.lock().unwrap();
247        let ids: Vec<u32> = calls.iter().map(|(_, id)| *id).collect();
248        assert_eq!(ids, vec![0, 1]);
249    }
250
251    #[test]
252    fn test_simulate_runs_once_without_fixed_timestep() {
253        let count = Arc::new(Mutex::new(0u32));
254        let mut runtime = ViewportRuntime::new()
255            .with_plugin(CallCounter { priority: phase::SIMULATE, count: count.clone() });
256
257        let camera = Camera::default();
258        let input = ActionFrame::default();
259        let mut scene = Scene::new();
260        let mut sel = Selection::new();
261        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
262
263        assert_eq!(*count.lock().unwrap(), 1);
264    }
265
266    #[test]
267    fn test_simulate_runs_n_times_with_fixed_timestep() {
268        let count = Arc::new(Mutex::new(0u32));
269        let hz = 60.0_f32;
270        let step_dt = 1.0 / hz;
271        let mut runtime = ViewportRuntime::new()
272            .with_fixed_timestep(FixedTimestep::new(hz))
273            .with_plugin(CallCounter { priority: phase::SIMULATE, count: count.clone() });
274
275        let camera = Camera::default();
276        let input = ActionFrame::default();
277        let mut scene = Scene::new();
278        let mut sel = Selection::new();
279
280        // A frame spanning exactly 3 steps plus a tiny remainder yields 3 steps.
281        let dt = step_dt * 3.0 + 0.0001;
282        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, dt));
283
284        assert_eq!(*count.lock().unwrap(), 3);
285    }
286
287    #[test]
288    fn test_simulate_dt_equals_step_dt_with_fixed_timestep() {
289        let dts = Arc::new(Mutex::new(Vec::<f32>::new()));
290        let hz = 60.0_f32;
291        let step_dt = 1.0 / hz;
292        let mut runtime = ViewportRuntime::new()
293            .with_fixed_timestep(FixedTimestep::new(hz))
294            .with_plugin(DtRecorder { dts: dts.clone() });
295
296        let camera = Camera::default();
297        let input = ActionFrame::default();
298        let mut scene = Scene::new();
299        let mut sel = Selection::new();
300
301        // Frame spanning 2 steps.
302        let dt = step_dt * 2.0 + 0.0001;
303        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, dt));
304
305        let recorded = dts.lock().unwrap();
306        assert_eq!(recorded.len(), 2);
307        for &d in recorded.iter() {
308            assert!((d - step_dt).abs() < 1e-6, "expected {step_dt}, got {d}");
309        }
310    }
311
312    #[test]
313    fn test_writeback_flushes_to_scene() {
314        let target = glam::Affine3A::from_translation(glam::Vec3::new(3.0, 4.0, 5.0));
315        let mut scene = Scene::new();
316        let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
317
318        let mut runtime = ViewportRuntime::new()
319            .with_plugin(WritebackPlugin { node_id, transform: target });
320
321        let camera = Camera::default();
322        let input = ActionFrame::default();
323        let mut sel = Selection::new();
324        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
325
326        let node = scene.node(node_id).expect("node not found");
327        let pos = node.world_transform().col(3).truncate();
328        assert!((pos - glam::Vec3::new(3.0, 4.0, 5.0)).length() < 1e-5, "pos was {pos:?}");
329    }
330
331    #[test]
332    fn test_writeback_ops_in_output() {
333        let target = glam::Affine3A::from_translation(glam::Vec3::new(1.0, 0.0, 0.0));
334        let mut scene = Scene::new();
335        let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
336
337        let mut runtime = ViewportRuntime::new()
338            .with_plugin(WritebackPlugin { node_id, transform: target });
339
340        let camera = Camera::default();
341        let input = ActionFrame::default();
342        let mut sel = Selection::new();
343        let output = runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
344
345        assert_eq!(output.node_transform_ops.len(), 1);
346        assert_eq!(output.node_transform_ops[0].id, node_id);
347    }
348
349    #[test]
350    fn test_step_index_increments_each_simulate() {
351        let mut runtime = ViewportRuntime::new();
352        let camera = Camera::default();
353        let input = ActionFrame::default();
354        let mut scene = Scene::new();
355        let mut sel = Selection::new();
356
357        assert_eq!(runtime.step_index(), 0);
358        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
359        assert_eq!(runtime.step_index(), 1);
360        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
361        assert_eq!(runtime.step_index(), 2);
362    }
363
364    #[test]
365    fn test_step_index_increments_n_times_with_fixed_timestep() {
366        let hz = 60.0_f32;
367        let step_dt = 1.0 / hz;
368        let mut runtime = ViewportRuntime::new()
369            .with_fixed_timestep(FixedTimestep::new(hz));
370
371        let camera = Camera::default();
372        let input = ActionFrame::default();
373        let mut scene = Scene::new();
374        let mut sel = Selection::new();
375
376        // 3 fixed steps in one frame.
377        let dt = step_dt * 3.0 + 0.0001;
378        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, dt));
379        assert_eq!(runtime.step_index(), 3);
380    }
381
382    #[test]
383    fn test_snapshot_updated_after_writeback() {
384        let target = glam::Affine3A::from_translation(glam::Vec3::new(7.0, 0.0, 0.0));
385        let mut scene = Scene::new();
386        let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
387
388        let mut runtime = ViewportRuntime::new()
389            .with_plugin(WritebackPlugin { node_id, transform: target });
390
391        let camera = Camera::default();
392        let input = ActionFrame::default();
393        let mut sel = Selection::new();
394        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
395
396        let snap = runtime.snapshots().get(node_id).expect("snapshot missing");
397        assert!((snap.curr.translation.x - 7.0).abs() < 1e-5);
398    }
399
400    // ---- new tests ----------------------------------------------------------
401
402    #[test]
403    fn test_post_sim_runs_after_simulate() {
404        let log = Arc::new(Mutex::new(Vec::<(i32, u32)>::new()));
405        let mut runtime = ViewportRuntime::new()
406            .with_plugin(OrderTracker { priority: phase::POST_SIM,  id: 1, log: log.clone() })
407            .with_plugin(OrderTracker { priority: phase::SIMULATE,  id: 0, log: log.clone() });
408
409        let camera = Camera::default();
410        let input = ActionFrame::default();
411        let mut scene = Scene::new();
412        let mut sel = Selection::new();
413        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
414
415        let calls = log.lock().unwrap();
416        let ids: Vec<u32> = calls.iter().map(|(_, id)| *id).collect();
417        assert_eq!(ids, vec![0, 1], "simulate must run before post_sim");
418    }
419
420    #[test]
421    fn test_plugin_between_bands() {
422        let log = Arc::new(Mutex::new(Vec::<(i32, u32)>::new()));
423        let mid = phase::ANIMATE + 50;
424        let mut runtime = ViewportRuntime::new()
425            .with_plugin(OrderTracker { priority: phase::SIMULATE, id: 2, log: log.clone() })
426            .with_plugin(OrderTracker { priority: mid,             id: 1, log: log.clone() })
427            .with_plugin(OrderTracker { priority: phase::ANIMATE,  id: 0, log: log.clone() });
428
429        let camera = Camera::default();
430        let input = ActionFrame::default();
431        let mut scene = Scene::new();
432        let mut sel = Selection::new();
433        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
434
435        let calls = log.lock().unwrap();
436        let ids: Vec<u32> = calls.iter().map(|(_, id)| *id).collect();
437        assert_eq!(ids, vec![0, 1, 2], "plugins must execute in priority order");
438    }
439
440    // Plugin that records RuntimeEvent::NodeAdded / NodeRemoved calls.
441    struct EventRecorder {
442        added: Arc<Mutex<Vec<NodeId>>>,
443        removed: Arc<Mutex<Vec<NodeId>>>,
444    }
445
446    impl RuntimePlugin for EventRecorder {
447        fn priority(&self) -> i32 {
448            phase::PREPARE
449        }
450        fn on_event(&mut self, event: &RuntimeEvent, _ctx: &mut RuntimeStepContext<'_>) {
451            match event {
452                RuntimeEvent::NodeAdded(id)   => self.added.lock().unwrap().push(*id),
453                RuntimeEvent::NodeRemoved(id) => self.removed.lock().unwrap().push(*id),
454            }
455        }
456        fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {}
457    }
458
459    #[test]
460    fn test_lifecycle_events_node_added() {
461        let added = Arc::new(Mutex::new(Vec::<NodeId>::new()));
462        let removed = Arc::new(Mutex::new(Vec::<NodeId>::new()));
463        let mut runtime = ViewportRuntime::new()
464            .with_plugin(EventRecorder { added: added.clone(), removed: removed.clone() });
465
466        let camera = Camera::default();
467        let input = ActionFrame::default();
468        let mut scene = Scene::new();
469        let mut sel = Selection::new();
470
471        // First step: initializes snapshot, no events.
472        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
473        assert!(added.lock().unwrap().is_empty(), "no events on first step");
474
475        // Add a node, then step.
476        let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
477        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
478
479        let a = added.lock().unwrap();
480        assert!(a.contains(&node_id), "expected NodeAdded event for {:?}", node_id);
481        assert!(removed.lock().unwrap().is_empty());
482    }
483
484    #[test]
485    fn test_lifecycle_events_node_removed() {
486        let added = Arc::new(Mutex::new(Vec::<NodeId>::new()));
487        let removed = Arc::new(Mutex::new(Vec::<NodeId>::new()));
488        let mut runtime = ViewportRuntime::new()
489            .with_plugin(EventRecorder { added: added.clone(), removed: removed.clone() });
490
491        let camera = Camera::default();
492        let input = ActionFrame::default();
493        let mut scene = Scene::new();
494        let mut sel = Selection::new();
495
496        let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
497
498        // First step with the node present.
499        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
500        added.lock().unwrap().clear();
501
502        // Remove the node, then step.
503        scene.remove(node_id);
504        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
505
506        let r = removed.lock().unwrap();
507        assert!(r.contains(&node_id), "expected NodeRemoved event for {:?}", node_id);
508    }
509
510    // Plugin that records which of submit, step, collect were called each frame.
511    #[derive(Default)]
512    struct LifecycleRecorder {
513        log: Arc<Mutex<Vec<&'static str>>>,
514    }
515
516    impl RuntimePlugin for LifecycleRecorder {
517        fn priority(&self) -> i32 {
518            phase::PREPARE
519        }
520        fn submit(&mut self, _ctx: &RuntimeStepContext<'_>) {
521            self.log.lock().unwrap().push("submit");
522        }
523        fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
524            self.log.lock().unwrap().push("step");
525        }
526        fn collect(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
527            self.log.lock().unwrap().push("collect");
528        }
529    }
530
531    #[test]
532    fn test_submit_collect_called() {
533        let log = Arc::new(Mutex::new(Vec::<&'static str>::new()));
534        let mut runtime = ViewportRuntime::new()
535            .with_plugin(LifecycleRecorder { log: log.clone() });
536
537        let camera = Camera::default();
538        let input = ActionFrame::default();
539        let mut scene = Scene::new();
540        let mut sel = Selection::new();
541
542        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
543
544        let calls = log.lock().unwrap();
545        assert!(calls.contains(&"submit"),  "submit must be called");
546        assert!(calls.contains(&"step"),    "step must be called");
547        assert!(calls.contains(&"collect"), "collect must be called");
548        // submit before step before collect.
549        let si = calls.iter().position(|&s| s == "submit").unwrap();
550        let st = calls.iter().position(|&s| s == "step").unwrap();
551        let co = calls.iter().position(|&s| s == "collect").unwrap();
552        assert!(si < st, "submit must come before step");
553        assert!(st < co, "step must come before collect");
554    }
555
556    // ---- resource tests -----------------------------------------------------
557
558    #[test]
559    fn test_resource_insert_get() {
560        let mut res = RuntimeResources::new();
561        res.insert(42u32);
562        assert_eq!(res.get::<u32>(), Some(&42));
563        assert!(res.get::<u64>().is_none());
564    }
565
566    #[test]
567    fn test_resource_insert_overwrites() {
568        let mut res = RuntimeResources::new();
569        res.insert(1u32);
570        res.insert(2u32);
571        assert_eq!(res.get::<u32>(), Some(&2));
572    }
573
574    #[test]
575    fn test_resource_get_mut() {
576        let mut res = RuntimeResources::new();
577        res.insert(0u32);
578        *res.get_mut::<u32>().unwrap() = 99;
579        assert_eq!(res.get::<u32>(), Some(&99));
580    }
581
582    #[test]
583    fn test_resource_remove() {
584        let mut res = RuntimeResources::new();
585        res.insert(7u32);
586        assert!(res.contains::<u32>());
587        let val = res.remove::<u32>();
588        assert_eq!(val, Some(7));
589        assert!(!res.contains::<u32>());
590        // remove again returns None
591        assert!(res.remove::<u32>().is_none());
592    }
593
594    #[test]
595    fn test_resource_missing_returns_none() {
596        let res = RuntimeResources::new();
597        assert!(res.get::<u32>().is_none());
598        assert!(!res.contains::<u32>());
599    }
600
601    // Plugin that inserts a counter resource in step().
602    struct CounterWriter {
603        value: u32,
604    }
605
606    impl RuntimePlugin for CounterWriter {
607        fn priority(&self) -> i32 { phase::ANIMATE }
608        fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
609            ctx.resources.insert(self.value);
610        }
611    }
612
613    // Plugin that reads the counter resource and records it.
614    struct CounterReader {
615        recorded: Arc<Mutex<Vec<u32>>>,
616    }
617
618    impl RuntimePlugin for CounterReader {
619        fn priority(&self) -> i32 { phase::POST_SIM }
620        fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
621            if let Some(&v) = ctx.resources.get::<u32>() {
622                self.recorded.lock().unwrap().push(v);
623            }
624        }
625    }
626
627    #[test]
628    fn test_resource_shared_across_plugins_same_frame() {
629        // CounterWriter runs at ANIMATE, CounterReader at POST_SIM.
630        // The value written by CounterWriter must be visible to CounterReader in the same frame.
631        let recorded = Arc::new(Mutex::new(Vec::<u32>::new()));
632        let mut runtime = ViewportRuntime::new()
633            .with_plugin(CounterWriter { value: 42 })
634            .with_plugin(CounterReader { recorded: recorded.clone() });
635
636        let camera = Camera::default();
637        let input = ActionFrame::default();
638        let mut scene = Scene::new();
639        let mut sel = Selection::new();
640        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
641
642        let vals = recorded.lock().unwrap();
643        assert_eq!(vals.as_slice(), &[42], "reader must see value written by writer in the same frame");
644    }
645
646    #[test]
647    fn test_resource_persists_across_frames() {
648        // A resource inserted in frame 1 must still be present in frame 2 if not removed.
649        let mut runtime = ViewportRuntime::new()
650            .with_plugin(CounterWriter { value: 10 });
651
652        let camera = Camera::default();
653        let input = ActionFrame::default();
654        let mut scene = Scene::new();
655        let mut sel = Selection::new();
656
657        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
658        assert!(runtime.resources().contains::<u32>(), "resource must persist after step");
659    }
660
661    #[test]
662    fn test_runtime_works_without_resources() {
663        // A runtime with no plugins and no resource usage must compile and step correctly.
664        let mut runtime = ViewportRuntime::new();
665        let camera = Camera::default();
666        let input = ActionFrame::default();
667        let mut scene = Scene::new();
668        let mut sel = Selection::new();
669        let output = runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
670        assert!(output.contact_events.is_empty());
671        assert!(output.node_transform_ops.is_empty());
672    }
673
674    // ---- event bus tests ----------------------------------------------------
675
676    // Two distinct event types to verify typed routing.
677    #[derive(Debug, PartialEq)]
678    struct GameplayEvent { id: u32 }
679
680    #[derive(Debug, PartialEq)]
681    struct DiagnosticsEvent { frame_ms: f32 }
682
683    // Plugin that emits GameplayEvents.
684    struct GameplayEmitter { count: u32 }
685
686    impl RuntimePlugin for GameplayEmitter {
687        fn priority(&self) -> i32 { phase::SIMULATE }
688        fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
689            for i in 0..self.count {
690                ctx.output.events.emit(GameplayEvent { id: i });
691            }
692        }
693    }
694
695    // Plugin that emits DiagnosticsEvents.
696    struct DiagnosticsEmitter;
697
698    impl RuntimePlugin for DiagnosticsEmitter {
699        fn priority(&self) -> i32 { phase::POST_SIM }
700        fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
701            ctx.output.events.emit(DiagnosticsEvent { frame_ms: ctx.dt * 1000.0 });
702        }
703    }
704
705    // Plugin that reads GameplayEvents from the bus during its own step.
706    struct GameplayReader {
707        seen: Arc<Mutex<Vec<u32>>>,
708    }
709
710    impl RuntimePlugin for GameplayReader {
711        fn priority(&self) -> i32 { phase::WRITEBACK }
712        fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
713            for ev in ctx.output.events.read::<GameplayEvent>() {
714                self.seen.lock().unwrap().push(ev.id);
715            }
716        }
717    }
718
719    fn run_one_frame(runtime: &mut ViewportRuntime) -> RuntimeOutput {
720        let camera = Camera::default();
721        let input = ActionFrame::default();
722        let mut scene = Scene::new();
723        let mut sel = Selection::new();
724        runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0))
725    }
726
727    #[test]
728    fn test_event_bus_emit_and_read() {
729        let mut bus = RuntimeEventBus::new();
730        bus.emit(GameplayEvent { id: 1 });
731        bus.emit(GameplayEvent { id: 2 });
732        let ids: Vec<u32> = bus.read::<GameplayEvent>().map(|e| e.id).collect();
733        assert_eq!(ids, vec![1, 2]);
734    }
735
736    #[test]
737    fn test_event_bus_typed_isolation() {
738        // Reading a different type returns nothing even when other types have events.
739        let mut bus = RuntimeEventBus::new();
740        bus.emit(GameplayEvent { id: 42 });
741        assert!(!bus.has::<DiagnosticsEvent>());
742        assert_eq!(bus.count::<DiagnosticsEvent>(), 0);
743        let diag: Vec<_> = bus.read::<DiagnosticsEvent>().collect();
744        assert!(diag.is_empty());
745    }
746
747    #[test]
748    fn test_event_bus_drain() {
749        let mut bus = RuntimeEventBus::new();
750        bus.emit(GameplayEvent { id: 7 });
751        bus.emit(GameplayEvent { id: 8 });
752        let drained = bus.drain::<GameplayEvent>();
753        assert_eq!(drained, vec![GameplayEvent { id: 7 }, GameplayEvent { id: 8 }]);
754        // Second drain returns empty.
755        assert!(bus.drain::<GameplayEvent>().is_empty());
756        assert!(!bus.has::<GameplayEvent>());
757    }
758
759    #[test]
760    fn test_event_bus_has_and_count() {
761        let mut bus = RuntimeEventBus::new();
762        assert!(!bus.has::<GameplayEvent>());
763        assert_eq!(bus.count::<GameplayEvent>(), 0);
764        bus.emit(GameplayEvent { id: 1 });
765        bus.emit(GameplayEvent { id: 2 });
766        assert!(bus.has::<GameplayEvent>());
767        assert_eq!(bus.count::<GameplayEvent>(), 2);
768    }
769
770    #[test]
771    fn test_event_bus_is_empty() {
772        let mut bus = RuntimeEventBus::new();
773        assert!(bus.is_empty());
774        bus.emit(GameplayEvent { id: 0 });
775        assert!(!bus.is_empty());
776    }
777
778    #[test]
779    fn test_events_emitted_by_plugin_visible_in_output() {
780        let mut runtime = ViewportRuntime::new()
781            .with_plugin(GameplayEmitter { count: 3 });
782        let output = run_one_frame(&mut runtime);
783        assert_eq!(output.events.count::<GameplayEvent>(), 3);
784        let ids: Vec<u32> = output.events.read::<GameplayEvent>().map(|e| e.id).collect();
785        assert_eq!(ids, vec![0, 1, 2]);
786    }
787
788    #[test]
789    fn test_events_typed_routing_across_plugins() {
790        // GameplayEmitter (SIMULATE) emits GameplayEvents; GameplayReader (WRITEBACK)
791        // reads them in the same frame via ctx.output.events.
792        let seen = Arc::new(Mutex::new(Vec::<u32>::new()));
793        let mut runtime = ViewportRuntime::new()
794            .with_plugin(GameplayEmitter { count: 2 })
795            .with_plugin(GameplayReader { seen: seen.clone() });
796        run_one_frame(&mut runtime);
797        let ids = seen.lock().unwrap();
798        assert_eq!(ids.as_slice(), &[0, 1]);
799    }
800
801    #[test]
802    fn test_multiple_event_types_do_not_interfere() {
803        // Both GameplayEmitter and DiagnosticsEmitter emit; each type is independent.
804        let mut runtime = ViewportRuntime::new()
805            .with_plugin(GameplayEmitter { count: 2 })
806            .with_plugin(DiagnosticsEmitter);
807        let output = run_one_frame(&mut runtime);
808        assert_eq!(output.events.count::<GameplayEvent>(), 2);
809        assert_eq!(output.events.count::<DiagnosticsEvent>(), 1);
810    }
811
812    #[test]
813    fn test_events_cleared_each_frame() {
814        // Events from frame N must not appear in frame N+1.
815        let mut runtime = ViewportRuntime::new()
816            .with_plugin(GameplayEmitter { count: 1 });
817        run_one_frame(&mut runtime);
818        let output2 = run_one_frame(&mut runtime);
819        // The second frame also emits 1 event, but it must be exactly 1, not 2.
820        assert_eq!(output2.events.count::<GameplayEvent>(), 1);
821    }
822
823    #[test]
824    fn test_existing_output_fields_unaffected() {
825        // Backward-compat: contact_events, selection_ops, node_transform_ops,
826        // and camera_follow_target continue to work with no changes.
827        let mut runtime = ViewportRuntime::new();
828        let output = run_one_frame(&mut runtime);
829        assert!(output.contact_events.is_empty());
830        assert!(output.selection_ops.is_empty());
831        assert!(output.node_transform_ops.is_empty());
832        assert!(output.camera_follow_target.is_none());
833        assert!(output.events.is_empty());
834    }
835
836    // ---- job handoff tests --------------------------------------------------
837
838    use crate::runtime::jobs::{JobPoll, JobSlot};
839
840    #[test]
841    fn test_job_slot_empty_by_default() {
842        let slot: JobSlot<u32> = JobSlot::empty();
843        assert!(slot.is_empty());
844        assert!(!slot.is_pending());
845        assert!(!slot.is_ready());
846    }
847
848    #[test]
849    fn test_job_slot_pending_after_new() {
850        let (slot, _sender) = JobSlot::<u32>::new();
851        assert!(slot.is_pending());
852        assert!(!slot.is_empty());
853        assert!(!slot.is_ready());
854    }
855
856    #[test]
857    fn test_job_handoff_complete() {
858        let (mut slot, sender) = JobSlot::<u32>::new();
859        assert!(slot.is_pending());
860        sender.complete(42);
861        assert!(slot.is_ready());
862        match slot.take() {
863            JobPoll::Ready(v) => assert_eq!(v, 42),
864            _ => panic!("expected Ready, got other variant"),
865        }
866        // Slot resets to empty after taking the result.
867        assert!(slot.is_empty());
868    }
869
870    #[test]
871    fn test_job_handoff_fail() {
872        let (mut slot, sender) = JobSlot::<u32>::new();
873        sender.fail("disk error");
874        match slot.take() {
875            JobPoll::Failed(msg) => assert_eq!(msg, "disk error"),
876            _ => panic!("expected Failed"),
877        }
878        assert!(slot.is_empty());
879    }
880
881    #[test]
882    fn test_job_handoff_cancel() {
883        let (mut slot, sender) = JobSlot::<u32>::new();
884        sender.cancel();
885        match slot.take() {
886            JobPoll::Cancelled => {}
887            _ => panic!("expected Cancelled"),
888        }
889        assert!(slot.is_empty());
890    }
891
892    #[test]
893    fn test_job_sender_drop_cancels() {
894        let (mut slot, sender) = JobSlot::<u32>::new();
895        drop(sender);
896        match slot.take() {
897            JobPoll::Cancelled => {}
898            _ => panic!("expected Cancelled after sender drop"),
899        }
900        assert!(slot.is_empty());
901    }
902
903    #[test]
904    fn test_job_take_on_empty_returns_empty() {
905        let mut slot: JobSlot<u32> = JobSlot::empty();
906        match slot.take() {
907            JobPoll::Empty => {}
908            _ => panic!("expected Empty"),
909        }
910    }
911
912    #[test]
913    fn test_job_take_while_pending_returns_pending() {
914        let (mut slot, _sender) = JobSlot::<u32>::new();
915        match slot.take() {
916            JobPoll::Pending => {}
917            _ => panic!("expected Pending"),
918        }
919        // Still pending after a non-consuming poll.
920        assert!(slot.is_pending());
921    }
922
923    #[test]
924    fn test_job_async_handoff_across_frames() {
925        // Plugin holds a JobSlot. Frame 1: start job. Frame 2+: result arrives.
926        struct LoaderPlugin {
927            slot: JobSlot<u32>,
928            result: Arc<Mutex<Option<u32>>>,
929        }
930
931        impl RuntimePlugin for LoaderPlugin {
932            fn priority(&self) -> i32 { phase::PREPARE }
933
934            fn submit(&mut self, _ctx: &RuntimeStepContext<'_>) {
935                if self.slot.is_empty() {
936                    let (new_slot, sender) = JobSlot::new();
937                    self.slot = new_slot;
938                    // Inline completion simulating a fast background thread.
939                    sender.complete(99);
940                }
941            }
942
943            fn collect(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
944                if let JobPoll::Ready(v) = self.slot.take() {
945                    *self.result.lock().unwrap() = Some(v);
946                }
947            }
948
949            fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {}
950        }
951
952        let result = Arc::new(Mutex::new(None::<u32>));
953        let mut runtime = ViewportRuntime::new()
954            .with_plugin(LoaderPlugin { slot: JobSlot::empty(), result: result.clone() });
955
956        run_one_frame(&mut runtime);
957        assert_eq!(*result.lock().unwrap(), Some(99));
958    }
959
960    #[test]
961    fn test_job_staged_resource_update_ordering() {
962        // A low-priority plugin integrates a job result into resources in collect.
963        // A high-priority plugin's collect must NOT see it (runs first due to sort order).
964        // A later-running plugin's collect DOES see it.
965
966        struct Integrator {
967            slot: JobSlot<u32>,
968        }
969
970        impl RuntimePlugin for Integrator {
971            fn priority(&self) -> i32 { phase::PREPARE }  // low number = runs early in collect
972
973            fn submit(&mut self, _ctx: &RuntimeStepContext<'_>) {
974                if self.slot.is_empty() {
975                    let (s, sender) = JobSlot::new();
976                    self.slot = s;
977                    sender.complete(77);
978                }
979            }
980
981            fn collect(&mut self, ctx: &mut RuntimeStepContext<'_>) {
982                if let JobPoll::Ready(v) = self.slot.take() {
983                    ctx.resources.insert(v);
984                }
985            }
986
987            fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {}
988        }
989
990        struct Reader {
991            seen: Arc<Mutex<Option<u32>>>,
992        }
993
994        impl RuntimePlugin for Reader {
995            fn priority(&self) -> i32 { phase::POST_SIM }  // higher number = runs later in collect
996
997            fn collect(&mut self, ctx: &mut RuntimeStepContext<'_>) {
998                *self.seen.lock().unwrap() = ctx.resources.get::<u32>().copied();
999            }
1000
1001            fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {}
1002        }
1003
1004        let seen = Arc::new(Mutex::new(None::<u32>));
1005        let mut runtime = ViewportRuntime::new()
1006            .with_plugin(Integrator { slot: JobSlot::empty() })
1007            .with_plugin(Reader { seen: seen.clone() });
1008
1009        run_one_frame(&mut runtime);
1010        // Integrator (PREPARE) integrates into resources in collect before Reader (POST_SIM) collect.
1011        assert_eq!(*seen.lock().unwrap(), Some(77));
1012    }
1013
1014    #[test]
1015    fn test_job_slot_in_resources() {
1016        // A JobSlot stored in RuntimeResources is accessible to other plugins.
1017        let (slot, sender) = JobSlot::<u32>::new();
1018        let mut res = RuntimeResources::new();
1019        res.insert(slot);
1020        sender.complete(55);
1021        let found = res.get_mut::<JobSlot<u32>>().unwrap();
1022        match found.take() {
1023            JobPoll::Ready(v) => assert_eq!(v, 55),
1024            _ => panic!("expected Ready"),
1025        }
1026    }
1027
1028    // ---- skeleton / skinning tests ------------------------------------------
1029
1030    use crate::resources::SkinWeights;
1031    use crate::runtime::plugins::skeleton_plugin::{
1032        Joint, JointMatrices, Pose, Skeleton, SkeletonPlugin, apply_skin,
1033    };
1034
1035    fn two_joint_skeleton() -> Skeleton {
1036        Skeleton::new(vec![
1037            Joint {
1038                name: "root".into(),
1039                parent: None,
1040                inverse_bind: glam::Affine3A::IDENTITY,
1041            },
1042            Joint {
1043                name: "child".into(),
1044                parent: Some(0),
1045                inverse_bind: glam::Affine3A::from_translation(-glam::Vec3::Y),
1046            },
1047        ])
1048    }
1049
1050    fn single_vertex_weights(joint: u8, weight: f32) -> SkinWeights {
1051        SkinWeights {
1052            joint_indices: vec![[joint, 0, 0, 0]],
1053            joint_weights: vec![[weight, 0.0, 0.0, 0.0]],
1054        }
1055    }
1056
1057    #[test]
1058    fn test_skeleton_joint_count() {
1059        let sk = two_joint_skeleton();
1060        assert_eq!(sk.joint_count(), 2);
1061    }
1062
1063    #[test]
1064    fn test_skeleton_find_joint() {
1065        let sk = two_joint_skeleton();
1066        assert_eq!(sk.find_joint("root"), Some(0));
1067        assert_eq!(sk.find_joint("child"), Some(1));
1068        assert_eq!(sk.find_joint("missing"), None);
1069    }
1070
1071    #[test]
1072    fn test_pose_identity_transforms() {
1073        let pose = Pose::identity(3);
1074        assert_eq!(pose.joint_count(), 3);
1075        for t in &pose.local_transforms {
1076            assert_eq!(*t, glam::Affine3A::IDENTITY);
1077        }
1078    }
1079
1080    #[test]
1081    fn test_joint_matrices_single_root_identity() {
1082        let sk = Skeleton::new(vec![Joint {
1083            name: "root".into(),
1084            parent: None,
1085            inverse_bind: glam::Affine3A::IDENTITY,
1086        }]);
1087        let pose = Pose::identity(1);
1088        let mats = JointMatrices::compute(&sk, &pose);
1089        let m = mats.as_slice()[0];
1090        // identity local * identity inverse_bind = identity
1091        assert!(m.matrix3.col(0).abs_diff_eq(glam::Vec3A::X, 1e-5));
1092        assert!(m.translation.abs_diff_eq(glam::Vec3A::ZERO, 1e-5));
1093    }
1094
1095    #[test]
1096    fn test_joint_matrices_parent_child_chain() {
1097        // Root translated by (1,0,0) in local space.
1098        // Child at local identity, parent = 0.
1099        // child.inverse_bind offsets by -Y.
1100        // Expected child world = root_world * child_local = translate(1,0,0)
1101        // Final matrix = world * inverse_bind = translate(1,0,0) * translate(0,-1,0) = translate(1,-1,0)
1102        let sk = two_joint_skeleton();
1103        let mut pose = Pose::identity(2);
1104        pose.local_transforms[0] = glam::Affine3A::from_translation(glam::Vec3::X);
1105        let mats = JointMatrices::compute(&sk, &pose);
1106        let child = mats.as_slice()[1];
1107        let expected_translation = glam::Vec3A::new(1.0, -1.0, 0.0);
1108        assert!(
1109            child.translation.abs_diff_eq(expected_translation, 1e-5),
1110            "child translation was {:?}, expected {:?}",
1111            child.translation,
1112            expected_translation,
1113        );
1114    }
1115
1116    #[test]
1117    fn test_apply_skin_identity_pose() {
1118        let sk = Skeleton::new(vec![Joint {
1119            name: "root".into(),
1120            parent: None,
1121            inverse_bind: glam::Affine3A::IDENTITY,
1122        }]);
1123        let pose = Pose::identity(1);
1124        let mats = JointMatrices::compute(&sk, &pose);
1125
1126        let positions = vec![[1.0f32, 2.0, 3.0]];
1127        let normals = vec![[0.0f32, 1.0, 0.0]];
1128        let weights = single_vertex_weights(0, 1.0);
1129        let (out_pos, out_nrm) = apply_skin(&positions, &normals, &weights, &mats);
1130
1131        assert!((out_pos[0][0] - 1.0).abs() < 1e-5);
1132        assert!((out_pos[0][1] - 2.0).abs() < 1e-5);
1133        assert!((out_pos[0][2] - 3.0).abs() < 1e-5);
1134        assert!((out_nrm[0][1] - 1.0).abs() < 1e-5);
1135    }
1136
1137    #[test]
1138    fn test_apply_skin_single_joint_full_weight() {
1139        // Joint 0 translates by (5,0,0). Vertex at origin must move to (5,0,0).
1140        let sk = Skeleton::new(vec![Joint {
1141            name: "root".into(),
1142            parent: None,
1143            inverse_bind: glam::Affine3A::IDENTITY,
1144        }]);
1145        let mut pose = Pose::identity(1);
1146        pose.local_transforms[0] = glam::Affine3A::from_translation(glam::Vec3::new(5.0, 0.0, 0.0));
1147        let mats = JointMatrices::compute(&sk, &pose);
1148
1149        let positions = vec![[0.0f32, 0.0, 0.0]];
1150        let normals = vec![[1.0f32, 0.0, 0.0]];
1151        let weights = single_vertex_weights(0, 1.0);
1152        let (out_pos, _) = apply_skin(&positions, &normals, &weights, &mats);
1153        assert!((out_pos[0][0] - 5.0).abs() < 1e-5);
1154        assert!(out_pos[0][1].abs() < 1e-5);
1155    }
1156
1157    #[test]
1158    fn test_apply_skin_two_joint_blend() {
1159        // Two joints: joint 0 at (0,0,0), joint 1 at (10,0,0).
1160        // Vertex at origin, 50/50 blend -> output at (5,0,0).
1161        let sk = Skeleton::new(vec![
1162            Joint { name: "a".into(), parent: None, inverse_bind: glam::Affine3A::IDENTITY },
1163            Joint { name: "b".into(), parent: None, inverse_bind: glam::Affine3A::IDENTITY },
1164        ]);
1165        let mut pose = Pose::identity(2);
1166        pose.local_transforms[1] = glam::Affine3A::from_translation(glam::Vec3::new(10.0, 0.0, 0.0));
1167        let mats = JointMatrices::compute(&sk, &pose);
1168
1169        let positions = vec![[0.0f32, 0.0, 0.0]];
1170        let normals = vec![[1.0f32, 0.0, 0.0]];
1171        let weights = SkinWeights {
1172            joint_indices: vec![[0, 1, 0, 0]],
1173            joint_weights: vec![[0.5, 0.5, 0.0, 0.0]],
1174        };
1175        let (out_pos, _) = apply_skin(&positions, &normals, &weights, &mats);
1176        assert!((out_pos[0][0] - 5.0).abs() < 1e-5, "expected 5.0, got {}", out_pos[0][0]);
1177    }
1178
1179    #[test]
1180    fn test_apply_skin_normal_renormalized() {
1181        // Even with a uniform scale, output normals must have unit length.
1182        let sk = Skeleton::new(vec![Joint {
1183            name: "root".into(),
1184            parent: None,
1185            inverse_bind: glam::Affine3A::IDENTITY,
1186        }]);
1187        let mut pose = Pose::identity(1);
1188        pose.local_transforms[0] = glam::Affine3A::from_scale(glam::Vec3::splat(2.0));
1189        let mats = JointMatrices::compute(&sk, &pose);
1190
1191        let positions = vec![[1.0f32, 0.0, 0.0]];
1192        let normals = vec![[1.0f32, 0.0, 0.0]];
1193        let weights = single_vertex_weights(0, 1.0);
1194        let (_, out_nrm) = apply_skin(&positions, &normals, &weights, &mats);
1195        let len = (out_nrm[0][0].powi(2) + out_nrm[0][1].powi(2) + out_nrm[0][2].powi(2)).sqrt();
1196        assert!((len - 1.0).abs() < 1e-5, "normal length was {len}");
1197    }
1198
1199    #[test]
1200    fn test_skeleton_plugin_writes_update_to_output() {
1201        let sk = Skeleton::new(vec![Joint {
1202            name: "root".into(),
1203            parent: None,
1204            inverse_bind: glam::Affine3A::IDENTITY,
1205        }]);
1206        let positions = vec![[0.0f32, 0.0, 0.0]];
1207        let normals = vec![[0.0f32, 1.0, 0.0]];
1208        let weights = single_vertex_weights(0, 1.0);
1209        let mesh_id = crate::resources::mesh_store::MeshId(0);
1210
1211        let mut runtime = ViewportRuntime::new()
1212            .with_plugin(SkeletonPlugin::new(sk, mesh_id, positions, normals, weights));
1213
1214        // Insert a pose so the plugin fires.
1215        runtime.resources_mut().insert(Pose::identity(1));
1216
1217        let output = run_one_frame(&mut runtime);
1218        assert_eq!(output.skinned_mesh_updates.len(), 1);
1219        assert_eq!(output.skinned_mesh_updates[0].mesh_id, mesh_id);
1220    }
1221
1222    #[test]
1223    fn test_skinned_mesh_updates_empty_without_pose() {
1224        let sk = Skeleton::new(vec![Joint {
1225            name: "root".into(),
1226            parent: None,
1227            inverse_bind: glam::Affine3A::IDENTITY,
1228        }]);
1229        let mesh_id = crate::resources::mesh_store::MeshId(0);
1230        let mut runtime = ViewportRuntime::new()
1231            .with_plugin(SkeletonPlugin::new(
1232                sk,
1233                mesh_id,
1234                vec![[0.0f32; 3]],
1235                vec![[0.0f32, 1.0, 0.0]],
1236                single_vertex_weights(0, 1.0),
1237            ));
1238        // No Pose inserted -> no update.
1239        let output = run_one_frame(&mut runtime);
1240        assert!(output.skinned_mesh_updates.is_empty());
1241    }
1242
1243    #[test]
1244    fn test_skinned_mesh_updates_cleared_each_frame() {
1245        let sk = Skeleton::new(vec![Joint {
1246            name: "root".into(),
1247            parent: None,
1248            inverse_bind: glam::Affine3A::IDENTITY,
1249        }]);
1250        let mesh_id = crate::resources::mesh_store::MeshId(0);
1251        let mut runtime = ViewportRuntime::new()
1252            .with_plugin(SkeletonPlugin::new(
1253                sk,
1254                mesh_id,
1255                vec![[0.0f32; 3]],
1256                vec![[0.0f32, 1.0, 0.0]],
1257                single_vertex_weights(0, 1.0),
1258            ));
1259        runtime.resources_mut().insert(Pose::identity(1));
1260
1261        run_one_frame(&mut runtime);
1262        let output2 = run_one_frame(&mut runtime);
1263        // Each frame produces exactly 1 update, not 2.
1264        assert_eq!(output2.skinned_mesh_updates.len(), 1);
1265    }
1266}
1267
1268/// Per-frame scene orchestration layer.
1269///
1270/// Owns plugins, an optional fixed timestep accumulator, and a transform snapshot
1271/// table. Does not own the scene, selection, GPU resources, or window.
1272///
1273/// # Priority execution order
1274///
1275/// Each call to [`step`](Self::step) executes plugins in ascending priority order:
1276///
1277/// 1. `[i32::MIN, SELECT)` -- Prepare (100), Pick (200): once, at wall dt
1278/// 2. `[SELECT, MANIPULATE)` -- Select (300): once (built-in SelectionSystem runs first)
1279/// 3. `[MANIPULATE, SIMULATE)` -- Manipulate (400), Animate (500): once
1280/// 4. `[SIMULATE, POST_SIM)` -- Simulate (600): once per fixed step or once at wall dt
1281/// 5. `[POST_SIM, i32::MAX]` -- PostSim (700), Writeback (800): once at wall dt
1282pub struct ViewportRuntime {
1283    mode: SceneRuntimeMode,
1284    plugins: Vec<Box<dyn RuntimePlugin>>,
1285    gpu_plugins: Vec<Box<dyn GpuPlugin>>,
1286    /// False until every registered GPU plugin has had `init_gpu` called.
1287    /// Cleared again when a new GPU plugin is registered.
1288    gpu_initialized: bool,
1289    fixed_timestep: Option<FixedTimestep>,
1290    snapshots: TransformSnapshotTable,
1291    step_index: u64,
1292    selection_system: Option<SelectionSystem>,
1293    manipulation_system: Option<ManipulationSystem>,
1294    camera_follow: Option<CameraFollow>,
1295    /// Typed resource registry shared across plugins each frame.
1296    resources: RuntimeResources,
1297    /// Node IDs present at the end of the previous frame.
1298    prev_node_ids: std::collections::HashSet<crate::interaction::selection::NodeId>,
1299    /// False on the very first step call; after that, lifecycle events are emitted.
1300    scene_initialized: bool,
1301}
1302
1303impl Default for ViewportRuntime {
1304    fn default() -> Self {
1305        Self::new()
1306    }
1307}
1308
1309impl ViewportRuntime {
1310    /// Create a runtime with default settings and no plugins.
1311    pub fn new() -> Self {
1312        Self {
1313            mode: SceneRuntimeMode::default(),
1314            plugins: Vec::new(),
1315            gpu_plugins: Vec::new(),
1316            gpu_initialized: false,
1317            fixed_timestep: None,
1318            snapshots: TransformSnapshotTable::new(),
1319            step_index: 0,
1320            selection_system: None,
1321            manipulation_system: None,
1322            camera_follow: None,
1323            resources: RuntimeResources::new(),
1324            prev_node_ids: std::collections::HashSet::new(),
1325            scene_initialized: false,
1326        }
1327    }
1328
1329    /// Enable the built-in click-to-select system.
1330    ///
1331    /// The SelectionSystem runs before the `Select` phase each frame. It reads
1332    /// `RuntimeFrameContext::clicked`, `pick_hit`, and `shift_held` to produce
1333    /// SelectionOp entries in RuntimeOutput.
1334    pub fn with_selection_system(mut self) -> Self {
1335        self.selection_system = Some(SelectionSystem::new());
1336        self
1337    }
1338
1339    /// Enable the built-in manipulation system.
1340    ///
1341    /// The ManipulationSystem runs before the `Manipulate` phase each frame. It
1342    /// drives G/R/S sessions and gizmo drag from RuntimeFrameContext inputs, writing
1343    /// transform changes via TransformWriteback.
1344    pub fn with_manipulation_system(mut self) -> Self {
1345        self.manipulation_system = Some(ManipulationSystem::new());
1346        self
1347    }
1348
1349    /// True while the built-in manipulation system has an active G/R/S session or gizmo drag.
1350    ///
1351    /// Use this to suppress orbit camera movement while manipulating objects.
1352    pub fn is_manipulating(&self) -> bool {
1353        self.manipulation_system
1354            .as_ref()
1355            .map_or(false, |m| m.is_active())
1356    }
1357
1358    /// Access the built-in manipulation system, if enabled.
1359    pub fn manipulation_system(&self) -> Option<&ManipulationSystem> {
1360        self.manipulation_system.as_ref()
1361    }
1362
1363    /// Set the runtime mode.
1364    pub fn with_mode(mut self, mode: SceneRuntimeMode) -> Self {
1365        self.mode = mode;
1366        self
1367    }
1368
1369    /// Register a plugin. Plugins run in ascending priority order each frame.
1370    /// Plugins with equal priority run in registration order (stable sort).
1371    pub fn with_plugin(mut self, plugin: impl RuntimePlugin) -> Self {
1372        self.plugins.push(Box::new(plugin));
1373        self
1374    }
1375
1376    /// Register a GPU plugin. GPU plugins encode wgpu command buffers from
1377    /// [`ViewportRuntime::pre_prepare`], which the host calls each frame after
1378    /// `step` and before `renderer.prepare()`. Plugins run in ascending
1379    /// priority order; equal priorities preserve registration order.
1380    ///
1381    /// Registering a plugin after the runtime has already run a frame re-runs
1382    /// every GPU plugin's `init_gpu` on the next `pre_prepare`. Implementations
1383    /// should be idempotent or guard their own one-time setup.
1384    pub fn with_gpu_plugin(mut self, plugin: impl GpuPlugin) -> Self {
1385        self.gpu_plugins.push(Box::new(plugin));
1386        self.gpu_initialized = false;
1387        self
1388    }
1389
1390    /// Register a GPU plugin scoped to a single viewport.
1391    ///
1392    /// The plugin's `pre_prepare` and `post_paint` only execute when the
1393    /// host's [`GpuFrameContext::viewport_id`] equals `viewport`. `init_gpu`
1394    /// and `on_device_recreated` still run unconditionally.
1395    ///
1396    /// Use this in multi-viewport hosts (CAD quad-view, split-screen) that
1397    /// call `pre_prepare` / `post_paint` once per viewport with a populated
1398    /// `viewport_id`. Single-viewport hosts that never set `viewport_id`
1399    /// effectively disable scoped plugins; use [`with_gpu_plugin`](Self::with_gpu_plugin)
1400    /// instead.
1401    pub fn with_gpu_plugin_for_viewport(
1402        mut self,
1403        viewport: crate::renderer::ViewportId,
1404        plugin: impl GpuPlugin,
1405    ) -> Self {
1406        let scoped = gpu_plugin::ViewportScopedPlugin {
1407            viewport,
1408            inner: plugin,
1409        };
1410        self.gpu_plugins.push(Box::new(scoped));
1411        self.gpu_initialized = false;
1412        self
1413    }
1414
1415    /// Notify every registered GPU plugin that the wgpu device has been
1416    /// recreated (device loss, surface re-init, host-driven reset).
1417    ///
1418    /// Calls [`GpuPlugin::on_device_recreated`] on each plugin, then clears
1419    /// the init flag so `init_gpu` runs again with the new device on the
1420    /// next [`pre_prepare`](Self::pre_prepare). The runtime does not detect
1421    /// device loss on its own; the host is responsible for invoking this
1422    /// when it recreates the device.
1423    pub fn notify_device_recreated(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
1424        for p in self.gpu_plugins.iter_mut() {
1425            p.on_device_recreated(device, queue);
1426        }
1427        self.gpu_initialized = false;
1428    }
1429
1430    /// Run every registered GPU plugin's `pre_prepare` in ascending priority
1431    /// order and return the concatenated command buffers.
1432    ///
1433    /// Call after [`step`](Self::step) and before `renderer.prepare()`. The
1434    /// returned buffers should be submitted ahead of the renderer's own:
1435    ///
1436    /// ```text
1437    /// let plugin_bufs  = runtime.pre_prepare(device, queue, &gpu_ctx);
1438    /// let prepare_bufs = renderer.pass().prepare(device, queue, &frame);
1439    /// queue.submit(plugin_bufs.into_iter().chain(prepare_bufs));
1440    /// ```
1441    ///
1442    /// wgpu submit ordering guarantees plugin work completes before
1443    /// `prepare()`'s, so any storage buffer written by a plugin is observable
1444    /// by the standard render passes in the same frame.
1445    ///
1446    /// On the first call (or after a new plugin is registered), every plugin's
1447    /// `init_gpu` is invoked once before any `pre_prepare`.
1448    pub fn pre_prepare(
1449        &mut self,
1450        device: &wgpu::Device,
1451        queue: &wgpu::Queue,
1452        ctx: &GpuFrameContext<'_>,
1453    ) -> Vec<wgpu::CommandBuffer> {
1454        if !self.gpu_initialized {
1455            for p in self.gpu_plugins.iter_mut() {
1456                p.init_gpu(device);
1457            }
1458            self.gpu_initialized = true;
1459        }
1460        // Stable sort: equal priorities preserve registration order. Same
1461        // contract as the CPU plugin list.
1462        self.gpu_plugins.sort_by_key(|p| p.priority());
1463
1464        let mut out = Vec::new();
1465        for p in self.gpu_plugins.iter_mut() {
1466            out.extend(p.pre_prepare(device, queue, ctx));
1467        }
1468        out
1469    }
1470
1471    /// Run every registered GPU plugin's `post_paint` in ascending priority
1472    /// order and return the concatenated command buffers.
1473    ///
1474    /// Call after `renderer.paint_to()` has run for the frame. The host
1475    /// supplies views of the just-rendered color and depth targets (and
1476    /// optionally a pick-id view), and is responsible for compositing any
1477    /// plugin overlay output into the final image: the lib does not loop
1478    /// plugin output back into the rendered color.
1479    ///
1480    /// Plugins registered after the first frame still go through `init_gpu`
1481    /// on the next `pre_prepare`; this method does not trigger init on its
1482    /// own to keep the post-paint path deterministic.
1483    pub fn post_paint(
1484        &mut self,
1485        device: &wgpu::Device,
1486        queue: &wgpu::Queue,
1487        targets: &PostPaintTargets<'_>,
1488        ctx: &GpuFrameContext<'_>,
1489    ) -> Vec<wgpu::CommandBuffer> {
1490        // Stable sort: equal priorities preserve registration order. The list
1491        // is already sorted if `pre_prepare` ran first this frame; sorting
1492        // again is cheap and keeps `post_paint` safe to call standalone.
1493        self.gpu_plugins.sort_by_key(|p| p.priority());
1494
1495        let mut out = Vec::new();
1496        for p in self.gpu_plugins.iter_mut() {
1497            out.extend(p.post_paint(device, queue, targets, ctx));
1498        }
1499        out
1500    }
1501
1502    /// Enable fixed-timestep accumulation for physics plugins.
1503    ///
1504    /// When set, plugins in the `[SIMULATE, POST_SIM)` range run once per
1505    /// accumulated fixed step rather than once per frame at wall dt. All other
1506    /// priority ranges always run once per frame.
1507    pub fn with_fixed_timestep(mut self, ts: FixedTimestep) -> Self {
1508        self.fixed_timestep = Some(ts);
1509        self
1510    }
1511
1512    /// Replace the fixed timestep at runtime. Resets the accumulator.
1513    ///
1514    /// Use this to change the simulation rate after construction without
1515    /// rebuilding the runtime or its plugins.
1516    pub fn set_fixed_timestep(&mut self, ts: FixedTimestep) {
1517        self.fixed_timestep = Some(ts);
1518    }
1519
1520    /// Remove the fixed timestep, reverting to one simulate call per frame at wall dt.
1521    pub fn clear_fixed_timestep(&mut self) {
1522        self.fixed_timestep = None;
1523    }
1524
1525    /// The current runtime mode.
1526    pub fn mode(&self) -> SceneRuntimeMode {
1527        self.mode
1528    }
1529
1530    /// The transform snapshot table for interpolated rendering.
1531    ///
1532    /// Updated each frame during writeback. Pass [`alpha`](Self::alpha) as the
1533    /// blend factor to [`TransformSnapshotTable::interpolated`].
1534    pub fn snapshots(&self) -> &TransformSnapshotTable {
1535        &self.snapshots
1536    }
1537
1538    /// Read access to the shared resource registry.
1539    ///
1540    /// Use this after `step` to inspect resources without running the frame loop.
1541    /// During the frame loop, access resources through `RuntimeStepContext::resources`.
1542    pub fn resources(&self) -> &RuntimeResources {
1543        &self.resources
1544    }
1545
1546    /// Write access to the shared resource registry.
1547    ///
1548    /// Use this to pre-populate resources before the first `step`, or to inject
1549    /// resources from outside the plugin system (e.g. from the application).
1550    pub fn resources_mut(&mut self) -> &mut RuntimeResources {
1551        &mut self.resources
1552    }
1553
1554    /// Blend factor for rendering interpolation between fixed steps.
1555    ///
1556    /// Returns `1.0` if no fixed timestep is configured (render directly from
1557    /// current scene transforms without interpolation).
1558    pub fn alpha(&self) -> f32 {
1559        self.fixed_timestep.as_ref().map_or(1.0, |ts| ts.alpha())
1560    }
1561
1562    /// Monotonically increasing simulation step counter.
1563    ///
1564    /// Incremented once per simulate execution. With a fixed timestep this
1565    /// means it may increment multiple times per rendered frame. Wraps on overflow.
1566    pub fn step_index(&self) -> u64 {
1567        self.step_index
1568    }
1569
1570    /// Set a camera follow binding (builder style).
1571    ///
1572    /// Each call to [`step`](Self::step) will compute a suggested camera center
1573    /// from the followed node and return it in [`RuntimeOutput::camera_follow_target`].
1574    pub fn with_camera_follow(mut self, follow: CameraFollow) -> Self {
1575        self.camera_follow = Some(follow);
1576        self
1577    }
1578
1579    /// Update the camera follow binding at runtime.
1580    pub fn set_camera_follow(&mut self, follow: CameraFollow) {
1581        self.camera_follow = Some(follow);
1582    }
1583
1584    /// Remove the camera follow binding. [`RuntimeOutput::camera_follow_target`]
1585    /// will be `None` after this call.
1586    pub fn clear_camera_follow(&mut self) {
1587        self.camera_follow = None;
1588    }
1589
1590    /// The current camera follow binding.
1591    pub fn camera_follow(&self) -> Option<&CameraFollow> {
1592        self.camera_follow.as_ref()
1593    }
1594
1595    /// Run one frame of the runtime.
1596    ///
1597    /// Plugins run in ascending priority order. The simulate range
1598    /// `[SIMULATE, POST_SIM)` runs once per accumulated fixed step (or once at
1599    /// `frame.dt` when no fixed timestep is configured). All other ranges run
1600    /// once per frame. Transform ops are flushed to `scene` and the snapshot
1601    /// table is updated after the writeback range.
1602    ///
1603    /// Selection ops produced by plugins are applied to `selection` before returning.
1604    /// Contact events and the applied transform ops are returned in [`RuntimeOutput`].
1605    pub fn step(
1606        &mut self,
1607        scene: &mut Scene,
1608        selection: &mut Selection,
1609        frame: &RuntimeFrameContext,
1610    ) -> RuntimeOutput {
1611        let mut output = RuntimeOutput::default();
1612        let mut writeback = TransformWriteback::default();
1613
1614        // --- Lifecycle event detection ---------------------------------------
1615        // Collect current node IDs from the scene.
1616        let current_ids: std::collections::HashSet<crate::interaction::selection::NodeId> =
1617            scene.nodes().map(|n| n.id()).collect();
1618
1619        if self.scene_initialized {
1620            // Diff against previous frame and dispatch events.
1621            let mut events: Vec<RuntimeEvent> = Vec::new();
1622            for &id in current_ids.difference(&self.prev_node_ids) {
1623                events.push(RuntimeEvent::NodeAdded(id));
1624            }
1625            for &id in self.prev_node_ids.difference(&current_ids) {
1626                events.push(RuntimeEvent::NodeRemoved(id));
1627            }
1628
1629            if !events.is_empty() {
1630                // Take plugins out to avoid aliasing self while dispatching.
1631                let mut plugins = std::mem::take(&mut self.plugins);
1632                for event in &events {
1633                    for plugin in plugins.iter_mut() {
1634                        let mut ctx = RuntimeStepContext {
1635                            priority: plugin.priority(),
1636                            dt: frame.dt,
1637                            scene,
1638                            writeback: &mut writeback,
1639                            output: &mut output,
1640                            pick_hit: frame.pick_hit,
1641                            resources: &mut self.resources,
1642                        };
1643                        plugin.on_event(event, &mut ctx);
1644                    }
1645                }
1646                self.plugins = plugins;
1647            }
1648        } else {
1649            self.scene_initialized = true;
1650        }
1651        self.prev_node_ids = current_ids;
1652
1653        // --- Sort plugins by priority (stable: preserves registration order within a band).
1654        self.plugins.sort_by_key(|p| p.priority());
1655
1656        // Take plugins out so we can access self freely during the step loop.
1657        let mut plugins = std::mem::take(&mut self.plugins);
1658
1659        // --- Submit pass: all plugins in priority order ----------------------
1660        for plugin in plugins.iter_mut() {
1661            let ctx = RuntimeStepContext {
1662                priority: plugin.priority(),
1663                dt: frame.dt,
1664                scene,
1665                writeback: &mut writeback,
1666                output: &mut output,
1667                pick_hit: frame.pick_hit,
1668                resources: &mut self.resources,
1669            };
1670            plugin.submit(&ctx);
1671        }
1672
1673        // --- Step loop -------------------------------------------------------
1674
1675        // Prepare + Pick: [MIN, SELECT)
1676        run_range(
1677            &mut plugins,
1678            i32::MIN,
1679            phase::SELECT,
1680            frame.dt,
1681            frame,
1682            scene,
1683            &mut writeback,
1684            &mut output,
1685            &mut self.resources,
1686        );
1687
1688        // Built-in selection system runs before the Select range.
1689        if let Some(sel_sys) = &self.selection_system {
1690            sel_sys.step(frame, &mut output);
1691        }
1692        // Select: [SELECT, MANIPULATE)
1693        run_range(
1694            &mut plugins,
1695            phase::SELECT,
1696            phase::MANIPULATE,
1697            frame.dt,
1698            frame,
1699            scene,
1700            &mut writeback,
1701            &mut output,
1702            &mut self.resources,
1703        );
1704
1705        // Built-in manipulation system runs before the Manipulate range.
1706        if self.manipulation_system.is_some() {
1707            let mut manip_sys = self.manipulation_system.take().unwrap();
1708            manip_sys.step(frame, scene, selection, &mut writeback, &mut output);
1709            self.manipulation_system = Some(manip_sys);
1710        }
1711        // Manipulate + Animate: [MANIPULATE, SIMULATE)
1712        run_range(
1713            &mut plugins,
1714            phase::MANIPULATE,
1715            phase::SIMULATE,
1716            frame.dt,
1717            frame,
1718            scene,
1719            &mut writeback,
1720            &mut output,
1721            &mut self.resources,
1722        );
1723
1724        // Simulate: [SIMULATE, POST_SIM) -- once per fixed step or once at wall dt.
1725        if self.fixed_timestep.is_some() {
1726            let mut ts = self.fixed_timestep.take().unwrap();
1727            for step_dt in ts.advance(frame.dt) {
1728                run_range(
1729                    &mut plugins,
1730                    phase::SIMULATE,
1731                    phase::POST_SIM,
1732                    step_dt,
1733                    frame,
1734                    scene,
1735                    &mut writeback,
1736                    &mut output,
1737                    &mut self.resources,
1738                );
1739                self.step_index = self.step_index.wrapping_add(1);
1740            }
1741            self.fixed_timestep = Some(ts);
1742        } else {
1743            run_range(
1744                &mut plugins,
1745                phase::SIMULATE,
1746                phase::POST_SIM,
1747                frame.dt,
1748                frame,
1749                scene,
1750                &mut writeback,
1751                &mut output,
1752                &mut self.resources,
1753            );
1754            self.step_index = self.step_index.wrapping_add(1);
1755        }
1756
1757        // PostSim + Writeback: [POST_SIM, MAX]
1758        run_range(
1759            &mut plugins,
1760            phase::POST_SIM,
1761            i32::MAX,
1762            frame.dt,
1763            frame,
1764            scene,
1765            &mut writeback,
1766            &mut output,
1767            &mut self.resources,
1768        );
1769
1770        // --- Collect pass: all plugins in priority order ---------------------
1771        for plugin in plugins.iter_mut() {
1772            let mut ctx = RuntimeStepContext {
1773                priority: plugin.priority(),
1774                dt: frame.dt,
1775                scene,
1776                writeback: &mut writeback,
1777                output: &mut output,
1778                pick_hit: frame.pick_hit,
1779                resources: &mut self.resources,
1780            };
1781            plugin.collect(&mut ctx);
1782        }
1783
1784        // Restore plugins.
1785        self.plugins = plugins;
1786
1787        // --- Flush writeback -------------------------------------------------
1788        let ops = writeback.into_ops();
1789        for op in &ops {
1790            scene.set_local_transform(op.id, glam::Mat4::from(op.transform));
1791            self.snapshots.update(op.id, op.transform);
1792        }
1793        if !ops.is_empty() {
1794            scene.update_transforms();
1795        }
1796        output.node_transform_ops = ops;
1797
1798        // Apply selection ops produced by plugins.
1799        for op in &output.selection_ops {
1800            op.apply_to(selection);
1801        }
1802
1803        // Compute camera follow target.
1804        if let Some(CameraFollow::Node { id, offset, .. }) = &self.camera_follow {
1805            let id = *id;
1806            let offset = *offset;
1807            let alpha = self.alpha();
1808            let pos = self
1809                .snapshots
1810                .interpolated(id, alpha)
1811                .map(|t| glam::Vec3::from(t.translation))
1812                .or_else(|| {
1813                    scene
1814                        .node(id)
1815                        .map(|n| n.world_transform().col(3).truncate())
1816                });
1817            if let Some(pos) = pos {
1818                output.camera_follow_target = Some(pos + offset);
1819            }
1820        }
1821
1822        output
1823    }
1824}