concinnity-engine 0.19.53

Runtime engine for Concinnity: ECS schedule, graphics, spawn, streaming
//! Client-side ecs runtime: what only a renderer-bearing runtime has. The
//! system table itself, its gates, the load-time decomposition pass, and the
//! resources the render band parks in a world.
//!
//! The renderer-free half is concinnity-core's `ecs` and is named there: the
//! metadata, asset registry, registration macros, asset-construction API,
//! `PipelineContext`, the `System` behavior trait, and the `World` that runs
//! systems over its data.
//!
//! TO ADD A NEW COMPONENT: register it in concinnity-core's `ecs::registry`
//! (`define_components!`). TO ADD A NEW ENGINE SYSTEM: implement the `System`
//! behavior trait on it, write its gate in this crate's `ecs::schedule`, and add
//! one entry to the `define_systems!` table in `ecs::registry` -- the table is
//! the registry AND the schedule (table order is run order).
//!
//! A system written outside the engine needs none of that: it is registered on
//! the world with `World::add_system`, naming the `Phase` it runs in. The table
//! entries stay internal; the phases are what an outside registration anchors
//! to.
//!
//! A system concinnity-core owns is listed in ITS table too
//! (`ecs::HEADLESS_SYSTEMS`, what a world with no host runs), and the two must
//! agree on order, gate description, and the edges among the systems both know
//! about. `headless_drift_tests` is what holds them to that.

pub(crate) mod access_ids;
#[cfg(test)]
mod active_scene_flow_tests;
pub(crate) mod by_asset_id;
#[cfg(test)]
mod consumed_columns_tests;
pub(crate) mod decompose;
#[cfg(test)]
mod determinism_tests;
#[cfg(test)]
mod headless_drift_tests;
mod registry;
/// How a world runs -- windowed or headless -- resolved once before it starts.
pub mod render_mode;
pub mod schedule;
mod world_queries;

use concinnity_core::ecs::MeshBoundsRecord;
use concinnity_core::ecs::Resources;
use concinnity_core::ecs::SceneGroup;
use concinnity_core::ecs::asset_id::AssetId;
use concinnity_core::render::backend;
use concinnity_core::render::scene_flow;
use concinnity_core::render::scene_residency;
use concinnity_core::window::display_mode;
// The `SYSTEMS` table is written client-side, since its gates name the client's
// own system types (see `registry`); a gate builds one `BuiltSystem` per
// present entry. Everything that runs it is in concinnity-core.
pub use registry::SYSTEMS;
// What stays client-side is the content only a renderer-bearing runtime has:
// the resources below, and the queries over them in `world_queries`.
pub use world_queries::{
    RenderHandoff, animation_system_mut, gpu_profile, memory_budget, memory_drift, render_handoff,
    renders, state_tree, streaming_pressure, streaming_stats, take_hot_reload_sources,
    take_render_backend, thread_budget,
};

/// A render backend transplanted out of a previous world, carried into a freshly
/// built world so its GraphicsSystem reuses the live GPU device + window instead
/// of constructing a new one. Published by the `cn editor` live SAVE swap between
/// building the post-edit world and starting it; GraphicsSystem `run_init` takes
/// it and calls `RenderBackend::reload_world` (reusing the window) instead of
/// `init_backend`, so a save applies without recreating the OS window. A shipped
/// runtime never publishes it; it exists only on the editor's live-update path.
pub struct PendingBackend(pub Box<dyn backend::RenderBackend>);

/// Why the renderer could not be built, left by GraphicsSystem's init for
/// `Runtime::start` to pick up and report.
///
/// Only a world that already resolved to a windowed run can leave one, so this
/// always means a GPU that refused. A machine with no GPU never builds the
/// system that publishes it.
pub struct RenderInitFailure(pub concinnity_core::render::error::RenderError);

// The frame's sampled window input, deposited beside the backend right after
// the draw (whose event pump produced it) and taken by InputSystem later the
// same tick. The pipelined driver deposits it from the render half's feedback
// instead; a missed consume merges into the next deposit so no edge is lost.
#[derive(Default)]
pub(crate) struct InputMailbox(pub Option<concinnity_core::input::snapshot::InputPacket>);

impl InputMailbox {
    // Deposit a fresh packet, merging onto an unconsumed one.
    pub(crate) fn deposit(&mut self, packet: concinnity_core::input::snapshot::InputPacket) {
        match &mut self.0 {
            Some(pending) => pending.merge_from(packet),
            None => self.0 = Some(packet),
        }
    }
}

// The frame's recording surfaces, taken and re-parked together by each
// recording system (the same handoff `ActiveRenderBackend` uses, so a step
// never re-boxes them into the resource map): the op queue the tick's backend
// effects accumulate into (drained into the frame snapshot by GraphicsSystem's
// extract, replayed in record order before the draw) and the slot-allocation
// authority ops name destinations from. Published by graphics init; absent in
// a world with no graphics, so recording systems no-op.
pub(crate) struct RenderQueues {
    pub ops: concinnity_core::render::ops::RenderOps,
    pub slots: crate::gfx::render_slots::RenderSlots,
}

