Chunk streaming
verifiedAgainst f1eaefe · verifiedOn 2026-09-10 · the automated staleness banner is planned, not built.
In one paragraph
Section titled “In one paragraph”Chunk streaming has three deliberately separate responsibilities. ChunkStreamingPlanner is a pure function: given the focus chunk and horizontal/vertical radii, it returns an immutable desired set in deterministic near-first order. ChunkScheduler is another Unity-free core: it turns that plan into per-coordinate ChunkLifecycle state machines, starts only the work allowed by each frame budget, and accepts a completion only when it carries the current ChunkVersionToken. ChunkStreamingRuntime is the Unity boundary. On the main thread it rebuilds plans when the player crosses a chunk boundary, schedules and completes Burst generation/meshing jobs, applies sparse world edits, uploads meshes, pools views, and disposes every native allocation. Do not collapse those layers: the planner and scheduler are testable policy; the runtime is the engine-owned effectful adapter.
Figure 1: The streaming chunk mesh horizon during golden hour. ChunkStreamingPlanner computes desired chunk windows in deterministic near-first order, streaming meshes seamlessly into the camera frustum.
The pieces
Section titled “The pieces”| Symbol | File | Responsibility |
|---|---|---|
ChunkStreamingPlanner.Build |
World/Streaming/ChunkStreamingPlanner.cs:14 |
Builds the cuboid desired set and its stable, near-first request order. |
ChunkStreamingPlan |
World/Streaming/ChunkStreamingPlan.cs:10 |
Immutable centre, ordered requests, and membership lookup; owns no jobs or Unity objects. |
ChunkStreamingRequest |
World/Streaming/ChunkStreamingRequest.cs:10 |
A coordinate plus a lower-is-sooner priority score. |
ChunkScheduler |
World/Streaming/ChunkScheduler.cs:12 |
Main-thread lifecycle policy: apply plans, queue bounded generation/meshing starts, and report valid completions. |
ChunkLifecycle / ChunkLifecycleState |
World/Streaming/ChunkLifecycle.cs:10 · ChunkLifecycleState.cs:7 |
The guarded state machine for one coordinate. Only its transition methods advance it. |
ChunkVersionToken |
World/Streaming/ChunkVersionToken.cs:10 |
Coordinate plus monotonically increasing request version; rejects work made stale by unload or cancellation. |
ChunkStreamingRuntime.Update |
World/Presentation/ChunkStreamingRuntime.cs:285 |
The Unity owner: plan change detection, unloads, generation, meshing, activation, and shutdown cleanup. |
GenerationJobRequest / MeshJobRequest |
World/Presentation/ChunkStreamingRuntime.cs:802 · :833 |
Runtime-only handles to scheduled work and the native buffers that must be completed and disposed. |
The flow
Section titled “The flow”flowchart LR F["Focus transform\nworld position"] --> R["ChunkStreamingRuntime.Update\nreplan when centre/radius changes"] R --> P["ChunkStreamingPlanner.Build\ndeterministic desired set"] P --> S["ChunkScheduler.ApplyPlan\nlifecycle + bounded queues"] S --> G["Terrain generation jobs\nBurst data only"] G --> C["ChunkData + world edits\nmain-thread apply"] C --> M["Chunk meshing jobs\nBurst face data"] M --> A["Upload mesh + collider\nacquire pooled ChunkView"] A --> V["Active chunk view\nChunkActivated event"] S --> U["Unloading\nrelease view + remove data"]
Update() first converts render-space focus.position back to a true-world WorldBlockPosition by adding renderOriginOffset, then maps it to a chunk coordinate. It rebuilds only when that centre or either radius has changed (ChunkStreamingRuntime.cs:292). The planner scores horizontal distance squared plus four times vertical distance, then breaks equal scores by Y, Z, and X, so the same input always produces the same first request (ChunkStreamingPlanner.cs:41).
Applying the plan sends no-longer-desired lifecycles to unload and requests only coordinates that are newly pooled or cancelled (ChunkScheduler.cs:23). The runtime then performs its three passes in a fixed order: unload, generation, meshing (ChunkStreamingRuntime.cs:310). Frame budgets cap the starts; they do not cancel work already owned by the runtime.
Generation completion copies the worker output into ChunkData, applies the sparse WorldEditOverlay, and queues the new chunk plus its six neighbours for a mesh refresh (ChunkStreamingRuntime.cs:458). On a valid initial mesh completion, the runtime acquires or reuses a ChunkView, uploads its mesh/collider, then asks the scheduler to move Ready → Active; only then does it raise ChunkActivated (ChunkStreamingRuntime.cs:585).
One coordinate’s lifecycle
Section titled “One coordinate’s lifecycle”stateDiagram-v2 [*] --> Pooled Pooled --> Requested: Request() / new token Cancelled --> Requested: Request() / newer token Requested --> Generating: TryBeginGeneration(token) Generating --> Generated: TryCompleteGeneration(token) Generated --> Meshing: TryBeginMeshing(token) Meshing --> Ready: TryCompleteMeshing(token) Ready --> Active: TryActivate(token) Requested --> Cancelled: TryCancel() Generating --> Cancelled: TryCancel() Generated --> Cancelled: TryCancel() Meshing --> Cancelled: TryCancel() Ready --> Cancelled: TryCancel() Active --> Cancelled: TryCancel() Requested --> Unloading: TryBeginUnload() Generating --> Unloading: TryBeginUnload() Generated --> Unloading: TryBeginUnload() Meshing --> Unloading: TryBeginUnload() Ready --> Unloading: TryBeginUnload() Active --> Unloading: TryBeginUnload() Cancelled --> Unloading: TryBeginUnload() Unloading --> Pooled: TryCompleteUnload()
The state machine does not trust a coordinate alone. Request(), TryBeginUnload(), and TryCancel() increment its private version; every ordinary transition compares both that version and the expected current state (ChunkLifecycle.cs:24, :98). A generation result that finishes after its chunk has begun unloading therefore cannot put old data into a new request.
Invariants you must not break
Section titled “Invariants you must not break”- Every async lifecycle result presents its original
ChunkVersionToken. Never complete by coordinate alone.ReportGenerationCompleted,ReportMeshingCompleted, andTryActivateintentionally returnfalsefor a missing, stale, or wrong-phase token (ChunkScheduler.cs:65). - Only
ChunkLifecyclechanges lifecycle state. Do not expose a state setter or bypassTryTransition. The valid phase and matching version are one atomic rule, not two optional checks. - The planner’s order is deterministic. Preserve its distance calculation and Y/Z/X tie-breakers. Queue order drives what a player sees first and is asserted by
ChunkStreamingPlannerTests(Tests/EditMode/World/ChunkStreamingPlannerTests.cs:36). - Budgets limit starts, not correctness. Use the scheduler’s allocation-free overloads with the runtime’s retained scratch lists. A new priority or work type must obey the corresponding per-frame start budget rather than scheduling an unbounded batch.
- The runtime owns every job and native allocation it starts. Complete and dispose generation buffers on every completion path, dispose mesh buffers even when a result is stale, and complete/dispose all outstanding work before
scheduler.Reset()on disable (ChunkStreamingRuntime.cs:315). - A mesh result needs its own freshness check. Edit-driven remeshes have no lifecycle token, so
IsCurrentMeshResultchecks a per-coordinate mesh version and theChunkDatareference before upload (ChunkStreamingRuntime.cs:577). - Generated terrain is not saved. Apply
WorldEditOverlayafter generation; save only its sparse deltas. Streaming must remain able to regenerate a chunk from the selected seed and generator version.
Where it connects
Section titled “Where it connects”- Upstream:
VoxelMath/WorldBlockPosition/ChunkCoordinatedefine the true-world coordinate conversion.WorldGenerationSettingsbecomesTerrainGenerationParametersbefore a worker is scheduled. The runtime must account for floating-origin offset before it asks the planner for a centre. - Downstream: generation jobs produce
ChunkData;ChunkMeshingJobproduces face data;GreedyMeshMergerandChunkViewturn that data into a Unity mesh and collider.ChunkViewPoolowns reusable GameObjects. The scheduler does not reference any of these Unity-facing types. - Persistence:
WorldEditOverlayis applied immediately after a generated result becomesChunkData;CreateWorldSaveData()serializes the overlay, neverchunks(ChunkStreamingRuntime.cs:223). - Presentation and gameplay: consumers use
IsChunkColliderActive,TryGetActiveChunkView, orChunkActivatedrather than reaching into the chunk/view dictionaries. That boundary is what keeps spawn guards and visual companions from observing a half-active chunk. - See Assemblies & boundaries for why all of this remains inside
VoxelSandbox.World; see the synced System maps and Build and test pages for the wider runtime and test entry points.
See also
Section titled “See also”- Add a voxel block type — changes generation palette/data and the managed/Burst terrain resolvers that this runtime schedules.
Assets/_Game/Tests/EditMode/World/ChunkLifecycleTests.cs,ChunkSchedulerTests.cs, andChunkStreamingPlannerTests.cs— the fast contract tests for the three pure layers.Docs/MASTERPLAN.md§30 (streaming and presentation direction) and §39 (world generation contracts).
User-contributed notes
Corrections, clarifications, and practical tips for this page. Anonymous is fine — a name is optional. Basic Markdown works:
**bold**,*italic*,`code`, and links.Notes policy
Notes are lightly filtered for spam and may be edited or removed. Keep them about this page — no support requests, no personal data, nothing you would not publish. Links are limited and marked
nofollow.