Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e3436fd
[Patch] Stabilize tiled streaming world mutations
untoldengine May 20, 2026
ec4a36c
[Patch] Guard floor-proximity gate on interior tiles only
untoldengine May 21, 2026
224d528
[Patch] Enforce minimum residency for parsed tiles before unload or e…
untoldengine May 21, 2026
291a626
[Patch] [Feature] Replace radius/distance importance with tile view i…
untoldengine May 21, 2026
96f1b39
[Patch] Add screen-space AABB occlusion factor to tile importance sort
untoldengine May 21, 2026
c5f3c50
[Patch] Spread tile parse-completion batching work across frames
untoldengine May 21, 2026
751398b
[Patch] Skip near-plane-clipping tiles as occluders in screen-space o…
untoldengine May 21, 2026
c378cb1
[Patch] Disable occlusion sort when no active camera is available
untoldengine May 21, 2026
9d3bfac
[Patch] Replace additive occlusion coverage with 8×8 grid union bitmask
untoldengine May 21, 2026
d72106a
[Patch] Use closest AABB point for view alignment instead of tile center
untoldengine May 21, 2026
abd3dbd
[Patch] Floor occlusionScore at occlusionMinWeight to prevent hard lo…
untoldengine May 21, 2026
2b5713c
[Patch] Harden tile-resident drain queue in BatchingSystem
untoldengine May 21, 2026
ecc8d46
[Patch] Guard rectToScreenMask against zero-area rects
untoldengine May 21, 2026
aec4112
[Patch] Route tile-resident drain entities through quiescence to prev…
untoldengine May 21, 2026
aab3697
[Chores] Removed stat logging from demo
untoldengine May 21, 2026
9ec7e56
[Chores] formatted files
untoldengine May 21, 2026
4d41033
[Release] Preparing release 0.12.13
untoldengine May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
# Changelog
## v0.12.13 - 2026-05-21
### 🐞 Fixes
- [Patch] Added AA parameters to serializer (3d81b55…)
- [Patch] Set ambient default value to 0.4 (c5a72a2…)
- [Patch] Use recursive traversal for derived asset node ids in serialization (41acc07…)
- [Patch] Route native OCC stubs through child entities (e1b2935…)
- [Patch] Reimplented setEntityMesh (6250f0a…)
- [Patch] Added radii to structured content to stream system (5392935…)
- [Patch] Stabilize tiled streaming world mutations (e3436fd…)
- [Patch] Guard floor-proximity gate on interior tiles only (ec4a36c…)
- [Patch] Enforce minimum residency for parsed tiles before unload or eviction (224d528…)
- [Patch] [Feature] Replace radius/distance importance with tile view importance (291a626…)
- [Patch] Add screen-space AABB occlusion factor to tile importance sort (96f1b39…)
- [Patch] Spread tile parse-completion batching work across frames (c5f3c50…)
- [Patch] Skip near-plane-clipping tiles as occluders in screen-space occlusion sort (751398b…)
- [Patch] Disable occlusion sort when no active camera is available (c378cb1…)
- [Patch] Replace additive occlusion coverage with 8×8 grid union bitmask (9d3bfac…)
- [Patch] Use closest AABB point for view alignment instead of tile center (d72106a…)
- [Patch] Floor occlusionScore at occlusionMinWeight to prevent hard load suppression (abd3dbd…)
- [Patch] Harden tile-resident drain queue in BatchingSystem (2b5713c…)
- [Patch] Guard rectToScreenMask against zero-area rects (ecc8d46…)
- [Patch] Route tile-resident drain entities through quiescence to prevent repeated cell rebuilds (aec4112…)
### 📚 Docs
- [Docs] Updated streaming docs (ed14520…)
## v0.12.12 - 2026-05-18
### 🐞 Fixes
- [Patch] Replace FXAA toggle with anti-aliasing mode selector (0111454…)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Clone the repository and launch the demo:
```bash
git clone https://github.com/untoldengine/UntoldEngine.git
cd UntoldEngine
git checkout v0.12.12
git checkout v0.12.13
swift run untolddemo
```

Expand Down
2 changes: 1 addition & 1 deletion Sources/DemoGame/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
private enum Constants {
static let appVersion = "0.12.12"
static let appVersion = "0.12.13"
static let defaultWindowSize = NSSize(width: 1920, height: 1080)
static let minimumWindowSize = NSSize(width: 640, height: 480)
}
Expand Down
4 changes: 3 additions & 1 deletion Sources/DemoGame/GameScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