// The active backend's capability flags, published by graphics init. The
// backend itself is parked (and on a pipelined frame, owned by the render
// thread), so a system that only needs to know what it supports reads this
// instead of reaching for it. Absent in a world with no graphics.
#[derive(Clone, Copy)]
pub(crate) struct ActiveDeviceCaps(pub concinnity_core::render::backend::DeviceCapabilities);

// The world's parked `RenderQueues` slot. `None` only while a step has it
// taken.
pub(crate) struct ActiveRenderQueues(pub Option<RenderQueues>);

impl ActiveRenderQueues {
    // Take the parked queues for the duration of one system step.
    pub(crate) fn take(resources: &mut Resources) -> Option<RenderQueues> {
        resources.get_mut::<Self>()?.0.take()
    }

    // Park the queues again at the end of the step that took them.
    pub(crate) fn put(resources: &mut Resources, queues: RenderQueues) {
        match resources.get_mut::<Self>() {
            Some(slot) => slot.0 = Some(queues),
            None => {
                resources.insert(Self(Some(queues)));
            }
        }
    }
}

// Recorded backend effects that failed at replay and need a simulation-side
// rollback (a streamed-mesh upload refused by a full region, a chunk add).
// Written by GraphicsSystem after submission; StreamingSystem drains it at the
// top of its next step.
#[derive(Default)]
pub(crate) struct RenderOpFailures(pub Vec<concinnity_core::render::ops::OpFailure>);

// The pipelined driver's channel pair, published (parked, so the per-step
// take never re-boxes) before the world moves to the simulation thread.
// Present exactly when frames are pipelined: GraphicsSystem's step extracts
// and sends the snapshot through it instead of submitting against a parked
// backend (which the render half owns), and applies the render half's
// feedback. Absent in serial execution.
pub(crate) struct PipelinedFrames(pub Option<PipelineChannels>);

pub(crate) struct PipelineChannels {
    pub(crate) snapshot_tx:
        std::sync::mpsc::SyncSender<concinnity_core::render::snapshot::RenderSnapshot>,
    pub(crate) feedback_rx:
        std::sync::mpsc::Receiver<concinnity_core::render::feedback::FrameFeedback>,
}

impl PipelinedFrames {
    // Take the parked channels for the duration of one step.
    pub(crate) fn take(resources: &mut Resources) -> Option<PipelineChannels> {
        resources.get_mut::<Self>()?.0.take()
    }

    // Park the channels again at the end of the step that took them.
    pub(crate) fn put(resources: &mut Resources, channels: PipelineChannels) {
        match resources.get_mut::<Self>() {
            Some(slot) => slot.0 = Some(channels),
            None => {
                resources.insert(Self(Some(channels)));
            }
        }
    }
}

/// The world's live render backend, parked here between system steps.
/// GraphicsSystem's init builds it and parks it; each system that drives the
/// GPU (GraphicsSystem's frame encode, InputSystem's poll) takes it out at the
/// top of its step and puts it back before returning, so the backend and the
/// `PipelineContext` are never borrowed together. `None` while a step has it
/// taken, or once the editor's live SAVE transplanted it out.
pub struct ActiveRenderBackend(pub Option<Box<dyn backend::RenderBackend>>);

impl ActiveRenderBackend {
    // Take the parked backend for the duration of one system step.
    pub(crate) fn take(resources: &mut Resources) -> Option<Box<dyn backend::RenderBackend>> {
        resources.get_mut::<Self>()?.0.take()
    }

    // Park the backend again at the end of the step that took it.
    pub(crate) fn put(resources: &mut Resources, backend: Box<dyn backend::RenderBackend>) {
        match resources.get_mut::<Self>() {
            Some(slot) => slot.0 = Some(backend),
            None => {
                resources.insert(Self(Some(backend)));
            }
        }
    }
}

// The active scene-flow bookkeeping, shared between SettingsSystem (which
// applies imperative scene jumps from `SceneCommand`) and GraphicsSystem
// (which ticks the timed advance + fades and submits the visibility changes).
// Published by GraphicsSystem's init when the world declares `Scene` assets;
// `flow` is `None` when it declared none, so both systems no-op. Both read the
// flow's clock through `elapsed`, so fade timing is shared.
pub(crate) struct ActiveSceneFlow {
    pub flow: Option<scene_flow::SceneFlow>,
    // `FrameTime::elapsed` at the flow's first read.
    epoch: Option<f32>,
}

impl ActiveSceneFlow {
    pub(crate) fn new(flow: Option<scene_flow::SceneFlow>) -> Self {
        Self { flow, epoch: None }
    }

