Skip to content

Architecture

Teo, Lim edited this page Oct 13, 2026 · 52 revisions

Architecture

NWBLot is not arranged as a single monolithic application. It is organized around reusable engine infrastructure, implementation-specific engine systems, a deterministic asset pipeline, a loader/runtime shell, and project code. The current project is the Testbed, but the loader/project boundary is intentionally small enough for other projects to use the same runtime path.

Repository Map

Major source roots:

  • global/: platform-independent utilities, containers, strings, smart pointers, math (including the NEON/SSE-accelerated f64 triangle-area path in global/simd/collision_triangle.h), filesystem helpers, text helpers (including IsAsciiIdentifierChar in global/text_utils.h and HasEnvironmentValue/EnvironmentVariableEquals in global/environment.h), blocking read/write helpers, compile/platform macros, synchronization primitives, process helpers, atomics, shared texture-payload layouts (global/texture_payload.h, the single source of truth also consumed by impl/assets_texture/ and utilities/tex_conv), and low-level algorithms. global/ is the neutral global-unit domain, not the owner of the NWB namespace wrapper: root engine_namespace.h is the sole owner of the NWB_BEGIN/NWB_END macro definitions. Domain umbrellas such as core/global.h, impl/global.h, and logger/global.h include that header and define their own domain wrappers. Applications own separate namespace headers, such as CoolStuff/Testbed/namespace.h, without directly including the engine namespace header.
  • core/: reusable engine services, including allocation, application/log helpers, crash capture, assets, filesystem volumes, frame/window loop, input, graphics, CPU and GPU task scheduling, ECS, mesh utilities, metascript parsing, performance capture, and telemetry.
  • impl/: concrete engine implementations built on the core service layer, including authored/cooked asset types, asset cook modules, ECS scene/content components, renderer systems, mesh/skinning/model systems, CSG integration, and the custom UI stack (CPU paint/layout/text toolkit under impl/ecs_ui/toolkit plus the impl/ecs_ui ECS adapter). The renderer implementation is split by domain under impl/ecs_render: kernel, shared, shader, mesh, material, deferred, csg, avboit, optics, raytrace, and reflection; the model-renderer adapter is an explicit neighboring module at impl/ecs_model_renderer.
  • pipeline/: the staged asset tools that scan authored asset roots, build standalone cooked artifacts, and publish the runtime graphics volume. The three executables are dependency_computer, asset_builder, and asset_gatherer; pipeline/launch.py is the orchestrator discovered as the pipeline launcher command.
  • loader/: the runtime executable shell. It sets up logging, crash capture, frame/graphics/input, mounts cooked resources through IFilesystem, registers assets, and runs project callbacks.
  • CoolStuff/Testbed/: the current project using the engine. It builds a startup scene with a skinned character, ground plane, camera, lights, and custom UI controls.
  • tests/: focused GTest suites and smoke executables for core systems, rendering behavior, CSG, transparency, scene components, crash packaging, telemetry, mesh processing, and Testbed/window capture.
  • utilities/: standalone helper tools such as nwb_fbx_to_nwb and tex_conv, plus Python authoring workflows such as ui_skin. Each runnable utility owns its leaf launch.py, discovered through the utilities category launcher.
  • launcher/: the root python -m launcher package for automatic configure/build workflows, project and utility dispatch, and executable launches, with shared Windows process helpers under launcher/repository_windows_process.py. Native projects and utilities support --build-only; build <targets...> also builds libraries and aggregate targets.
  • logger/: the logging client/server system (and standalone logger) used by the loader, tools, and tests.
  • configuration/: toolchain and build-configuration support.
  • 3rd_parties/: flat, offline third-party packages used by the engine; see Third-Party Packages for vendoring metadata and CMake wiring rules.

global/ and core/ are separate top-level architectural domains. Low-level, engine-independent facilities belong in global/, while reusable engine services belong in core/.

Layering

