Merged
Conversation
Replace entity-derived batch selection with direct AABB frustum tests on BatchGroups. Each BatchGroup's precomputed world-space AABB is now tested against currentFrameFrustum (stored after the culling compute pass) using a positive-vertex isAABBInFrustum test — one test per batch group instead of one per entity. Add visibleBatchGroupsSnapshot() with per-frame caching so batchedModel and batchedShadow passes share a single cull result without redundant work. Add notifyTileParsedEntities() to BatchingSystem so fullLoad tile completions bypass the quiescence delay. Cells containing tile-parsed entities are promoted to batchPending in the same tick, reducing the latency from tile-parsed to first draw from 3+ frames to ~2 frames. OCC streaming tiles are unchanged and still use the quiescence window.
Extend visibleBatchGroupsSnapshot() with a two-stage cull: frustum test (Phase 1) followed by an HZB occlusion test that reuses the existing entity-level GPU cull result. Batched entities already participate in the GPU frustum+HZB pipeline and appear in visibleEntityIds when not occluded. A batch group whose every member entity was culled by the HZB is itself fully occluded and its draw call is now skipped.
Root cause: on the small-file tile parse path, MDLAsset.loadTextures()
could block indefinitely when encountering an unsupported image format
inside a USDZ archive. Because loadTextures() is a blocking C/ObjC call
that ignores Swift cooperative cancellation, the inner Task created by
setEntityMeshAsync never reached its AssetLoadingState.finishLoading()
call. With AssetLoadingGate.isLoadingAny permanently true, RenderingSystem
skipped all ECS traversal and culling every frame, freezing the rendered
output while the app remained alive.
Fixes:
- Wrap loadTextures() in a DispatchQueue + 15 s deadline using a
ResumeOnce guard, isolating the blocking call from the Swift cooperative
thread pool and ensuring the continuation resumes exactly once
- Add TileComponent.meshEntityId to store the child mesh entity created
at parse-dispatch time, giving the timeout guard a direct handle to the
hung Task's loading-gate entry without reverse-iterating the map
- Extend the tile parse timeout guard to force-release AssetLoadingGate
via AssetLoadingState.shared.finishLoading(entityId: meshEntityId),
unblocking the render loop on the next frame after a timeout fires
Also includes code quality fixes from review:
- HLOD unload race: set hlodState = .unloading before cancel() to close
the window where an in-flight completion callback could proceed on a
destroyed entity
- collectRenderDescendantIds: replace recursive Set return with inout
helper to eliminate intermediate Set allocations for deep hierarchies
- Cache Set<EntityID> from visibleEntityIds once per frame (RuntimeState)
and reuse across RenderPasses and SpatialDebugBoundsCollector
- Move currentFrameFrustum from nonisolated(unsafe) global into
RuntimeGlobalsStore under its existing NSLock
- Add maxTileNodeCount cap to tile bounds debug visualization, consistent
with existing maxLeafNodeCount and maxStaticBatchCellCount limits
- Add tile bounds debug toggle to DemoHUD / DemoState / AppDelegate
- Add parseStartTime = 0 defensive reset at the .unloaded transition in
unloadTile
unloadHLOD and unloadLODLevel destroy the child entity and cancel the in-flight setEntityMeshAsync Task, but Task.cancel() is cooperative — the Task continues running and its completion callback finds the entity already destroyed, returns early, and never calls finishLoading(). This left the AssetLoadingGate counter permanently elevated, causing isLoadingAny to stay true and the render loop to freeze indefinitely (stale frame rendered every frame, culling and ECS traversal skipped). The 1016 "entity missing" errors were a symptom of the same race. Fix: capture the child entity ID before teardown and explicitly call AssetLoadingState.shared.finishLoading(entityId:) afterwards. finishLoading is idempotent — if the Task already called it, the second call is a no-op and the gate counter is not double-decremented.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR implements asset remote streaming support in the engine