private enum Constants {
static let orbitTargetOffset: Float = 25.0
static let cameraMoveSpeed: Float = 1.0
static let cameraMoveSpeed: Float = 0.5
static let cameraInputDeltaTime: Float = 0.1
static let streamingPriority: Int = 10
static let citySceneID = "city"
Expand Down Expand Up @@ -76,6 +76,8 @@

applyIBL = true
renderEnvironment = false

// setEngineStatsLogging(enabled: true, profile: .verbose, intervalSeconds: 1.0)
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Sandbox/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
private enum Constants {
static let appVersion = "0.12.12"
static let appVersion = "0.12.13"
static let windowSize = NSSize(width: 1600, height: 900)
}

Expand Down
22 changes: 22 additions & 0 deletions Sources/UntoldEngine/ECS/Components.swift
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,22 @@ public class TileComponent: Component {
/// tiles on the camera being inside the scene's interior_zone.
public var isInterior: Bool = false

/// True when this tile came from a floor-aware manifest entry.
/// The floor-proximity gate must key off this explicit metadata, not a nonzero
/// Y center, because older uniform-grid manifests can have large padded Y bounds.
public var hasFloorMetadata: Bool = false

/// Floor index from the manifest. 0 is a valid ground-floor value when
/// `hasFloorMetadata` is true.
/// Used by the floor-proximity gate in GeometryStreamingSystem to skip tiles
/// whose floor band is vertically distant from the camera.
public var floorId: Int = 0

/// Y-axis centre of the tile's world-space AABB.
/// Stored at registration time so the floor-proximity gate can skip vertically
/// distant tiles without a repeated LocalTransformComponent lookup per tick.
public var worldYCenter: Float = 0

/// Asset-level lifecycle state driven by the streaming bootstrap.
public var state: TileAssetState = .unloaded

Expand Down Expand Up @@ -526,6 +542,12 @@ public class TileComponent: Component {
/// re-dispatched, preventing tight load-cancel cycles at the boundary.
public var pendingUnloadSince: CFAbsoluteTime = 0

/// CFAbsoluteTime at which this tile most recently became `.parsed`.
/// Used by the streaming system to enforce a minimum resident dwell so a
/// newly visible full tile cannot be immediately torn down when the camera
/// hovers near an unload boundary.
public var parsedResidentSince: CFAbsoluteTime = 0

// MARK: - Visual Readiness (OCC sub-mesh tracking)

/// Total number of OCC (out-of-core) streaming stubs created during tile parse.
Expand Down
15 changes: 14 additions & 1 deletion Sources/UntoldEngine/Renderer/RenderPasses.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import Metal
import MetalKit

public enum RenderPasses {
/// Count of unbatched shadow-caster entities from the most recent frame.
/// Updated by shadowCasterEntityIds (main thread); read by the streaming heartbeat (main thread).
public nonisolated(unsafe) static var lastShadowCasterCount: Int = 0
public typealias RenderPassExecution = @Sendable (MTLCommandBuffer) -> Void

private static let runtimeState = RuntimeState()
Expand Down Expand Up @@ -350,7 +353,16 @@ public enum RenderPasses {

for entityId in entities {
if shouldSkipShadowEntity(entityId) { continue }
if BatchingSystem.shared.isEnabled(), BatchingSystem.shared.isBatched(entityId: entityId) { continue }
if BatchingSystem.shared.isEnabled() {
// Batch-eligible entities (StaticBatchComponent present) are always drawn
// via shadowCasterBatchGroups — whether or not the batch rebuild has landed
// yet. Skipping them here prevents O(n_loaded_tiles) shadow draw calls that
// scale with the scene and eventually overflow the GPU command buffer budget.
// During the brief batch-rebuild window their shadow is absent; this is
// preferable to the alternative of the app freezing at ~300+ loaded tiles.
if scene.get(component: StaticBatchComponent.self, for: entityId) != nil { continue }
if BatchingSystem.shared.isBatched(entityId: entityId) { continue }
}

guard let renderComponent = scene.get(component: RenderComponent.self, for: entityId),
renderComponent.isVisible,
Expand All @@ -368,6 +380,7 @@ public enum RenderPasses {
}
}

lastShadowCasterCount = result.count
return result
}

Expand Down
4 changes: 3 additions & 1 deletion Sources/UntoldEngine/Renderer/UntoldEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ public class UntoldRenderer: NSObject, MTKViewDelegate {

CameraSystem.shared.activeCamera = gameCamera

Logger.log(message: "Untold Engine Starting. Version 0.12.12")
Logger.log(message: "Untold Engine Starting. Version 0.12.13")
}

public func initSizeableResources() {
Expand Down Expand Up @@ -326,6 +326,8 @@ public class UntoldRenderer: NSObject, MTKViewDelegate {
}
#endif
EngineProfiler.shared.beginFrame()
lockWorldAccessGate()
defer { unlockWorldAccessGate() }

// finalize destroys once per frame
if needsFinalizeDestroys {
Expand Down
46 changes: 38 additions & 8 deletions Sources/UntoldEngine/Systems/AssetLoadingState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,50 @@ public final class AssetLoadingGate: @unchecked Sendable {
}
}

/// Execute a world-mutation critical section while pausing XR scene traversal.
private final class WorldAccessGate: @unchecked Sendable {
static let shared = WorldAccessGate()

private let lock = NSRecursiveLock()

private init() {}

func sync<T>(_ body: () throws -> T) rethrows -> T {
lock.lock()
defer { lock.unlock() }
return try body()
}

func lockAccess() {
lock.lock()
}

func unlockAccess() {
lock.unlock()
}
}

/// Execute a world/ECS access critical section without changing render loading state.
@inline(__always)
public func withWorldMutationGate<T>(_ body: () throws -> T) rethrows -> T {
AssetLoadingGate.shared.beginLoading()
defer { AssetLoadingGate.shared.finishLoading() }
return try body()
public func withWorldAccessGate<T>(_ body: () throws -> T) rethrows -> T {
try WorldAccessGate.shared.sync(body)
}

/// Async variant for world-mutation critical sections.
@inline(__always)
public func withWorldMutationGate<T>(_ body: () async throws -> T) async rethrows -> T {
public func lockWorldAccessGate() {
WorldAccessGate.shared.lockAccess()
}

@inline(__always)
public func unlockWorldAccessGate() {
WorldAccessGate.shared.unlockAccess()
}

/// Execute a world-mutation critical section while pausing XR scene traversal.
@inline(__always)
public func withWorldMutationGate<T>(_ body: () throws -> T) rethrows -> T {
AssetLoadingGate.shared.beginLoading()
defer { AssetLoadingGate.shared.finishLoading() }
return try await body()
return try WorldAccessGate.shared.sync(body)
}

/// Loading phase enum
Expand Down
Loading
Loading