concinnity_core/ecs/protocol.rs
1// src/ecs/protocol.rs
2//
3// Renderer-free protocol types: the resource singletons the runtime systems
4// publish and read to coordinate a tick, plus the world's cook-counted physics
5// reservation, published once at blob load. They name no graphics backend,
6// windowing, physics, or audio type, so they live in core where every subsystem
7// crate can reach them without depending on the renderer. The client `ecs`
8// module re-exports them under the historical `crate::ecs::*` paths.
9
10use alloc::string::String;
11use alloc::vec::Vec;
12
13use crate::blob::PhysicsBudgetRecord;
14use crate::ecs::asset_id::AssetId;
15use concinnity_asset::FontHandle;
16
17/// The world's physics reservation as cook counted it, published at blob load.
18/// Absent when the world declares no physics content, or when the world was
19/// built in memory rather than loaded from a blob; the simulation then counts
20/// the loaded components itself.
21///
22/// Lives here rather than with the engine's other blob resources because the
23/// simulation driver reads it, and the engine depends on the driver.
24pub struct WorldPhysicsBudget(pub PhysicsBudgetRecord);
25
26/// Per-frame menu state, published as a resource by the overlay build (which runs
27/// first in the schedule) and read by the simulation systems the same tick.
28/// `true` while any world-pausing screen is open: physics and animation then freeze so they
29/// stop consuming resources behind the menu. Each system keeps its own clock
30/// aligned across the freeze, so resuming costs one normal frame -- no catch-up
31/// burst, no pose jump.
32#[derive(Debug, Clone, Copy, Default)]
33pub struct MenuActive(pub bool);
34
35/// Fixed-timestep budget for the current frame, published by the App-level
36/// simulation clock before each world step. `ticks` is how many fixed steps the
37/// simulation systems (physics, behavior) run this frame; `tick_dt` is the
38/// seconds each step advances; `alpha` is the accumulator remainder as a
39/// fraction of `tick_dt`, used to blend the previous and current simulated
40/// states when writing render-facing transforms. Absent (a directly-stepped
41/// world with no App), the default is exactly one tick per step with no
42/// blending, which makes bare `World::step` loops deterministic.
43#[derive(Debug, Clone, Copy)]
44pub struct SimTiming {
45 /// Fixed simulation steps to run this frame.
46 pub ticks: u32,
47 /// Seconds each fixed step advances.
48 pub tick_dt: f32,
49 /// Accumulator remainder as a fraction of `tick_dt`.
50 pub alpha: f32,
51}
52
53impl SimTiming {
54 /// Seconds each fixed simulation step advances (60 Hz).
55 pub const TICK_DT: f32 = 1.0 / 60.0;
56}
57
58impl Default for SimTiming {
59 fn default() -> Self {
60 Self {
61 ticks: 1,
62 tick_dt: Self::TICK_DT,
63 alpha: 1.0,
64 }
65 }
66}
67
68/// The live frame-rate cap in FPS (0 = unlimited), published by GraphicsSystem
69/// (from GraphicsConfig at init, refreshed by the settings row's live change)
70/// and read by the App-level frame pacer before each world step. Independent of
71/// the quality preset (a user/hardware preference, like vsync).
72#[derive(Debug, Clone, Copy, Default)]
73pub struct FrameRateCap(pub u32);
74
75/// An external per-frame driver (the `cn editor` HUD) can force the world's
76/// "menu active" state through this resource: `Some(true)` frees the cursor and
77/// freezes gameplay/physics/animation (edit mode), `Some(false)` captures the
78/// cursor and lets the world run (play mode), both regardless of whether the
79/// world has its own menu UI. GraphicsSystem also puts the backend in menu mode
80/// while it is set, so a click frees to a UI action instead of re-capturing the
81/// camera. `None` (the default absence) leaves the world's own menu logic in
82/// charge; a shipped runtime never publishes it.
83#[derive(Debug, Clone, Copy, Default)]
84pub struct MenuOverride(pub Option<bool>);
85
86/// Keeps a preview session out of the user's real save files: while present and
87/// true, the systems that persist play state (behavior variables / once flags,
88/// story position) neither read nor write their disk saves -- every session
89/// starts fresh and leaves no trace. In-memory state is unaffected, so a `save`
90/// node still works within the session. Published by the `cn editor` HUD
91/// injection (sampled at each system's init); a shipped runtime never
92/// publishes it.
93#[derive(Debug, Clone, Copy, Default)]
94pub struct TransientSaves(pub bool);
95
96/// One hop of a behavior-node address, mirroring the world checker's fault
97/// paths: object fields by key, list members by position. A node's path walks
98/// from the behavior's args to the node (e.g. `do[1].if.then[0]` is
99/// `[Field("do"), Index(1), Field("if"), Field("then"), Index(0)]` minus the
100/// node's own trailing verb), so the editor can resolve a traced node to the
101/// same outline row / chart card its checker faults land on. Field names are
102/// the fixed authoring keys, so they borrow statically.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum TraceStep {
105 /// An object field, by its fixed authoring key.
106 Field(&'static str),
107 /// A list member, by position.
108 Index(u32),
109}
110
111/// A behavior node's address: the hops from the behavior's args down to it.
112pub type TracePath = alloc::vec::Vec<TraceStep>;
113
114/// A behavior-body value in its cross-boundary form: what
115/// [`Val`](crate::behavior::Val) publishes to an observer. Entities travel as
116/// their id bits.
117#[derive(Debug, Clone, Copy, PartialEq)]
118pub enum TraceVal {
119 /// A boolean value.
120 Bool(bool),
121 /// An integer value.
122 Int(i32),
123 /// A floating-point value.
124 Float(f32),
125 /// A 3-component vector value.
126 Vec3([f32; 3]),
127 /// An entity, as its id bits.
128 Entity(u64),
129}
130
131/// One node execution: which behavior, and the node's compile-assigned
132/// pre-order id (an index into that behavior's [TracePaths] entry).
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub struct TraceEvent {
135 /// The behavior the node belongs to.
136 pub behavior: AssetId,
137 /// The node's compile-assigned pre-order id.
138 pub node: u32,
139}
140
141/// An external observer's request for execution tracing, published per frame by
142/// the `cn editor` HUD while its Behavior panel is open and removed when it
143/// closes. While present, the behavior system records which nodes ran each
144/// simulated tick and publishes [ExecutionTrace]; absent (the shipped runtime,
145/// or the panel closed), the system does no recording work beyond noticing the
146/// absence. `entity` selects whose per-entity locals to surface; `breakpoints`
147/// are nodes whose execution should be reported as a [ExecutionTrace::hit] so
148/// the observer can pause the simulation.
149#[derive(Debug, Clone, Default)]
150pub struct TraceRequest {
151 /// Whose per-entity locals to surface, as entity id bits.
152 pub entity: Option<u64>,
153 /// Nodes whose execution should be reported as a hit.
154 pub breakpoints: alloc::vec::Vec<TraceEvent>,
155}
156
157/// What the behavior system observed over one simulated tick, published while a
158/// [TraceRequest] stands. `frame` increments per published tick so the observer
159/// can tell fresh data from the stale resource a paused world leaves behind.
160/// `events` are the nodes that ran (deduplicated); `vars` the world variables
161/// with their current values in slot order; `locals` the requested entity's
162/// per-behavior locals; `hit` the first executed breakpoint, if any.
163#[derive(Debug, Clone, Default)]
164pub struct ExecutionTrace {
165 /// Increments per published tick, so stale data is recognisable.
166 pub frame: u64,
167 /// The nodes that ran this tick, deduplicated.
168 pub events: alloc::vec::Vec<TraceEvent>,
169 /// World variables with their current values, in slot order.
170 pub vars: alloc::vec::Vec<(String, TraceVal)>,
171 /// The requested entity's per-behavior locals.
172 pub locals: alloc::vec::Vec<(AssetId, String, TraceVal)>,
173 /// The first executed breakpoint, if any.
174 pub hit: Option<TraceEvent>,
175}
176
177/// Each behavior's node paths, indexed by the node ids [ExecutionTrace] events
178/// carry. Published once when tracing is first requested (the compile that
179/// derives it runs at init either way; the publish just exposes it).
180#[derive(Debug, Clone, Default)]
181pub struct TracePaths(pub alloc::vec::Vec<(AssetId, alloc::vec::Vec<TracePath>)>);
182
183/// The silhouette the in-engine cursor sprite should draw this frame. Published
184/// by the `cn editor` HUD when the pointer is over a resizable panel's edge or
185/// corner (or while a resize drag is in flight) and read by the overlay build,
186/// which draws the matching shape at the pointer in place of the arrow. `Default`
187/// is the plain arrow; the four resize shapes are double-headed arrows along a
188/// window edge (east/west), edge (north/south), and the two diagonals. A shipped
189/// runtime never publishes it, so the arrow always stands.
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
191pub enum CursorShape {
192 #[default]
193 /// The plain arrow.
194 Default,
195 /// Double-headed arrow along the east/west edge.
196 ResizeEW,
197 /// Double-headed arrow along the north/south edge.
198 ResizeNS,
199 /// Double-headed diagonal arrow, north-west to south-east.
200 ResizeNWSE,
201 /// Double-headed diagonal arrow, north-east to south-west.
202 ResizeNESW,
203}
204
205#[derive(Debug, Clone, Copy, Default)]
206/// The silhouette the in-engine cursor sprite should draw this frame.
207pub struct DesiredCursor(pub CursorShape);
208
209/// Per-frame draw-layer overrides for HUD Sprites / TextLabels / TextInputs, keyed
210/// by asset id and published by the `cn editor` HUD so its floating panels occlude
211/// cleanly. Overlay draw calls render in two passes (all sprites, then all text),
212/// so two overlapping panels' contents merge -- one panel's text draws over the
213/// other's background. GraphicsSystem stable-sorts the overlay calls by this layer
214/// (higher draws on top) when the map is non-empty, so the focused panel's whole
215/// content sits above the others'. An id absent from the map is layer 0; an empty /
216/// absent resource (the shipped runtime) leaves draw order at insertion order,
217/// unchanged.
218#[derive(Debug, Clone, Default)]
219pub struct HudLayers(pub alloc::collections::BTreeMap<AssetId, i32>);
220
221/// The active screen stack, published by UiInputSystem at init and whenever the
222/// stack changes, and read a frame later (the same one-frame lag screen
223/// visibility flips already have). `layers` maps each active Screen's id to its
224/// computed draw layer (authored layer band + stack position; screen-less HUD
225/// elements sit at 0); the overlay build spreads these onto the elements each
226/// screen owns. `pauses_world` is true while any active screen pauses the
227/// world; `captures_input` is true while any active screen captures input
228/// (gameplay keys are suppressed even when the world keeps simulating).
229/// Absent / empty in a world with no active screen.
230#[derive(Debug, Clone, Default)]
231pub struct ScreenStack {
232 /// Each active screen's computed draw layer, keyed by screen id.
233 pub layers: alloc::collections::BTreeMap<AssetId, i32>,
234 /// `true` while any active screen pauses the world.
235 pub pauses_world: bool,
236 /// `true` while any active screen captures input.
237 pub captures_input: bool,
238}
239
240/// World-space lines to draw this frame (trajectories, tethers, path previews,
241/// the editor's origin axes), republished by their producer every frame:
242/// GraphicsSystem expands whatever it finds into ribbon geometry and hands it
243/// to the backend, so a stale list would keep drawing. Absent when nothing
244/// draws lines, which keeps the line pass out of the frame graph.
245#[derive(Debug, Clone, Default)]
246pub struct WorldLines(pub alloc::vec::Vec<crate::gfx::lines::Line>);
247
248/// Device-memory pressure signal, published by GraphicsSystem whenever GPU
249/// work fails for lack of device memory. Renderer-free counters so the
250/// streaming valve can react (tighten budgets, evict) without naming the
251/// renderer; nothing consumes it yet.
252#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
253pub struct GpuMemoryPressure {
254 /// Device-memory failures observed since startup.
255 pub events: u64,
256 /// Frame index of the most recent failure.
257 pub last_frame: u64,
258}
259
260/// The editor's fly-camera state. While true (published only by the `cn
261/// editor` HUD drive), InputSystem keeps the navigation keys and mouse deltas
262/// live and GraphicsSystem captures the cursor even though the world is frozen
263/// behind the editor's menu override -- the editor integrates Camera3D itself,
264/// so the viewport can be flown without running the simulation. Absent / false
265/// in a shipped runtime.
266#[derive(Debug, Clone, Copy, Default)]
267pub struct FlyCam(pub bool);
268
269/// Assets suppressed from rendering for this frame. GraphicsSystem collapses
270/// each listed asset's draw slots to a degenerate transform (so it neither
271/// rasterizes nor casts shadows) and drops it from the [PickIndex]. Authored
272/// data is untouched, and the collapse is re-derived every frame, so clearing
273/// an id restores the object immediately. Published by the `cn editor` HUD
274/// drive; absent / empty otherwise.
275#[derive(Debug, Clone, Default)]
276pub struct HiddenAssets(pub alloc::collections::BTreeSet<AssetId>);
277
278/// The viewport's view mode + show flags, published per frame by the editor.
279/// GraphicsSystem forwards it to the backend's FrameParams: the mode selects
280/// what the composite presents, the flags skip feature passes for the frame.
281/// Absent outside the editor, which reads as the lit default.
282#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
283pub struct ViewOverrides {
284 /// What the composite presents.
285 pub mode: crate::gfx::view_modes::ViewMode,
286 /// Feature passes to skip for the frame.
287 pub show: crate::gfx::view_modes::ShowFlags,
288}
289
290/// One pickable entity in the [PickIndex]: its asset id and current world-space
291/// AABB. Ray-tested by the editor with `gfx::pick::ray_aabb`.
292#[derive(Debug, Clone, Copy)]
293pub struct PickEntry {
294 /// The pickable entity's asset id.
295 pub asset_id: AssetId,
296 /// Lower corner of the world-space AABB.
297 pub bb_min: [f32; 3],
298 /// Upper corner of the world-space AABB.
299 pub bb_max: [f32; 3],
300}
301
302/// The per-frame viewport-picking index: every renderable prop entity's asset id
303/// and world-space AABB, refreshed by GraphicsSystem from the live transforms.
304/// Opt-in: GraphicsSystem only builds it when the resource is already present at
305/// init (the `cn editor` HUD injection inserts an empty one), so a shipped
306/// runtime never pays for it. Rooms, instanced clusters, and voxel chunks are
307/// not indexed; picking targets authored prop placements.
308#[derive(Debug, Clone, Default)]
309pub struct PickIndex {
310 /// One entry per indexed prop entity.
311 pub entries: Vec<PickEntry>,
312}
313
314/// One extra RGBA8 image for the sprite/text atlas pool, bound to a reserved
315/// [TextureHandle](crate::components) the inserting tool chose. The handle space
316/// must stay clear of the compiled world's dense texture handles (tools use a
317/// high base).
318#[derive(Debug, Clone)]
319pub struct OverlayImage {
320 /// The reserved handle a sprite names to sample this image.
321 pub handle: crate::ecs::TextureHandle,
322 /// Image width in pixels.
323 pub width: u32,
324 /// Image height in pixels.
325 pub height: u32,
326 /// Row-major RGBA8 pixels.
327 pub rgba: Vec<u8>,
328}
329
330/// Extra images appended to the sprite/text atlas pool at graphics init: a
331/// sprite whose `texture` names one of these handles samples the image like any
332/// compiled texture. Opt-in like [PickIndex]: inserted before start (the `cn
333/// editor` HUD injection adds baked asset thumbnails); absent everywhere else,
334/// so a shipped runtime never pays for it. Read once at init -- images added to
335/// the resource later join the pool on the next world rebuild.
336#[derive(Debug, Clone, Default)]
337pub struct OverlayImages(pub Vec<OverlayImage>);
338
339/// The latest sampled cursor state (window pixels, top-left origin), published
340/// by InputSystem after each poll. GraphicsSystem reads it when building the
341/// next frame's draw list: `follow_cursor` sprites are positioned a frame after
342/// the input that moved them, and the in-engine cursor stops drawing once the
343/// real cursor has left the window (`outside_window` is false in fullscreen,
344/// where the backend confines the cursor, and on backends without window-bounds
345/// tracking).
346#[derive(Debug, Clone, Copy, Default)]
347pub struct CursorState {
348 /// Cursor position in window pixels, top-left origin.
349 pub pos: (f32, f32),
350 /// `true` once the real cursor has left the window.
351 pub outside_window: bool,
352}
353
354/// Per-frame stats-HUD visibility, published as a resource by GraphicsSystem
355/// (which runs first) and read by `StatHudSystem` the same tick. Each field is
356/// the effective on/off for that chip: the master "Display performance stats"
357/// toggle AND the per-readout toggle from the video settings. Absent (a HUD-only
358/// unit test with no GraphicsSystem) is treated as both shown.
359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
360pub struct HudPrefs {
361 /// Whether the FPS chip is shown.
362 pub show_fps: bool,
363 /// Whether the VRAM chip is shown.
364 pub show_vram: bool,
365}
366
367/// A settings dropdown's open floating option list, or `None` when none is open.
368/// `UiInputSystem` owns the interaction state (open on a `setting:<key>:open`
369/// click, close on a pick / outside click / Escape / scroll) and publishes this
370/// each frame; GraphicsSystem reads it the next tick to draw the list on top of
371/// the menu. GraphicsSystem runs first, so the list appears one frame after the
372/// row is clicked (the same lag the cursor + cycle labels already carry).
373#[derive(Debug, Clone, Default)]
374pub struct OpenDropdown(pub Option<DropdownView>);
375
376/// What GraphicsSystem needs to draw an open dropdown list: the anchor control
377/// rect (reference space), the option labels top-to-bottom, the selected +
378/// hovered OPTION indices to highlight, the scroll position (`first`, the top
379/// shown option of a list longer than the layout window), and the row value
380/// label's font / scale / color so the list text matches the row it drops from.
381#[derive(Debug, Clone)]
382pub struct DropdownView {
383 /// Anchor control rect in reference space.
384 pub anchor: [f32; 4],
385 /// Option labels, top to bottom.
386 pub options: Vec<String>,
387 /// Index of the selected option.
388 pub selected: usize,
389 /// Index of the top shown option, for a scrolled list.
390 pub first: usize,
391 /// Index of the hovered option, if any.
392 pub hovered: Option<usize>,
393 /// The screen the row belongs to, when it belongs to one.
394 pub screen: Option<AssetId>,
395 /// Font of the row's value label, so list text matches it.
396 pub font: Option<FontHandle>,
397 /// Text scale of the row's value label.
398 pub scale: f32,
399 /// Linear RGB text colour of the row's value label.
400 pub color: [f32; 3],
401}
402
403/// How a tick's independent work executes. `Parallel` lets systems fan their
404/// safe internal work across the job pool; `Serial` (or the resource being
405/// absent, the editor's case) keeps every system's work on the stepping
406/// thread -- the determinism oracle and the escape hatch
407/// (`cn run --serial-schedule`). Both modes must produce identical world
408/// state; the engine's schedule-determinism test is the gate on that claim.
409#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
410pub enum ScheduleMode {
411 /// Keep every system's work on the stepping thread.
412 Serial,
413 #[default]
414 /// Let systems fan safe internal work across the job pool.
415 Parallel,
416}
417
418impl ScheduleMode {
419 /// The mode a world runs under: the published resource, or `Serial` when
420 /// nothing published one.
421 pub fn current(resources: &crate::ecs::Resources) -> ScheduleMode {
422 resources
423 .get::<ScheduleMode>()
424 .copied()
425 .unwrap_or(ScheduleMode::Serial)
426 }
427}