global is the shared substrate. It provides math, containers, string/storage primitives, filesystem helpers, process helpers, synchronization, compiler/platform macros, and shared texture-payload layouts (global/texture_payload.h) consumed by both impl/assets_texture/ and utilities/tex_conv. NWBLot-owned utility, configuration, diagnostic, namespace, and shader feature macro APIs use NWB_, including NWB_TEXT, NWB_ASSERT, NWB_DEBUG, engine namespace wrappers, logging, and graphics selection. Application-owned macro APIs use their own prefix, such as TESTBED_. External/compiler interoperability spellings and existing g_* shader resource-view aliases retain their current contracts. The defining header and directory identify each macro's domain. Global utility types and functions retain their existing scopes; the common prefix does not move them into the engine namespace. Root engine_namespace.h alone defines NWB_BEGIN/NWB_END. Each domain reaches those macros through its own umbrella header, including core/global.h, rather than redefining them in global/.

CMake emits NWB_DEBUG, NWB_OPTIMIZE, and NWB_FINAL directly for dbg, opt, and fin. Project-owned C++ abstract interface definitions and forward declarations use the canonical lowercase interface annotation. global/compile.h expands it to struct under #ifndef interface, preserving an existing SDK definition. This is an intentional exception to the NWB_ macro-prefix rule. Local push/undef/pop boundaries suspend the annotation around external Wayland headers that use interface as an ordinary identifier, then immediately restore it. Existing access, inheritance, destructor, virtual, lifetime, and exception contracts remain explicit. Ordinary aggregates and concrete implementations retain their struct or class declarations.

The core service layer under core/ owns reusable engine infrastructure. It does not own production scene content. Its systems are content-agnostic: assets, ECS mechanics, frame ownership, graphics abstractions, CPU and GPU task scheduling, filesystem volumes, logging, crash handling, telemetry, and common runtime utilities.

impl owns concrete engine behavior. Scene components, renderer-facing ECS systems, material/shader/mesh/model assets, CSG support, skinning, and UI integration live there. This keeps the core service layer generic while still letting the engine ship a complete rendering/content stack.

pipeline is the cook workflow. It is not part of the runtime process; the loader only consumes published volumes.

loader is the runtime shell. Project code supplies callbacks and creates an ECS world, while the loader owns the common startup/shutdown path.

Testbed is an engine client. It links the loader, asset modules, scene/mesh/model/skinning/render/UI ECS modules, and authored project assets. The Testbed does not directly parse authored assets at runtime; it asks the loader-provided AssetManager for cooked typed assets.

Result and error contracts

Global Expected<T, E>, Unexpected<E> and MakeUnexpected(error) from global/expected.h define fallible producer results. The default error is empty Failure; meaningful domain enums, ErrorCode, diagnostic aggregates and guaranteed borrowed error text remain explicit. Return answers, owned candidates and coherent reports directly, and check admission before value/error access. Successful false, zero, empty and documented no-work values remain valid. Preserve supplied arenas, borrowed lifetimes, publication/partial-progress semantics, complete noexcept contracts and terminal exception unwinding. Genuine buffer reuse, lifecycle mutation and required SDK/callback signatures remain in/out contracts; fallible mutators can return Expected<void, E>. See Diagnostics and Invariants and the source result guide.

Repeated scratch-arena results preserve reclamation order: destroy the prior owned result before producing its replacement when it is no longer needed. Clearing a container retains its storage, and replacing that live allocation can retain non-LIFO scratch memory. Production operations that need capacity reuse keep that state explicit.

Namespace Ownership

Owner Header and namespace
Neutral utilities global/; utility symbols retain their existing global or utility-detail scopes, and owned macros use NWB_.
Engine namespace Root engine_namespace.h defines only NWB_BEGIN/NWB_END for namespace NWB.
Engine domain Its umbrella, such as core/global.h or impl/global.h, includes the engine namespace header and defines its domain wrappers.
Application A local header, such as CoolStuff/Testbed/namespace.h, defines TESTBED_BEGIN/TESTBED_END for namespace Testbed independently of the engine namespace header.
Loader integration loader/project_entry.h declares the required engine/project entry functions; application adapters implement those functions in NWB and delegate to the application namespace.

