feat: add streaming primitives (IChunkRenderer, IJobExecutor, NeighborSnapshots, ChunkJob)

This commit is contained in:
2026-06-01 10:48:56 +02:00
parent 39375b83ea
commit dc74fdadcc
5 changed files with 95 additions and 1 deletions

View File

@@ -0,0 +1,21 @@
using OpenTK.Mathematics;
using VoxelEngine.World.Chunk;
namespace VoxelEngine.World.Streaming;
// Job dispatched to an IJobExecutor. For a fresh load, ExistingChunk is null
// and the worker generates + applies diff + meshes. For a remesh-only job,
// ExistingChunk holds the chunk and the worker only meshes (no generation).
internal sealed record ChunkJob(
Vector3i Pos,
Chunk.Chunk? ExistingChunk,
NeighborSnapshots Neighbors,
IReadOnlyDictionary<int, ushort>? Diff);
// Posted by the worker back to the main thread via a ConcurrentQueue.
internal sealed record ChunkJobResult(
Vector3i Pos,
Chunk.Chunk? Chunk,
float[]? Mesh,
Exception? Error,
bool IsRemeshOnly);

View File

@@ -0,0 +1,6 @@
namespace VoxelEngine.World.Streaming;
public interface IChunkRenderer : IDisposable
{
void Draw();
}

View File

@@ -0,0 +1,15 @@
namespace VoxelEngine.World.Streaming;
public interface IJobExecutor
{
void Submit(Action work);
}
public sealed class ThreadPoolJobExecutor : IJobExecutor
{
// Fire-and-forget: the Task is intentionally discarded. The caller's `work`
// delegate MUST catch its own exceptions and report them via a dedicated
// channel (e.g. ChunkJobResult.Error in the streaming pipeline). Unhandled
// exceptions here are silently lost.
public void Submit(Action work) => _ = Task.Run(work);
}

View File

@@ -0,0 +1,52 @@
using VoxelEngine.World.Chunk;
namespace VoxelEngine.World.Streaming;
// Snapshots of the six cardinal neighbor chunks' block arrays, taken on the
// main thread at job-dispatch time. Each slot is null if that neighbor is not
// currently loaded. The ChunkMesher reads only these arrays from a worker
// thread — never the live World.
public sealed class NeighborSnapshots
{
public ushort[]? NegX { get; }
public ushort[]? PosX { get; }
public ushort[]? NegY { get; }
public ushort[]? PosY { get; }
public ushort[]? NegZ { get; }
public ushort[]? PosZ { get; }
// Snapshots take ownership of the arrays passed in. Callers must hand
// in fresh copies (not live references into a chunk's _ids buffer) —
// the snapshot does NOT copy defensively. This is enforced at the
// single call-site in ChunkStreamer.SnapshotNeighbors which allocates
// fresh arrays. The arrays must not be mutated after construction.
public NeighborSnapshots(
ushort[]? negX, ushort[]? posX,
ushort[]? negY, ushort[]? posY,
ushort[]? negZ, ushort[]? posZ)
{
this.NegX = negX; this.PosX = posX;
this.NegY = negY; this.PosY = posY;
this.NegZ = negZ; this.PosZ = posZ;
}
// Returns true if the out-of-chunk position (nx, ny, nz) — exactly one of
// which is out of [0, Chunk.Chunk.Size) — is occupied by an opaque block
// in the snapshot. Returns false if the relevant neighbor is null.
public bool IsOpaqueOutside(int nx, int ny, int nz, Func<ushort, bool> isOpaque)
{
ushort[]? n;
int lx = nx, ly = ny, lz = nz;
if (nx < 0) { n = this.NegX; lx = Chunk.Chunk.Size - 1; }
else if (nx >= Chunk.Chunk.Size) { n = this.PosX; lx = 0; }
else if (ny < 0) { n = this.NegY; ly = Chunk.Chunk.Size - 1; }
else if (ny >= Chunk.Chunk.Size) { n = this.PosY; ly = 0; }
else if (nz < 0) { n = this.NegZ; lz = Chunk.Chunk.Size - 1; }
else if (nz >= Chunk.Chunk.Size) { n = this.PosZ; lz = 0; }
else return false; // not actually outside — shouldn't happen, mesher guards
if (n == null) return false;
int index = (lx * Chunk.Chunk.Size + ly) * Chunk.Chunk.Size + lz;
return isOpaque(n[index]);
}
}