StreamingRegionManager
StreamingRegionManager provides a lightweight, region-based geometry streaming API that is an alternative to the tile manifest system (setEntityStreamScene). Rather than consuming a JSON manifest, callers register explicit StreamingRegion values — each describing a world-space AABB and a list of asset references — and the manager loads or unloads them based on camera proximity.
Use this API for handcrafted or procedurally defined streaming zones where a manifest file is not practical. Use setEntityStreamScene for large outdoor/indoor scenes exported by the Blender pipeline.
When to use which API
| Scenario | Preferred API |
|---|---|
| Large scene exported by the Blender pipeline (tiles, HLOD, LOD levels) | setEntityStreamScene(entityId:url:) |
| Handcrafted streaming zones (e.g. dungeon rooms, level sectors) | StreamingRegionManager |
| Always-resident objects (characters, props, HUD elements) | setEntityMeshAsync(entityId:filename:withExtension:) |
Core Types
StreamingRegion
public struct StreamingRegion: Identifiable {
public let id: UUID
public let bounds: AABB // World-space AABB that triggers load/unload
public let priority: Int // Higher = loaded first when slots compete
public let assets: [AssetReference]
public var state: StreamingState
public var loadedEntities: [EntityID]
public var estimatedMemorySize: Int // Bytes; used for memory budget gate
}
AssetReference names a single asset by filename and extension:
public struct AssetReference: Equatable, Hashable {
public let filename: String
public let fileExtension: String
}
StreamingState
| State | Meaning |
|---|---|
.unloaded |
No geometry loaded for this region |
.loading |
Async load task running |
.loaded |
All region assets are GPU-resident |
.unloading |
Teardown in progress |
Configuration
StreamingRegionManager.shared.enabled = true
StreamingRegionManager.shared.streamingRadius = 100.0 // load within this distance
StreamingRegionManager.shared.unloadRadius = 150.0 // unload beyond this distance
StreamingRegionManager.shared.maxConcurrentLoads = 3 // max simultaneous loads
StreamingRegionManager.shared.checkInterval = 0.5 // seconds between evaluations
| Property | Default | Notes |
|---|---|---|
streamingRadius |
100 m | Camera-to-AABB distance inside which the region loads |
unloadRadius |
150 m | Camera-to-AABB distance beyond which the region unloads |
maxConcurrentLoads |
3 | Hard cap on simultaneous region load tasks |
checkInterval |
0.5 s | Tick rate; skips frames between evaluations |
These are shared global defaults. Per-region load distance is not currently configurable; all regions use the same radii. For per-region control, use setEntityStreamScene with a manifest.
Usage
Registering regions
let region = StreamingRegion(
bounds: AABB(min: simd_float3(-50, 0, -50), max: simd_float3(50, 10, 50)),
priority: 1,
assets: [
AssetReference(filename: "dungeon_room_A", withExtension: "usdz"),
AssetReference(filename: "dungeon_room_A_props", withExtension: "usdz"),
],
estimatedMemorySize: 40_000_000 // 40 MB estimate
)
StreamingRegionManager.shared.registerRegion(region)
Updating each frame
// Call from your game loop:
StreamingRegionManager.shared.update(cameraPosition: cameraPos, deltaTime: dt)
update() throttles internally — real work only runs every checkInterval seconds.
Removing regions
Unregistering cancels any in-flight load task immediately.
Force load / unload (testing or cutscenes)
let didLoad = await StreamingRegionManager.shared.forceLoadRegion(id: region.id)
let didUnload = await StreamingRegionManager.shared.forceUnloadRegion(id: region.id)
Per-frame Update Logic
Each tick (every checkInterval seconds):
- Find load candidates —
.unloadedregions whose AABB is withinstreamingRadiusof the camera. Sorted by priority (descending) then distance (ascending). - Find unload candidates —
.loadedregions whose AABB is beyondunloadRadius. - Unload first — frees memory before committing to new loads.
- Load up to
maxConcurrentLoadscandidates — each spawns an asyncTask.
Distance is measured as the closest point on the region AABB to the camera position (AABB distance, not center distance), so an AABB that surrounds the camera has distance 0.
Load Path
loadRegion(id:) (internal):
- Marks region
.loading. - Checks
MemoryBudgetManager.canAccept(sizeBytes:). If the budget is full, attemptsevictLRUbefore proceeding; if still full, marks region.unloadedand returns. - For each
AssetReferenceinregion.assets: - Calls
createEntity()+setEntityMeshAsync(entityId:filename:withExtension:). - Registers memory with
MemoryBudgetManagerfor the root entity and all children. - Marks region
.loaded; recordsloadedEntities. - Emits
AssetResidencyChangedEvent(isResident: true)for each entity (including children) soBatchingSystemandLODSystemsee the new geometry.
Unload Path
unloadRegion(id:) (internal):
- Marks region
.unloading. - Emits
AssetResidencyChangedEvent(isResident: false)for all entities (children first) before destroying them — ensuresBatchingSystemremoves them from pending queues cleanly. - Calls
MemoryBudgetManager.unregisterMesh(entityId:)for all entities. - Calls
destroyEntity(entityId:)for each root (cascades to children). - Marks region
.unloaded; clearsloadedEntities.
Stats
let stats = StreamingRegionManager.shared.getStats()
// stats.totalRegions, loadedRegions, loadingRegions, activeLoads
// stats.totalRootEntities, totalEntitiesWithChildren
// stats.regionMemory (actual GPU bytes from MemoryBudgetManager)
// stats.estimatedMemory (sum of user-supplied estimates)
// stats.totalEngineMemory (entire engine, from MemoryBudgetManager)
Relationship to GeometryStreamingSystem
StreamingRegionManager is independent of GeometryStreamingSystem. They can run simultaneously — for example, a tile-streamed outdoor scene (setEntityStreamScene) with handcrafted interior sectors (StreamingRegionManager). Both systems share MemoryBudgetManager, so memory pressure from one is visible to the other.
StreamingRegionManager does not use the octree, frustum gating, prefetch radius, grace-period teardown, or HLOD/LOD systems. It is intentionally simpler. For any of those features, use setEntityStreamScene with a manifest.
See Also
tilebasedstreaming.md— tile lifecycle, manifest schema, HLOD, LOD bandsgeometryStreamingSystem.md— mesh-level OCC streaming, memory pressure, evictionstreamingCacheLifecycle.md— how GPU and CPU residency are managed across both systems