Applications include the engine API headers they use and may receive engine_namespace.h transitively through those APIs. They do not include it directly to declare their own classes or helpers. Testbed classes, UI galleries, and implementation helpers live in Testbed.

Existing application-facing features under NWB::Impl, including the UI toolkit and renderer systems, remain engine contracts that projects may use. The namespace split does not turn the entire impl/ tree into a private layer. Private/detail headers remain with their owners, and native graphics-provider headers remain behind backend_selection. The current target include roots still expose the repository root; this migration does not introduce an exported-header architecture or claim filesystem-level isolation.

Ownership Boundaries

Important ownership boundaries:

  • global should not depend on engine systems.
  • core should stay content-agnostic.
  • impl may depend on the core service layer and owns engine-provided content systems.
  • projects depend on the loader/project API and concrete engine modules they choose to use.
  • shader-language compilation stays in cook targets.
  • runtime graphics consumes cooked shader bytecode and shader archive records.
  • project-authored assets live under project trees such as CoolStuff/Testbed/assets.
  • engine-owned renderer shaders and shared CPU/GPU ABI assets live under impl/assets.
  • material interfaces, surfaces, and BXDF policy are project-owned and live under project asset roots.

Core Systems

global

global/ contains the low-level substrate used across the repository:

  • scalar and SIMD math (global/simd/): four-lane SIMDVectorSingle/SIMDVectorDouble vectors with SIMDMatrix/SIMDMatrixDouble row-stored matrices, aligned Float#/Double# storage, and boundary-only Load/Store conversion
  • row-stored matrix conventions
  • fixed/vector/hash containers
  • string and compact string types
  • shared text helpers such as trimming, prefix checks, UTF-8 decoding, BOM stripping, and JSON text escaping
  • filesystem paths and archive helpers
  • platform blocking I/O helpers for complete file-descriptor or handle reads/writes
  • process and shared-library helpers
  • platform/compiler macros
  • bit operations and representation-preserving BitCast in global/bit.h
  • smart pointers and refcount helpers
  • synchronization and atomics
  • generic arena base classes, arena object helpers, and allocation accounting in global/arena_base.h, global/arena_object.h, and global/arena_memory.h; concrete arena implementations remain under core/alloc

The public math API lives at global scope. SIMDMatrix stores row vectors in v[0..3] (and SIMDMatrixDouble the binary64 counterpart), translation lives in the fourth column, and vector transforms evaluate result = M * v. Scene transforms use decomposed position, quaternion rotation, and scale in ECS components, while asset/model transforms use explicit matrix payloads where that better matches authored data.

Generic arena foundations are intentionally part of global, so low-level code can use their accounting, object construction, and container-allocator propagation without taking a dependency on the higher-level core/alloc domain. ArenaAllocator propagates on container move-assignment and swap so storage keeps the arena that owns it. core/alloc owns the concrete global, persistent, and scratch allocator services plus heap backing. Scratch LIFO reclaim spans chunk boundaries: emptying the active chunk exposes the previous live chunk and caches the empty one for reuse. CPU and GPU execution services live in core/task, not in core/alloc.

Project code uses focused wrappers from global/ instead of introducing direct standard-library facilities throughout engine modules. For example, call BitCast<To>(value) from global/bit.h; the wrapper is the single project boundary around std::bit_cast.

core/assets

The asset system is typed. Asset classes derive from TypedAsset<T>, expose a stable asset type name, and load binary payloads through registered codecs.

Runtime loading goes through:

  • AssetRegistry: maps asset types to codecs.
  • AssetManager: loads typed assets synchronously or asynchronously.
  • IAssetBinarySource: abstracts where cooked binary data comes from.
  • AssetRef<T>: stores typed virtual-path references between assets/components.

