concinnity_engine/ecs/mod.rs
1//! Client-side ecs runtime. The renderer-free metadata, asset registry,
2//! registration macros, asset-construction API, `PipelineContext`, the `System`
3//! behavior trait, and the `World` that runs systems over its data all live in
4//! concinnity-core; this module re-exports them under the historical
5//! `crate::ecs::*` paths and adds what only a renderer-bearing runtime has: the
6//! system table itself, its gates, the load-time decomposition pass, and the
7//! resources the render band parks in a world.
8//!
9//! TO ADD A NEW COMPONENT: register it in concinnity-core's `ecs::registry`
10//! (`define_components!`). TO ADD A NEW SYSTEM: implement the `System` behavior
11//! trait on it, write its gate in this crate's `ecs::schedule`, and add one
12//! entry to the `define_systems!` table in `ecs::registry` -- the table is the
13//! registry AND the schedule (table order is run order).
14//!
15//! A system concinnity-core owns is listed in ITS table too
16//! (`ecs::HEADLESS_SYSTEMS`, what a world with no host runs), and the two must
17//! agree on order, gate description, and the edges among the systems both know
18//! about. `headless_drift_tests` is what holds them to that.
19
20pub(crate) mod access_ids;
21pub(crate) mod by_asset_id;
22#[cfg(test)]
23mod consumed_columns_tests;
24pub(crate) mod decompose;
25#[cfg(test)]
26mod determinism_tests;
27#[cfg(test)]
28mod headless_drift_tests;
29mod registry;
30pub mod schedule;
31mod world_queries;
32
33// Renderer-free metadata, registry types, the asset-construction API, and the
34// `PipelineContext`, re-exported from concinnity-core so the rest of the client
35// keeps its historical `crate::ecs::*` import paths.
36pub use concinnity_core::ecs::{
37 Access, Arena, AudioClipHandle, BlobAssetDef, ColumnTicks, Component, ComponentAsset,
38 ComponentId, ComponentMask, ComponentSlot, ComponentStorage, Entity, EventCursor, EventStore,
39 Events, FontHandle, FrameContext, FrameVec, MAX_CHANGE_AGE, MaterialHandle, MeshBoundsRecord,
40 MeshHandle, PayloadLocator, PipelineContext, Resources, RuntimeComponent, SceneGroup,
41 ScratchStats, SkinnedMeshHandle, TextureHandle, Tick,
42};
43
44// The name interner keeps a per-thread table, so it lives in
45// `concinnity_host::thread`, whose module re-exports the vocabulary's
46// `AssetId` / `AssetRef` alongside.
47pub use concinnity_host::thread::asset_id;
48
49// Renderer-free per-frame protocol resources, moved to concinnity-core so the
50// physics / audio subsystem crates can reach them without a renderer dependency.
51// Re-exported here to keep the historical `crate::ecs::*` paths for every reader
52// (engine systems and the editor's hook drive).
53pub use concinnity_core::ecs::{
54 CursorShape, CursorState, DesiredCursor, DropdownView, ExecutionTrace, FlyCam, FrameRateCap,
55 GpuMemoryPressure, HiddenAssets, HudLayers, HudPrefs, MenuActive, MenuOverride, OpenDropdown,
56 OverlayImage, OverlayImages, PickEntry, PickIndex, ScheduleMode, ScreenStack, SimTiming,
57 TraceEvent, TracePath, TracePaths, TraceRequest, TraceStep, TraceVal, TransientSaves,
58 ViewOverrides, WorldLines,
59};
60
61// The `SYSTEMS` table is written client-side, since its gates name the client's
62// own system types (see `registry`); a gate builds one `BuiltSystem` per
63// present entry. Everything that runs it is in concinnity-core.
64pub use concinnity_core::ecs::{BuiltSystem, SystemEntry, SystemTable};
65pub use registry::SYSTEMS;
66
67// The world itself, its data and the systems that run over it, is
68// concinnity-core's; re-exported here under its historical path. What stays
69// client-side is the content only a renderer-bearing runtime has: the resources
70// below, and the queries over them in `world_queries`.
71pub use concinnity_core::ecs::World;
72pub use world_queries::{
73 gpu_profile, memory_budget, memory_drift, renders, state_tree, streaming_pressure,
74 streaming_stats, systems_and_render_backend, take_render_backend, thread_budget,
75};
76
77// The `System` behavior trait + its `StepResult` control signal are renderer-free
78// (they name only `PipelineContext`), so they live in concinnity-core; re-export
79// them under the historical `crate::ecs::*` paths for every reader (engine
80// systems, the `define_systems!` table, and the editor's hook drive).
81pub use concinnity_core::ecs::{Clock, StepResult, System};
82
83/// A render backend transplanted out of a previous world, carried into a freshly
84/// built world so its GraphicsSystem reuses the live GPU device + window instead
85/// of constructing a new one. Published by the `cn editor` live SAVE swap between
86/// building the post-edit world and starting it; GraphicsSystem `run_init` takes
87/// it and calls `RenderBackend::reload_world` (reusing the window) instead of
88/// `init_backend`, so a save applies without recreating the OS window. A shipped
89/// runtime never publishes it; it exists only on the editor's live-update path.
90pub struct PendingBackend(pub Box<dyn crate::gfx::backend::RenderBackend>);
91
92// The frame's sampled window input, deposited beside the backend right after
93// the draw (whose event pump produced it) and taken by InputSystem later the
94// same tick. The pipelined driver deposits it from the render half's feedback
95// instead; a missed consume merges into the next deposit so no edge is lost.
96#[derive(Default)]
97pub(crate) struct InputMailbox(pub Option<concinnity_core::render::input::InputPacket>);
98
99impl InputMailbox {
100 // Deposit a fresh packet, merging onto an unconsumed one.
101 pub(crate) fn deposit(&mut self, packet: concinnity_core::render::input::InputPacket) {
102 match &mut self.0 {
103 Some(pending) => pending.merge_from(packet),
104 None => self.0 = Some(packet),
105 }
106 }
107}
108
109// The frame's recording surfaces, taken and re-parked together by each
110// recording system (the same handoff `ActiveRenderBackend` uses, so a step
111// never re-boxes them into the resource map): the op queue the tick's backend
112// effects accumulate into (drained into the frame snapshot by GraphicsSystem's
113// extract, replayed in record order before the draw) and the slot-allocation
114// authority ops name destinations from. Published by graphics init; absent in
115// a world with no graphics, so recording systems no-op.
116pub(crate) struct RenderQueues {
117 pub ops: concinnity_core::render::ops::RenderOps,
118 pub slots: crate::gfx::render_slots::RenderSlots,
119}
120
121// The active backend's capability flags, published by graphics init. The
122// backend itself is parked (and on a pipelined frame, owned by the render
123// thread), so a system that only needs to know what it supports reads this
124// instead of reaching for it. Absent in a world with no graphics.
125#[derive(Clone, Copy)]
126pub(crate) struct ActiveDeviceCaps(pub concinnity_core::render::backend::DeviceCapabilities);
127
128// The world's parked `RenderQueues` slot. `None` only while a step has it
129// taken.
130pub(crate) struct ActiveRenderQueues(pub Option<RenderQueues>);
131
132impl ActiveRenderQueues {
133 // Take the parked queues for the duration of one system step.
134 pub(crate) fn take(resources: &mut Resources) -> Option<RenderQueues> {
135 resources.get_mut::<Self>()?.0.take()
136 }
137
138 // Park the queues again at the end of the step that took them.
139 pub(crate) fn put(resources: &mut Resources, queues: RenderQueues) {
140 match resources.get_mut::<Self>() {
141 Some(slot) => slot.0 = Some(queues),
142 None => {
143 resources.insert(Self(Some(queues)));
144 }
145 }
146 }
147}
148
149// Recorded backend effects that failed at replay and need a simulation-side
150// rollback (a streamed-mesh upload refused by a full region, a chunk add).
151// Written by GraphicsSystem after submission; StreamingSystem drains it at the
152// top of its next step.
153#[derive(Default)]
154pub(crate) struct RenderOpFailures(pub Vec<concinnity_core::render::ops::OpFailure>);
155
156// The pipelined driver's channel pair, published (parked, so the per-step
157// take never re-boxes) before the world moves to the simulation thread.
158// Present exactly when frames are pipelined: GraphicsSystem's step extracts
159// and sends the snapshot through it instead of submitting against a parked
160// backend (which the render half owns), and applies the render half's
161// feedback. Absent in serial execution.
162pub(crate) struct PipelinedFrames(pub Option<PipelineChannels>);
163
164pub(crate) struct PipelineChannels {
165 pub(crate) snapshot_tx:
166 std::sync::mpsc::SyncSender<concinnity_core::render::snapshot::RenderSnapshot>,
167 pub(crate) feedback_rx:
168 std::sync::mpsc::Receiver<concinnity_core::render::feedback::FrameFeedback>,
169}
170
171impl PipelinedFrames {
172 // Take the parked channels for the duration of one step.
173 pub(crate) fn take(resources: &mut Resources) -> Option<PipelineChannels> {
174 resources.get_mut::<Self>()?.0.take()
175 }
176
177 // Park the channels again at the end of the step that took them.
178 pub(crate) fn put(resources: &mut Resources, channels: PipelineChannels) {
179 match resources.get_mut::<Self>() {
180 Some(slot) => slot.0 = Some(channels),
181 None => {
182 resources.insert(Self(Some(channels)));
183 }
184 }
185 }
186}
187
188/// The world's live render backend, parked here between system steps.
189/// GraphicsSystem's init builds it and parks it; each system that drives the
190/// GPU (GraphicsSystem's frame encode, InputSystem's poll) takes it out at the
191/// top of its step and puts it back before returning, so the backend and the
192/// `PipelineContext` are never borrowed together. `None` while a step has it
193/// taken, or once the editor's live SAVE transplanted it out.
194pub struct ActiveRenderBackend(pub Option<Box<dyn crate::gfx::backend::RenderBackend>>);
195
196impl ActiveRenderBackend {
197 // Take the parked backend for the duration of one system step.
198 pub(crate) fn take(
199 resources: &mut Resources,
200 ) -> Option<Box<dyn crate::gfx::backend::RenderBackend>> {
201 resources.get_mut::<Self>()?.0.take()
202 }
203
204 // Park the backend again at the end of the step that took it.
205 pub(crate) fn put(
206 resources: &mut Resources,
207 backend: Box<dyn crate::gfx::backend::RenderBackend>,
208 ) {
209 match resources.get_mut::<Self>() {
210 Some(slot) => slot.0 = Some(backend),
211 None => {
212 resources.insert(Self(Some(backend)));
213 }
214 }
215 }
216}
217
218// The active scene-flow bookkeeping, shared between SettingsSystem (which
219// applies imperative scene jumps from `SceneCommand`) and GraphicsSystem
220// (which ticks the timed advance + fades and submits the visibility changes).
221// Published by GraphicsSystem's init when the world declares `Scene` assets;
222// `flow` is `None` when it declared none, so both systems no-op. `epoch` is the
223// shared clock both derive their `elapsed` from, set to GraphicsSystem's own
224// `start_time` so fade timing matches the render clock.
225pub(crate) struct ActiveSceneFlow {
226 pub flow: Option<crate::gfx::scene_flow::SceneFlow>,
227 pub(crate) epoch: std::time::Instant,
228}
229
230/// The blob's baked per-scene exclusive content groups, published at blob load
231/// for the streaming/residency wiring to consume at graphics init.
232pub struct BlobSceneGroups(pub Vec<crate::ecs::SceneGroup>);
233
234/// The blob's baked per-mesh geometry summaries (AABB + counts by mesh-source
235/// handle), published at blob load so graphics init can build draw records for
236/// deferred scene-owned meshes without decoding their payloads.
237pub struct BlobMeshBounds(pub Vec<MeshBoundsRecord>);
238
239// Per-scene streamed-content load status, republished by StreamingSystem
240// whenever it changes: `(scene, state, fraction of members resident)` in
241// declaration order. Consumers (menus, loading screens) read, never write.
242pub(crate) struct SceneResidencyStatus {
243 pub scenes: Vec<(
244 asset_id::AssetId,
245 crate::gfx::scene_residency::SceneLoadState,
246 f32,
247 )>,
248}
249
250// Setting rows the engine has disabled at runtime (their keys, e.g. `show_fps`
251// while "Display performance stats" is off). Published each frame by
252// GraphicsSystem and read by `UiInputSystem`, which makes a matching row inert
253// (no hover, no click) while its labels are grayed independently. Distinct from
254// the init-time capability gating (which marks `HitRegion.disabled` before the
255// regions are drained); this drives the same effect after they are drained.
256#[derive(Debug, Clone, Default)]
257pub(crate) struct DisabledSettingRows(pub std::collections::HashSet<String>);
258
259// The display modes offered by the "Resolution" settings row, published once by
260// GraphicsSystem at init (enumerated from the backend's display, or the static
261// fallback when it cannot enumerate) and read by `UiInputSystem` to seed the
262// row's dropdown list. Ordered as displayed; a pick's `SetIndex` indexes it.
263#[derive(Debug, Clone, Default)]
264pub(crate) struct DisplayModes(pub Vec<crate::gfx::display_mode::DisplayMode>);
265
266/// The system table. Generates the `SYSTEMS` table a world starts from; table
267/// order is run order.
268///
269/// The three leading fields are the load-time passes around the systems: one
270/// that completes the world before the gates read it, one that runs over the
271/// world once the gates have built them and before their `init`, and one that
272/// pre-creates the event queues a scheduled system's declared access can
273/// touch.
274///
275/// Every system is internal: it has no declarable asset, is never parsed from a
276/// world or written to a blob, and is constructed by its gate from world
277/// content. Each entry maps a name to the behavior type that implements
278/// `System`, the gate that builds it, and a human-readable gate description;
279/// the entry name doubles as the system's stable display name for profiling and
280/// logging.
281#[macro_export]
282macro_rules! define_systems {
283 ( complete_world: $complete_world:path,
284 before_init: $before_init:path,
285 prepare_events: $prepare_events:path,
286 $( $name:ident => $behavior:path {
287 gate: $gate:path,
288 present_when: $present_when:literal,
289 after: [ $( $after:ident ),* $(,)? ],
290 before: [ $( $before:ident ),* $(,)? ] $(,)?
291 } ),* $(,)? ) => {
292 /// The system table: one entry per system, in run order, plus the
293 /// load-time passes that bracket them. `World::start` runs each gate
294 /// against the world's content and builds the systems they return.
295 pub const SYSTEMS: &$crate::ecs::SystemTable = &$crate::ecs::SystemTable {
296 entries: &[
297 $( $crate::ecs::SystemEntry {
298 name: stringify!($name),
299 present_when: $present_when,
300 // Boxing happens here rather than in the gates, so each
301 // gate returns its own system type and the entry's behavior
302 // path has to name it.
303 gate: {
304 fn build(
305 world: &$crate::ecs::World,
306 ) -> Option<::std::boxed::Box<dyn $crate::ecs::System>> {
307 let built: Option<$behavior> = $gate(world);
308 built.map(|s| -> ::std::boxed::Box<dyn $crate::ecs::System> {
309 ::std::boxed::Box::new(s)
310 })
311 }
312 build
313 },
314 after: &[ $( stringify!($after) ),* ],
315 before: &[ $( stringify!($before) ),* ],
316 }, )*
317 ],
318 complete_world: Some($complete_world),
319 before_init: Some($before_init),
320 prepare_events: Some($prepare_events),
321 };
322 };
323}