Runtime and ECS - limztudio/NWBLot GitHub Wiki

Runtime and ECS

The runtime is built around a loader-owned frame loop, typed cooked assets, and an ECS world that content systems extend. The loader and project callback boundary is covered in Runtime Boot and Project API. This page focuses on runtime state once the world exists.

Loader Responsibilities

The loader is responsible for common runtime setup:

  • install crash capture early
  • parse runtime options
  • initialize logging
  • create Frame
  • initialize graphics and window state
  • call ConfigureProjectRuntime before device initialization
  • mount cooked resources from res through IFilesystem
  • register asset codecs
  • create AssetManager
  • load shader archive records
  • build ProjectRuntimeContext
  • create and call project callbacks
  • run the frame loop
  • tear down project and engine resources in order

The loader keeps project code small. Projects supply the window title, preferred frame client size, pre-device runtime configuration, startup/shutdown callbacks, and world creation/destruction functions.

Project Runtime Context

ProjectRuntimeContext is the bridge from project code into engine services. It provides access to systems such as:

  • graphics
  • input
  • clipboard and native text-input services
  • asset manager
  • mounted IFilesystem
  • frame-owned CpuTaskScheduler and GpuTaskScheduler
  • loader-owned project CpuTaskScope
  • frame graph registry
  • perf session and perf-capture setter
  • telemetry hooks
  • shader path resolver
  • project quit callback

Project code requests shutdown through ProjectRuntimeContext::requestQuit.

Frame Ownership

Frame owns:

  • platform setup and window lifecycle
  • graphics initialization
  • input dispatcher
  • clipboard and native text-input services
  • project update callback
  • render pass execution
  • frame timing
  • project and graphics arenas
  • CpuTaskScheduler and GpuTaskScheduler
  • telemetry and frame graph capture

The frame owns one CPU scheduler and one GPU scheduler as peer execution services. Graphics, ECS worlds, and the loader borrow those services through scopes instead of creating private pools. See CPU Task Scheduler.

Scheduler Domains

Core::CpuTaskScheduler and Core::GpuTaskScheduler are the current execution services. The frame owns both. GraphicsRuntime borrows them, ECS worlds own a CpuTaskScope on the shared CPU scheduler, and the loader owns a project scope joined before callbacks unload.

CPU cost, priority, and main-thread targeting are independent CpuTaskOptions. Ready queues accept concurrent producers from any thread. Completion includes the callback, descendants, and capture retirement. A CPU handle does not imply GPU completion.

Unexpected native-worker exceptions terminate at the native thread. The CPU scheduler does not capture, store, or defer them. Inline or caller-thread failures unwind to the shared application-entry boundary. Normal owner teardown joins outstanding work; during an existing exception unwind, loader, frame, graphics, and tool owners use callback-free drain() so the original exception reaches the application-entry boundary.

Graphics, projects, and ECS systems borrow the shared schedulers rather than owning independent worker pools.

ECS Infrastructure

core/ecs owns generic ECS behavior:

  • entity lifetime
  • component pools
  • system storage
  • queries
  • message buffers
  • ticking
  • CPU task-scope integration

World is the ownership source of truth. Entity is the component operation facade.

Core ECS does not know about production scene, renderer, model, skinning, CSG, or UI components. Those content systems live in impl.

Important world operations:

  • createEntity
  • entity(EntityID) to obtain an entity facade for a stored id
  • destroyEntity
  • tryGetComponent<T>
  • componentMutationVersion<T>
  • view<T...>
  • view<T...>().parallelEach(...)
  • addSystem<T>
  • getSystem<T>
  • postMessage<T>
  • consumeMessages<T>
  • tick
  • clear

Component pools are created lazily. Adding the first component of a type creates the pool for that component type. Mutation versions are tracked per component pool, which lets systems observe when component data changed.

View<T...> supports serial each iteration and parallelEach over a CpuTaskScope. The parallel form can take an explicit grain size and CpuTaskOptions, and it uses the view's anchor pool just like serial iteration, so single-component and multi-component views share the same candidate filtering behavior.