The loader's runtime binary source is a mounted IFilesystem, so assets are loaded from cooked volume records rather than directly from authored source files. The default implementation is VolumeFileSystem. Projects may supply a FilesystemFactory from ConfigureProjectRuntime before device initialization; an empty factory selects the volume backend for every mount. See Filesystem.

More detail:

  • TypedAsset<T> binds a C++ type to an asset type name such as material, mesh, skin, skeleton, or model. Shader assets use shared interface IShader storage and TypedShader<T> with final stage types such as ComputeShader/compute_shader and PixelShader/pixel_shader; each type supplies a compile-time physical stage and its registered codec.
  • AssetCodec<T> owns binary deserialization for one typed asset class.
  • cook builds can also use codecs for serialization.
  • Asset modules register runtime codecs through AssetCodecAutoRegistrar. The asset builder drains CookEntryAutoRegistrar entries into CookEntryRegistry and graphics-specific cook preparation through AssetVolumePrepareAutoRegistrar. The common CreateAssetCodec<T> helper constructs the standard codec factory product, while collection deduplicates factories before registration.
  • At startup the loader drains the collected codec factories into AssetRegistry. Cook registration lives in the builder's cook-entry and volume-prepare registries.
  • AssetManager::loadSync reads bytes from an IAssetBinarySource, then asks AssetRegistry to deserialize by asset type.
  • async loading is represented by request IDs and AssetLoadResult, with optional external async executor support.

Shared cook infrastructure also lives here: core/assets/bunch expands generic asset bunches, while core/assets/volume owns the staged volume writer, object cache, and pack-manifest support. Asset-specific parsing and serialization remain in implementation modules.

core/filesystem

The filesystem layer separates interface, factory, and volume-backend responsibilities. filesystem.h defines IFilesystem and resource-free FileCursor helpers. factory.h owns FilesystemFactory and CreateFilesystem, including default-backend selection. volume_types.h carries shared mount descriptions; volume_file_system.h is the default segmented backend; volume_build.h handles whole-map batch builds; and volume_staging.h owns stage-directory cleanup and transactional segment promotion.

Runtime assets are stored in cooked volume segments. The loader mounts the graphics volume in res through CreateFilesystem as RuntimeReadOnly. Pipeline-cache namespaces use the same factory with RuntimeReadOnly or RuntimeReadWrite.

Volume publishing is staged. The asset gatherer builds a volume in an empty staging directory through VolumeFileSystem with CookWrite usage and promotes the finished segments into the output directory. Existing segments can be backed up and restored if promotion fails. Pack-manifest object-cache payloads are written through that same staged volume path.

core/metascript

Metascript is the parser/lexer layer used for authored .nwb metadata. Cookers parse source metadata through this layer and validate asset-specific schemas before producing runtime binaries.

core/ecs

The ECS owns entity lifetime, component pools, system storage, queries, message buffers, and ticking. World is the ownership source of truth. Entity is the component operation facade.

Core ECS is content-agnostic. Scene/render/model/skinning components live under impl/, not in core/ecs.

The world borrows the frame-owned CpuTaskScheduler and owns a CpuTaskScope for system updates and parallelEach work. Systems declare component access, and the world handles component pool creation, entity component lists, and message buffers. See CPU Task Scheduler.

core/task

CPU and GPU execution are peer services with parallel module layouts:

Domain Public entry Target Work
CPU core/task/cpu/scheduler.h nwb_cpu_task Callables, CpuTaskScope, CPU completion handles
GPU core/task/gpu/scheduler.h nwb_gpu_task Task graphs, compiled plans, GPU submission tokens

Frame::cpuTasks() and Frame::gpuTasks() expose the same instances borrowed by GraphicsRuntime and ProjectRuntimeContext. GPU submit() coordinates recording and submission; wait() joins device work. CPU and GPU completion remain distinct.

Dependencies run in one direction:

