π¦ Vulfram β Architecture, Lifecycle and Main Loop
This document explains how the Vulfram core is structured at a high level, how its lifecycle works, and how the host is expected to drive the main loop.
1. High-Level Architecture
Conceptual data flow:
Host β (commands & uploads) β Vulfram Core β WGPU / GPU
1.1 Host Responsibilities
The host is any runtime that calls the C-ABI functions (or WASM exports), for example:
- Node.js (N-API)
- Lua
- Python
- Any other FFI-capable environment
- Browser runtimes via WASM (WebGPU + DOM canvas)
The host is responsible for:
- Managing the game logic and world state.
- Generating logical IDs (window, camera, model, light, geometry, material, texture, etc.).
- Building MessagePack command batches and sending them to the core.
- Feeding time (
time,delta_time) intovulfram_tick. - Reading events and responses from the core and reacting to them.
The host does not:
- Create windows manually (handled by the core via platform proxies).
- Talk to GPU APIs directly.
- Manage WGPU devices, queues, or pipelines.
1.2 Core Responsibilities
The core is the Rust dynamic library that implements Vulfram. It uses:
wgpufor rendering (WebGPU)winitfor native window + OS eventsgilrsfor native gamepad inputweb-sysfor browser window/input plumbing (WASM)imagefor texture decodingglam+bytemuckfor math and buffer packingserde+rmp-serdefor MessagePack
Core responsibilities:
- Keep track of resources:
- Geometries, materials, textures (and shadows).
- Keep track of instances (components) per host ID:
- Cameras, models, lights.
- Maintain Realm/Surface/RealmGraph state:
Realmowns a render graph and outputs to aSurface.Presentmaps aSurfaceto a window.Connectorcomposes one realm into another.
- Manage GPU buffers, textures, pipelines, and render passes.
- Collect and expose input/window events via platform proxies.
- Perform rendering in
vulfram_tick.
2. Components, Resources and Instances
2.1 Components
Components represent high-level logic and are attached to entities:
CameraModel(mesh instance)Light
They are created and updated via commands in vulfram_send_queue.
Each component is associated with a host-chosen ID (e.g. camera_id, model_id, light_id).
2.2 Resources
Resources are reusable data assets such as:
- Geometries
- Textures
- Materials
They are referenced from components via logical IDs:
GeometryId,MaterialId,TextureId, etc.
Some data (static, per-component values like local colors or viewports) live inside the component and are not standalone resources.
2.3 Internal Instances
Internally, the core maintains per-entity instances like cameras, models, and lights. These instances hold GPU bindings, visibility masks, and render state derived from the host payloads.
These internal instances are indexed by host IDs and are not visible to the host. The host always refers to entities by their logical IDs, and the core resolves that to its internal instance structures.
2.4 Realm, Surface and RealmGraph (Current)
The render architecture is split into three layers:
- Realm: execution scope with a
RenderGraph(3D or 2D) and an outputSurface. - Surface: renderable + sampleable target (virtual swapchain). The core handles format, size, alpha conversions and MSAA resolve when needed.
- RealmGraph: a DAG generated from
Connectors+Presentsthat defines cross-realm composition order and cycle breaking.
Each window creates a default Realm and Surface. Present links the window to the
surface, and Connector layers control how realms compose (zIndex, blendMode, resolved rect, clip).
UI rendering uses a TwoD realm with a dedicated ui render pass. The UI realm outputs to
regular surfaces (alpha respected via blendMode) and is composed through the same
TargetGraph rules as other realms.
UI resources are referenced by logical IDs owned by the host: UiThemeId, UiFontId,
and UiImageId.
2.5 Auto-Graph (Experimental)
The host does not construct graphs directly. Instead it provides logical maps:
RealmMap: logical realm IDs and kindsTargetMap: logical targets (Window,WidgetRealmViewport,RealmPlane,Texture)TargetLayerMap:realmId -> targetIdwithlayout(left/top/width/height, zIndex, clip, blendMode)
The core builds TargetGraph and RealmGraph automatically and creates or updates
Surface, Present, and Connector tables based on the layers.
Surface, Present, and Connector are internal-only and are not exposed as host commands.
Auto resolution (Phase H)
- Each
TargetLayer(realm -> target)produces aSurface. - The Realm output surface is set automatically from its primary layer.
- If target is
Windowand the source realm is the window host realm, the core creates aPresent. - If target is
Window(non-host realm layer),WidgetRealmViewport, orRealmPlane, the core creates aConnectortargeting the host realm for that window. - Layout (
left/top/width/height,zIndex,clip,blendMode) is applied on connector creation and updated when layers change. - Layers are resolved deterministically: per realm, the smallest
targetIdwins.
Resolution rules
Windowtargets act as presentation roots.Texturetargets are offscreen roots.Windowconnectors,WidgetRealmViewport, andRealmPlaneare resolved automatically by the core.- Conflicts are resolved deterministically and surfaced via diagnostics/events.
3. Asynchronous Resource Linking (Fallback-Driven)
Vulfram allows resources to be created out of order:
- Models can reference geometry or material IDs that do not exist yet.
- Materials can reference texture IDs that do not exist yet.
When a referenced resource is missing, the core uses fallback resources so rendering continues. When the real resource appears later with the same ID, the core picks it up automatically on the next frame.
This enables async streaming, independent loading pipelines, and decoupled creation order.
4. LayerMask and Visibility
The core uses a u32 bitmask to filter visibility:
- Each camera has a
layerMaskCamera. - Each model/mesh has a
layerMaskComponent. - (Future) Each light may have a
layerMaskLight.
Visibility rule for a given camera/model pair:
Visible if:
(layerMaskCamera & layerMaskComponent) > 0
This enables:
- World-only or UI-only cameras.
- Team or category-based rendering.
- Dedicated special passes (e.g. picking, debug-only geometry).
4.1 Resource Reuse Semantics
- A single geometry can be referenced by many models.
- A single material can be referenced by many models.
- A single texture can be referenced by many materials.
There is no ownership tracking. The host is responsible for disposing resources when no longer needed; if a resource is disposed while still referenced, rendering falls back gracefully.
4.2 Render Ordering & Batching (Per Camera)
- Opaque/masked objects are sorted by
(material_id, geometry_id)to reduce state changes and batch draw calls. - Transparent objects are sorted by depth for correct blending.
Draw calls are batched by runs of (material_id, geometry_id) after sorting.
4.3 Forward Shading (Standard vs PBR)
- The Standard branch favors cheaper shading; the PBR branch favors realism.
- Light evaluation only runs the relevant path per light kind to avoid wasted work.
- Specular in the Standard branch only applies to directional/point/spot lights.
5. Core Lifecycle
5.1 Startup
The host loads the Vulfram dynamic library.
The host calls:
cvulfram_init();The core initializes:
- Platform proxy (desktop or browser)
- WGPU instance (device/queue created on first window)
- Gilrs (native gamepad) and web gamepad polling (WASM)
- Internal resource/component tables
- Profiling and internal queues
5.2 Loading / Initial Configuration
In the loading phase, the host typically:
- Uploads heavy data (meshes, textures) via
vulfram_upload_buffer. - Sends one or more command batches via
vulfram_send_queueto:- upsert resources (
CmdGeometryUpsert,CmdTextureCreateFromBuffer,CmdMaterialUpsert, etc.) - upsert components (
CmdCameraUpsert,CmdModelUpsert,CmdLightUpsert, β¦)
- upsert resources (
The core processes these commands on subsequent calls to vulfram_tick.
5.3 Main Loop
Once the initial state is ready, the host enters its main loop, where:
vulfram_tickdrives the core each frame and consumes queued commands.- The host sends updates and receives events/responses.
Inside each tick, the core:
- Builds a
RealmGraphPlanfromConnectors+Presents. - Executes render graphs per realm (3D/2D).
- Composes inter-realm surfaces in zIndex order.
- Routes input events through connectors and emits
eventTrace(includingtargetId).
5.4 Shutdown
When the application is closing:
The host stops calling
vulfram_tick.The host calls:
cvulfram_dispose();The core releases:
- GPU resources
- Window and OS handles
- Internal allocations
6. Recommended Main Loop (Host Side)
The exact structure of the host loop is flexible, but a recommended pattern is:
while (running) {
1. Update host-side logic
2. Perform uploads (optional)
3. Send command batch
4. Call vulfram_tick (processes queued commands)
5. Receive responses (consumes response queue)
6. Receive events
7. (Optional) Receive profiling
}
In more detail:
6.1 Update Host Logic
- Compute new game state (ECS systems, scripts, AI, etc.).
- Decide which entities/components/resources need:
- to be created
- to be updated
- to be destroyed (future).
6.2 Upload Heavy Data (Optional)
For any new or replaced heavy asset:
Call:
cvulfram_upload_buffer(buffer_id, type, ptr, length);Typical uploaded data:
- Vertex/index buffers
- Texture images
These uploads will later be consumed by Create* commands referenced by buffer_id.
6.3 Send Command Batch
Build a batch of commands describing what changed this frame:
- Component create/update
- Resource create/update
- Maintenance (e.g.
CmdUploadBufferDiscardAll)
Serialize this to MessagePack.
Call:
cvulfram_send_queue(buffer, length);
The core will copy the buffer and queue the commands for processing.
6.4 Advance the Core (`vulfram_tick`)
Call:
cvulfram_tick(time, delta_time);
The core will:
- Process all queued commands.
- Update internal component state (camera matrices, transforms, etc.).
- Collect input/window events.
- Execute rendering using WGPU.
- Fill internal queues for responses, events and profiling.
6.5 Receive Responses (Optional)
Call:
cuint8_t* ptr = NULL; size_t len = 0; vulfram_receive_queue(&ptr, &len);If
len > 0:- Copy the bytes to host memory (JS Buffer / Python bytes / Lua string, etc.).
- Free the core buffer via the mechanism defined in the binding.
- Deserialize MessagePack and process the responses.
vulfram_receive_queue consumes and clears the internal response queue.
6.6 Receive Events
Call:
cuint8_t* ptr = NULL; size_t len = 0; vulfram_receive_events(&ptr, &len);If
len > 0:- Copy the bytes.
- Free the core buffer.
- Deserialize MessagePack into an event list.
- Integrate these into the hostβs own input/window systems.
6.7 Profiling (Optional)
For debug or tooling:
Call:
cuint8_t* ptr = NULL; size_t len = 0; vulfram_get_profiling(&ptr, &len);If
len > 0:- Copy the bytes.
- Free the core buffer.
- Deserialize MessagePack into profiling data.
- Display or log via in-engine tools, overlays, or external tools.
7. One-shot Uploads and Cleanup
Heavy binary uploads use vulfram_upload_buffer and BufferId:
- The host calls
vulfram_upload_buffer(buffer_id, type, bytes, len). - The core stores the blob in an internal upload table as:
BufferId β { type, bytes, used_flag }. - A
Create*engine command (viasend_queue) referencesbuffer_idand uses its data to create a resource (geometry buffers, textures, etc.). - Once consumed, the upload entry is marked as used and can be removed.
- A maintenance command (
CmdUploadBufferDiscardAll) may be used to clean up any remaining, never-used uploads.
This model:
- Avoids shared
BufferIds. - Keeps memory usage predictable.
- Fits well with load phases and streaming scenarios.