Skip to main content

viewport_lib/runtime/
guide.rs

1//! Plugin authoring guide.
2//!
3//! This module documents how to write a [`super::RuntimePlugin`] and register it
4//! with [`super::ViewportRuntime`].
5//!
6//! # The RuntimePlugin trait
7//!
8//! A plugin is any type that implements [`super::RuntimePlugin`]:
9//!
10//! ```rust,ignore
11//! pub trait RuntimePlugin: Send + 'static {
12//!     fn priority(&self) -> i32;
13//!     fn submit(&mut self, ctx: &RuntimeStepContext<'_>) {}
14//!     fn collect(&mut self, ctx: &mut RuntimeStepContext<'_>) {}
15//!     fn on_event(&mut self, event: &RuntimeEvent, ctx: &mut RuntimeStepContext<'_>) {}
16//!     fn step(&mut self, ctx: &mut RuntimeStepContext<'_>);
17//! }
18//! ```
19//!
20//! `step` is the only required method. The others have default no-op implementations.
21//!
22//! Hook call order each frame:
23//!
24//! 1. `on_event` -- called once per lifecycle event (node added or removed) before
25//!    the step loop begins. Only fired when the scene's node set changes.
26//! 2. `submit` -- called once per frame before the step loop, in priority order.
27//!    Use this to kick off background work (e.g. spawning a compute task) that will
28//!    be collected on the same or next frame. The context is `&RuntimeStepContext`
29//!    (shared reference), so only `get` and `contains` on resources are available.
30//! 3. `step` -- called once per phase execution, in priority order. For simulate-range
31//!    plugins with a fixed timestep this is called once per accumulated step
32//!    (possibly multiple times per frame). For all other plugins it is called once
33//!    per frame. This is where the main per-frame or per-step work happens.
34//! 4. `collect` -- called once per frame after the step loop, in priority order.
35//!    Use this to read results from background work started in `submit`.
36//!
37//! # RuntimePhase ordering
38//!
39//! Phase constants are plain `i32` values in the [`super::plugin::phase`] module.
40//! Plugins execute in ascending numeric order each frame:
41//!
42//! | Phase        | Value | When it runs                                       |
43//! |--------------|-------|----------------------------------------------------|
44//! | Prepare      | 100   | First. Update time-dependent state before queries. |
45//! | Pick         | 200   | Ray-cast and object picking.                       |
46//! | Select       | 300   | Selection updates driven by pick results.          |
47//! | Manipulate   | 400   | Gizmo drag and keyboard transform sessions.        |
48//! | Animate      | 500   | Procedural or keyframe animation.                  |
49//! | Simulate     | 600   | Physics or simulation. Fixed timestep: runs N times per frame. |
50//! | PostSimulate | 700   | After all Simulate iterations, before Writeback.   |
51//! | Writeback    | 800   | Flush accumulated transform ops to the scene.      |
52//!
53//! You can use values between bands to place a plugin at a specific sub-position.
54//! For example `phase::ANIMATE + 50` runs after standard Animate plugins but before
55//! any plugin at `phase::SIMULATE`. Two plugins at the same priority run in
56//! registration order (stable sort).
57//!
58//! The Simulate phase is special: with a fixed timestep configured, plugins in the
59//! `[SIMULATE, POST_SIM)` range may be called zero or more times per rendered frame
60//! depending on how much wall time accumulated since the last frame. All other phase
61//! ranges always run exactly once per frame.
62//!
63//! # Hook registration
64//!
65//! Build the runtime and add plugins with `with_plugin`:
66//!
67//! ```rust,ignore
68//! use viewport_lib::runtime::{ViewportRuntime, FixedTimestep};
69//!
70//! let mut runtime = ViewportRuntime::new()
71//!     .with_fixed_timestep(FixedTimestep::new(60.0))
72//!     .with_plugin(MyPhysicsPlugin::new())
73//!     .with_plugin(MyAudioPlugin::new());
74//! ```
75//!
76//! Multiple plugins can share the same priority. They run in registration order
77//! within the same band. Registration order is preserved by a stable sort on
78//! `priority()` at the start of each frame.
79//!
80//! Call `runtime.step(&mut scene, &mut selection, &frame_ctx)` once per frame to
81//! drive all plugins. It returns a [`super::output::RuntimeOutput`] with selection
82//! ops, contact events, transform ops, and any application-defined events.
83//!
84//! # Accessing scene data
85//!
86//! Each hook receives a [`super::context::RuntimeStepContext`] with the following fields:
87//!
88//! - `ctx.scene` -- read-only reference to the scene. Use this to query node
89//!   transforms, materials, and spatial structure.
90//! - `ctx.writeback` -- write transforms here. Call `ctx.writeback.set(node_id, affine)`.
91//!   The runtime flushes accumulated writes to the scene after the writeback phase.
92//! - `ctx.output` -- accumulate selection operations, contact events, camera commands,
93//!   and typed application events here.
94//! - `ctx.resources` -- shared typed resource registry. Use this to pass data between
95//!   plugins within the same frame or across frames.
96//! - `ctx.pick_hit` -- forwarded from `RuntimeFrameContext`. The pick result under the
97//!   cursor for this frame, if picking was done by the caller.
98//! - `ctx.dt` -- delta time for this step. For simulate-range plugins with a fixed
99//!   timestep this is the fixed step size, not wall dt.
100//!
101//! # Resource sharing between plugins
102//!
103//! [`super::RuntimeResources`] is a typed map keyed by `TypeId`. Any `Send + 'static`
104//! type can be stored as a resource. One value per type: inserting again overwrites.
105//!
106//! Typical pattern: one plugin writes, another reads in the same frame.
107//!
108//! ```rust,ignore
109//! // PhysicsPlugin at SIMULATE inserts contact data.
110//! fn step(&mut self, ctx: &mut RuntimeStepContext) {
111//!     let contacts = self.solver.solve(ctx.scene, ctx.dt);
112//!     ctx.resources.insert(contacts);
113//! }
114//!
115//! // AudioPlugin at POST_SIM reads it.
116//! fn step(&mut self, ctx: &mut RuntimeStepContext) {
117//!     if let Some(contacts) = ctx.resources.get::<ContactList>() {
118//!         for c in contacts.iter() {
119//!             self.audio.play_impact(c.impulse);
120//!         }
121//!     }
122//! }
123//! ```
124//!
125//! Resources persist across frames until explicitly removed or the runtime is dropped.
126//! Use `runtime.resources_mut().insert(...)` before the first `step` to pre-populate
127//! resources that plugins expect to find on frame one.
128//!
129//! Available methods on `RuntimeResources`:
130//! - `insert(value)` -- store a value, replacing any existing value of that type
131//! - `get::<T>()` -- shared reference, returns `None` if not present
132//! - `get_mut::<T>()` -- mutable reference
133//! - `remove::<T>()` -- take ownership and remove
134//! - `contains::<T>()` -- presence check without borrowing the value
135//!
136//! In `submit` (called with `&RuntimeStepContext`), only `get` and `contains` are
137//! accessible. In `step`, `collect`, and `on_event` (called with `&mut RuntimeStepContext`),
138//! full insert/get_mut/remove access is available.
139//!
140//! # Debug visualization with DebugDraw
141//!
142//! [`super::DebugDraw`] is a resource, not a renderer type. Plugins draw to it by
143//! calling `ctx.resources.get_mut::<DebugDraw>()` in their `step` implementations.
144//! After the frame, the host converts the accumulated primitives into render items.
145//!
146//! Setup pattern:
147//!
148//! ```rust,ignore
149//! use viewport_lib::runtime::debug_draw::DebugDraw;
150//!
151//! // At startup: insert a DebugDraw resource.
152//! runtime.resources_mut().insert(DebugDraw::new());
153//!
154//! // Each frame, before runtime.step():
155//! if let Some(dd) = runtime.resources_mut().get_mut::<DebugDraw>() {
156//!     dd.begin_frame();  // clears transient (one-frame) primitives
157//! }
158//! let output = runtime.step(&mut scene, &mut selection, &frame_ctx);
159//!
160//! // After step: convert accumulated primitives to render items.
161//! if let Some(dd) = runtime.resources().get::<DebugDraw>() {
162//!     frame_data.scene.polylines.extend(dd.to_polylines());
163//!     if let Some(pc) = dd.to_point_cloud() {
164//!         frame_data.scene.point_clouds.push(pc);
165//!     }
166//! }
167//! ```
168//!
169//! Inside a plugin's `step`:
170//!
171//! ```rust,ignore
172//! fn step(&mut self, ctx: &mut RuntimeStepContext) {
173//!     if let Some(dd) = ctx.resources.get_mut::<DebugDraw>() {
174//!         dd.line(contact.point_a, contact.point_b, [1.0, 0.0, 0.0, 1.0]);
175//!     }
176//! }
177//! ```
178//!
179//! Primitives submitted with `line`, `point`, `aabb`, `sphere`, and `label` are
180//! transient: `begin_frame` clears them. Persistent primitives are added with
181//! `add_persistent(key, prim)` keyed by a `u64` and stay until `remove_persistent(key)`
182//! is called. See the [`super::debug_draw`] module for the full API.
183//!
184//! # Fixed timestep and the Simulate phase
185//!
186//! Configure a fixed timestep with `ViewportRuntime::with_fixed_timestep`:
187//!
188//! ```rust,ignore
189//! let mut runtime = ViewportRuntime::new()
190//!     .with_fixed_timestep(FixedTimestep::new(60.0));  // 60 Hz
191//! ```
192//!
193//! The runtime accumulates wall time. Each call to `runtime.step(...)` advances the
194//! accumulator by `frame_ctx.dt`. If the accumulated time exceeds the step size, the
195//! runtime calls `step()` on simulate-range plugins once per step. If wall dt is less
196//! than the step size, `step()` may not be called at all for that frame.
197//!
198//! Inside simulate-range plugins, `ctx.dt` is the fixed step size (e.g. `1.0/60.0`),
199//! not wall dt. This makes physics integration deterministic regardless of frame rate.
200//!
201//! For rendering, use `runtime.alpha()` to interpolate between the previous and current
202//! simulation state:
203//!
204//! ```rust,ignore
205//! let alpha = runtime.alpha();
206//! if let Some(t) = runtime.snapshots().interpolated(node_id, alpha) {
207//!     // use t as the render transform instead of the scene node transform
208//! }
209//! ```
210//!
211//! `alpha` is in `[0.0, 1.0]`: the fractional position between the last completed step
212//! and the next. This eliminates the jitter that appears when frame rate and step rate
213//! are out of phase. When no fixed timestep is configured, `alpha()` returns `1.0`.
214//!
215//! # File layout for new plugins
216//!
217//! New plugins live in a subdirectory under `src/runtime/plugins/`, not as a single
218//! file. The [`super::plugins::skeleton_plugin`] module is the reference template:
219//!
220//! ```text
221//! plugins/
222//!   my_plugin/
223//!     mod.rs        -- re-exports the public surface
224//!     plugin.rs     -- the RuntimePlugin impl
225//!     <feature>.rs  -- substrate types, math, helpers, internal state
226//! ```
227//!
228//! Using a subdirectory from the start avoids a rename-and-rewire when the plugin
229//! grows. Re-export all public types from `mod.rs` and add the module to
230//! `src/runtime/plugins/mod.rs`.
231//!
232//! # Minimal plugin example
233//!
234//! A plugin that counts frames and logs every 60 steps:
235//!
236//! ```rust,ignore
237//! use viewport_lib::runtime::{RuntimePlugin, RuntimeStepContext, ViewportRuntime};
238//! use viewport_lib::runtime::plugin::phase;
239//!
240//! struct FrameCounter {
241//!     count: u64,
242//! }
243//!
244//! impl FrameCounter {
245//!     fn new() -> Self {
246//!         Self { count: 0 }
247//!     }
248//! }
249//!
250//! impl RuntimePlugin for FrameCounter {
251//!     fn priority(&self) -> i32 {
252//!         phase::PREPARE
253//!     }
254//!
255//!     fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
256//!         self.count += 1;
257//!         if self.count % 60 == 0 {
258//!             println!("frame {}", self.count);
259//!         }
260//!     }
261//! }
262//!
263//! // Registration:
264//! let mut runtime = ViewportRuntime::new()
265//!     .with_plugin(FrameCounter::new());
266//! ```
267//!
268//! This plugin runs once per frame at the Prepare phase. It owns its state as a
269//! struct field. To share state with another plugin, insert it into `ctx.resources`
270//! instead of keeping it in the struct.