UntoldEngine streams large worlds through a manifest-driven tiled scene pipeline.
The public rule is simple:
| Use case | API |
|---|---|
| Streamed world geometry (manifest-driven) | setEntityStreamScene(entityId:manifest:withExtension:completion:) |
| Handcrafted streaming zones (no manifest) | StreamingRegionManager — register StreamingRegion AABB + asset lists directly |
| Always-resident assets | setEntityMeshAsync(entityId:filename:withExtension:completion:) |
GeometryStreamingSystem manages the runtime once a streamed scene is loaded. It is not a public component-authoring workflow for standalone entities.
For handcrafted zone streaming without a manifest (e.g. dungeon rooms, level sectors), use
StreamingRegionManager.shared. See the StreamingRegionManager architecture doc for the full API.
let sceneRoot = createEntity()
setEntityName(entityId: sceneRoot, name: "city")
setEntityStreamScene(entityId: sceneRoot, manifest: "city", withExtension: "json") { success in
setSceneReady(success)
}let sceneRoot = createEntity()
setEntityName(entityId: sceneRoot, name: "city")
if let url = URL(string: "https://cdn.example.com/city/city.json") {
setEntityStreamScene(entityId: sceneRoot, url: url) { success in
setSceneReady(success)
}
}Legacy overloads —
loadTiledScene(manifest:)andloadTiledScene(url:)remain available for backwards compatibility. They create an internal root entity automatically. PrefersetEntityStreamScene(entityId:...)when you need a stable handle to the scene.
Remote manifests are downloaded and cached locally. Tile, HLOD, and per-tile LOD URLs are resolved relative to the manifest URL and fetched on demand.
The engine uses multiple geometry layers:
- Full tile: the main tile payload loaded by
loadTile(). Tile assets use the.untoldbinary format, loaded byUntoldReaderwithout ModelIO. - Per-tile LOD: intermediate meshes shown while the full tile is still out of range
- HLOD: coarse far-distance proxy
- OCC sub-mesh stubs: fine-grained
StreamingComponententities created internally inside large tiles
StreamingComponent is internal to the tile-owned OCC path. External callers should not attach it manually or rely on enableStreaming(...).
These are the important fields for geometry streaming:
| Field | Meaning |
|---|---|
streaming_radius |
Full tile display zone |
unload_radius |
Tile teardown threshold |
prefetch_radius |
Background parse threshold before the tile becomes visible |
priority |
Tile load ordering when many tiles compete |
hlod_levels |
Optional far proxy meshes |
lod_levels |
Optional per-tile intermediate LOD meshes |
file_size_bytes |
Parse-budget hint used by the runtime gate |
If prefetch_radius is omitted, the engine computes it automatically from the gap between streaming_radius and unload_radius.
Each update tick, GeometryStreamingSystem:
- Queries the octree within
maxQueryRadius. - Chooses tile parse candidates using predictive camera motion (velocity look-ahead), a frustum gate, and an optional interior zone gate.
- Parses up to
maxConcurrentTileLoadstiles, subject totileParseMemoryBudgetMB. - Streams OCC child meshes inside loaded tiles using
maxConcurrentLoads. - Unloads tiles, LODs, HLODs, and OCC meshes when they leave range or memory pressure requires eviction.
Important defaults:
maxConcurrentTileLoads = 2maxConcurrentLoads = 3maxConcurrentLODLoads = 4maxConcurrentHLODLoads = 4updateInterval = 0.1burstTickInterval = 0.016
// Tile concurrency
GeometryStreamingSystem.shared.maxConcurrentTileLoads = 2
GeometryStreamingSystem.shared.maxConcurrentLoads = 3
GeometryStreamingSystem.shared.maxConcurrentLODLoads = 4
GeometryStreamingSystem.shared.maxConcurrentHLODLoads = 4
// Frustum gate
GeometryStreamingSystem.shared.enableFrustumGate = true
GeometryStreamingSystem.shared.tileFrustumGatePadding = 20.0 // m — wider pad for tiles
GeometryStreamingSystem.shared.frustumGatePadding = 5.0 // m — pad for mesh-level OCC
// Spatial query
GeometryStreamingSystem.shared.maxQueryRadius = 500.0 // must cover farthest unload_radius
// Velocity predictor (predictive tile loading)
GeometryStreamingSystem.shared.velocityLookAheadTime = 0.5 // s — how far ahead to project
GeometryStreamingSystem.shared.velocityLookAheadMinSpeed = 1.5 // m/s — activation threshold
// Interior zone gating (v4 quadtree-floor manifests)
// Tiles tagged interior=true only load when the camera is inside this AABB.
// Set automatically from the manifest; override if needed:
GeometryStreamingSystem.shared.interiorZone = AABB(
min: simd_float3(-10, 0, -10),
max: simd_float3(10, 5, 10)
)
// Parse safety
GeometryStreamingSystem.shared.tileParseTimeoutSeconds = 60.0 // watchdog deadline per tileUse maxQueryRadius large enough to cover the farthest unload_radius in the scene, or out-of-range tiles may not be discovered for teardown.
GeometryStreamingSystem.shared.forceUnloadAllParsedTiles()This call immediately unloads every .parsed tile and all resident HLOD and LOD representations, bypassing the 3-second grace period and the 2-per-tick unload cap. It runs synchronously on the main thread: for each tile it destroys the mesh-root child entity and all its descendants via unloadTile(), calls finalizePendingDestroys(), and unregisters the GPU allocation from MemoryBudgetManager. When it returns, shouldEvictGeometry() reflects the freed memory.
Full-load tile GPU memory is tracked by MemoryBudgetManager but is not in loadedStreamingEntities — the set that evictLRU targets. If you start a new tile-loading session while old tiles are still resident, this sequence locks the load loop:
shouldEvictGeometry()istrue— old tiles still occupy the budget.evictLRUruns and frees nothing (wrong entity set).guard !shouldEvictGeometry() else { break }fires every tick.- No new tiles load. The scene freezes until the slow distance-based unload completes (3-second grace period × 2-per-tick cap = 10+ seconds for a large scene).
Call forceUnloadAllParsedTiles() whenever you are about to start a new tile-loading session while tiles from a previous one may still be in GPU memory. The two common cases are:
1. Switching between full-scale and calibration/inspection mode
When a user scales the scene down to inspect or reposition it (e.g. a miniature placement workflow), the previous full-scale session's tiles must be freed before they switch back to full scale:
// Entering calibration — free the previous full-scale session's memory.
GeometryStreamingSystem.shared.forceUnloadAllParsedTiles()
scaleSceneTo(calibrationScale)
translateSceneTo(position: placementTarget)Without this call, every calibration → full-scale cycle leaves more tiles in memory. After a few cycles the memory budget is exhausted, shouldEvictGeometry() permanently breaks the load loop, and the scene freezes.
2. Switching directly from one tiled scene to another
When the user cancels a scene mid-load and immediately loads a different one, tiles from the first scene may be partially or fully parsed:
// User tapped "wrong scene" and is loading a different one.
GeometryStreamingSystem.shared.forceUnloadAllParsedTiles()
destroyEntity(entityId: oldSceneRoot)
setEntityStreamScene(entityId: newSceneRoot, manifest: "correct_scene") { ... }This guarantees the memory budget is clean before the new scene's first streaming tick runs. Without it there is a race between finalizePendingDestroys() (which frees GPU memory when the old root entity is torn down) and the streaming tick that checks shouldEvictGeometry() — the tick can fire before the destroy finalizes, blocking early tile loads in the new scene.
- Normal camera movement and room transitions — the distance-based unload pass handles these with appropriate hysteresis. Calling
forceUnloadAllParsedTiles()here would discard tiles the user is about to need. - Returning to a menu with no subsequent tile loading —
destroyEntityon the scene root cascades through all tile stubs; their component cleanup (removeTileComponent) cancels in-flight tasks and removes them from tracking sets. No explicit unload is needed.
The rule of thumb: call it whenever you know a new tile-streaming session is about to start and you cannot guarantee the previous session has already finished unloading.
- Texture streaming:
setEntityStreamScene(...)automatically aligns texture distance bands to the manifest radii. - Batching: full-load tiles, per-tile LODs, and HLODs notify
BatchingSystemautomatically. OCC sub-mesh uploads join batching incrementally through normal residency events. - Memory pressure: texture quality is shed first; geometry eviction follows only when geometry pressure remains high.
- Increase
GeometryStreamingSystem.shared.tileFrustumGatePadding - Keep
enableFrustumGate = true
- Increase the gap between
streaming_radiusandunload_radius - Increase or explicitly author
prefetch_radius
- Lower
maxConcurrentTileLoads - Reduce per-tile file sizes in the exported manifest
- Verify you loaded the scene through
setEntityStreamScene(...) - Verify the manifest radii are reasonable for your scene scale
- Do not expect standalone
StreamingComponententities to stream; tile ownership is enforced