feat: add streaming primitives (IChunkRenderer, IJobExecutor, NeighborSnapshots, ChunkJob)
This commit is contained in:
@@ -2,7 +2,7 @@ using OpenTK.Graphics.OpenGL4;
|
|||||||
|
|
||||||
namespace VoxelEngine.Rendering;
|
namespace VoxelEngine.Rendering;
|
||||||
|
|
||||||
public sealed class ChunkRenderer : IDisposable
|
public sealed class ChunkRenderer : VoxelEngine.World.Streaming.IChunkRenderer
|
||||||
{
|
{
|
||||||
private const int FloatsPerVertex = 6; // pos.xyz, uv, shade
|
private const int FloatsPerVertex = 6; // pos.xyz, uv, shade
|
||||||
|
|
||||||
|
|||||||
21
World/Streaming/ChunkJob.cs
Normal file
21
World/Streaming/ChunkJob.cs
Normal 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);
|
||||||
6
World/Streaming/IChunkRenderer.cs
Normal file
6
World/Streaming/IChunkRenderer.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace VoxelEngine.World.Streaming;
|
||||||
|
|
||||||
|
public interface IChunkRenderer : IDisposable
|
||||||
|
{
|
||||||
|
void Draw();
|
||||||
|
}
|
||||||
15
World/Streaming/IJobExecutor.cs
Normal file
15
World/Streaming/IJobExecutor.cs
Normal 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);
|
||||||
|
}
|
||||||
52
World/Streaming/NeighborSnapshots.cs
Normal file
52
World/Streaming/NeighborSnapshots.cs
Normal 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user