Skip to content
Edit on GitHub

Chunk streaming

verifiedAgainst f1eaefe · verifiedOn 2026-09-10 · the automated staleness banner is planned, not built.

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.

Chunk streaming horizon under HDRP atmospheric scattering 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.

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.
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).

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.

  1. Every async lifecycle result presents its original ChunkVersionToken. Never complete by coordinate alone. ReportGenerationCompleted, ReportMeshingCompleted, and TryActivate intentionally return false for a missing, stale, or wrong-phase token (ChunkScheduler.cs:65).
  2. Only ChunkLifecycle changes lifecycle state. Do not expose a state setter or bypass TryTransition. The valid phase and matching version are one atomic rule, not two optional checks.
  3. 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).
  4. 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.
  5. 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).
  6. A mesh result needs its own freshness check. Edit-driven remeshes have no lifecycle token, so IsCurrentMeshResult checks a per-coordinate mesh version and the ChunkData reference before upload (ChunkStreamingRuntime.cs:577).
  7. Generated terrain is not saved. Apply WorldEditOverlay after generation; save only its sparse deltas. Streaming must remain able to regenerate a chunk from the selected seed and generator version.
  • Upstream: VoxelMath / WorldBlockPosition / ChunkCoordinate define the true-world coordinate conversion. WorldGenerationSettings becomes TerrainGenerationParameters before 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; ChunkMeshingJob produces face data; GreedyMeshMerger and ChunkView turn that data into a Unity mesh and collider. ChunkViewPool owns reusable GameObjects. The scheduler does not reference any of these Unity-facing types.
  • Persistence: WorldEditOverlay is applied immediately after a generated result becomes ChunkData; CreateWorldSaveData() serializes the overlay, never chunks (ChunkStreamingRuntime.cs:223).
  • Presentation and gameplay: consumers use IsChunkColliderActive, TryGetActiveChunkView, or ChunkActivated rather 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.
  • 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, and ChunkStreamingPlannerTests.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.

  1. Loading notes…