nwb_graphics (runtime) -> nwb_gpu_task -> nwb_graphics_backend -> nwb_cpu_task -> nwb_alloc

Backend code cannot include GPU graph or graphics-runtime headers, GPU tasks cannot include the graphics runtime, and CPU tasks cannot include graphics. Tests mirror the layout under tests/unit/task/cpu and tests/unit/task/gpu; graphics tests using selected public contracts remain under tests/unit/graphics.

Neutral shader bytecode

The neutral nwb_shader_bytecode target owns core/graphics/spirv_entry_point.h and .cpp and declares a public nwb_common dependency, alongside the standard internal target defaults. Shader asset runtime/cook targets use it for admission, and nwb_graphics_backend uses the same parser for module validation. Payload encoding, concrete types and codecs remain in impl/assets_shader; provider-specific headers remain behind graphics selection.

core/frame

Frame owns platform setup, window lifecycle, graphics initialization, input dispatch, clipboard and native text-input services, project update callbacks, render pass execution, timing, arenas, the shared CPU and GPU task schedulers, telemetry, and frame graph capture. Normal teardown joins outstanding CPU work; terminal exception unwind drains the CPU scheduler callback-free so the application-entry exception remains primary.

Project code requests shutdown through ProjectRuntimeContext::requestQuit, which maps to Frame::requestQuit.

core/graphics

The graphics layer selects one concrete provider at compile time. The NWB_GRAPHICS_BACKEND CMake cache setting defaults to Vulkan, currently the only implemented provider; selecting Metal fails configuration until that provider is implemented. The Vulkan target publishes NWB_GRAPHICS_BACKEND_VULKAN=1 and NWB_GRAPHICS_BACKEND_METAL=0, and core/graphics/backend_selection/configuration.h rejects conflicting or unsupported flags.

Neutral contracts live in core/graphics/rhi/. Graphics consumers and tests use those contracts or narrow selected headers under core/graphics/backend_selection/; backend_selection/backend.h selects public device and resource definitions, while backend_selection.h selects the full context. Vulkan native types and operations stay inside core/graphics/vulkan/, reached through the selection boundary. Selection preserves direct concrete calls, ownership, and layout without adding runtime dispatch, per-resource storage, or allocations. All tests, fixtures, and test-only helpers belong under tests/. Graphics tests run after normal backend selection and exercise existing public contracts. Vulkan-, Metal-, and other provider-specific tests, native SDK assertions, private provider fixtures, direct provider replay benchmarks, and production test hooks are prohibited. Harnesses may configure platform loaders and recognize native validation diagnostics to run the selected backend and reject runtime warnings.

The backend contract covers:

  • device and adapter enumeration
  • swapchain and backbuffer handling
  • buffers, textures, heaps, and staging resources
  • shader modules and shader libraries
  • graphics, compute, meshlet, and ray tracing pipelines
  • descriptor-buffer-compatible layouts, heap-backed resource tables, and a live global descriptor heap
  • physical Graphics/Compute/Transfer queues, native command recording and accepted submissions, completion tokens, and owner-aware resource-state handoffs
  • barriers and resource state tracking
  • framebuffers and render passes
  • acceleration structures and shader binding tables
  • GPU timing
  • GPU crash diagnostics hooks
  • feature queries

Feature queries expose the active backend capabilities used by the renderer, including meshlets, ray query, ray tracing acceleration structures, ray tracing pipelines, variable rate shading, shader specializations, wave/subgroup lane counts, cooperative vector support, and advanced ray tracing extensions. queryFeatureSupport(feature) is a direct bool predicate with a const noexcept contract, with no feature-info output parameter. Device::getWaveLaneCounts() returns an infallible WaveLaneCountRange containing minimum and maximum lane counts. The Vulkan backend obtains them from core subgroup properties. GraphicsRuntime::queryWaveLaneCount() consumes the range, using the maximum when present and its existing conservative 64-lane numerical default otherwise.