Systems derive from ISystem and are stored in the world. World ticking submits system updates as CPU tasks and tracks system access declarations. RendererSystem is both an ECS system and an IRenderPass; MeshSkinningSystem and UiLayerSystem also participate in render pass ordering.

The ownership boundary is intentionally narrow: the loader owns shared services and the frame loop, while the project owns the ECS world and ticks it from the update callback.

flowchart TD
    Loader["loader"]
    Frame["Core::Frame<br/>loop, input, render passes"]
    Context["ProjectRuntimeContext<br/>engine services"]
    Project["Project callbacks"]
    World["ECS World"]
    Systems["Systems and components"]
    RenderPasses["Registered render passes<br/>skinning, renderer, UI"]

    Loader --> Frame
    Loader --> Context --> Project --> World --> Systems
    Project --> Frame
    Systems --> RenderPasses --> Frame
Loading

Entity Facade

Entity wraps an EntityID plus a reference to the world. createEntity() returns one, and world.entity(id) obtains one when code holds a stored id. It is the public facade for operations associated with a particular entity:

  • entity.addComponent<T>()
  • entity.removeComponent<T>()
  • entity.getComponent<T>()
  • entity.hasComponent<T>()
  • entity.destroy()

World::addComponent, removeComponent, getComponent, and hasComponent are internal implementation operations. The facade keeps project code concise while preserving world ownership; World remains the source of truth for lifetime and storage.

Content Components

Important runtime components include:

  • TransformComponent: aligned decomposed position, rotation quaternion, and scale.
  • CameraComponent: camera projection data.
  • ActiveCameraComponent: identifies the active camera entity.
  • LightComponent: light color/intensity/range state.
  • SkeletonPoseComponent: local skeleton joint transforms and parent indices.
  • SkeletonJointPaletteComponent: renderer/skinning-ready joint palette data.
  • MeshComponent: typed reference to a static mesh.
  • SkinnedMeshBindingComponent: typed Skin reference, skeleton entity, and runtime mesh handle; the loaded Skin owns the authoritative mesh reference.
  • ModelComponent: typed reference to a model asset.
  • ModelRuntimeComponent: expanded model runtime summary on the owner entity.
  • ModelObjectComponent: common metadata for expanded model object entities.
  • ModelSkeletonComponent: skeleton asset reference for a model skeleton object.
  • ModelStaticMeshAttachmentComponent: parent object/joint attachment data for static mesh objects.
  • RendererComponent: material reference and visibility flag.
  • MaterialInstanceComponent: mutable material parameter overrides.
  • CsgReceiverComponent: shared CSG-receiver base with receiver group, enabled flag, and affect-opaque / affect-transparent pass flags.
  • StaticCsgMeshComponent and SkinnedCsgMeshComponent: mark CSG receivers (both derive from CsgReceiverComponent).
  • CsgCutterComponent: shape type, receiver group, transforms, and parameter bytes.
  • UiPaintComponent: paint callback (UiPaintCallback) receiving a UiPaintContext, plus visible and order fields.

This split keeps ECS infrastructure generic while scene, model, render, CSG, and UI behavior remain in implementation modules.

Scene Runtime

Scene components live under impl/ecs_scene.

TransformComponent is an aligned, standard-layout component containing:

  • position
  • rotation quaternion
  • scale

CameraComponent stores projection parameters in one aligned vector:

  • vertical field of view
  • near plane
  • far plane
  • optional aspect ratio

If camera aspect ratio is zero, renderer code derives aspect from the active framebuffer.

ActiveCameraComponent stores the active camera entity. Renderer camera state is resolved from ECS scene components, not from project input/controller state.

LightComponent supports:

  • directional lights
  • point lights
  • spot lights

It also carries physical soft-shadow source size: angularRadius for directional lights and sourceRadius for point/spot lights. enableCaustics controls whether a directional or spot light can receive a caustic slot when refractive casters are present.

Scene helpers gather renderer-ready SceneLight records and provide a default neutral directional light when the world has no lights.

Model Expansion

Model assets describe object composition through typed references. At runtime, the model ECS system expands a ModelComponent into entities and components for static meshes, skinned meshes, skeleton objects, material references, and renderer participation.

