Skip to main content

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, streaming_pressure, streaming_stats,
74    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 two leading fields are the load-time passes that bracket the systems:
270/// one that runs over the world once the gates have built them and before
271/// their `init`, and one that pre-creates the event queues a scheduled
272/// system's declared access can touch.
273///
274/// Every system is internal: it has no declarable asset, is never parsed from a
275/// world or written to a blob, and is constructed by its gate from world
276/// content. Each entry maps a name to the behavior type that implements
277/// `System`, the gate that builds it, and a human-readable gate description;
278/// the entry name doubles as the system's stable display name for profiling and
279/// logging.
280#[macro_export]
281macro_rules! define_systems {
282    ( before_init: $before_init:path,
283      prepare_events: $prepare_events:path,
284      $( $name:ident => $behavior:path {
285            gate: $gate:path,
286            present_when: $present_when:literal,
287            after: [ $( $after:ident ),* $(,)? ],
288            before: [ $( $before:ident ),* $(,)? ] $(,)?
289        } ),* $(,)? ) => {
290        /// The system table: one entry per system, in run order, plus the
291        /// load-time passes that bracket them. `World::start` runs each gate
292        /// against the world's content and builds the systems they return.
293        pub const SYSTEMS: &$crate::ecs::SystemTable = &$crate::ecs::SystemTable {
294            entries: &[
295                $( $crate::ecs::SystemEntry {
296                    name: stringify!($name),
297                    present_when: $present_when,
298                    // Boxing happens here rather than in the gates, so each
299                    // gate returns its own system type and the entry's behavior
300                    // path has to name it.
301                    gate: {
302                        fn build(
303                            world: &$crate::ecs::World,
304                        ) -> Option<::std::boxed::Box<dyn $crate::ecs::System>> {
305                            let built: Option<$behavior> = $gate(world);
306                            built.map(|s| -> ::std::boxed::Box<dyn $crate::ecs::System> {
307                                ::std::boxed::Box::new(s)
308                            })
309                        }
310                        build
311                    },
312                    after: &[ $( stringify!($after) ),* ],
313                    before: &[ $( stringify!($before) ),* ],
314                }, )*
315            ],
316            before_init: Some($before_init),
317            prepare_events: Some($prepare_events),
318        };
319    };
320}