    // Seconds on the flow's clock at runtime time `now` (`FrameTime::elapsed`).
    // The first read anchors the clock at zero, so a world started partway
    // through a runtime's life times its fades from its own first frame.
    pub(crate) fn elapsed(&mut self, now: f32) -> f32 {
        (now - *self.epoch.get_or_insert(now)).max(0.0)
    }
}

/// The blob's baked per-scene exclusive content groups, published at blob load
/// for the streaming/residency wiring to consume at graphics init.
pub struct BlobSceneGroups(pub Vec<SceneGroup>);

/// The blob's baked per-mesh geometry summaries (AABB + counts by mesh-source
/// handle), published at blob load so graphics init can build draw records for
/// deferred scene-owned meshes without decoding their payloads.
pub struct BlobMeshBounds(pub Vec<MeshBoundsRecord>);

// Per-scene streamed-content load status, republished by StreamingSystem
// whenever it changes: `(scene, state, fraction of members resident)` in
// declaration order. Consumers (menus, loading screens) read, never write.
pub(crate) struct SceneResidencyStatus {
    pub scenes: Vec<(AssetId, scene_residency::SceneLoadState, f32)>,
}

// Setting rows the engine has disabled at runtime (their keys, e.g. `show_fps`
// while "Display performance stats" is off). Published each frame by
// GraphicsSystem and read by `UiInputSystem`, which makes a matching row inert
// (no hover, no click) while its labels are grayed independently. Distinct from
// the init-time capability gating (which marks `HitRegion.disabled` before the
// regions are drained); this drives the same effect after they are drained.
#[derive(Debug, Clone, Default)]
pub(crate) struct DisabledSettingRows(
    pub std::collections::HashSet<concinnity_core::settings::SettingKey>,
);

// The display modes offered by the "Resolution" settings row, published once by
// GraphicsSystem at init (enumerated from the backend's display, or the static
// fallback when it cannot enumerate) and read by `UiInputSystem` to seed the
// row's dropdown list. Ordered as displayed; a pick's `SetIndex` indexes it.
#[derive(Debug, Clone, Default)]
pub(crate) struct DisplayModes(pub Vec<display_mode::DisplayMode>);

/// The system table. Generates the `SYSTEMS` table a world starts from; table
/// order is run order.
///
/// The three leading fields are the load-time passes around the systems: one
/// that completes the world before the gates read it, one that runs over the
/// world once the gates have built them and before their `init`, and one that
/// pre-creates the event queues a scheduled system's declared access can
/// touch.
///
/// Every system is internal: it has no declarable asset, is never parsed from a
/// world or written to a blob, and is constructed by its gate from world
/// content. Each entry maps a name to the behavior type that implements
/// `System`, the gate that builds it, a human-readable gate description, and
/// the `Phase` it runs in; the entry name doubles as the system's stable
/// display name for profiling and logging.
///
/// Entries are in phase order, and a system registered on a world with
/// `World::add_system` runs after every entry sharing its phase. That is the
/// whole of what a table entry owes an outside registration: the entries
/// themselves stay internal.
#[macro_export]
macro_rules! define_systems {
    ( complete_world: $complete_world:path,
      before_init: $before_init:path,
      prepare_events: $prepare_events:path,
      $( $name:ident => $behavior:path {
            gate: $gate:path,
            present_when: $present_when:literal,
            phase: $phase:ident,
            after: [ $( $after:ident ),* $(,)? ],
            before: [ $( $before:ident ),* $(,)? ] $(,)?
        } ),* $(,)? ) => {
        /// The system table: one entry per system, in run order, plus the
        /// load-time passes that bracket them. `World::start` runs each gate
        /// against the world's content and builds the systems they return.
        pub const SYSTEMS: &::concinnity_core::ecs::SystemTable = &::concinnity_core::ecs::SystemTable {
            entries: &[
                $( ::concinnity_core::ecs::SystemEntry {
                    name: stringify!($name),
                    present_when: $present_when,
                    phase: ::concinnity_core::ecs::Phase::$phase,
                    // Boxing happens here rather than in the gates, so each
                    // gate returns its own system type and the entry's behavior
                    // path has to name it.
                    gate: {
                        fn build(
                            world: &::concinnity_core::ecs::World,
                        ) -> Option<::std::boxed::Box<dyn ::concinnity_core::ecs::System>> {
                            let built: Option<$behavior> = $gate(world);
                            built.map(|s| -> ::std::boxed::Box<dyn ::concinnity_core::ecs::System> {
                                ::std::boxed::Box::new(s)
                            })
                        }
                        build
                    },
                    after: &[ $( stringify!($after) ),* ],
                    before: &[ $( stringify!($before) ),* ],
                }, )*
            ],
            complete_world: Some($complete_world),
            before_init: Some($before_init),
            prepare_events: Some($prepare_events),
        };
    };
}