That keeps authored model composition in assets while letting runtime systems operate on focused ECS components.

ModelSystem::prepare calls syncModelRuntimes, while update keeps expanded object transforms and static mesh attachments current. Testbed and smoke helpers can call syncModelRuntimes after startup entity creation so model-owned child entities exist before the first render frame.

Model expansion produces object entities that can include:

  • transform/model object metadata
  • skeleton data
  • static mesh references
  • static mesh attachment data
  • skinned mesh binding data
  • renderer material data

For asset-level detail, see Mesh Model and Skinning.

Skinning

Skinned mesh runtime state is owned by the ECS mesh/skinning implementation. Skinning selects the Skin asset and skeleton entity, resolves the authoritative mesh from the loaded Skin, and exposes a renderer-visible runtime mesh handle. Source owners and runtime instances use Skin identity directly as their cache key; there is no explicit mesh hint, composite-key hashing, or source scan. GPU skinning work produces renderer-consumable skinned mesh data each frame.

Skinning systems work with:

  • Skin assets containing influences and inverse bind matrices
  • Skeleton assets containing hierarchy and bind pose data
  • skeleton ECS components containing pose and palette state
  • SkinnedMeshBindingComponent linking Skin, skeleton entity, and runtime mesh handle; the loaded Skin supplies its mesh
  • GPU resources for skinned vertex data and updated meshlet bounds

Skinning runs before the main renderer in the Testbed render pass order.

UI

impl/ecs_ui provides UiLayerSystem (both an ECS system and an IRenderPass, fed by the CPU paint/layout/text toolkit under impl/ecs_ui/toolkit plus OS clipboard/text-input services). Input routing owns platform-neutral hit testing and interaction state in impl/ecs_ui/toolkit/input (control.h declares ControlToken and control-action kinds; popup.h declares PopupToken/PopupScope plus dismissal reasons; gesture records and menu triggers are copied, not borrowed; see impl/ecs_ui/toolkit/input/README.md). Accept, Submit, and Activate arm an eligible focused declaration on the initial press and submit once on the matching source release; Cancel retires every pending pointer, command, and context-menu press before popup or editor handling. Pointer providers share the PointerInput/PointerPhase/PointerIntent contract (pointer.h): stable device/contact identity through release or cancellation, with hover as an explicit adapter capability. Layout shares rectangle/padding/color/extent predicates through impl/ecs_ui/toolkit/layout/validation.h. Clipboard and text-input admission share the core/os/utf8_text_internal.h decoder (Utf8TextDetail rejects NUL, overlong forms, surrogates, and out-of-range scalars before either queue copies bytes). Project code adds a UiPaintComponent with a paint callback. Keyboard/pointer/text-input capture state (wantsKeyboard()/wantsPointer()/wantsTextInput()) is exposed so project camera controls can yield to UI when needed. Automatic combo popups close at a Tab/Shift+Tab boundary and advance from their field in the parent scope; ordinary user/modal popups retain their focus trap (see the ComboState combo contract in impl/ecs_ui/toolkit/widgets/README.md).

UiPaintContext (impl/ecs_ui/components.h) includes:

  • world reference
  • clipboard and text-input services
  • CPU Ui::PaintBuilder (paint) and Ui::TextService (text)
  • display metrics and Ui::Builder layout handle (display, ui)
  • UI entity id and delta seconds

The UI system owns the custom UI GPU renderer and exposes keyboard/pointer capture state to project code. Testbed uses this to avoid applying fly-camera input while the UI wants keyboard or pointer capture. With UiLayerPresentation::Scene, UI contributes an independent display-linear layer that the scene renderer blends at final presentation. Standalone presents the UI layer over black without a scene renderer. Scene mode registers an output-layer contributor whose independent raster work produces a frozen premultiplied linear Rec.709 generation consumed by accepted final presentation. Standalone rejection requests device recreation because the adapter cannot safely abort an acquired frame.

Root collection and retention still run at the native input boundaries. The system sorts live roots by paint order and then entity ID immediately before painting, avoiding repeated sorting during input-only collection. Popup and modal input routing continues to use the declared layers and paint order.

⚠️ **GitHub.com Fallback** ⚠️