For target and GPU policy, see Platform and GPU Support. For loader details, see Runtime Boot. For project callbacks and world creation, see Project API.

The device owns a live global descriptor heap addressed by an opaque GpuDescriptorHandle. VK_EXT_descriptor_buffer is required: its manager owns the mapped resource and sampler segments used by every renderer pipeline, including generation-safe TLAS blocks. Descriptor indexing remains the required shader feature floor for non-uniform heap indexing, but active code has no classic descriptor-set or alternate heap transport. Pipeline-local layouts carry push constants only; all resources are selected through heap slots. The heap supplies traversal geometry, raster mesh/frame data, scene and light buffers, renderer targets, material-authored cooked texture assets and built-in resources, and UI bindings. Its descriptor segments declare Graphics/AsyncCompute sharing and become Vulkan-concurrent only when those effective lanes use distinct queue families. See Bindless Descriptor Heap.

GraphicsRuntime owns device creation, resources, render passes, and presentation. It borrows the frame-owned CPU and GPU schedulers, attaches the GPU scheduler after device creation, joins GPU work before teardown, and detaches before device destruction. The renderer declares GPU work through GpuTaskGraph; GpuTaskScheduler records and submits compiled packets. Compiler access observes a coherent declaration revision, and the first recording claim freezes that declaration until the attempt is terminal. The compiler chooses compatible physical queues, forms native packets, and emits packet-boundary state/ownership transitions. Exact command-list recording capabilities publish immutable Recorded lists only after native close and retention; diagnostic reads, submission, and revocation serialize through the publication state. Accepted submission tokens establish execution: same-queue dependencies use queue order, while distinct physical queues wait on the producer timeline. Concurrent resources retain their sharing contract; exclusive cross-family resources receive paired release/acquire barriers. Buffer uses may declare byte ranges; disjoint intervals do not create resource hazards. The submission transaction commits irreversible accepted work before observers can propagate an exception and terminalizes unaccepted work callback-free during unwind. CommandListResourceStateHandoff remains the current state-seeding and ownership-transfer contract, not the renderer's scheduling source of truth. See GPU Task Graph, CPU Task Scheduler, and Renderer Frame Flow.

Memory and Execution Ownership

The engine uses explicit arenas and engine-owned allocators rather than broad process-global allocation. Important runtime ownership areas include:

  • graphics object arena
  • project object arena
  • persistent and scratch arenas
  • asset arena
  • crash reporting arena
  • domain-specific arena names for diagnostics, including core/task/cpu/scheduler

core/alloc provides GlobalArena, PersistentArena, ScratchArena, heap backing, and standalone allocation runtime support. PersistentArena is a native fixed-capacity block pool for reusable metadata whose size is known up front. Choose an arena according to the allocation lifetime and reuse pattern: persistent storage owns reusable domain state, while scratch storage holds temporary work whose consumers finish before reclamation.

Frame owns one CpuTaskScheduler and one GpuTaskScheduler. Graphics, ECS, the loader, and tools borrow those services through scopes. asset_builder and nwb_fbx_to_nwb each construct one process CPU scheduler at entry. Unexpected native-worker exceptions terminate at the native thread; the CPU scheduler does not capture or defer them. Self-waits on a task, ancestor, or required dependent work are rejected; callback-free drain() is noexcept teardown. Production tools use InvokeTerminalEntry from global/terminal_entry.h so unexpected exceptions reach the application-entry boundary.

Runtime Data Direction

The main runtime data direction is:

  1. Cooked volume records provide binary payloads.
  2. AssetManager loads typed assets.
  3. ECS components store typed asset references and runtime state.
  4. ECS systems expand content into renderer-ready caches and draw state.
  5. RendererSystem prepares frame targets, material pipelines, CSG state, shadow state, caustic state, transparent state, and composite work.
  6. core/graphics executes command lists through the active backend.

This direction is deliberate. Runtime code should not reach backward into authored source files for shader compilation, material parsing, or asset schema validation.

Clone this wiki locally