diff --git a/Numos.slnx b/Numos.slnx index 1d731ad..e25baa0 100644 --- a/Numos.slnx +++ b/Numos.slnx @@ -23,6 +23,7 @@ + diff --git a/benchmarks/Numos.CoreSim.Benchmarks/Infrastructure/SimulationWorkload.cs b/benchmarks/Numos.CoreSim.Benchmarks/Infrastructure/SimulationWorkload.cs index c0dccb9..779db3f 100644 --- a/benchmarks/Numos.CoreSim.Benchmarks/Infrastructure/SimulationWorkload.cs +++ b/benchmarks/Numos.CoreSim.Benchmarks/Infrastructure/SimulationWorkload.cs @@ -1,3 +1,4 @@ +using Numos.Chunks; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.GasReactions; using Numos.CoreSim.Replay; @@ -34,7 +35,7 @@ internal SimulationWorkload(ScalingWorkloadOptions options) if (options.GasCount <= 0 || options.CondensingGasCount < 0 || options.CondensingGasCount > options.GasCount) throw new ArgumentException("Gas dimensions are invalid.", nameof(options)); - if (options.ActiveVoxelCount is < 0 or > AtmosChunkConstants.MaximumVoxelCount) + if (options.ActiveVoxelCount is < 0 or > ChunkConstants.MaximumVoxelCount) throw new ArgumentException("Active voxel count is invalid.", nameof(options)); Options = options; @@ -159,7 +160,7 @@ private static IEnumerable CreateReactions(AtmosConfig config private void CreateChunk(ScalingWorkloadOptions options, Int3 position, int chunkIndex, bool awake) { - Kernel.CreateAndRegisterChunk(position, options.ChunkWidth, options.ChunkHeight, options.ChunkDepth); + Kernel.CreateAndRegisterChunk(position); var chunk = Kernel.GetChunkForDangerousAccess(position); if (!awake) { diff --git a/benchmarks/Numos.CoreSim.Benchmarks/Scaling/ExplicitTopologyBenchmarks.cs b/benchmarks/Numos.CoreSim.Benchmarks/Scaling/ExplicitTopologyBenchmarks.cs index 9b7b18a..462e25a 100644 --- a/benchmarks/Numos.CoreSim.Benchmarks/Scaling/ExplicitTopologyBenchmarks.cs +++ b/benchmarks/Numos.CoreSim.Benchmarks/Scaling/ExplicitTopologyBenchmarks.cs @@ -1,5 +1,7 @@ using BenchmarkDotNet.Attributes; using Numos.API; +using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim.Datatypes.Primitives; namespace Numos.CoreSim.Benchmarks.Scaling; @@ -15,8 +17,8 @@ namespace Numos.CoreSim.Benchmarks.Scaling; [BenchmarkCategory("Scaling", "ExplicitTopology")] public class ExplicitTopologyBenchmarks { - private AtmosChunkHandle _chunk; - private ExplicitLinkDefinition[] _definitions = []; + private ChunkHandle _chunk; + private ExplicitLinkDefinition[] _definitions = []; private ExplicitLinkSetHandle _links; private AtmosSimulation _simulation = null!; private AtmosWorld _world = null!; @@ -56,13 +58,13 @@ public void Setup() _simulation.AddGasToVoxel(_chunk, 0, "BenchmarkGas", EdgeCount, 300f); var source = _simulation.GetCellRef(_chunk, 0); - _definitions = new ExplicitLinkDefinition[EdgeCount]; + _definitions = new ExplicitLinkDefinition[EdgeCount]; for (int index = 0; index < EdgeCount; index++) { - _definitions[index] = new ExplicitLinkDefinition( + _definitions[index] = new ExplicitLinkDefinition( source, _simulation.GetCellRef(_chunk, checked((ushort)(index + 1))), - ExplicitLinkFlags.GasTransport); + AtmosLinkFlags.GasTransport); } _links = _world.CreateLinks(_definitions); diff --git a/benchmarks/Numos.Replay.Benchmarks/Program.cs b/benchmarks/Numos.Replay.Benchmarks/Program.cs index 4c5309e..227cffa 100644 --- a/benchmarks/Numos.Replay.Benchmarks/Program.cs +++ b/benchmarks/Numos.Replay.Benchmarks/Program.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Runtime.InteropServices; using Numos.API; +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Replay; using Numos.Maths; @@ -65,7 +66,7 @@ var timeline = new AtmosReplayTimeline(simulation); for (int tick = 0; tick < 200; tick++) { - if (tick % 17 == 0) simulation.AddGasToVoxel(new AtmosChunkHandle(default), 0, 0, 0.25f, 300f); + if (tick % 17 == 0) simulation.AddGasToVoxel(new ChunkHandle(default), 0, 0, 0.25f, 300f); simulation.Tick(); timeline.ObserveLiveState(); } diff --git a/docs/deterministic_replay.md b/docs/deterministic_replay.md index 7f099c1..57d3631 100644 --- a/docs/deterministic_replay.md +++ b/docs/deterministic_replay.md @@ -380,7 +380,7 @@ all of them, so don't skip steps on the assumption that a missing one will alway pointing at the registration host class. 3. Most operations need nothing else here -- the field order in the record's primary constructor becomes the wire layout, mapped through `Numos.Replay.SourceGen`'s fixed set of recognized field types (primitives, `Int3`, - `VoxelClassification`, `AtmosSimulationId`, `ExplicitLinkSetHandle`, and a nested-operation kind for a world + `VoxelClassification`, `SimulationId`, `ExplicitLinkSetHandle`, and a nested-operation kind for a world operation embedding a whole `AtmosOperation`). An unrecognized field type fails the build with `NUMOSREPLAYGEN001` rather than silently miscoding. diff --git a/eng/package_manifest.py b/eng/package_manifest.py index 1d87985..dc778e8 100644 --- a/eng/package_manifest.py +++ b/eng/package_manifest.py @@ -11,7 +11,8 @@ "Numos.Maths": (), "Numos.Units": (), "Numos.Collections": ("Numos.Maths",), - "Numos.CoreSim": ("Numos.Maths", "Numos.Collections", "Numos.Units",), + "Numos.Chunks": ("Numos.Maths", "Numos.Collections",), + "Numos.CoreSim": ("Numos.Maths", "Numos.Collections", "Numos.Chunks", "Numos.Units",), "Numos.API": ("Numos.CoreSim",), "Numos.API.Dangerous": ("Numos.API",), "Numos.Serialization": ("Numos.API",), @@ -22,6 +23,7 @@ PACKAGE_VERSION_FILES = { "Numos.Maths": Path("src/Numos.CoreSim/Version.props"), "Numos.Collections": Path("src/Numos.CoreSim/Version.props"), + "Numos.Chunks": Path("src/Numos.CoreSim/Version.props"), "Numos.Units": Path("src/Numos.CoreSim/Version.props"), "Numos.CoreSim": Path("src/Numos.CoreSim/Version.props"), "Numos.API": Path("src/Numos.CoreSim/Version.props"), @@ -35,6 +37,7 @@ "coresim": ( "Numos.Maths", "Numos.Collections", + "Numos.Chunks", "Numos.Units", "Numos.CoreSim", "Numos.API", diff --git a/src/Numos.API.Dangerous/AtmosDangerousApi.cs b/src/Numos.API.Dangerous/AtmosDangerousApi.cs index 6eb5769..33fbebb 100644 --- a/src/Numos.API.Dangerous/AtmosDangerousApi.cs +++ b/src/Numos.API.Dangerous/AtmosDangerousApi.cs @@ -1,3 +1,5 @@ +using Numos.Chunks; + namespace Numos.API.Dangerous; /// @@ -34,7 +36,7 @@ internal AtmosDangerousApi(AtmosSimulation simulation) /// /// No chunk is registered at the handle's position. /// The simulation has been disposed. - public AtmosDangerousChunk GetChunk(AtmosChunkHandle chunk) + public AtmosDangerousChunk GetChunk(ChunkHandle chunk) { return new AtmosDangerousChunk(_simulation.Kernel.GetChunkForDangerousAccess(chunk.Position)); } diff --git a/src/Numos.API.Dangerous/AtmosWorldSolverDangerousExtensions.cs b/src/Numos.API.Dangerous/AtmosWorldSolverDangerousExtensions.cs index 4063ed0..9b0a99d 100644 --- a/src/Numos.API.Dangerous/AtmosWorldSolverDangerousExtensions.cs +++ b/src/Numos.API.Dangerous/AtmosWorldSolverDangerousExtensions.cs @@ -1,3 +1,5 @@ +using Numos.Chunks.Topology; + namespace Numos.API.Dangerous; /// @@ -35,7 +37,7 @@ internal AtmosDangerousWorldSolverApi(AtmosWorldSolverContext context) /// A current cell reference from the callback's world. /// The unchecked live chunk view. /// The cell does not identify live storage in this world. - public AtmosDangerousChunk GetChunk(AtmosCellRef cell) + public AtmosDangerousChunk GetChunk(VoxelRef cell) { if (!_context.World.TryGetSimulation(cell.Simulation, out var simulation) || simulation == null) throw new ArgumentException("The cell does not identify a live simulation in this world.", nameof(cell)); diff --git a/src/Numos.API/AtmosChunkHandle.cs b/src/Numos.API/AtmosChunkHandle.cs deleted file mode 100644 index 416ae4f..0000000 --- a/src/Numos.API/AtmosChunkHandle.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Numos.Maths; - -namespace Numos.API; - -/// -/// Identifies a chunk owned by an . -/// -public readonly record struct AtmosChunkHandle(Int3 Position); \ No newline at end of file diff --git a/src/Numos.API/AtmosSimulation.GasMixtures.cs b/src/Numos.API/AtmosSimulation.GasMixtures.cs index 42ee639..93d7270 100644 --- a/src/Numos.API/AtmosSimulation.GasMixtures.cs +++ b/src/Numos.API/AtmosSimulation.GasMixtures.cs @@ -1,4 +1,5 @@ using JetBrains.Annotations; +using Numos.Chunks; using Numos.CoreSim; namespace Numos.API; @@ -34,7 +35,7 @@ public GasMixture CreateGasMixture( /// becomes stale if that chunk is removed or replaced at the same grid position. /// [PublicAPI] - public IGasMixture GetVoxelGasMixture(AtmosChunkHandle chunk, ushort localVoxelIndex) + public IGasMixture GetVoxelGasMixture(ChunkHandle chunk, ushort localVoxelIndex) { lock (_mixtureGate) { @@ -46,7 +47,7 @@ public IGasMixture GetVoxelGasMixture(AtmosChunkHandle chunk, ushort localVoxelI /// Creates sandboxed live access to one voxel addressed by local coordinates. [PublicAPI] - public IGasMixture GetVoxelGasMixture(AtmosChunkHandle chunk, int x, int y, int z) + public IGasMixture GetVoxelGasMixture(ChunkHandle chunk, int x, int y, int z) { lock (_mixtureGate) { diff --git a/src/Numos.API/AtmosSimulation.SolverStorage.cs b/src/Numos.API/AtmosSimulation.SolverStorage.cs index 08aeabd..bfaa8ad 100644 --- a/src/Numos.API/AtmosSimulation.SolverStorage.cs +++ b/src/Numos.API/AtmosSimulation.SolverStorage.cs @@ -1,4 +1,5 @@ using JetBrains.Annotations; +using Numos.Chunks; using Numos.Collections; using Numos.CoreSim; @@ -183,7 +184,7 @@ public T GetOrCreateGasSolverData(int gasId, object key, Func [PublicAPI] public T[] GetOrCreateChunkSolverArray( - AtmosChunkHandle chunk, object key, bool captureForRollback, int? length = null) + ChunkHandle chunk, object key, bool captureForRollback, int? length = null) { ThrowIfDisposed(); return _kernel.GetOrCreateChunkSolverArray(chunk.Position, key, captureForRollback, length); @@ -224,7 +225,7 @@ public T[] GetOrCreateChunkSolverArray( /// /// [PublicAPI] - public FlatArray GetOrCreateChunkSolverFlatArray(AtmosChunkHandle chunk, object key, bool captureForRollback) + public FlatArray GetOrCreateChunkSolverFlatArray(ChunkHandle chunk, object key, bool captureForRollback) { ThrowIfDisposed(); return _kernel.GetOrCreateChunkSolverFlatArray(chunk.Position, key, captureForRollback); diff --git a/src/Numos.API/AtmosSimulation.cs b/src/Numos.API/AtmosSimulation.cs index 066bf30..4fc9162 100644 --- a/src/Numos.API/AtmosSimulation.cs +++ b/src/Numos.API/AtmosSimulation.cs @@ -1,4 +1,6 @@ using JetBrains.Annotations; +using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.Datatypes.Snapshots; @@ -20,7 +22,7 @@ namespace Numos.API; /// the solver pipeline, but it must not recursively execute or dispose the simulation or change chunk ownership /// during the current tick. /// -public sealed partial class AtmosSimulation : IDisposable +public sealed partial class AtmosSimulation : IChunkSimulation, IDisposable { /// /// The fixed simulation rate, in ticks per second. @@ -47,9 +49,9 @@ public sealed partial class AtmosSimulation : IDisposable /// . /// public AtmosSimulation( - int chunkWidth = AtmosChunkConstants.DefaultWidth, - int chunkHeight = AtmosChunkConstants.DefaultHeight, - int chunkDepth = AtmosChunkConstants.DefaultDepth) + int chunkWidth = ChunkConstants.DefaultWidth, + int chunkHeight = ChunkConstants.DefaultHeight, + int chunkDepth = ChunkConstants.DefaultDepth) : this(new AtmosConfig(), chunkWidth, chunkHeight, chunkDepth) { } @@ -71,9 +73,9 @@ public AtmosSimulation( /// public AtmosSimulation( AtmosConfig config, - int chunkWidth = AtmosChunkConstants.DefaultWidth, - int chunkHeight = AtmosChunkConstants.DefaultHeight, - int chunkDepth = AtmosChunkConstants.DefaultDepth) + int chunkWidth = ChunkConstants.DefaultWidth, + int chunkHeight = ChunkConstants.DefaultHeight, + int chunkDepth = ChunkConstants.DefaultDepth) : this(new AtmosWorld(config), true, chunkWidth, chunkHeight, chunkDepth) { } @@ -87,30 +89,30 @@ internal AtmosSimulation( int chunkWidth, int chunkHeight, int chunkDepth, - AtmosSimulationId? requestedRegistration = null) + SimulationId? requestedRegistration = null) { ArgumentNullException.ThrowIfNull(world); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(chunkWidth); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(chunkHeight); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(chunkDepth); - if (chunkWidth > AtmosChunkConstants.MaximumVoxelCount || - chunkHeight > AtmosChunkConstants.MaximumVoxelCount || - chunkDepth > AtmosChunkConstants.MaximumVoxelCount) + if (chunkWidth > ChunkConstants.MaximumVoxelCount || + chunkHeight > ChunkConstants.MaximumVoxelCount || + chunkDepth > ChunkConstants.MaximumVoxelCount) { throw new ArgumentOutOfRangeException( nameof(chunkWidth), chunkWidth, - $"No chunk dimension may exceed {AtmosChunkConstants.MaximumVoxelCount}."); + $"No chunk dimension may exceed {ChunkConstants.MaximumVoxelCount}."); } long voxelCount = (long)chunkWidth * chunkHeight * chunkDepth; - if (voxelCount > AtmosChunkConstants.MaximumVoxelCount) + if (voxelCount > ChunkConstants.MaximumVoxelCount) { throw new ArgumentOutOfRangeException( nameof(chunkWidth), chunkWidth, $"Chunk dimensions contain {voxelCount} voxels, but at most " + - $"{AtmosChunkConstants.MaximumVoxelCount} are supported."); + $"{ChunkConstants.MaximumVoxelCount} are supported."); } _chunkWidth = chunkWidth; @@ -132,12 +134,18 @@ internal AtmosSimulation( /// [PublicAPI] public AtmosWorld World { get; } + + /// + /// Gets the world that owns shared time, configuration, and cross-simulation topology. + /// + [PublicAPI] + public IChunkWorld ChunkWorld => World; /// /// Gets this simulation's stable generational identifier within . /// [PublicAPI] - public AtmosSimulationId Id { get; } + public SimulationId Id { get; } /// /// Gets the fixed dimensions used by every chunk owned by this simulation. @@ -425,11 +433,11 @@ public AtmosRecording StopRecording() /// /// The simulation has been disposed. [PublicAPI] - public AtmosChunkHandle CreateAndRegisterChunk(Int3 position) + public ChunkHandle CreateAndRegisterChunk(Int3 position) { ThrowIfDisposed(); - _kernel.CreateAndRegisterChunk(position, _chunkWidth, _chunkHeight, _chunkDepth); - return new AtmosChunkHandle(position); + _kernel.CreateAndRegisterChunk(position); + return new ChunkHandle(position); } /// @@ -444,7 +452,7 @@ public AtmosChunkHandle CreateAndRegisterChunk(Int3 position) /// The simulation has been disposed. /// Called from a solver callback. [PublicAPI] - public bool UnregisterChunk(AtmosChunkHandle chunk) + public bool UnregisterChunk(ChunkHandle chunk) { ThrowIfDisposed(); bool removed = _kernel.UnregisterChunk(chunk.Position); @@ -464,7 +472,7 @@ public bool UnregisterChunk(AtmosChunkHandle chunk) /// The local voxel index is outside the chunk. /// The simulation has been disposed. [PublicAPI] - public AtmosCellRef GetCellRef(AtmosChunkHandle chunk, ushort localVoxelIndex) + public VoxelRef GetCellRef(ChunkHandle chunk, ushort localVoxelIndex) { ThrowIfDisposed(); if (!_kernel.TryResolveExplicitEndpoint(chunk.Position, localVoxelIndex, out _)) @@ -475,7 +483,7 @@ public AtmosCellRef GetCellRef(AtmosChunkHandle chunk, ushort localVoxelIndex) throw new ArgumentOutOfRangeException(nameof(localVoxelIndex)); } - return new AtmosCellRef(Id, chunk, localVoxelIndex); + return new VoxelRef(Id, chunk, localVoxelIndex); } /// @@ -487,7 +495,7 @@ public AtmosCellRef GetCellRef(AtmosChunkHandle chunk, ushort localVoxelIndex) /// /// The simulation has been disposed. [PublicAPI] - public AtmosChunkHandle[] GetChunkHandles() + public ChunkHandle[] GetChunkHandles() { ThrowIfDisposed(); Int3[] positions = _kernel.GetChunkPositions(); @@ -505,7 +513,7 @@ public AtmosChunkHandle[] GetChunkHandles() public bool TryGetChunkHandles( long knownRevision, out long revision, - out AtmosChunkHandle[] handles) + out ChunkHandle[] handles) { ThrowIfDisposed(); if (!_kernel.TryGetChunkPositions(knownRevision, out revision, out Int3[] positions)) @@ -518,7 +526,7 @@ public bool TryGetChunkHandles( return true; } - private static AtmosChunkHandle[] CreateSortedHandles(Int3[] positions) + private static ChunkHandle[] CreateSortedHandles(Int3[] positions) { Array.Sort( positions, @@ -532,9 +540,9 @@ private static AtmosChunkHandle[] CreateSortedHandles(Int3[] positions) return y != 0 ? y : left.Z.CompareTo(right.Z); }); - var handles = new AtmosChunkHandle[positions.Length]; + var handles = new ChunkHandle[positions.Length]; for (int index = 0; index < positions.Length; index++) - handles[index] = new AtmosChunkHandle(positions[index]); + handles[index] = new ChunkHandle(positions[index]); return handles; } @@ -550,7 +558,7 @@ private static AtmosChunkHandle[] CreateSortedHandles(Int3[] positions) /// No chunk is registered at the handle's position. /// The simulation has been disposed. [PublicAPI] - public AtmosChunkSnapshot GetChunkSnapshot(AtmosChunkHandle chunk) + public AtmosChunkSnapshot GetChunkSnapshot(ChunkHandle chunk) { ThrowIfDisposed(); return _kernel.GetChunkSnapshot(chunk.Position); @@ -564,7 +572,7 @@ public AtmosChunkSnapshot GetChunkSnapshot(AtmosChunkHandle chunk) /// Scalar values plus one moles value per active gas channel. [PublicAPI] public AtmosVoxelSnapshot GetVoxelSnapshot( - AtmosChunkHandle chunk, + ChunkHandle chunk, ushort localVoxelIndex) { ThrowIfDisposed(); @@ -586,7 +594,7 @@ public AtmosVoxelSnapshot GetVoxelSnapshot( /// only when the expected version is still current. [PublicAPI] public bool TryGetVoxelSnapshot( - AtmosChunkHandle chunk, + ChunkHandle chunk, ushort localVoxelIndex, AtmosChunkVersion expectedVersion, out AtmosVoxelSnapshot snapshot) @@ -608,7 +616,7 @@ public bool TryGetVoxelSnapshot( /// when a new snapshot was created; otherwise . [PublicAPI] public bool TryGetChunkSnapshot( - AtmosChunkHandle chunk, + ChunkHandle chunk, AtmosChunkVersion knownVersion, out AtmosChunkSnapshot snapshot) { @@ -633,7 +641,7 @@ public bool TryGetChunkSnapshot( /// when a new snapshot was created; otherwise . [PublicAPI] public bool TryGetChunkSnapshot( - AtmosChunkHandle chunk, + ChunkHandle chunk, AtmosChunkVersion knownVersion, AtmosChunkSnapshotFields fields, out AtmosChunkSnapshot snapshot) @@ -671,7 +679,7 @@ public AtmosChunkSnapshotBatch GetChangedChunkSnapshots( /// No chunk is registered at the handle's position. /// The simulation has been disposed. [PublicAPI] - public void SetChunkClassification(AtmosChunkHandle chunk, VoxelClassification classification) + public void SetChunkClassification(ChunkHandle chunk, VoxelClassification classification) { ThrowIfDisposed(); _kernel.SetChunkClassification(chunk.Position, classification); @@ -691,7 +699,7 @@ public void SetChunkClassification(AtmosChunkHandle chunk, VoxelClassification c /// The simulation has been disposed. [PublicAPI] public void SetChunkBoundaryClassification( - AtmosChunkHandle chunk, + ChunkHandle chunk, VoxelClassification classification) { ThrowIfDisposed(); @@ -710,7 +718,7 @@ public void SetChunkBoundaryClassification( /// The simulation has been disposed. [PublicAPI] public void SetVoxelClassification( - AtmosChunkHandle chunk, ushort localVoxelIndex, + ChunkHandle chunk, ushort localVoxelIndex, VoxelClassification classification) { ThrowIfDisposed(); @@ -730,7 +738,7 @@ public void SetVoxelClassification( /// The simulation has been disposed. [PublicAPI] public void SetVoxelClassification( - AtmosChunkHandle chunk, int x, int y, int z, + ChunkHandle chunk, int x, int y, int z, VoxelClassification classification) { ThrowIfDisposed(); @@ -753,7 +761,7 @@ public void SetVoxelClassification( /// No chunk is registered at the handle's position. /// The simulation has been disposed. [PublicAPI] - public void SetVoxelTemperature(AtmosChunkHandle chunk, ushort localVoxelIndex, float temperature) + public void SetVoxelTemperature(ChunkHandle chunk, ushort localVoxelIndex, float temperature) { ThrowIfDisposed(); _kernel.SetVoxelTemperature(chunk.Position, localVoxelIndex, temperature); @@ -777,7 +785,7 @@ public void SetVoxelTemperature(AtmosChunkHandle chunk, ushort localVoxelIndex, /// No chunk is registered at the handle's position. /// The simulation has been disposed. [PublicAPI] - public void SetVoxelTemperature(AtmosChunkHandle chunk, int x, int y, int z, float temperature) + public void SetVoxelTemperature(ChunkHandle chunk, int x, int y, int z, float temperature) { ThrowIfDisposed(); _kernel.SetVoxelTemperature(chunk.Position, x, y, z, temperature); @@ -811,7 +819,7 @@ public void SetVoxelTemperature(AtmosChunkHandle chunk, int x, int y, int z, flo /// The simulation has been disposed. [PublicAPI] public void AddGasToVoxel( - AtmosChunkHandle chunk, ushort localVoxelIndex, int gasId, float moles, + ChunkHandle chunk, ushort localVoxelIndex, int gasId, float moles, float temperature) { lock (_mixtureGate) @@ -851,7 +859,7 @@ public void AddGasToVoxel( /// The simulation has been disposed. [PublicAPI] public void AddGasToVoxel( - AtmosChunkHandle chunk, int x, int y, int z, int gasId, float moles, + ChunkHandle chunk, int x, int y, int z, int gasId, float moles, float temperature) { lock (_mixtureGate) @@ -877,7 +885,7 @@ public void AddGasToVoxel( /// The simulation has been disposed. [PublicAPI] public void AddGasToVoxel( - AtmosChunkHandle chunk, ushort localVoxelIndex, + ChunkHandle chunk, ushort localVoxelIndex, string gasName, float moles, float temperature) { lock (_mixtureGate) @@ -903,7 +911,7 @@ public void AddGasToVoxel( /// The simulation has been disposed. [PublicAPI] public void AddGasToVoxel( - AtmosChunkHandle chunk, int x, int y, int z, + ChunkHandle chunk, int x, int y, int z, string gasName, float moles, float temperature) { lock (_mixtureGate) @@ -919,7 +927,7 @@ public void AddGasToVoxel( /// No chunk is registered at the handle's position. /// The simulation has been disposed. [PublicAPI] - public void WakeChunk(AtmosChunkHandle chunk) + public void WakeChunk(ChunkHandle chunk) { ThrowIfDisposed(); _kernel.WakeChunk(chunk.Position); @@ -933,7 +941,7 @@ public void WakeChunk(AtmosChunkHandle chunk) /// No chunk is registered at the handle's position. /// The simulation has been disposed. [PublicAPI] - public void SleepChunk(AtmosChunkHandle chunk) + public void SleepChunk(ChunkHandle chunk) { ThrowIfDisposed(); _kernel.SleepChunk(chunk.Position); diff --git a/src/Numos.API/AtmosSolver.cs b/src/Numos.API/AtmosSolver.cs index 77c4f4e..edc9a11 100644 --- a/src/Numos.API/AtmosSolver.cs +++ b/src/Numos.API/AtmosSolver.cs @@ -16,7 +16,7 @@ public static class AtmosBuiltInSolvers /// /// Sparse gas transport across explicit portal, dock, and arbitrary links whose flags include - /// . + /// . /// public const string ExplicitGasTransport = "explicit-gas-transport"; @@ -32,7 +32,7 @@ public static class AtmosBuiltInSolvers /// /// Sparse thermal transport across explicit portal, dock, and arbitrary links whose flags include - /// . Runs on the same cadence as + /// . Runs on the same cadence as /// (see AtmosSolverConstants.ThermodynamicsTickInterval). /// public const string ExplicitThermalTransport = "explicit-thermal-transport"; diff --git a/src/Numos.API/AtmosWorld.Replay.cs b/src/Numos.API/AtmosWorld.Replay.cs index 333b06e..a67dbe0 100644 --- a/src/Numos.API/AtmosWorld.Replay.cs +++ b/src/Numos.API/AtmosWorld.Replay.cs @@ -1,4 +1,6 @@ using System.Diagnostics; +using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim.Replay; namespace Numos.API; @@ -241,7 +243,7 @@ public AtmosWorldStateHash ComputeStateHash() /// /// The operation is atomic with respect to deterministic world state. If validation or application fails, /// the state present before this call is restored. Simulation objects created after the source checkpoint - /// may be replaced; reacquire them by after seeking. + /// may be replaced; reacquire them by after seeking. /// /// A required argument is . /// The history is malformed or incompatible with the checkpoint. @@ -385,7 +387,7 @@ private void Apply(AtmosWorldSimulationOperation simulationOperation) simulation.Kernel.ApplyRecordedOperation(simulationOperation.Operation); if (simulationOperation.Operation is RemoveChunkOperation removed) - InvalidateLinksForChunk(simulation, new AtmosChunkHandle(removed.Position)); + InvalidateLinksForChunk(simulation, new ChunkHandle(removed.Position)); } private void Apply(SetAtmosWorldConfigOperation config) @@ -533,7 +535,7 @@ internal static AtmosWorldStateHash Hash(AtmosWorldCheckpoint checkpoint) return new AtmosWorldStateHash(checkpoint.Position, hash.Value); } - private static void AddCell(ref AtmosStateHasher hash, AtmosCellRef cell) + private static void AddCell(ref AtmosStateHasher hash, VoxelRef cell) { hash.Add(cell.Simulation.Index); hash.Add(cell.Simulation.Generation); diff --git a/src/Numos.API/AtmosWorld.cs b/src/Numos.API/AtmosWorld.cs index da1ad99..39f8b3e 100644 --- a/src/Numos.API/AtmosWorld.cs +++ b/src/Numos.API/AtmosWorld.cs @@ -1,5 +1,7 @@ using System.Buffers; using JetBrains.Annotations; +using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim; using Numos.CoreSim.Replay; using Numos.CoreSim.Solvers; @@ -22,22 +24,22 @@ namespace Numos.API; /// AtmosSimulation station = world.CreateSimulation(16, 16, 16); /// AtmosSimulation shuttle = world.CreateSimulation(8, 8, 8); /// -/// AtmosChunkHandle stationChunk = station.CreateAndRegisterChunk(new Int3(0, 0, 0)); -/// AtmosChunkHandle shuttleChunk = shuttle.CreateAndRegisterChunk(new Int3(0, 0, 0)); -/// AtmosCellRef stationCell = station.GetCellRef(stationChunk, 0); -/// AtmosCellRef shuttleCell = shuttle.GetCellRef(shuttleChunk, 0); -/// AtmosPortalHandle portal = world.CreatePortal(stationCell, shuttleCell); +/// ChunkHandle stationChunk = station.CreateAndRegisterChunk(new Int3(0, 0, 0)); +/// ChunkHandle shuttleChunk = shuttle.CreateAndRegisterChunk(new Int3(0, 0, 0)); +/// VoxelRef stationCell = station.GetCellRef(stationChunk, 0); +/// VoxelRef shuttleCell = shuttle.GetCellRef(shuttleChunk, 0); +/// PortalHandle portal = world.CreatePortal(stationCell, shuttleCell); /// world.Tick(); // Activates the portal, then advances both simulations. /// /// -public sealed partial class AtmosWorld : IDisposable +public sealed partial class AtmosWorld : IChunkWorld, IDisposable { private readonly ExplicitAtmosTransportSolver _explicitTransport = new(); private readonly SortedSet _freeLinkSlots = []; private readonly SortedSet _freeSimulationSlots = []; private readonly List _linkSlots = []; private readonly Dictionary> _linkSlotsByChunk = []; - private readonly Dictionary> _linkSlotsBySimulation = []; + private readonly Dictionary> _linkSlotsBySimulation = []; private readonly SortedSet _pendingLinkSlots = []; private readonly HashSet _reservedEdges = []; private readonly List _simulationSlots = []; @@ -131,6 +133,13 @@ public IReadOnlyList Simulations } } + /// + /// Gets a detached, stable-ID-ordered list of registered chunk simulations. + /// + /// The world has been disposed. + [PublicAPI] + public IReadOnlyList ChunkSimulations => Simulations; + /// /// Gets a monotonic revision that changes whenever simulation membership changes. /// @@ -305,7 +314,7 @@ public IReadOnlyList GetLinkSets() /// Canonical definitions ordered by their complete stable endpoint addresses. /// The world has been disposed. [PublicAPI] - public IReadOnlyList GetActiveLinks() + public IReadOnlyList> GetActiveLinks() { lock (Gate) { @@ -330,9 +339,9 @@ public IReadOnlyList GetActiveLinks() /// A chunk dimension or combined voxel count is invalid. [PublicAPI] public AtmosSimulation CreateSimulation( - int chunkWidth = AtmosChunkConstants.DefaultWidth, - int chunkHeight = AtmosChunkConstants.DefaultHeight, - int chunkDepth = AtmosChunkConstants.DefaultDepth) + int chunkWidth = ChunkConstants.DefaultWidth, + int chunkHeight = ChunkConstants.DefaultHeight, + int chunkDepth = ChunkConstants.DefaultDepth) { lock (Gate) { @@ -403,13 +412,13 @@ public bool SetAtmosConfig(AtmosConfig config) /// /// The world has been disposed. [PublicAPI] - public ExplicitLinkSetHandle CreateLinks(ReadOnlySpan links) + public ExplicitLinkSetHandle CreateLinks(ReadOnlySpan> links) { return CreateLinksCore(links, ExplicitLinkSetKind.Arbitrary); } private ExplicitLinkSetHandle CreateLinksCore( - ReadOnlySpan links, + ReadOnlySpan> links, ExplicitLinkSetKind kind) { lock (Gate) @@ -499,14 +508,14 @@ public void DestroyLinks(ExplicitLinkSetHandle handle) /// /// The world has been disposed. [PublicAPI] - public AtmosPortalHandle CreatePortal( - AtmosCellRef first, - AtmosCellRef second, - ExplicitLinkFlags flags = ExplicitLinkFlags.All) + public PortalHandle CreatePortal( + VoxelRef first, + VoxelRef second, + AtmosLinkFlags flags = AtmosLinkFlags.All) { - ExplicitLinkDefinition definition = new(first, second, flags); - return new AtmosPortalHandle( - CreateLinksCore(new ReadOnlySpan(in definition), ExplicitLinkSetKind.Portal)); + ExplicitLinkDefinition definition = new(first, second, flags); + return new PortalHandle( + CreateLinksCore(new ReadOnlySpan>(in definition), ExplicitLinkSetKind.Portal)); } /// @@ -517,7 +526,7 @@ public AtmosPortalHandle CreatePortal( /// Removal of this portal is already pending. /// The world has been disposed. [PublicAPI] - public void DestroyPortal(AtmosPortalHandle portal) + public void DestroyPortal(PortalHandle portal) { DestroyLinks(portal.Links); } @@ -535,9 +544,9 @@ public void DestroyPortal(AtmosPortalHandle portal) /// /// The world has been disposed. [PublicAPI] - public AtmosDockHandle CreateDock(ReadOnlySpan surfaceLinks) + public DockHandle CreateDock(ReadOnlySpan> surfaceLinks) { - return new AtmosDockHandle(CreateLinksCore(surfaceLinks, ExplicitLinkSetKind.Dock)); + return new DockHandle(CreateLinksCore(surfaceLinks, ExplicitLinkSetKind.Dock)); } /// @@ -548,7 +557,7 @@ public AtmosDockHandle CreateDock(ReadOnlySpan surfaceLi /// Removal of this dock is already pending. /// The world has been disposed. [PublicAPI] - public void DestroyDock(AtmosDockHandle dock) + public void DestroyDock(DockHandle dock) { DestroyLinks(dock.Links); } @@ -561,17 +570,17 @@ public void DestroyDock(AtmosDockHandle dock) /// The handle is invalid or stale. /// The world has been disposed. [PublicAPI] - public IReadOnlyList GetLinks(ExplicitLinkSetHandle handle) + public IReadOnlyList> GetLinks(ExplicitLinkSetHandle handle) { lock (Gate) { ThrowIfDisposed(); var slot = GetLinkSlot(handle); - var result = new ExplicitLinkDefinition[slot.Edges.Length]; + var result = new ExplicitLinkDefinition[slot.Edges.Length]; for (int index = 0; index < result.Length; index++) { var edge = slot.Edges[index]; - result[index] = new ExplicitLinkDefinition(edge.First, edge.Second, edge.Flags); + result[index] = new ExplicitLinkDefinition(edge.First, edge.Second, edge.Flags); } return result; @@ -588,7 +597,7 @@ public IReadOnlyList GetLinks(ExplicitLinkSetHandle hand /// is . /// The world has been disposed. [PublicAPI] - public bool HasExplicitLinks(AtmosSimulation simulation, AtmosChunkHandle chunk) + public bool HasExplicitLinks(AtmosSimulation simulation, ChunkHandle chunk) { ArgumentNullException.ThrowIfNull(simulation); lock (Gate) @@ -633,7 +642,7 @@ public AtmosWorldCheckpoint CaptureCheckpoint() for (int index = 0; index < _linkSlots.Count; index++) { var slot = _linkSlots[index]; - ExplicitLinkDefinition[] definitions = ToDefinitions(slot.Edges); + ExplicitLinkDefinition[] definitions = ToDefinitions(slot.Edges); slotCheckpoints[index] = new AtmosWorldLinkSlotCheckpoint( slot.Generation, (byte)slot.State, @@ -851,9 +860,9 @@ public bool DestroySimulation(AtmosSimulation simulation) /// /// Registers a fully initialized simulation and returns its stable world identifier. /// - internal AtmosSimulationId RegisterSimulation( + internal SimulationId RegisterSimulation( AtmosSimulation simulation, - AtmosSimulationId? requestedRegistration = null) + SimulationId? requestedRegistration = null) { lock (Gate) { @@ -899,7 +908,7 @@ internal AtmosSimulationId RegisterSimulation( _simulationSlots[index] = slot; RebuildOrderedSimulations(); IncrementSimulationCollectionRevision(); - return new AtmosSimulationId(index, slot.Generation); + return new SimulationId(index, slot.Generation); } } @@ -937,7 +946,7 @@ private bool UnregisterSimulationCore(AtmosSimulation simulation, bool record) /// /// Invalidates link sets that reference a chunk removed from its owning simulation. /// - internal void InvalidateLinksForChunk(AtmosSimulation simulation, AtmosChunkHandle chunk) + internal void InvalidateLinksForChunk(AtmosSimulation simulation, ChunkHandle chunk) { lock (Gate) { @@ -1126,8 +1135,8 @@ private void ValidateCheckpoint(AtmosWorldCheckpoint checkpoint) nameof(checkpoint)); } - var validCells = new Dictionary>(); - AtmosSimulationId? previousSimulation = null; + var validCells = new Dictionary>(); + SimulationId? previousSimulation = null; foreach (var saved in checkpoint.Simulations) { if (!saved.Simulation.IsValid || @@ -1409,7 +1418,7 @@ private void WakeEndpoints(ExplicitAtmosEdge[] edges) } } - private void WakeEndpoint(AtmosCellRef cell) + private void WakeEndpoint(VoxelRef cell) { var simulation = TryGetSimulationCore(cell.Simulation); if (simulation != null && @@ -1511,7 +1520,7 @@ private void RemoveLinkSlotIndexes(int slotIndex, ExplicitAtmosEdge[] edges) } } - private void IndexLinkSlot(int slotIndex, AtmosCellRef cell) + private void IndexLinkSlot(int slotIndex, VoxelRef cell) { AddIndex(_linkSlotsBySimulation, cell.Simulation, slotIndex); AddIndex(_linkSlotsByChunk, new AtmosChunkKey(cell.Simulation, cell.Chunk.Position), slotIndex); @@ -1543,7 +1552,7 @@ private static void UnindexLinkSlot( index.Remove(key); } - private void InvalidateLinksForSimulation(AtmosSimulationId simulationId) + private void InvalidateLinksForSimulation(SimulationId simulationId) { if (!_linkSlotsBySimulation.TryGetValue(simulationId, out HashSet? slots)) return; @@ -1553,7 +1562,7 @@ private void InvalidateLinksForSimulation(AtmosSimulationId simulationId) DestroyLinkSlot(slotIndex); } - private void ValidateCell(AtmosCellRef cell, string parameterName) + private void ValidateCell(VoxelRef cell, string parameterName) { var simulation = TryGetSimulationCore(cell.Simulation); if (simulation == null || @@ -1566,12 +1575,12 @@ private void ValidateCell(AtmosCellRef cell, string parameterName) } } - private static void ValidateFlags(ExplicitLinkFlags flags, string parameterName) + private static void ValidateFlags(AtmosLinkFlags flags, string parameterName) { // Bits outside GasTransport|ThermalTransport are reserved for host-defined capabilities: a link can carry - // them so a host-registered solver's AtmosExplicitLinkSelector can pick it out, without engaging Numos' + // them so a host-registered solver's ExplicitLinkSelector can pick it out, without engaging Numos' // built-in transport stages, which only ever look at the bits they know about. - if (flags == ExplicitLinkFlags.None) + if (flags == AtmosLinkFlags.None) throw new ArgumentException("An explicit link must select at least one transport capability.", parameterName); } @@ -1602,7 +1611,7 @@ private bool ContainsSimulation(AtmosSimulation simulation) /// /// The world has been disposed. [PublicAPI] - public bool TryGetSimulation(AtmosSimulationId id, out AtmosSimulation? simulation) + public bool TryGetSimulation(SimulationId id, out AtmosSimulation? simulation) { lock (Gate) { @@ -1611,8 +1620,15 @@ public bool TryGetSimulation(AtmosSimulationId id, out AtmosSimulation? simulati return simulation != null; } } + + public bool TryGetChunkSimulation(SimulationId id, out IChunkSimulation? simulation) + { + var success = TryGetSimulation(id, out var sim); + simulation = sim; + return success; + } - private AtmosSimulation? TryGetSimulationCore(AtmosSimulationId id) + private AtmosSimulation? TryGetSimulationCore(SimulationId id) { if (!id.IsValid || (uint)id.Index >= (uint)_simulationSlots.Count) return null; @@ -1621,7 +1637,12 @@ public bool TryGetSimulation(AtmosSimulationId id, out AtmosSimulation? simulati return slot.Generation == id.Generation ? slot.Simulation : null; } - internal bool TryResolveCell(AtmosCellRef cell, out AtmosSimulation? simulation) + public bool TryResolveCell(VoxelRef cell) + { + return TryResolveCell(cell, out _); + } + + internal bool TryResolveCell(VoxelRef cell, out AtmosSimulation? simulation) { lock (Gate) { @@ -1634,7 +1655,7 @@ internal bool TryResolveCell(AtmosCellRef cell, out AtmosSimulation? simulation) } } - internal IReadOnlyList GetActiveLinksCore() + internal IReadOnlyList> GetActiveLinksCore() { return ToDefinitions(_activeEdges); } @@ -1653,7 +1674,7 @@ private AtmosWorldLinkSetSnapshot[] CreateLinkSetSnapshots() if (slot.State == LinkSetState.Free) continue; - ExplicitLinkDefinition[] definitions = ToDefinitions(slot.Edges); + ExplicitLinkDefinition[] definitions = ToDefinitions(slot.Edges); result.Add( new AtmosWorldLinkSetSnapshot( new ExplicitLinkSetHandle(index, slot.Generation), @@ -1665,7 +1686,7 @@ private AtmosWorldLinkSetSnapshot[] CreateLinkSetSnapshots() return result.ToArray(); } - private bool AreImplicitNeighbors(AtmosCellRef first, AtmosCellRef second) + private bool AreImplicitNeighbors(VoxelRef first, VoxelRef second) { if (first.Simulation != second.Simulation) return false; @@ -1727,7 +1748,7 @@ private void IncrementLinkSetCollectionRevision() _linkSetCollectionRevision = checked(_linkSetCollectionRevision + 1); } - private static void AddChunkEdgeCount(Dictionary counts, AtmosCellRef cell) + private static void AddChunkEdgeCount(Dictionary counts, VoxelRef cell) { var key = new AtmosChunkKey(cell.Simulation, cell.Chunk.Position); counts.TryGetValue(key, out int count); @@ -1735,8 +1756,8 @@ private static void AddChunkEdgeCount(Dictionary counts, Atm } private static bool CheckpointContainsCell( - Dictionary> validCells, - AtmosCellRef cell) + Dictionary> validCells, + VoxelRef cell) { return validCells.TryGetValue(cell.Simulation, out Dictionary? chunks) && chunks.TryGetValue(cell.Chunk.Position, out int voxelCount) && @@ -1755,13 +1776,13 @@ private static bool IsSolverActive(LinkSetState state, bool applyPendingChanges) : IsSolverActive(state); } - private static ExplicitLinkDefinition[] ToDefinitions(ExplicitAtmosEdge[] edges) + private static ExplicitLinkDefinition[] ToDefinitions(ExplicitAtmosEdge[] edges) { - var result = new ExplicitLinkDefinition[edges.Length]; + var result = new ExplicitLinkDefinition[edges.Length]; for (int index = 0; index < result.Length; index++) { var edge = edges[index]; - result[index] = new ExplicitLinkDefinition(edge.First, edge.Second, edge.Flags); + result[index] = new ExplicitLinkDefinition(edge.First, edge.Second, edge.Flags); } return result; @@ -1821,14 +1842,14 @@ private void ThrowIfDisposed() private readonly record struct SimulationSlot(AtmosSimulation? Simulation, uint Generation); - private readonly record struct AtmosChunkKey(AtmosSimulationId Simulation, Int3 ChunkPosition); + private readonly record struct AtmosChunkKey(SimulationId Simulation, Int3 ChunkPosition); - private readonly record struct ExplicitEdgeKey(AtmosCellRef First, AtmosCellRef Second); + private readonly record struct ExplicitEdgeKey(VoxelRef First, VoxelRef Second); private readonly record struct ExplicitAtmosEdge( - AtmosCellRef First, - AtmosCellRef Second, - ExplicitLinkFlags Flags); + VoxelRef First, + VoxelRef Second, + AtmosLinkFlags Flags); private sealed class ExplicitAtmosEdgeComparer : IComparer { diff --git a/src/Numos.API/AtmosWorldCheckpoint.cs b/src/Numos.API/AtmosWorldCheckpoint.cs index a29c895..a19ecc0 100644 --- a/src/Numos.API/AtmosWorldCheckpoint.cs +++ b/src/Numos.API/AtmosWorldCheckpoint.cs @@ -1,3 +1,4 @@ +using Numos.Chunks.Topology; using Numos.CoreSim; using Numos.CoreSim.Replay; @@ -110,7 +111,7 @@ public readonly record struct AtmosWorldSolverCheckpoint( /// The stable simulation registration. /// The simulation-owned chunk and solver continuation state. public sealed record AtmosWorldSimulationCheckpoint( - AtmosSimulationId Simulation, + SimulationId Simulation, AtmosSimulationCheckpoint Checkpoint); /// @@ -145,10 +146,10 @@ public sealed record AtmosWorldLinkSetCheckpoint( ExplicitLinkSetHandle Handle, ExplicitLinkSetKind Kind, AtmosWorldLinkSetState State, - IReadOnlyList Links); + IReadOnlyList> Links); internal readonly record struct AtmosWorldLinkSlotCheckpoint( uint Generation, byte State, ExplicitLinkSetKind Kind, - ExplicitLinkDefinition[] Links); \ No newline at end of file + ExplicitLinkDefinition[] Links); diff --git a/src/Numos.API/AtmosWorldRecording.cs b/src/Numos.API/AtmosWorldRecording.cs index f2980f7..6ab48fe 100644 --- a/src/Numos.API/AtmosWorldRecording.cs +++ b/src/Numos.API/AtmosWorldRecording.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using Numos.Chunks.Topology; using Numos.CoreSim; using Numos.CoreSim.Replay; using Numos.Maths; @@ -63,7 +64,7 @@ public abstract record AtmosWorldOperation /// The exact simulation registration that receives the operation. /// The immutable component operation payload. public sealed record AtmosWorldSimulationOperation( - AtmosSimulationId Simulation, + SimulationId Simulation, AtmosOperation Operation) : AtmosWorldOperation { /// @@ -86,7 +87,7 @@ public sealed record SetAtmosWorldConfigOperation(AtmosConfigSnapshot Config) : /// The identifier issued by the simulation registry. /// The fixed dimensions used by chunks in the new simulation. public sealed record CreateAtmosSimulationOperation( - AtmosSimulationId Simulation, + SimulationId Simulation, Int3 ChunkDimensions) : AtmosWorldOperation { /// @@ -97,7 +98,7 @@ public sealed record CreateAtmosSimulationOperation( /// Records destruction of one simulation and the automatic invalidation of incident topology. /// /// The registration that was removed. -public sealed record DestroyAtmosSimulationOperation(AtmosSimulationId Simulation) : AtmosWorldOperation +public sealed record DestroyAtmosSimulationOperation(SimulationId Simulation) : AtmosWorldOperation { /// public override AtmosWorldOperationCode Code => AtmosWorldOperationCode.DestroySimulation; @@ -118,12 +119,12 @@ public sealed record CreateAtmosLinkSetOperation : AtmosWorldOperation public CreateAtmosLinkSetOperation( ExplicitLinkSetHandle handle, ExplicitLinkSetKind kind, - IEnumerable links) + IEnumerable> links) { ArgumentNullException.ThrowIfNull(links); Handle = handle; Kind = kind; - Links = new ReadOnlyCollection(links.ToArray()); + Links = new ReadOnlyCollection>(links.ToArray()); } /// @@ -142,7 +143,7 @@ public CreateAtmosLinkSetOperation( /// /// Gets immutable canonical links owned by the set. /// - public IReadOnlyList Links { get; } + public IReadOnlyList> Links { get; } } /// diff --git a/src/Numos.API/AtmosWorldSolver.cs b/src/Numos.API/AtmosWorldSolver.cs index 95a1251..f62d3d7 100644 --- a/src/Numos.API/AtmosWorldSolver.cs +++ b/src/Numos.API/AtmosWorldSolver.cs @@ -1,3 +1,5 @@ +using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim; namespace Numos.API; @@ -8,17 +10,6 @@ namespace Numos.API; /// The tick-wide simulations, configuration, and compiled neighborhood view. public delegate void AtmosWorldSolver(AtmosWorldSolverContext context); -/// -/// Selects an explicit link while Numos compiles a custom solver's neighborhood view. -/// -/// The canonical, value-only link being compiled. -/// when the link participates in the solver. -/// -/// Numos calls selectors only when topology or solver registration changes. Selectors must be deterministic and must -/// not mutate their world. -/// -public delegate bool AtmosExplicitLinkSelector(AtmosExplicitLinkInfo link); - /// /// Identifies whether a world solver stage is provided by Numos or its host. /// @@ -35,67 +26,6 @@ public enum AtmosWorldSolverKind : byte Custom } -/// -/// Describes one canonical explicit link to a topology selector. -/// -/// The canonical first endpoint. -/// The canonical second endpoint. -/// The interactions enabled by the link. -public readonly record struct AtmosExplicitLinkInfo( - AtmosCellRef First, - AtmosCellRef Second, - ExplicitLinkFlags Flags); - -/// -/// Configures the neighborhood compiled for one custom world solver. -/// -/// A stable identifier describing the selection policy for checkpoint compatibility. -/// Whether ordinary Cartesian neighbors participate. -/// The selector evaluated for active explicit links. -/// -/// Set to for a solver that only interacts with -/// portals, docks, or other explicit links: it keeps the compiled view limited to the sparse explicit edge set -/// instead of re-deriving all six ordinary neighbors of every voxel in every chunk. Reach for -/// only when the same stage genuinely needs to traverse ordinary walls too, such as fire -/// or sound propagating through both open doorways and portals. -/// -public sealed class AtmosNeighborSelection( - string key, - bool includeCartesian, - AtmosExplicitLinkSelector explicitLinks) -{ - /// - /// Gets the stable compatibility key for this selection policy. - /// - /// - /// This string is checkpointed alongside the solver's registration and folded into world state hashing, so a - /// restored checkpoint compiles topology using the key it was captured with. Give a selection a new key when - /// its changes what it matches; reusing a key for a semantically - /// different selection lets a restored checkpoint silently compile the wrong edges for it. - /// - public string Key { get; } = string.IsNullOrWhiteSpace(key) - ? throw new ArgumentException("A neighbor selection key cannot be empty.", nameof(key)) - : key; - - /// - /// Gets whether ordinary Cartesian neighbors participate. - /// - public bool IncludeCartesian { get; } = includeCartesian; - - internal AtmosExplicitLinkSelector ExplicitLinks { get; } = - explicitLinks ?? throw new ArgumentNullException(nameof(explicitLinks)); - - /// - /// Creates a selection that includes Cartesian neighbors and every active explicit link. - /// - /// The stable compatibility key for the consuming solver. - /// A selection covering the complete world topology. - public static AtmosNeighborSelection All(string key) - { - return new AtmosNeighborSelection(key, true, static _ => true); - } -} - /// /// Tick-scoped inputs supplied to a world solver callback. /// @@ -108,7 +38,7 @@ public sealed class AtmosWorldSolverContext internal AtmosWorldSolverContext( AtmosWorld world, IReadOnlyList simulations, - AtmosWorldNeighborTopology topology) + WorldNeighborTopology topology) { World = world; Simulations = simulations; @@ -144,7 +74,7 @@ internal AtmosWorldSolverContext( /// neighbors, no explicit edges. Use whenever /// the callback needs this property. /// - public AtmosWorldNeighborTopology Topology { get; } + public WorldNeighborTopology Topology { get; } } /// @@ -158,4 +88,4 @@ public readonly record struct AtmosWorldSolverStep( string Name, AtmosWorldSolverKind Kind, bool IsEnabled, - string? NeighborSelectionKey); \ No newline at end of file + string? NeighborSelectionKey); diff --git a/src/Numos.API/AtmosWorldSolverPipeline.cs b/src/Numos.API/AtmosWorldSolverPipeline.cs index 454cfaa..168e75e 100644 --- a/src/Numos.API/AtmosWorldSolverPipeline.cs +++ b/src/Numos.API/AtmosWorldSolverPipeline.cs @@ -1,4 +1,6 @@ using JetBrains.Annotations; +using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim.Replay; using Numos.CoreSim.Solvers; @@ -70,7 +72,7 @@ public void Register(string name, AtmosWorldSolver solver) [PublicAPI] public void RegisterNeighborSolver( string name, - AtmosNeighborSelection selection, + VoxelNeighborSelection selection, AtmosWorldSolver solver) { ArgumentNullException.ThrowIfNull(selection); @@ -100,7 +102,7 @@ public void RegisterBefore(string existingName, string name, AtmosWorldSolver so public void RegisterNeighborSolverBefore( string existingName, string name, - AtmosNeighborSelection selection, + VoxelNeighborSelection selection, AtmosWorldSolver solver) { ArgumentNullException.ThrowIfNull(selection); @@ -130,7 +132,7 @@ public void RegisterAfter(string existingName, string name, AtmosWorldSolver sol public void RegisterNeighborSolverAfter( string existingName, string name, - AtmosNeighborSelection selection, + VoxelNeighborSelection selection, AtmosWorldSolver solver) { ArgumentNullException.ThrowIfNull(selection); @@ -215,9 +217,9 @@ internal void Execute(WorldSolverRegistration[] steps, AtmosWorldExecutionContex step.Execute(context); } - internal void RecompileTopology(IReadOnlyList links) + internal void RecompileTopology(IReadOnlyList> links) { - var compiled = new AtmosWorldNeighborTopology[_steps.Count]; + var compiled = new WorldNeighborTopology[_steps.Count]; for (int index = 0; index < _steps.Count; index++) compiled[index] = _steps[index].BuildTopology(_world, links); @@ -233,7 +235,7 @@ internal AtmosSolverCheckpoint[] CaptureCheckpointSteps() private void RegisterCore( string name, AtmosWorldSolver solver, - AtmosNeighborSelection? selection, + VoxelNeighborSelection? selection, int insertionIndex) { ArgumentException.ThrowIfNullOrWhiteSpace(name); @@ -257,7 +259,7 @@ private void RegisterRelative( string existingName, string name, AtmosWorldSolver solver, - AtmosNeighborSelection? selection, + VoxelNeighborSelection? selection, bool before) { ArgumentException.ThrowIfNullOrWhiteSpace(existingName); @@ -342,14 +344,14 @@ internal sealed class WorldSolverRegistration { private readonly Action? _builtInSolver; private readonly AtmosWorldSolver? _customSolver; - private AtmosWorldNeighborTopology _topology = AtmosWorldNeighborTopology.Empty; + private WorldNeighborTopology _topology = WorldNeighborTopology.Empty; private WorldSolverRegistration( string name, AtmosWorldSolverKind kind, Action? builtInSolver, AtmosWorldSolver? customSolver, - AtmosNeighborSelection? selection) + VoxelNeighborSelection? selection) { Name = name; Kind = kind; @@ -360,7 +362,7 @@ private WorldSolverRegistration( internal string Name { get; } internal AtmosWorldSolverKind Kind { get; } - internal AtmosNeighborSelection? Selection { get; } + internal VoxelNeighborSelection? Selection { get; } internal bool IsEnabled { get; set; } = true; internal static WorldSolverRegistration CreateBuiltIn( @@ -373,7 +375,7 @@ internal static WorldSolverRegistration CreateBuiltIn( internal static WorldSolverRegistration CreateCustom( string name, AtmosWorldSolver solver, - AtmosNeighborSelection? selection) + VoxelNeighborSelection? selection) { return new WorldSolverRegistration(name, AtmosWorldSolverKind.Custom, null, solver, selection); } @@ -389,16 +391,16 @@ internal void Execute(AtmosWorldExecutionContext context) _builtInSolver!(context); } - internal AtmosWorldNeighborTopology BuildTopology( + internal WorldNeighborTopology BuildTopology( AtmosWorld world, - IReadOnlyList links) + IReadOnlyList> links) { return Selection == null - ? AtmosWorldNeighborTopology.EmptyFor(world) - : AtmosWorldNeighborTopology.Compile(world, Selection, links); + ? WorldNeighborTopology.EmptyFor(world) + : WorldNeighborTopology.Compile(world, Selection, links); } - internal void InstallTopology(AtmosWorldNeighborTopology topology) + internal void InstallTopology(WorldNeighborTopology topology) { _topology = topology; } diff --git a/src/Numos.API/ExplicitAtmosTopology.cs b/src/Numos.API/ExplicitAtmosTopology.cs index b1c8d0b..854ffd5 100644 --- a/src/Numos.API/ExplicitAtmosTopology.cs +++ b/src/Numos.API/ExplicitAtmosTopology.cs @@ -1,82 +1,7 @@ -using Numos.Maths; +using Numos.Chunks.Topology; namespace Numos.API; -/// -/// Identifies one simulation registration within an . -/// -/// The stable registry slot. -/// The slot generation that prevents a stale identifier from naming a replacement simulation. -/// -/// The default value is invalid. Identifiers are meaningful only within the world that issued them. -/// -public readonly record struct AtmosSimulationId(int Index, uint Generation) : IComparable -{ - /// - /// Gets whether this value could have been issued by a world registry. - /// - public bool IsValid => Index >= 0 && Generation != 0; - - /// - /// Compares stable slot and generation values without depending on object identity or hash iteration. - /// - /// The identifier to compare. - /// - /// A negative value when this identifier sorts first, zero when the identifiers match, or a positive value when - /// sorts first. - /// - public int CompareTo(AtmosSimulationId other) - { - int index = Index.CompareTo(other.Index); - return index != 0 ? index : Generation.CompareTo(other.Generation); - } -} - -/// -/// Stably identifies one voxel within an . -/// -/// The owning simulation registration. -/// The owning chunk. -/// The flat local voxel index within the chunk. -/// -/// The value contains no object references and is suitable for deterministic ordering, snapshots, and replay data. -/// It does not prove that the simulation, chunk, or voxel still exists; mutation methods validate the complete -/// address against the receiving world. -/// -public readonly record struct AtmosCellRef( - AtmosSimulationId Simulation, - AtmosChunkHandle Chunk, - ushort LocalVoxelIndex) : IComparable -{ - /// - /// Compares the complete stable address in simulation, chunk-coordinate, and voxel order. - /// - /// The cell reference to compare. - /// - /// A negative value when this address sorts first, zero when the addresses match, or a positive value when - /// sorts first. - /// - public int CompareTo(AtmosCellRef other) - { - int comparison = Simulation.CompareTo(other.Simulation); - if (comparison != 0) - return comparison; - - comparison = CompareChunkPositions(Chunk.Position, other.Chunk.Position); - return comparison != 0 ? comparison : LocalVoxelIndex.CompareTo(other.LocalVoxelIndex); - } - - private static int CompareChunkPositions(Int3 left, Int3 right) - { - int comparison = left.X.CompareTo(right.X); - if (comparison != 0) - return comparison; - - comparison = left.Y.CompareTo(right.Y); - return comparison != 0 ? comparison : left.Z.CompareTo(right.Z); - } -} - /// /// Selects the physical interactions allowed to cross an explicit atmosphere link. /// @@ -84,14 +9,14 @@ private static int CompareChunkPositions(Int3 left, Int3 right) /// and are the only bits Numos' own built-in /// / /// stages act on. The remaining bits of this -backed flag set are reserved for hosts: a -/// link can carry a host-defined bit (for example (ExplicitLinkFlags)(1 << 2)) purely so a -/// host-registered can pick it out. Numos' built-in stages ignore bits +/// link can carry a host-defined bit (for example (AtmosLinkFlags)(1 << 2)) purely so a +/// host-registered can pick it out. Numos' built-in stages ignore bits /// they do not recognize, so a link can mix built-in capabilities with host-defined ones, or use only /// host-defined ones to opt out of default physics entirely while still participating in checkpointing, /// recording, and topology enumeration like any other link. /// [Flags] -public enum ExplicitLinkFlags : byte +public enum AtmosLinkFlags : byte { /// /// Allows no solver interaction and is therefore invalid for a registered link. @@ -114,70 +39,6 @@ public enum ExplicitLinkFlags : byte All = GasTransport | ThermalTransport } -/// -/// Records which world API created an explicit link set. -/// -/// -/// The kind supports inspection, replay, and tooling. Every kind uses the same sparse solver representation, so it -/// does not change transport physics. -/// -public enum ExplicitLinkSetKind : byte -{ - /// - /// A generic batch created through . - /// - Arbitrary, - - /// - /// A one-edge portal created through . - /// - Portal, - - /// - /// A surface batch created through . - /// - Dock -} - -/// -/// Defines one undirected sparse atmospheric adjacency. -/// -/// One endpoint. Registration canonicalizes endpoint orientation. -/// The other endpoint. -/// The interactions allowed across the adjacency. -public readonly record struct ExplicitLinkDefinition( - AtmosCellRef First, - AtmosCellRef Second, - ExplicitLinkFlags Flags = ExplicitLinkFlags.All); - -/// -/// Identifies a batch of explicit links owned and removed as one lifecycle unit. -/// -/// The link-set storage slot. -/// The generation used to detect stale handles after slot reuse. -/// -/// The default value is invalid. -/// -public readonly record struct ExplicitLinkSetHandle(int Index, uint Generation) -{ - /// - /// Gets whether this value could identify a link set. - /// - public bool IsValid => Index >= 0 && Generation != 0; -} - -/// -/// Identifies a one-edge portal in the world's explicit topology. -/// -/// The underlying generic link set. -public readonly record struct AtmosPortalHandle(ExplicitLinkSetHandle Links); - -/// -/// Identifies a dock surface in the world's explicit topology. -/// -/// The underlying generic link set. -public readonly record struct AtmosDockHandle(ExplicitLinkSetHandle Links); - /// /// Captures a detached inspection view of one current or pending explicit link set. /// @@ -193,4 +54,4 @@ public sealed record AtmosWorldLinkSetSnapshot( ExplicitLinkSetHandle Handle, ExplicitLinkSetKind Kind, AtmosWorldLinkSetState State, - IReadOnlyList Links); \ No newline at end of file + IReadOnlyList> Links); diff --git a/src/Numos.Chunks/Chunk.cs b/src/Numos.Chunks/Chunk.cs new file mode 100644 index 0000000..f79a964 --- /dev/null +++ b/src/Numos.Chunks/Chunk.cs @@ -0,0 +1,167 @@ +using JetBrains.Annotations; +using Numos.Collections; +using Numos.Maths; + +namespace Numos.Chunks; + +/// +/// Represents the simulation state for a fixed-size voxel chunk. +/// +/// +/// Chunk-owned per-voxel data supports both flat-index and coordinate access. +/// Use and when converting indices +/// for scalar-indexed storage. +/// +public abstract class Chunk +{ + /// + /// The number of voxels along the x-axis. + /// + public int Width; + + /// + /// The number of voxels along the y-axis. + /// + public int Height; + + /// + /// The number of voxels along the z-axis. + /// + public int Depth; + + /// + /// The position of this chunk in the chunk grid. + /// + public Int3 GridPosition; + + /// + /// Total number of voxels in this chunk, equal to Width * Height * Depth. + /// + public int VoxelCount; + + /// + /// The number of voxels along each axis. + /// + public Int3 Dimensions => new(Width, Height, Depth); + + /// + /// A process-local index assigned when this chunk is registered. + /// + /// + /// Lets hot per-tick solver lookups (e.g. boundary flow's cross-chunk injection buffer) use array + /// indexing instead of a dictionary keyed on . Unique only among currently + /// registered chunks, and not part of the simulation's observable state. + /// Do not use it for anything but indexing a solver-owned lookup table. + /// Ids are recycled after a chunk is unregistered, so a DenseId-keyed lookup table must track which + /// chunk currently owns each slot and validate that before trusting stale contents. + /// + public int DenseId; + + /// + /// Converts local voxel coordinates to an index into the chunk's flat arrays. + /// + /// The local x coordinate, from zero through minus one. + /// The local y coordinate, from zero through minus one. + /// The local z coordinate, from zero through minus one. + /// The flat voxel index. + [PublicAPI] + public ushort GetIndex(int x, int y, int z) + { + return GetIndex(new Int3(x, y, z)); + } + + /// + [PublicAPI] + public ushort GetIndex(Int3 vec) + { + return (ushort)FlatArrayHelpers.GetIndex(vec, Dimensions); + } + + /// + [PublicAPI] + public ushort GetIndexUnsafe(Int3 vec) + { + return (ushort)FlatArrayHelpers.GetIndexUnsafe(vec, Dimensions); + } + + /// + /// Converts a flat voxel index to local x, y, and z coordinates. + /// + /// The flat voxel index. + /// The local coordinates as an (x, y, z) tuple. + [PublicAPI] + public (int x, int y, int z) GetXyz(ushort index) + { + var position = GetXyzInt3(index); + return (position.X, position.Y, position.Z); + } + + /// + /// Converts a flat voxel index to local coordinates as an . + /// + /// The flat voxel index. + /// The local voxel coordinates. + [PublicAPI] + public Int3 GetXyzInt3(ushort index) + { + return FlatArrayHelpers.GetPosition(index, Dimensions); + } + + /// + /// Method that is called before the chunk is released from the chunk map. + /// Here it must release all of its disposable resources. + /// + public virtual void Release() { } + + /// + /// Ensures that a chunk's has specified dimensions. + /// + /// The target flat array. + /// Dimensions to ensure on the flat array. + /// Type parameter of the flat array. + protected void EnsureInitialized(ref FlatArray array, Int3 dimensions) + { + if (!array.IsInitialized || array.Length != VoxelCount) + array = new FlatArray(new T[VoxelCount], dimensions); + else if (array.Dimensions != dimensions) + array = array.Reshape(dimensions); + } + + /// + /// Validates the specified dimensions and returns the total voxel count of the chunk. + /// + /// Width of the chunk. + /// Height of the chunk. + /// Depth of the chunk. + /// Total voxel count of the chunk. + /// + /// One of the dimensions is not in the supported size range. + /// + protected static int GetValidatedVoxelCount(int width, int height, int depth) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(width); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(height); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(depth); + if (width > ChunkConstants.MaximumVoxelCount || + height > ChunkConstants.MaximumVoxelCount || + depth > ChunkConstants.MaximumVoxelCount) + { + throw new ArgumentOutOfRangeException( + nameof(width), + width, + $"No chunk dimension may exceed {ChunkConstants.MaximumVoxelCount}."); + } + + long voxelCount = (long)width * height * depth; + if (voxelCount > ChunkConstants.MaximumVoxelCount) + { + throw new ArgumentOutOfRangeException( + nameof(width), + width, + $"Chunk dimensions contain {voxelCount} voxels, but at most " + + $"{ChunkConstants.MaximumVoxelCount} are supported."); + } + + return (int)voxelCount; + } +} diff --git a/src/Numos.Chunks/ChunkConstants.cs b/src/Numos.Chunks/ChunkConstants.cs new file mode 100644 index 0000000..4465ab3 --- /dev/null +++ b/src/Numos.Chunks/ChunkConstants.cs @@ -0,0 +1,19 @@ +namespace Numos.Chunks; + +/// +/// Default constants for Numos chunks. +/// +public static class ChunkConstants +{ + /// Default number of voxels along a chunk's x-axis. + public const int DefaultWidth = 16; + + /// Default number of voxels along a chunk's y-axis. + public const int DefaultHeight = 16; + + /// Default number of voxels along a chunk's z-axis. + public const int DefaultDepth = 16; + + /// Maximum voxel count representable by the chunk's unsigned 16-bit flat indices. + public const int MaximumVoxelCount = ushort.MaxValue; +} diff --git a/src/Numos.Chunks/ChunkHandle.cs b/src/Numos.Chunks/ChunkHandle.cs new file mode 100644 index 0000000..2ad524a --- /dev/null +++ b/src/Numos.Chunks/ChunkHandle.cs @@ -0,0 +1,8 @@ +using Numos.Maths; + +namespace Numos.Chunks; + +/// +/// Identifies a chunk owned by a . +/// +public readonly record struct ChunkHandle(Int3 Position); diff --git a/src/Numos.Chunks/ChunkMap.cs b/src/Numos.Chunks/ChunkMap.cs new file mode 100644 index 0000000..6503553 --- /dev/null +++ b/src/Numos.Chunks/ChunkMap.cs @@ -0,0 +1,210 @@ +using System.Collections.Concurrent; +using Numos.Maths; + +namespace Numos.Chunks; + +/// +/// A chunk map of voxel chunks where each chunk has fixed dimensions. +/// +/// +/// Chunk type that also implements an +/// interface to initialize newly created instances of this chunk. +/// +public sealed class ChunkMap(int x, int y, int z) : IDisposable where T : Chunk, IChunkInitializer +{ + /// + /// Dimensions of every chunk stored in this chunk map. + /// + public readonly Int3 Dimensions = new(x, y, z); + + private ConcurrentDictionary _chunkMap = new(); + + public long CollectionRevision; + + public int NextDenseId; + + // Reused before minting a new id, so DenseId stays bounded by peak concurrent chunk count rather + // than growing forever. See AtmosChunk.DenseId's remarks on why recycling ids is safe. + public readonly Stack FreeDenseIds = new(); + + /// + /// Gets the number of chunks currently registered with the kernel. + /// + /// The count includes both awake and sleeping chunks. + public int ChunkCount => _chunkMap.Count; + + /// + /// Returns a detached list of the currently registered chunk-grid positions. + /// + public Int3[] GetChunkPositions() => _chunkMap.Keys.ToArray(); + + /// + /// Returns live chunk storage for the opt-in Dangerous API. + /// + public T GetChunkForDangerousAccess(Int3 position) => GetChunk(position); + + public bool TryGetChunk(Int3 position, out T chunk) + { + return _chunkMap.TryGetValue(position, out chunk!); + } + + public bool HasPosition(Int3 position) => _chunkMap.ContainsKey(position); + + /// + /// Returns registered positions only when the chunk collection changed. + /// + public bool TryGetChunkPositions( + long knownRevision, + out long revision, + out Int3[] positions) + { + revision = CollectionRevision; + if (revision == knownRevision) + { + positions = []; + return false; + } + + positions = _chunkMap.Keys.ToArray(); + return true; + } + + /// + /// Adds the specified chunk to the chunk map internal dictionary and reserves a . + /// + /// The chunk object to register. + /// Chunk dimensions didn't match the dimensions of the chunk map. + /// A chunk is already registered at the specified position. + public void RegisterChunk(T chunk) + { + if (chunk.Dimensions != Dimensions) + throw new ArgumentException("Chunk dimensions must match the simulation.", nameof(chunk)); + + if (!_chunkMap.TryAdd(chunk.GridPosition, chunk)) + throw new InvalidOperationException($"A chunk is already registered at {chunk.GridPosition}."); + + chunk.DenseId = FreeDenseIds.Count > 0 ? FreeDenseIds.Pop() : NextDenseId++; + CollectionRevision++; + } + + /// + /// Removes and releases the chunk at a grid position. + /// + /// The chunk-grid position to remove. + /// if a chunk was removed; otherwise, . + public bool UnregisterChunk(Int3 position) + { + if (!_chunkMap.TryRemove(position, out var chunk)) + return false; + + chunk.Release(); + FreeDenseIds.Push(chunk.DenseId); + CollectionRevision++; + return true; + } + + /// + /// Creates, initializes, and registers a chunk owned by this kernel. + /// + /// The chunk's position in the chunk grid. + /// A chunk is already registered at . + public T CreateAndRegisterChunk(Int3 position) + { + if (_chunkMap.ContainsKey(position)) + throw new InvalidOperationException($"A chunk is already registered at {position}."); + + return T.CreateInitializeChunk(position, Dimensions.X, Dimensions.Y, Dimensions.Z); + } + + /// + /// Gets a chunk from the chunk map at a specific position. + /// + /// Position to get the chunk at. + /// Chunk object stored at that position. + /// Specified position doesn't have a chunk. + public T GetChunk(Int3 position) + { + if (_chunkMap.TryGetValue(position, out var chunk)) + return chunk; + + throw new KeyNotFoundException($"No atmospheric chunk is registered at ({position.X}, {position.Y}, {position.Z})."); + } + + /// + /// Returns an array of all chunks ordered by their position. + /// + public T[] OrderedChunks() + { + return _chunkMap.Values + .OrderBy(static chunk => chunk.GridPosition.X) + .ThenBy(static chunk => chunk.GridPosition.Y) + .ThenBy(static chunk => chunk.GridPosition.Z).ToArray(); + } + + /// + /// An unsafe method to access the chunk map dictionary directly. + /// Used by replays to set a new state. + /// + public ConcurrentDictionary UnsafeGetStorage() + { + return _chunkMap; + } + + /// + /// An unsafe method to replace the chunk map dictionary. + /// Used by replays to set a new state. + /// + public void UnsafeReplaceStorage(ConcurrentDictionary replacement) + { + _chunkMap = replacement; + } + + /// + /// Gets a voxel index in the array while validating its position. + /// + /// A chunk to get the voxel index from. + /// The X coordinate of the voxel. + /// The Y coordinate of the voxel. + /// The Z coordinate of the voxel. + /// A voxel index that is guaranteed to be valid for this chunk. + /// X, Y, or Z coordinate isn't in the dimensions of the chunk. + public static ushort GetValidatedVoxelIndex(T chunk, int x, int y, int z) + { + if (x < 0 || x >= chunk.Width) + throw new ArgumentOutOfRangeException(nameof(x)); + + if (y < 0 || y >= chunk.Height) + throw new ArgumentOutOfRangeException(nameof(y)); + + if (z < 0 || z >= chunk.Depth) + throw new ArgumentOutOfRangeException(nameof(z)); + + return chunk.GetIndex(x, y, z); + } + + /// + /// Validates that the given local voxel index is within the bounds of the chunk's voxel array. + /// + /// The chunk to validate against. + /// The local voxel index to validate. + /// Thrown if the local voxel index is out of bounds. + public static void ValidateVoxelIndex(T chunk, ushort localVoxelIndex) + { + if (localVoxelIndex >= chunk.VoxelCount) + { + throw new ArgumentOutOfRangeException( + nameof(localVoxelIndex), + localVoxelIndex, + $"Voxel index must be less than the chunk's voxel count ({chunk.VoxelCount})."); + } + } + + /// + /// Calls on all chunks on the map. + /// + public void Dispose() + { + foreach (var chunk in _chunkMap.Values) + chunk.Release(); + } +} diff --git a/src/Numos.Chunks/IChunkInitializer.cs b/src/Numos.Chunks/IChunkInitializer.cs new file mode 100644 index 0000000..80a9075 --- /dev/null +++ b/src/Numos.Chunks/IChunkInitializer.cs @@ -0,0 +1,24 @@ +using Numos.Maths; + +namespace Numos.Chunks; + +/// +/// Interface implemented by chunk types so that they can create +/// and initialize new instances in static context. +/// +/// Type of the chunk. +public interface IChunkInitializer where T : Chunk +{ + /// + /// Creates a new instance of the chunk at a specified position. + /// + /// The position in the grid. + /// Width of the chunk. + /// Height of the chunk. + /// Depth of the chunk. + /// A newly created and initialized chunk. + abstract static T CreateInitializeChunk(Int3 position, + int width = ChunkConstants.DefaultWidth, + int height = ChunkConstants.DefaultHeight, + int depth = ChunkConstants.DefaultDepth); +} diff --git a/src/Numos.Chunks/IChunkSimulation.cs b/src/Numos.Chunks/IChunkSimulation.cs new file mode 100644 index 0000000..1b06cb8 --- /dev/null +++ b/src/Numos.Chunks/IChunkSimulation.cs @@ -0,0 +1,15 @@ +using Numos.Chunks.Topology; +using Numos.Maths; + +namespace Numos.Chunks; + +public interface IChunkSimulation +{ + SimulationId Id { get; } + + IChunkWorld ChunkWorld { get; } + + Int3 ChunkDimensions { get; } + + ChunkHandle[] GetChunkHandles(); +} diff --git a/src/Numos.Chunks/IChunkWorld.cs b/src/Numos.Chunks/IChunkWorld.cs new file mode 100644 index 0000000..9d25bb8 --- /dev/null +++ b/src/Numos.Chunks/IChunkWorld.cs @@ -0,0 +1,16 @@ +using Numos.Chunks.Topology; + +namespace Numos.Chunks; + +/// +/// An interface for objects that hold multiple s +/// with the same . +/// +public interface IChunkWorld +{ + IReadOnlyList ChunkSimulations { get; } + + bool TryResolveCell(VoxelRef cell); + + bool TryGetChunkSimulation(SimulationId id, out IChunkSimulation? simulation); +} diff --git a/src/Numos.Chunks/Numos.Chunks.csproj b/src/Numos.Chunks/Numos.Chunks.csproj new file mode 100644 index 0000000..7a9a10a --- /dev/null +++ b/src/Numos.Chunks/Numos.Chunks.csproj @@ -0,0 +1,26 @@ + + + + + + net10.0 + enable + enable + Numos.Chunks + Numos.Chunks + Numos Chunks + Voxel Chunk utilities used by Numos. + simulation;chunks;voxel;nativeaot + true + + + + + + + + + + + + diff --git a/src/Numos.Chunks/Topology/ExplicitLinkDefinition.cs b/src/Numos.Chunks/Topology/ExplicitLinkDefinition.cs new file mode 100644 index 0000000..7b71f51 --- /dev/null +++ b/src/Numos.Chunks/Topology/ExplicitLinkDefinition.cs @@ -0,0 +1,12 @@ +namespace Numos.Chunks.Topology; + +/// +/// Defines one undirected sparse voxel adjacency. +/// +/// One endpoint. Registration canonicalizes endpoint orientation. +/// The other endpoint. +/// The interactions allowed across the adjacency. +public readonly record struct ExplicitLinkDefinition( + VoxelRef First, + VoxelRef Second, + T Flags) where T : Enum; diff --git a/src/Numos.Chunks/Topology/ExplicitLinkHandles.cs b/src/Numos.Chunks/Topology/ExplicitLinkHandles.cs new file mode 100644 index 0000000..4c405bf --- /dev/null +++ b/src/Numos.Chunks/Topology/ExplicitLinkHandles.cs @@ -0,0 +1,29 @@ +namespace Numos.Chunks.Topology; + +/// +/// Identifies a batch of explicit links owned and removed as one lifecycle unit. +/// +/// The link-set storage slot. +/// The generation used to detect stale handles after slot reuse. +/// +/// The default value is invalid. +/// +public readonly record struct ExplicitLinkSetHandle(int Index, uint Generation) +{ + /// + /// Gets whether this value could identify a link set. + /// + public bool IsValid => Index >= 0 && Generation != 0; +} + +/// +/// Identifies a one-edge portal in the world's explicit topology. +/// +/// The underlying generic link set. +public readonly record struct PortalHandle(ExplicitLinkSetHandle Links); + +/// +/// Identifies a dock surface in the world's explicit topology. +/// +/// The underlying generic link set. +public readonly record struct DockHandle(ExplicitLinkSetHandle Links); diff --git a/src/Numos.Chunks/Topology/ExplicitLinkSelector.cs b/src/Numos.Chunks/Topology/ExplicitLinkSelector.cs new file mode 100644 index 0000000..782cfba --- /dev/null +++ b/src/Numos.Chunks/Topology/ExplicitLinkSelector.cs @@ -0,0 +1,12 @@ +namespace Numos.Chunks.Topology; + +/// +/// Selects an explicit link while Numos compiles a custom solver's neighborhood view. +/// +/// The canonical, value-only link being compiled. +/// when the link participates in the solver. +/// +/// Numos calls selectors only when topology or solver registration changes. Selectors must be deterministic and must +/// not mutate their world. +/// +public delegate bool ExplicitLinkSelector(ExplicitLinkDefinition link) where T : struct, Enum; diff --git a/src/Numos.Chunks/Topology/ExplicitLinkSetKind.cs b/src/Numos.Chunks/Topology/ExplicitLinkSetKind.cs new file mode 100644 index 0000000..cec7c2d --- /dev/null +++ b/src/Numos.Chunks/Topology/ExplicitLinkSetKind.cs @@ -0,0 +1,26 @@ +namespace Numos.Chunks.Topology; + +/// +/// Records which world API created an explicit link set. +/// +/// +/// The kind supports inspection, replay, and tooling. Every kind uses the same sparse solver representation, so it +/// does not change transport physics. +/// +public enum ExplicitLinkSetKind : byte +{ + /// + /// A generic batch on a voxel. + /// + Arbitrary, + + /// + /// A one-edge portal on a voxel. + /// + Portal, + + /// + /// A surface batch on a voxel. + /// + Dock +} diff --git a/src/Numos.Chunks/Topology/SimulationId.cs b/src/Numos.Chunks/Topology/SimulationId.cs new file mode 100644 index 0000000..4170f4f --- /dev/null +++ b/src/Numos.Chunks/Topology/SimulationId.cs @@ -0,0 +1,31 @@ +namespace Numos.Chunks.Topology; + +/// +/// Identifies one simulation registration that owns a . +/// +/// The stable registry slot. +/// The slot generation that prevents a stale identifier from naming a replacement simulation. +/// +/// The default value is invalid. Identifiers are meaningful only within the world that issued them. +/// +public readonly record struct SimulationId(int Index, uint Generation) : IComparable +{ + /// + /// Gets whether this value could have been issued by a world registry. + /// + public bool IsValid => Index >= 0 && Generation != 0; + + /// + /// Compares stable slot and generation values without depending on object identity or hash iteration. + /// + /// The identifier to compare. + /// + /// A negative value when this identifier sorts first, zero when the identifiers match, or a positive value when + /// sorts first. + /// + public int CompareTo(SimulationId other) + { + int index = Index.CompareTo(other.Index); + return index != 0 ? index : Generation.CompareTo(other.Generation); + } +} diff --git a/src/Numos.Chunks/Topology/VoxelRef.cs b/src/Numos.Chunks/Topology/VoxelRef.cs new file mode 100644 index 0000000..b3413ed --- /dev/null +++ b/src/Numos.Chunks/Topology/VoxelRef.cs @@ -0,0 +1,48 @@ +using Numos.Maths; + +namespace Numos.Chunks.Topology; + +/// +/// Stably identifies one voxel within some holder simulation of a . +/// +/// The owning simulation registration. +/// The owning chunk. +/// The flat local voxel index within the chunk. +/// +/// The value contains no object references and is suitable for deterministic ordering, snapshots, and replay data. +/// It does not prove that the simulation, chunk, or voxel still exists; mutation methods validate the complete +/// address against the receiving world. +/// +public readonly record struct VoxelRef( + SimulationId Simulation, + ChunkHandle Chunk, + ushort LocalVoxelIndex) : IComparable +{ + /// + /// Compares the complete stable address in simulation, chunk-coordinate, and voxel order. + /// + /// The cell reference to compare. + /// + /// A negative value when this address sorts first, zero when the addresses match, or a positive value when + /// sorts first. + /// + public int CompareTo(VoxelRef other) + { + int comparison = Simulation.CompareTo(other.Simulation); + if (comparison != 0) + return comparison; + + comparison = CompareChunkPositions(Chunk.Position, other.Chunk.Position); + return comparison != 0 ? comparison : LocalVoxelIndex.CompareTo(other.LocalVoxelIndex); + } + + private static int CompareChunkPositions(Int3 left, Int3 right) + { + int comparison = left.X.CompareTo(right.X); + if (comparison != 0) + return comparison; + + comparison = left.Y.CompareTo(right.Y); + return comparison != 0 ? comparison : left.Z.CompareTo(right.Z); + } +} diff --git a/src/Numos.API/AtmosWorldNeighborTopology.cs b/src/Numos.Chunks/WorldNeighborTopology.cs similarity index 65% rename from src/Numos.API/AtmosWorldNeighborTopology.cs rename to src/Numos.Chunks/WorldNeighborTopology.cs index efcb9b4..0c055ba 100644 --- a/src/Numos.API/AtmosWorldNeighborTopology.cs +++ b/src/Numos.Chunks/WorldNeighborTopology.cs @@ -1,11 +1,12 @@ +using Numos.Chunks.Topology; using Numos.Maths; -namespace Numos.API; +namespace Numos.Chunks; /// /// Identifies how a solver-facing neighboring cell was discovered. /// -public enum AtmosNeighborKind : byte +public enum VoxelNeighborKind : byte { /// /// The cells are ordinary Cartesian neighbors. @@ -23,11 +24,11 @@ public enum AtmosNeighborKind : byte /// /// The neighboring cell. /// How the adjacency was discovered. -/// The explicit-link capabilities, or for Cartesian adjacency. -public readonly record struct AtmosNeighbor( - AtmosCellRef Cell, - AtmosNeighborKind Kind, - ExplicitLinkFlags Flags); +/// The explicit-link capabilities, or default enum value for Cartesian adjacency. +public readonly record struct VoxelNeighbor( + VoxelRef Cell, + VoxelNeighborKind Kind, + T Flags) where T : struct, Enum; /// /// Describes one canonically owned solver edge. @@ -35,28 +36,78 @@ public readonly record struct AtmosNeighbor( /// The canonical first endpoint. /// The canonical second endpoint. /// How the adjacency was discovered. -/// The explicit-link capabilities, or for Cartesian adjacency. -public readonly record struct AtmosNeighborEdge( - AtmosCellRef First, - AtmosCellRef Second, - AtmosNeighborKind Kind, - ExplicitLinkFlags Flags); +/// The explicit-link capabilities, or default enum value for Cartesian adjacency. +public readonly record struct VoxelNeighborEdge( + VoxelRef First, + VoxelRef Second, + VoxelNeighborKind Kind, + T Flags) where T : struct, Enum; + +/// +/// Configures the neighborhood compiled for one custom world solver. +/// +/// A stable identifier describing the selection policy for checkpoint compatibility. +/// Whether ordinary Cartesian neighbors participate. +/// The selector evaluated for active explicit links. +/// +/// Set to for a solver that only interacts with +/// portals, docks, or other explicit links: it keeps the compiled view limited to the sparse explicit edge set +/// instead of re-deriving all six ordinary neighbors of every voxel in every chunk. Reach for +/// only when the same stage genuinely needs to traverse ordinary walls too, such as fire +/// or sound propagating through both open doorways and portals. +/// +public sealed class VoxelNeighborSelection( + string key, + bool includeCartesian, + ExplicitLinkSelector explicitLinks) where T : struct, Enum +{ + /// + /// Gets the stable compatibility key for this selection policy. + /// + /// + /// This string is checkpointed alongside the solver's registration and folded into world state hashing, so a + /// restored checkpoint compiles topology using the key it was captured with. Give a selection a new key when + /// its changes what it matches; reusing a key for a semantically + /// different selection lets a restored checkpoint silently compile the wrong edges for it. + /// + public string Key { get; } = string.IsNullOrWhiteSpace(key) + ? throw new ArgumentException("A neighbor selection key cannot be empty.", nameof(key)) + : key; + + /// + /// Gets whether ordinary Cartesian neighbors participate. + /// + public bool IncludeCartesian { get; } = includeCartesian; + + internal ExplicitLinkSelector ExplicitLinks { get; } = + explicitLinks ?? throw new ArgumentNullException(nameof(explicitLinks)); + + /// + /// Creates a selection that includes Cartesian neighbors and every active explicit link. + /// + /// The stable compatibility key for the consuming solver. + /// A selection covering the complete world topology. + public static VoxelNeighborSelection All(string key) + { + return new VoxelNeighborSelection(key, true, static _ => true); + } +} /// /// Provides a solver-specific, immutable view of Cartesian and compiled explicit topology. /// -public sealed class AtmosWorldNeighborTopology +public sealed class WorldNeighborTopology where T : struct, Enum { - private readonly Dictionary _explicitByChunk; - private readonly ExplicitLinkDefinition[] _explicitEdges; + private readonly Dictionary> _explicitByChunk; + private readonly ExplicitLinkDefinition[] _explicitEdges; private readonly bool _includeCartesian; - private readonly AtmosWorld? _world; + private readonly IChunkWorld? _world; - private AtmosWorldNeighborTopology( - AtmosWorld? world, + private WorldNeighborTopology( + IChunkWorld? world, bool includeCartesian, - Dictionary explicitByChunk, - ExplicitLinkDefinition[] explicitEdges) + Dictionary> explicitByChunk, + ExplicitLinkDefinition[] explicitEdges) { _world = world; _includeCartesian = includeCartesian; @@ -64,12 +115,12 @@ private AtmosWorldNeighborTopology( _explicitEdges = explicitEdges; } - internal static AtmosWorldNeighborTopology Empty { get; } = + public static WorldNeighborTopology Empty { get; } = new(null, false, [], []); - internal static AtmosWorldNeighborTopology EmptyFor(AtmosWorld world) + public static WorldNeighborTopology EmptyFor(IChunkWorld world) { - return new AtmosWorldNeighborTopology(world, false, [], []); + return new WorldNeighborTopology(world, false, [], []); } /// @@ -78,12 +129,12 @@ internal static AtmosWorldNeighborTopology EmptyFor(AtmosWorld world) /// The simulation that owns the chunk. /// The chunk to inspect. /// An allocation-free chunk-local neighborhood view. - public AtmosChunkNeighborView GetChunk(AtmosSimulation simulation, AtmosChunkHandle chunk) + public VoxelChunkNeighborView GetChunk(IChunkSimulation simulation, ChunkHandle chunk) { ArgumentNullException.ThrowIfNull(simulation); var world = GetWorld(); - if (!ReferenceEquals(simulation.World, world) || - !world.TryResolveCell(new AtmosCellRef(simulation.Id, chunk, 0), out _)) + if (!ReferenceEquals(simulation.ChunkWorld, world) || + !world.TryResolveCell(new VoxelRef(simulation.Id, chunk, 0))) { throw new ArgumentException("The chunk is not registered in this world.", nameof(chunk)); } @@ -97,14 +148,13 @@ public AtmosChunkNeighborView GetChunk(AtmosSimulation simulation, AtmosChunkHan var adjacentPosition = chunk.Position + GetDirection(direction); if (world.TryResolveCell( - new AtmosCellRef(simulation.Id, new AtmosChunkHandle(adjacentPosition), 0), - out _)) + new VoxelRef(simulation.Id, new ChunkHandle(adjacentPosition), 0))) { adjacentChunkMask |= checked((byte)(1 << direction)); } } - return new AtmosChunkNeighborView( + return new VoxelChunkNeighborView( simulation.Id, chunk, simulation.ChunkDimensions, @@ -119,10 +169,10 @@ public AtmosChunkNeighborView GetChunk(AtmosSimulation simulation, AtmosChunkHan /// A live cell in the callback's world. /// The number of selected Cartesian and explicit neighbors. /// Solid and void classifications do not remove structural adjacency. - public int GetNeighborCount(AtmosCellRef cell) + public int GetNeighborCount(VoxelRef cell) { var world = GetWorld(); - if (!world.TryGetSimulation(cell.Simulation, out var simulation) || simulation == null) + if (!world.TryGetChunkSimulation(cell.Simulation, out var simulation) || simulation == null) throw new ArgumentException("The cell does not identify a live simulation in this world.", nameof(cell)); return GetChunk(simulation, cell.Chunk).GetNeighborCount(cell.LocalVoxelIndex); @@ -133,10 +183,10 @@ public int GetNeighborCount(AtmosCellRef cell) /// /// A live cell in the callback's world. /// An allocation-free incident-neighbor enumerable. - public AtmosNeighborEnumerable GetNeighbors(AtmosCellRef cell) + public VoxelNeighborEnumerable GetNeighbors(VoxelRef cell) { var world = GetWorld(); - if (!world.TryGetSimulation(cell.Simulation, out var simulation) || simulation == null) + if (!world.TryGetChunkSimulation(cell.Simulation, out var simulation) || simulation == null) throw new ArgumentException("The cell does not identify a live simulation in this world.", nameof(cell)); return GetChunk(simulation, cell.Chunk).GetNeighbors(cell.LocalVoxelIndex); @@ -152,16 +202,16 @@ public AtmosNeighborEnumerable GetNeighbors(AtmosCellRef cell) /// membership on every call, and when the selection includes Cartesian neighbors it walks all six directions /// of every voxel in every chunk in the world to find them — for a selection built with /// includeCartesian: false, it only walks the sparse explicit edge list instead. A - /// performance-sensitive tiled solver should acquire once per chunk via - /// and call per voxel instead of + /// performance-sensitive tiled solver should acquire once per chunk via + /// and call per voxel instead of /// calling this every tick. /// - public IEnumerable GetOwnedEdges() + public IEnumerable> GetOwnedEdges() { var world = GetWorld(); if (_includeCartesian) { - foreach (var simulation in world.Simulations) + foreach (var simulation in world.ChunkSimulations) { foreach (var chunk in simulation.GetChunkHandles()) { @@ -173,17 +223,17 @@ public IEnumerable GetOwnedEdges() for (ushort voxelIndex = 0; voxelIndex < voxelCount; voxelIndex++) { - AtmosCellRef first = new(simulation.Id, chunk, voxelIndex); + VoxelRef first = new(simulation.Id, chunk, voxelIndex); for (int direction = 1; direction < 6; direction += 2) { if (!view.TryGetCartesianNeighbor(voxelIndex, direction, out var second)) continue; - yield return new AtmosNeighborEdge( + yield return new VoxelNeighborEdge( first, second, - AtmosNeighborKind.Cartesian, - ExplicitLinkFlags.None); + VoxelNeighborKind.Cartesian, + default); } } } @@ -192,23 +242,23 @@ public IEnumerable GetOwnedEdges() foreach (var edge in _explicitEdges) { - yield return new AtmosNeighborEdge( + yield return new VoxelNeighborEdge( edge.First, edge.Second, - AtmosNeighborKind.Explicit, + VoxelNeighborKind.Explicit, edge.Flags); } } - internal static AtmosWorldNeighborTopology Compile( - AtmosWorld world, - AtmosNeighborSelection selection, - IReadOnlyList links) + public static WorldNeighborTopology Compile( + IChunkWorld world, + VoxelNeighborSelection selection, + IReadOnlyList> links) { - var selected = new List(links.Count); + var selected = new List>(links.Count); foreach (var link in links) { - if (selection.ExplicitLinks(new AtmosExplicitLinkInfo(link.First, link.Second, link.Flags))) + if (selection.ExplicitLinks(new ExplicitLinkDefinition(link.First, link.Second, link.Flags))) selected.Add(link); } @@ -219,10 +269,10 @@ internal static AtmosWorldNeighborTopology Compile( Add(entries, link.Second, link.First, link.Flags); } - var chunks = new Dictionary(entries.Count); + var chunks = new Dictionary>(entries.Count); foreach ((var key, List neighbors) in entries) { - if (!world.TryGetSimulation(key.Simulation, out var simulation) || simulation == null) + if (!world.TryGetChunkSimulation(key.Simulation, out var simulation) || simulation == null) continue; int voxelCount = checked( @@ -237,7 +287,7 @@ internal static AtmosWorldNeighborTopology Compile( }); int[] starts = new int[voxelCount + 1]; - var values = new AtmosNeighbor[neighbors.Count]; + var values = new VoxelNeighbor[neighbors.Count]; int neighborIndex = 0; for (int voxelIndex = 0; voxelIndex < voxelCount; voxelIndex++) { @@ -250,17 +300,17 @@ internal static AtmosWorldNeighborTopology Compile( } starts[voxelCount] = neighborIndex; - chunks.Add(key, new CompiledChunkAdjacency(starts, values)); + chunks.Add(key, new CompiledChunkAdjacency(starts, values)); } - return new AtmosWorldNeighborTopology(world, selection.IncludeCartesian, chunks, selected.ToArray()); + return new WorldNeighborTopology(world, selection.IncludeCartesian, chunks, selected.ToArray()); } private static void Add( Dictionary> entries, - AtmosCellRef source, - AtmosCellRef neighbor, - ExplicitLinkFlags flags) + VoxelRef source, + VoxelRef neighbor, + T flags) { var key = new CompiledChunkKey(source.Simulation, source.Chunk); if (!entries.TryGetValue(key, out List? values)) @@ -272,10 +322,10 @@ private static void Add( values.Add( new CompiledNeighborEntry( source.LocalVoxelIndex, - new AtmosNeighbor(neighbor, AtmosNeighborKind.Explicit, flags))); + new VoxelNeighbor(neighbor, VoxelNeighborKind.Explicit, flags))); } - private AtmosWorld GetWorld() + private IChunkWorld GetWorld() { return _world ?? throw new InvalidOperationException("This solver was not registered with a neighbor selection."); } @@ -293,26 +343,34 @@ private static Int3 GetDirection(int direction) _ => throw new ArgumentOutOfRangeException(nameof(direction)) }; } + + private readonly record struct CompiledChunkKey( + SimulationId Simulation, + ChunkHandle Chunk); + + private readonly record struct CompiledNeighborEntry( + ushort SourceIndex, + VoxelNeighbor Neighbor); } /// /// Reusable topology view for one chunk. /// -public readonly struct AtmosChunkNeighborView +public readonly struct VoxelChunkNeighborView where T : struct, Enum { - private readonly CompiledChunkAdjacency? _explicitAdjacency; + private readonly CompiledChunkAdjacency? _explicitAdjacency; private readonly byte _adjacentChunkMask; private readonly bool _includeCartesian; - private readonly AtmosChunkHandle _chunk; + private readonly ChunkHandle _chunk; private readonly Int3 _dimensions; - private readonly AtmosSimulationId _simulation; + private readonly SimulationId _simulation; - internal AtmosChunkNeighborView( - AtmosSimulationId simulation, - AtmosChunkHandle chunk, + internal VoxelChunkNeighborView( + SimulationId simulation, + ChunkHandle chunk, Int3 dimensions, bool includeCartesian, - CompiledChunkAdjacency? explicitAdjacency, + CompiledChunkAdjacency? explicitAdjacency, byte adjacentChunkMask) { _simulation = simulation; @@ -354,18 +412,18 @@ public int GetNeighborCount(ushort localVoxelIndex) /// /// The source voxel's chunk-local index. /// An allocation-free incident-neighbor enumerable. - public AtmosNeighborEnumerable GetNeighbors(ushort localVoxelIndex) + public VoxelNeighborEnumerable GetNeighbors(ushort localVoxelIndex) { ValidateIndex(localVoxelIndex); int explicitStart = _explicitAdjacency?.Starts[localVoxelIndex] ?? 0; int explicitEnd = _explicitAdjacency?.Starts[localVoxelIndex + 1] ?? 0; - return new AtmosNeighborEnumerable(this, localVoxelIndex, explicitStart, explicitEnd); + return new VoxelNeighborEnumerable(this, localVoxelIndex, explicitStart, explicitEnd); } internal bool TryGetCartesianNeighbor( ushort localVoxelIndex, int direction, - out AtmosCellRef neighbor) + out VoxelRef neighbor) { if (!_includeCartesian) { @@ -466,11 +524,11 @@ internal bool TryGetCartesianNeighbor( } ushort targetIndex = checked((ushort)(x + y * _dimensions.X + z * plane)); - neighbor = new AtmosCellRef(_simulation, new AtmosChunkHandle(chunkPosition), targetIndex); + neighbor = new VoxelRef(_simulation, new ChunkHandle(chunkPosition), targetIndex); return true; } - internal AtmosNeighbor GetExplicitNeighbor(int index) + internal VoxelNeighbor GetExplicitNeighbor(int index) { return _explicitAdjacency!.Neighbors[index]; } @@ -486,15 +544,15 @@ private void ValidateIndex(ushort localVoxelIndex) /// /// Allocation-free enumerable over one cell's structural neighbors. /// -public readonly struct AtmosNeighborEnumerable +public readonly struct VoxelNeighborEnumerable where T : struct, Enum { private readonly int _explicitEnd; private readonly int _explicitStart; private readonly ushort _localVoxelIndex; - private readonly AtmosChunkNeighborView _view; + private readonly VoxelChunkNeighborView _view; - internal AtmosNeighborEnumerable( - AtmosChunkNeighborView view, + internal VoxelNeighborEnumerable( + VoxelChunkNeighborView view, ushort localVoxelIndex, int explicitStart, int explicitEnd) @@ -509,25 +567,25 @@ internal AtmosNeighborEnumerable( /// Creates an enumerator. /// /// An allocation-free enumerator over this cell's neighbors. - public AtmosNeighborEnumerator GetEnumerator() + public VoxelNeighborEnumerator GetEnumerator() { - return new AtmosNeighborEnumerator(_view, _localVoxelIndex, _explicitStart, _explicitEnd); + return new VoxelNeighborEnumerator(_view, _localVoxelIndex, _explicitStart, _explicitEnd); } } /// /// Allocation-free enumerator over one cell's structural neighbors. /// -public struct AtmosNeighborEnumerator +public struct VoxelNeighborEnumerator where T : struct, Enum { private readonly int _explicitEnd; private readonly ushort _localVoxelIndex; - private readonly AtmosChunkNeighborView _view; + private readonly VoxelChunkNeighborView _view; private int _direction; private int _explicitIndex; - internal AtmosNeighborEnumerator( - AtmosChunkNeighborView view, + internal VoxelNeighborEnumerator( + VoxelChunkNeighborView view, ushort localVoxelIndex, int explicitStart, int explicitEnd) @@ -543,7 +601,7 @@ internal AtmosNeighborEnumerator( /// /// Gets the current neighbor. /// - public AtmosNeighbor Current { get; private set; } + public VoxelNeighbor Current { get; private set; } /// /// Advances to the next neighbor. @@ -557,7 +615,7 @@ public bool MoveNext() if (!_view.TryGetCartesianNeighbor(_localVoxelIndex, direction, out var cell)) continue; - Current = new AtmosNeighbor(cell, AtmosNeighborKind.Cartesian, ExplicitLinkFlags.None); + Current = new VoxelNeighbor(cell, VoxelNeighborKind.Cartesian, default); return true; } @@ -569,23 +627,15 @@ public bool MoveNext() } } -internal readonly record struct CompiledChunkKey( - AtmosSimulationId Simulation, - AtmosChunkHandle Chunk); - -internal readonly record struct CompiledNeighborEntry( - ushort SourceIndex, - AtmosNeighbor Neighbor); - -internal sealed class CompiledChunkAdjacency( +internal sealed class CompiledChunkAdjacency( int[] starts, - AtmosNeighbor[] neighbors) + VoxelNeighbor[] neighbors) where T : struct, Enum { internal int[] Starts { get; } = starts; - internal AtmosNeighbor[] Neighbors { get; } = neighbors; + internal VoxelNeighbor[] Neighbors { get; } = neighbors; internal int GetCount(ushort localVoxelIndex) { return Starts[localVoxelIndex + 1] - Starts[localVoxelIndex]; } -} \ No newline at end of file +} diff --git a/src/Numos.Collections/FlatArray.cs b/src/Numos.Collections/FlatArray.cs index bd0f874..5733824 100644 --- a/src/Numos.Collections/FlatArray.cs +++ b/src/Numos.Collections/FlatArray.cs @@ -112,10 +112,9 @@ public int GetIndex(Int3 position) /// public int GetIndexUnsafe(Int3 position) { - return position.X + position.Y * _dimensions.X + position.Z * _dimensions.X * _dimensions.Y; + return FlatArrayHelpers.GetIndexUnsafe(position, _dimensions); } - - + /// /// Converts a flat array index to its coordinate. /// @@ -123,11 +122,8 @@ public Int3 GetPosition(int index) { if ((uint)index >= (uint)Length) throw new IndexOutOfRangeException(); - - return new Int3( - index % _dimensions.X, - index / _dimensions.X % _dimensions.Y, - index / (_dimensions.X * _dimensions.Y)); + + return FlatArrayHelpers.GetPosition(index, _dimensions); } /// @@ -189,3 +185,36 @@ public T[] ToArray() return [.. _data]; } } + +public static class FlatArrayHelpers +{ + /// + /// Converts a coordinate to its flat array index. + /// + public static int GetIndex(Int3 position, Int3 dimensions) + { + if (!position.IsWithin(dimensions)) + throw new IndexOutOfRangeException(); + + return position.X + position.Y * dimensions.X + position.Z * dimensions.X * dimensions.Y; + } + + /// + /// Converts a coordinate to its flat array index. + /// + public static int GetIndexUnsafe(Int3 position, Int3 dimensions) + { + return position.X + position.Y * dimensions.X + position.Z * dimensions.X * dimensions.Y; + } + + /// + /// Converts a flat array index to its coordinate. + /// + public static Int3 GetPosition(int index, Int3 dimensions) + { + return new Int3( + index % dimensions.X, + index / dimensions.X % dimensions.Y, + index / (dimensions.X * dimensions.Y)); + } +} diff --git a/src/Numos.CoreSim/AtmosChunk.cs b/src/Numos.CoreSim/AtmosChunk.cs index 377ca72..a8f2eed 100644 --- a/src/Numos.CoreSim/AtmosChunk.cs +++ b/src/Numos.CoreSim/AtmosChunk.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using JetBrains.Annotations; +using Numos.Chunks; using Numos.Collections; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.Datatypes.Snapshots; @@ -16,10 +17,10 @@ namespace Numos.CoreSim; /// /// /// Chunk-owned per-voxel data supports both flat-index and coordinate access. -/// Use and when converting indices +/// Use and when converting indices /// for scalar-indexed storage such as gas channels (because... you know.... they aren't physical). /// -internal class AtmosChunk +internal sealed class AtmosChunk : Chunk, IChunkInitializer { private static long _nextGeneration; @@ -50,36 +51,7 @@ internal class AtmosChunk /// one moles value for every voxel in the chunk. /// public GasChannel[] ActiveGases; - - /// - /// A process-local index assigned when this chunk is registered. - /// - /// - /// Lets hot per-tick solver lookups (e.g. boundary flow's cross-chunk injection buffer) use array - /// indexing instead of a dictionary keyed on . Unique only among currently - /// registered chunks, and not part of the simulation's observable state, so it is absent from - /// and the state hash. Do not use it for anything but indexing a - /// solver-owned lookup table. - /// Ids are recycled after a chunk is unregistered, so a DenseId-keyed lookup table must track which - /// chunk currently owns each slot and validate that before trusting stale contents. - /// - public int DenseId; - - /// - /// The number of voxels along the z-axis. - /// - public int Depth; - - /// - /// The position of this chunk in the chunk grid. - /// - public Int3 GridPosition; - - /// - /// The number of voxels along the y-axis. - /// - public int Height; - + /// /// Whether this chunk is eligible to be processed by the simulation. /// A sleeping chunk is skipped during simulation ticks. @@ -126,11 +98,6 @@ internal class AtmosChunk [ElementQuantity("pressure")] public FlatArray TotalPressure; - /// - /// Total number of voxels in this chunk, equal to Width * Height * Depth. - /// - public int VoxelCount; - /// /// Room classification for each voxel, indexed by flat voxel index or local coordinate. /// @@ -145,11 +112,6 @@ internal class AtmosChunk /// public FlatArray VoxelRoomMap; - /// - /// The number of voxels along the x-axis. - /// - public int Width; - private long _generation; private long _revision; private Dictionary? _solverArrays; @@ -162,12 +124,12 @@ internal class AtmosChunk /// The number of voxels along the z axis. /// /// A dimension is non-positive or the combined voxel count exceeds - /// . + /// . /// public AtmosChunk( - int width = AtmosChunkConstants.DefaultWidth, - int height = AtmosChunkConstants.DefaultHeight, - int depth = AtmosChunkConstants.DefaultDepth) + int width = ChunkConstants.DefaultWidth, + int height = ChunkConstants.DefaultHeight, + int depth = ChunkConstants.DefaultDepth) { int voxelCount = GetValidatedVoxelCount(width, height, depth); Width = width; @@ -185,11 +147,6 @@ public AtmosChunk( /// public AtmosChunkVersion Version => new(_generation, Interlocked.Read(ref _revision)); - /// - /// The number of voxels along each axis. - /// - public Int3 Dimensions => new(Width, Height, Depth); - /// /// Ensures that the chunk's per-voxel arrays are initialized for its current dimensions. /// @@ -223,7 +180,7 @@ public void EnsureInitialized() /// The depth of the chunk. /// /// A dimension is non-positive or the combined voxel count exceeds - /// . + /// . /// /// /// Initialization puts the chunk to sleep, resets all active counts and timers, and clears @@ -232,9 +189,9 @@ public void EnsureInitialized() [PublicAPI] public void Initialize( Int3 position, - int width = AtmosChunkConstants.DefaultWidth, - int height = AtmosChunkConstants.DefaultHeight, - int depth = AtmosChunkConstants.DefaultDepth) + int width = ChunkConstants.DefaultWidth, + int height = ChunkConstants.DefaultHeight, + int depth = ChunkConstants.DefaultDepth) { int voxelCount = GetValidatedVoxelCount(width, height, depth); _solverArrays = null; @@ -277,7 +234,7 @@ public void MarkChanged() /// /// After releasing a chunk, do not use its active gas channels until they have been initialized again. /// - public void Release() + public override void Release() { _solverArrays = null; if (ActiveGases != null) @@ -381,7 +338,7 @@ internal void RestoreSolverArrays(IReadOnlyList snapsh /// Every non-solid, non-void voxel participates while the chunk is awake. Classification IDs do not /// partition simulation work. /// - public virtual void Wake() + public void Wake() { IsAwake = true; SleepTimer = 0; @@ -413,7 +370,7 @@ public void RebuildActiveAirIndices() /// /// Marks the chunk as sleeping so that it is skipped by simulation ticks. /// - public virtual void Sleep() + public void Sleep() { IsAwake = false; MarkChanged(); @@ -508,7 +465,6 @@ public bool TryGetThermalState( return true; } - /// /// Sets a specific voxel to a vacuum. This sets TotalPressure, ActiveGases, and TotalHeatCapacity to 0 and IsVacuum to /// true. @@ -544,7 +500,6 @@ public void SetChunkToVacuum() IsVacuum.Fill(true); } - /// /// Sets a specific voxel classification. Solid and void classifications clear the voxel to vacuum. /// @@ -573,7 +528,6 @@ public void SetVoxelClassification(ushort idx, VoxelClassification classificatio VoxelRoomMap[idx] = classification.RoomId; } - /// /// Sets every voxel classification. Solid and void classifications clear the chunk to vacuum. /// @@ -587,7 +541,6 @@ public void SetChunkClassification(int roomId) VoxelRoomMap.Fill(roomId); } - /// /// Sets the entire chunk classification. Solid and void classifications clear the chunk to vacuum. /// @@ -685,90 +638,14 @@ public AtmosChunkSnapshot GetNetworkSnapshot( return snapshot; } - /// - /// Converts local voxel coordinates to an index into the chunk's flat arrays. - /// - /// The local x coordinate, from zero through minus one. - /// The local y coordinate, from zero through minus one. - /// The local z coordinate, from zero through minus one. - /// The flat voxel index. - [PublicAPI] - public ushort GetIndex(int x, int y, int z) - { - return GetIndex(new Int3(x, y, z)); - } - - /// - [PublicAPI] - public ushort GetIndex(Int3 vec) - { - return (ushort)VoxelRoomMap.GetIndex(vec); - } - - /// - [PublicAPI] - public ushort GetIndexUnsafe(Int3 vec) - { - return (ushort)VoxelRoomMap.GetIndexUnsafe(vec); - } - - - /// - /// Converts a flat voxel index to local x, y, and z coordinates. - /// - /// The flat voxel index. - /// The local coordinates as an (x, y, z) tuple. - [PublicAPI] - public (int x, int y, int z) GetXyz(ushort index) - { - var position = GetXyzInt3(index); - return (position.X, position.Y, position.Z); - } - - /// - /// Converts a flat voxel index to local coordinates as an . - /// - /// The flat voxel index. - /// The local voxel coordinates. - [PublicAPI] - public Int3 GetXyzInt3(ushort index) - { - return VoxelRoomMap.GetPosition(index); - } - - private void EnsureInitialized(ref FlatArray array, Int3 dimensions) - { - if (!array.IsInitialized || array.Length != VoxelCount) - array = new FlatArray(new T[VoxelCount], dimensions); - else if (array.Dimensions != dimensions) - array = array.Reshape(dimensions); - } - - private static int GetValidatedVoxelCount(int width, int height, int depth) + public static AtmosChunk CreateInitializeChunk( + Int3 position, + int width = ChunkConstants.DefaultWidth, + int height = ChunkConstants.DefaultHeight, + int depth = ChunkConstants.DefaultDepth) { - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(width); - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(height); - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(depth); - if (width > AtmosChunkConstants.MaximumVoxelCount || - height > AtmosChunkConstants.MaximumVoxelCount || - depth > AtmosChunkConstants.MaximumVoxelCount) - { - throw new ArgumentOutOfRangeException( - nameof(width), - width, - $"No chunk dimension may exceed {AtmosChunkConstants.MaximumVoxelCount}."); - } - - long voxelCount = (long)width * height * depth; - if (voxelCount > AtmosChunkConstants.MaximumVoxelCount) - { - throw new ArgumentOutOfRangeException( - nameof(width), - width, - $"Chunk dimensions contain {voxelCount} voxels, but at most " + - $"{AtmosChunkConstants.MaximumVoxelCount} are supported."); - } - - return (int)voxelCount; + var chunk = new AtmosChunk(width, height, depth); + chunk.Initialize(position, width, height, depth); + return chunk; } -} \ No newline at end of file +} diff --git a/src/Numos.CoreSim/AtmosChunkConstants.cs b/src/Numos.CoreSim/AtmosChunkConstants.cs index 64eeced..dd9787e 100644 --- a/src/Numos.CoreSim/AtmosChunkConstants.cs +++ b/src/Numos.CoreSim/AtmosChunkConstants.cs @@ -5,20 +5,7 @@ namespace Numos.CoreSim; /// public static class AtmosChunkConstants { - /// Default number of voxels along a chunk's x-axis. - public const int DefaultWidth = 16; - - /// Default number of voxels along a chunk's y-axis. - public const int DefaultHeight = 16; - - /// Default number of voxels along a chunk's z-axis. - public const int DefaultDepth = 16; - - /// Initial number of distinct gas-channel slots allocated by a chunk. /// The channel table grows when a mixture introduces additional gas IDs. public const int InitialGasChannelCapacity = 16; - - /// Maximum voxel count representable by the chunk's unsigned 16-bit flat indices. - public const int MaximumVoxelCount = ushort.MaxValue; } \ No newline at end of file diff --git a/src/Numos.CoreSim/AtmosKernel.API.cs b/src/Numos.CoreSim/AtmosKernel.API.cs index d8d7458..5b0266e 100644 --- a/src/Numos.CoreSim/AtmosKernel.API.cs +++ b/src/Numos.CoreSim/AtmosKernel.API.cs @@ -1,3 +1,4 @@ +using Numos.Chunks; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.Datatypes.Snapshots; using Numos.CoreSim.Replay; @@ -18,7 +19,7 @@ internal int ChunkCount { lock (StateGate) { - return _chunkMap.Count; + return _chunkMap.ChunkCount; } } } @@ -41,7 +42,7 @@ internal Int3[] GetChunkPositions() { lock (StateGate) { - return _chunkMap.Keys.ToArray(); + return _chunkMap.GetChunkPositions(); } } @@ -66,14 +67,14 @@ internal bool TryGetChunkPositions( { lock (StateGate) { - revision = _chunkCollectionRevision; + revision = _chunkMap.CollectionRevision; if (revision == knownRevision) { positions = []; return false; } - positions = _chunkMap.Keys.ToArray(); + positions = _chunkMap.GetChunkPositions(); return true; } } @@ -105,7 +106,7 @@ internal void Update(Second elapsedSeconds) // One elapsed-time update is one externally atomic batch. Solver callbacks may edit the pipeline, but // chunk lifecycle changes are rejected until the batch completes. - AtmosChunk[] chunks = OrderedChunks(); + AtmosChunk[] chunks = _chunkMap.OrderedChunks(); int steps = 0; while (_accumulator >= AtmosSolverConstants.FixedTimeStep && steps < AtmosSolverConstants.MaximumStepsPerUpdate) @@ -260,15 +261,8 @@ internal void RegisterChunk(AtmosChunk chunk) lock (StateGate) { ThrowIfTickExecuting("register a chunk during the current tick"); - if (chunk.Dimensions != _dimensions) - throw new ArgumentException("Chunk dimensions must match the simulation.", nameof(chunk)); - - if (!_chunkMap.TryAdd(chunk.GridPosition, chunk)) - throw new InvalidOperationException($"A chunk is already registered at {chunk.GridPosition}."); - - chunk.DenseId = _freeChunkDenseIds.Count > 0 ? _freeChunkDenseIds.Pop() : _nextChunkDenseId++; + _chunkMap.RegisterChunk(chunk); WakeSleepingNeighbors(chunk.GridPosition); - _chunkCollectionRevision++; if (ShouldRecord) RecordOperation(new CreateChunkOperation(chunk.GridPosition)); } } @@ -283,12 +277,9 @@ internal bool UnregisterChunk(Int3 position) lock (StateGate) { ThrowIfTickExecuting("unregister a chunk used by the current tick"); - if (!_chunkMap.TryRemove(position, out var chunk)) + if (!_chunkMap.UnregisterChunk(position)) return false; - - chunk.Release(); - _freeChunkDenseIds.Push(chunk.DenseId); - _chunkCollectionRevision++; + if (ShouldRecord) RecordOperation(new RemoveChunkOperation(position)); return true; } @@ -298,20 +289,13 @@ internal bool UnregisterChunk(Int3 position) /// Creates, initializes, and registers a chunk owned by this kernel. /// /// The chunk's position in the chunk grid. - /// The number of voxels along the local x-axis. - /// The number of voxels along the local y-axis. - /// The number of voxels along the local z-axis. /// A chunk is already registered at . - internal void CreateAndRegisterChunk(Int3 position, int width, int height, int depth) + internal void CreateAndRegisterChunk(Int3 position) { lock (StateGate) { ThrowIfTickExecuting("register a chunk during the current tick"); - if (_chunkMap.ContainsKey(position)) - throw new InvalidOperationException($"A chunk is already registered at {position}."); - - var chunk = new AtmosChunk(width, height, depth); - chunk.Initialize(position, width, height, depth); + var chunk = _chunkMap.CreateAndRegisterChunk(position); RegisterChunk(chunk); } } @@ -370,7 +354,7 @@ private static AtmosVoxelSnapshot CreateVoxelSnapshot( Int3 position, ushort localVoxelIndex) { - ValidateVoxelIndex(chunk, localVoxelIndex); + ChunkMap.ValidateVoxelIndex(chunk, localVoxelIndex); var gases = new VoxelGasSnapshot[chunk.ActiveGasCount]; for (int gas = 0; gas < gases.Length; gas++) { @@ -461,7 +445,7 @@ internal AtmosChunkSnapshotBatch GetChangedChunkSnapshots( var request = requests[index]; // Handle lists are detached. A concurrent unregistration between enumeration // and this batch is represented by the chunk simply not being returned. - if (!_chunkMap.TryGetValue(request.Position, out var chunk) || + if (!_chunkMap.TryGetChunk(request.Position, out var chunk) || chunk.Version == request.KnownVersion && !(request.Fields.HasFlag(AtmosChunkSnapshotFields.SolverArrays) && chunk.HasCapturedSolverArrays)) { @@ -546,7 +530,7 @@ internal void SetVoxelClassification( lock (StateGate) { var chunk = GetChunk(position); - ValidateVoxelIndex(chunk, localVoxelIndex); + ChunkMap.ValidateVoxelIndex(chunk, localVoxelIndex); chunk.SetVoxelClassification(localVoxelIndex, classification); RebuildActiveTopology(chunk); chunk.MarkChanged(); @@ -571,7 +555,7 @@ internal void SetVoxelClassification( lock (StateGate) { var chunk = GetChunk(position); - SetVoxelClassification(position, GetValidatedVoxelIndex(chunk, x, y, z), classification); + SetVoxelClassification(position, ChunkMap.GetValidatedVoxelIndex(chunk, x, y, z), classification); } } @@ -588,7 +572,7 @@ internal void SetVoxelTemperature(Int3 position, ushort localVoxelIndex, Kelvin lock (StateGate) { var chunk = GetChunk(position); - ValidateVoxelIndex(chunk, localVoxelIndex); + ChunkMap.ValidateVoxelIndex(chunk, localVoxelIndex); chunk.Temperature[localVoxelIndex] = temperature; chunk.TotalPressure[localVoxelIndex] = AtmosSolverMath.CalculatePressureAtVoxel(_config, chunk, localVoxelIndex); @@ -613,7 +597,7 @@ internal void SetVoxelTemperature(Int3 position, int x, int y, int z, Kelvin tem lock (StateGate) { var chunk = GetChunk(position); - SetVoxelTemperature(position, GetValidatedVoxelIndex(chunk, x, y, z), temperature); + SetVoxelTemperature(position, ChunkMap.GetValidatedVoxelIndex(chunk, x, y, z), temperature); } } @@ -635,7 +619,7 @@ internal void AddGasToVoxel( lock (StateGate) { var chunk = GetChunk(position); - ValidateVoxelIndex(chunk, localVoxelIndex); + ChunkMap.ValidateVoxelIndex(chunk, localVoxelIndex); ValidateGasInjection(gasId, moles, temperature); int classification = chunk.VoxelRoomMap[localVoxelIndex]; @@ -668,7 +652,7 @@ internal void AddGasToVoxel( lock (StateGate) { var chunk = GetChunk(position); - AddGasToVoxel(position, GetValidatedVoxelIndex(chunk, x, y, z), gasId, moles, temperature); + AddGasToVoxel(position, ChunkMap.GetValidatedVoxelIndex(chunk, x, y, z), gasId, moles, temperature); } } @@ -708,14 +692,14 @@ internal void Tick() { lock (StateGate) { - AtmosChunk[] chunks = OrderedChunks(); + AtmosChunk[] chunks = _chunkMap.OrderedChunks(); TickSimulation(chunks); } } private AtmosChunk GetChunk(Int3 position) { - if (_chunkMap.TryGetValue(position, out var chunk)) + if (_chunkMap.TryGetChunk(position, out var chunk)) return chunk; throw new KeyNotFoundException($"No atmospheric chunk is registered at ({position.X}, {position.Y}, {position.Z})."); @@ -739,41 +723,10 @@ private void WakeSleepingNeighbors(Int3 position) private void WakeSleepingChunk(Int3 position) { - if (_chunkMap.TryGetValue(position, out var chunk) && !chunk.IsAwake) + if (_chunkMap.TryGetChunk(position, out var chunk) && !chunk.IsAwake) chunk.Wake(); } - private static ushort GetValidatedVoxelIndex(AtmosChunk chunk, int x, int y, int z) - { - if (x < 0 || x >= chunk.Width) - throw new ArgumentOutOfRangeException(nameof(x)); - - if (y < 0 || y >= chunk.Height) - throw new ArgumentOutOfRangeException(nameof(y)); - - if (z < 0 || z >= chunk.Depth) - throw new ArgumentOutOfRangeException(nameof(z)); - - return chunk.GetIndex(x, y, z); - } - - /// - /// Validates that the given local voxel index is within the bounds of the chunk's voxel array. - /// - /// The chunk to validate against. - /// The local voxel index to validate. - /// Thrown if the local voxel index is out of bounds. - private static void ValidateVoxelIndex(AtmosChunk chunk, ushort localVoxelIndex) - { - if (localVoxelIndex >= chunk.VoxelCount) - { - throw new ArgumentOutOfRangeException( - nameof(localVoxelIndex), - localVoxelIndex, - $"Voxel index must be less than the chunk's voxel count ({chunk.VoxelCount})."); - } - } - private void ValidateGasInjection(int gasId, Mole moles, Kelvin temperature) { if ((uint)gasId >= (uint)_config.GasRegistry.Count) @@ -790,4 +743,4 @@ private void ValidateGasInjection(int gasId, Mole moles, Kelvin temperature) "Temperature must be nonnegative and finite."); } } -} \ No newline at end of file +} diff --git a/src/Numos.CoreSim/AtmosKernel.GasMixtures.cs b/src/Numos.CoreSim/AtmosKernel.GasMixtures.cs index cb59360..1fd7a14 100644 --- a/src/Numos.CoreSim/AtmosKernel.GasMixtures.cs +++ b/src/Numos.CoreSim/AtmosKernel.GasMixtures.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using Numos.Chunks; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.Solvers; using Numos.Maths; @@ -48,7 +49,7 @@ internal void ExecuteMixtureTransaction(Action transaction) lock (StateGate) { var chunk = GetChunk(position); - ValidateVoxelIndex(chunk, localVoxelIndex); + ChunkMap.ValidateVoxelIndex(chunk, localVoxelIndex); return (chunk.Version.Generation, localVoxelIndex); } } @@ -65,7 +66,7 @@ internal void ExecuteMixtureTransaction(Action transaction) lock (StateGate) { var chunk = GetChunk(position); - ushort localVoxelIndex = GetValidatedVoxelIndex(chunk, x, y, z); + ushort localVoxelIndex = ChunkMap.GetValidatedVoxelIndex(chunk, x, y, z); return (chunk.Version.Generation, localVoxelIndex); } } @@ -580,7 +581,7 @@ private AtmosChunk GetMixtureChunk(Int3 position, long generation, ushort localV "The voxel gas mixture is stale because its original chunk was unregistered or replaced."); } - ValidateVoxelIndex(chunk, localVoxelIndex); + ChunkMap.ValidateVoxelIndex(chunk, localVoxelIndex); return chunk; } diff --git a/src/Numos.CoreSim/AtmosKernel.Replay.cs b/src/Numos.CoreSim/AtmosKernel.Replay.cs index e8afca2..66648de 100644 --- a/src/Numos.CoreSim/AtmosKernel.Replay.cs +++ b/src/Numos.CoreSim/AtmosKernel.Replay.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Diagnostics; +using Numos.Chunks; using Numos.CoreSim.Replay; using Numos.Maths; @@ -7,7 +8,6 @@ namespace Numos.CoreSim; internal sealed partial class AtmosKernel { - private readonly Int3 _dimensions; private Action? _externalOperationSink; private bool _isApplyingOperation; private bool _isReplaying; @@ -94,11 +94,11 @@ internal AtmosSimulationCheckpoint CaptureCheckpoint() { ThrowIfTickExecuting("capture a checkpoint during a tick"); return new AtmosSimulationCheckpoint( - _dimensions, + _chunkMap.Dimensions, TimelinePosition, _config, _solverCheckpointProvider(), - OrderedChunks().Select(static chunk => new AtmosChunkCheckpoint(chunk)).ToArray()); + _chunkMap.OrderedChunks().Select(static chunk => new AtmosChunkCheckpoint(chunk)).ToArray()); } } @@ -160,7 +160,7 @@ internal void ValidateCheckpoint(AtmosSimulationCheckpoint checkpoint) ArgumentNullException.ThrowIfNull(checkpoint); if (checkpoint.FormatVersion != AtmosSimulationCheckpoint.CurrentFormatVersion || checkpoint.CompatibilityVersion != AtmosSimulationCheckpoint.CurrentCompatibilityVersion || - checkpoint.Dimensions != _dimensions || + checkpoint.Dimensions != _chunkMap.Dimensions || checkpoint.Position.Tick > int.MaxValue) { throw new ArgumentException( @@ -185,7 +185,7 @@ internal void ValidateCheckpoint(AtmosSimulationCheckpoint checkpoint) var positions = new HashSet(); foreach (var chunk in checkpoint.Chunks) { - if (chunk.Dimensions != _dimensions || !positions.Add(chunk.Position)) + if (chunk.Dimensions != _chunkMap.Dimensions || !positions.Add(chunk.Position)) throw new ArgumentException("The checkpoint contains incompatible or duplicate chunks.", nameof(checkpoint)); } } @@ -213,13 +213,13 @@ private void InstallCheckpoint(AtmosSimulationCheckpoint checkpoint) throw; } - ConcurrentDictionary previous = _chunkMap; - _chunkMap = replacement; + ConcurrentDictionary previous = _chunkMap.UnsafeGetStorage(); + _chunkMap.UnsafeReplaceStorage(replacement); // Rebase on the restored chunk count instead of continuing to grow across repeated restores // (e.g. replay scrubbing), which would otherwise leave DenseId-indexed lookup tables oversized. // Ids are reassigned densely as 0..replacement.Count above, so none are free. - _nextChunkDenseId = replacement.Count; - _freeChunkDenseIds.Clear(); + _chunkMap.NextDenseId = replacement.Count; + _chunkMap.FreeDenseIds.Clear(); _config = checkpoint.Config; CurrentTickConfig.Capture(_config); CurrentTickConfig.ClearGasSolverData(); @@ -231,7 +231,7 @@ private void InstallCheckpoint(AtmosSimulationCheckpoint checkpoint) _setWorldSolverEnabled(step.Name, step.Enabled); _defaultSolvers.ClearTransientState(); - _chunkCollectionRevision++; + _chunkMap.CollectionRevision++; LastBoundaryTicks = 0; foreach (var chunk in previous.Values) chunk.Release(); @@ -357,7 +357,7 @@ private void Apply(SetAtmosConfigOperation op) private void Apply(CreateChunkOperation op) { - CreateAndRegisterChunk(op.Position, _dimensions.X, _dimensions.Y, _dimensions.Z); + CreateAndRegisterChunk(op.Position); } private void Apply(RemoveChunkOperation op) @@ -409,7 +409,7 @@ private void Apply(SetSolverEnabledOperation op) private void Apply(SetVoxelMixtureOperation op) { var chunk = GetChunk(op.Position); - ValidateVoxelIndex(chunk, op.LocalVoxelIndex); + ChunkMap.ValidateVoxelIndex(chunk, op.LocalVoxelIndex); chunk.Wake(); foreach (var gas in op.Gases) chunk.ActiveGases[chunk.GetOrCreateGasChannel(gas.GasId)].Moles[op.LocalVoxelIndex] = gas.Moles; @@ -425,12 +425,4 @@ private void ApplyOperation(AtmosOperation operation) { ApplyRecordedOperation(operation); } - - private AtmosChunk[] OrderedChunks() - { - return _chunkMap.Values - .OrderBy(static chunk => chunk.GridPosition.X) - .ThenBy(static chunk => chunk.GridPosition.Y) - .ThenBy(static chunk => chunk.GridPosition.Z).ToArray(); - } -} \ No newline at end of file +} diff --git a/src/Numos.CoreSim/AtmosKernel.cs b/src/Numos.CoreSim/AtmosKernel.cs index 5a7f91d..3c19dd5 100644 --- a/src/Numos.CoreSim/AtmosKernel.cs +++ b/src/Numos.CoreSim/AtmosKernel.cs @@ -1,5 +1,5 @@ -using System.Collections.Concurrent; using System.Diagnostics; +using Numos.Chunks; using Numos.CoreSim.Replay; using Numos.CoreSim.Solvers; using Numos.Maths; @@ -12,9 +12,6 @@ namespace Numos.CoreSim; internal sealed partial class AtmosKernel : IDisposable { private readonly DefaultAtmosSolvers _defaultSolvers; - // Reused before minting a new id, so DenseId stays bounded by peak concurrent chunk count rather - // than growing forever. See AtmosChunk.DenseId's remarks on why recycling ids is safe. - private readonly Stack _freeChunkDenseIds = new(); private readonly List _recordedOperations = []; private readonly SolverDataStorage _solverData = new(); @@ -27,25 +24,23 @@ internal sealed partial class AtmosKernel : IDisposable /// Number of completed fixed ticks on the owning world timeline. /// internal int TickCount; - private Second _accumulator; - private long _chunkCollectionRevision; - private ConcurrentDictionary _chunkMap = new(); + private ChunkMap _chunkMap; private AtmosConfigSnapshot _config = new AtmosConfig().CreateSnapshot(); private bool _hasRecording; private bool _isRecording; private bool _isTickExecuting; private ulong _lastOperationSequence; - private int _nextChunkDenseId; + private AtmosTimelinePosition _recordingHead; private AtmosTimelinePosition _recordingStart; internal AtmosKernel( - int chunkWidth = AtmosChunkConstants.DefaultWidth, - int chunkHeight = AtmosChunkConstants.DefaultHeight, - int chunkDepth = AtmosChunkConstants.DefaultDepth) + int chunkWidth = ChunkConstants.DefaultWidth, + int chunkHeight = ChunkConstants.DefaultHeight, + int chunkDepth = ChunkConstants.DefaultDepth) { - _dimensions = new Int3(chunkWidth, chunkHeight, chunkDepth); + _chunkMap = new ChunkMap(chunkWidth, chunkHeight, chunkDepth); _defaultSolvers = new DefaultAtmosSolvers(chunkWidth, chunkHeight, chunkDepth); CurrentTickConfig.Capture(_config); } @@ -65,10 +60,8 @@ public void Dispose() lock (StateGate) { ThrowIfTickExecuting("dispose the simulation"); - foreach (var chunk in _chunkMap.Values) - chunk.Release(); - - _chunkMap.Clear(); + + _chunkMap.Dispose(); CurrentTickConfig.ClearGasSolverData(); _solverData.Clear(); _defaultSolvers.Dispose(); @@ -77,7 +70,7 @@ public void Dispose() internal bool TryGetChunk(Int3 position, out AtmosChunk chunk) { - return _chunkMap.TryGetValue(position, out chunk!); + return _chunkMap.TryGetChunk(position, out chunk!); } /// @@ -94,7 +87,7 @@ internal bool TryResolveExplicitEndpoint( { lock (StateGate) { - if (!_chunkMap.TryGetValue(chunkPosition, out var chunk) || + if (!_chunkMap.TryGetChunk(chunkPosition, out var chunk) || localVoxelIndex >= chunk.VoxelCount) { endpoint = default; @@ -131,7 +124,7 @@ internal AtmosWorldTickExecution BeginWorldTick() { lock (StateGate) { - return new AtmosWorldTickExecution(BeginTickSimulation(OrderedChunks())); + return new AtmosWorldTickExecution(BeginTickSimulation(_chunkMap.OrderedChunks())); } } @@ -214,4 +207,4 @@ private readonly record struct ThermalBoundaryConductance( JoulePerKelvin Conductance); private readonly record struct ThermalBoundaryState(Kelvin Temperature, JoulePerKelvin HeatCapacity); -} \ No newline at end of file +} diff --git a/src/Numos.CoreSim/Numos.CoreSim.csproj b/src/Numos.CoreSim/Numos.CoreSim.csproj index e5a806d..d194155 100644 --- a/src/Numos.CoreSim/Numos.CoreSim.csproj +++ b/src/Numos.CoreSim/Numos.CoreSim.csproj @@ -27,6 +27,7 @@ + _batches = []; /// - /// Maps to this tick's batch index for that chunk, or -1 if the + /// Maps to this tick's batch index for that chunk, or -1 if the /// chunk has no batch yet. A flat array indexed by dense id is a direct replacement for a /// dictionary keyed on grid position — this lookup runs once per boundary-flow transfer and the /// dictionary hash/probe was the dominant cost of that hot loop. @@ -397,7 +398,7 @@ private sealed class InjectionBuffer /// /// The chunk each slot was last resolved for. Since - /// is recycled, a slot's batch index is only trusted when this + /// is recycled, a slot's batch index is only trusted when this /// still matches the chunk being looked up. /// private AtmosChunk?[] _ownerByChunkDenseId = []; diff --git a/src/Numos.Headless/Diagnostics/SimulationStateAnalyzer.cs b/src/Numos.Headless/Diagnostics/SimulationStateAnalyzer.cs index 659986e..b1a7ec4 100644 --- a/src/Numos.Headless/Diagnostics/SimulationStateAnalyzer.cs +++ b/src/Numos.Headless/Diagnostics/SimulationStateAnalyzer.cs @@ -1,4 +1,5 @@ using Numos.API; +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.Datatypes.Snapshots; @@ -26,7 +27,7 @@ public static SimulationStateReport Analyze( 0, SimulationObservationOptions.MaximumMaxIssueLocations); - AtmosChunkHandle[] handles = SelectHandles(simulation.GetChunkHandles(), options.Chunk); + ChunkHandle[] handles = SelectHandles(simulation.GetChunkHandles(), options.Chunk); AtmosChunkSnapshotRequest[] requests = handles .Select(static handle => new AtmosChunkSnapshotRequest( handle.Position, @@ -99,8 +100,8 @@ public static SimulationStateReport Analyze( issues.Truncated); } - private static AtmosChunkHandle[] SelectHandles( - AtmosChunkHandle[] handles, + private static ChunkHandle[] SelectHandles( + ChunkHandle[] handles, Coordinate? selectedChunk) { if (!selectedChunk.HasValue) diff --git a/src/Numos.Headless/SimulationSession.cs b/src/Numos.Headless/SimulationSession.cs index 442265e..51ec034 100644 --- a/src/Numos.Headless/SimulationSession.cs +++ b/src/Numos.Headless/SimulationSession.cs @@ -1,4 +1,5 @@ using Numos.API; +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.Headless.Diagnostics; @@ -341,9 +342,9 @@ private static HeadlessRequestException Missing(string property) return new HeadlessRequestException("missingProperty", $"The '{property}' property is required."); } - private static AtmosChunkHandle Handle(Coordinate position) + private static ChunkHandle Handle(Coordinate position) { - return new AtmosChunkHandle(ToInt3(position)); + return new ChunkHandle(ToInt3(position)); } private static Int3 ToInt3(Coordinate value) diff --git a/src/Numos.Replay.SourceGen/ReplayCodecGenerator.cs b/src/Numos.Replay.SourceGen/ReplayCodecGenerator.cs index 5d62fe9..3da034a 100644 --- a/src/Numos.Replay.SourceGen/ReplayCodecGenerator.cs +++ b/src/Numos.Replay.SourceGen/ReplayCodecGenerator.cs @@ -446,14 +446,14 @@ private static bool TryGetWireKind(ITypeSymbol type, out string writeTemplate, o writeTemplate = "writer.Write({0}.RoomId);"; readExpression = "new global::Numos.CoreSim.Datatypes.Primitives.VoxelClassification(reader.ReadInt32())"; return true; - case "global::Numos.API.AtmosSimulationId": + case "global::Numos.Chunks.Topology.SimulationId": // WriteSimulationId/ReadSimulationId are private helpers already declared directly on // NumosWorldReplaySerializer (shared with its hand-written checkpoint codec) -- only world // operations ever have a field of this type, so calling them unqualified is safe. writeTemplate = "WriteSimulationId(writer, {0});"; readExpression = "ReadSimulationId(reader)"; return true; - case "global::Numos.API.ExplicitLinkSetHandle": + case "global::Numos.Chunks.Topology.ExplicitLinkSetHandle": writeTemplate = "WriteHandle(writer, {0});"; readExpression = "ReadHandle(reader)"; return true; diff --git a/src/Numos.Serialization/NumosWorldReplaySerializer.cs b/src/Numos.Serialization/NumosWorldReplaySerializer.cs index 4f4cbdc..b1b8137 100644 --- a/src/Numos.Serialization/NumosWorldReplaySerializer.cs +++ b/src/Numos.Serialization/NumosWorldReplaySerializer.cs @@ -1,4 +1,6 @@ using Numos.API; +using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim.Replay; using Numos.Replay.SourceGen; @@ -336,7 +338,7 @@ private static WorldCheckpointPayload ReadCheckpoint(BinaryReader reader, NumosR byte state = reader.ReadByte(); var kind = (ExplicitLinkSetKind)reader.ReadByte(); int linkCount = NumosReplaySerializer.ReadCount(reader, 10_000_000, "explicit link"); - var links = new ExplicitLinkDefinition[linkCount]; + var links = new ExplicitLinkDefinition[linkCount]; for (int linkIndex = 0; linkIndex < linkCount; linkIndex++) links[linkIndex] = ReadLink(reader); @@ -510,49 +512,49 @@ private static CreateAtmosLinkSetOperation ReadCreateAtmosLinkSetOperation(Binar var handle = ReadHandle(reader); var kind = (ExplicitLinkSetKind)reader.ReadByte(); int count = NumosReplaySerializer.ReadCount(reader, 10_000_000, "explicit link"); - var links = new ExplicitLinkDefinition[count]; + var links = new ExplicitLinkDefinition[count]; for (int index = 0; index < count; index++) links[index] = ReadLink(reader); return new CreateAtmosLinkSetOperation(handle, kind, links); } - private static void WriteLink(BinaryWriter writer, ExplicitLinkDefinition link) + private static void WriteLink(BinaryWriter writer, ExplicitLinkDefinition link) { WriteCell(writer, link.First); WriteCell(writer, link.Second); writer.Write((byte)link.Flags); } - private static ExplicitLinkDefinition ReadLink(BinaryReader reader) + private static ExplicitLinkDefinition ReadLink(BinaryReader reader) { - return new ExplicitLinkDefinition(ReadCell(reader), ReadCell(reader), (ExplicitLinkFlags)reader.ReadByte()); + return new ExplicitLinkDefinition(ReadCell(reader), ReadCell(reader), (AtmosLinkFlags)reader.ReadByte()); } - private static void WriteCell(BinaryWriter writer, AtmosCellRef cell) + private static void WriteCell(BinaryWriter writer, VoxelRef cell) { WriteSimulationId(writer, cell.Simulation); NumosReplaySerializer.WriteInt3(writer, cell.Chunk.Position); writer.Write(cell.LocalVoxelIndex); } - private static AtmosCellRef ReadCell(BinaryReader reader) + private static VoxelRef ReadCell(BinaryReader reader) { - return new AtmosCellRef( + return new VoxelRef( ReadSimulationId(reader), - new AtmosChunkHandle(NumosReplaySerializer.ReadInt3(reader)), + new ChunkHandle(NumosReplaySerializer.ReadInt3(reader)), reader.ReadUInt16()); } - private static void WriteSimulationId(BinaryWriter writer, AtmosSimulationId id) + private static void WriteSimulationId(BinaryWriter writer, SimulationId id) { writer.Write(id.Index); writer.Write(id.Generation); } - private static AtmosSimulationId ReadSimulationId(BinaryReader reader) + private static SimulationId ReadSimulationId(BinaryReader reader) { - return new AtmosSimulationId(reader.ReadInt32(), reader.ReadUInt32()); + return new SimulationId(reader.ReadInt32(), reader.ReadUInt32()); } private static void WriteHandle(BinaryWriter writer, ExplicitLinkSetHandle handle) diff --git a/src/Numos.Viewer/SimulationViewer.Project.cs b/src/Numos.Viewer/SimulationViewer.Project.cs index 48343a0..0f72d44 100644 --- a/src/Numos.Viewer/SimulationViewer.Project.cs +++ b/src/Numos.Viewer/SimulationViewer.Project.cs @@ -1,4 +1,5 @@ using Numos.API; +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.GasReactions; @@ -237,7 +238,7 @@ private void AddProjectChunk(Int3 position, int roomId) } } - private void RemoveProjectChunk(AtmosChunkHandle chunk) + private void RemoveProjectChunk(ChunkHandle chunk) { if (_simulation == null) return; @@ -251,7 +252,7 @@ private void RemoveProjectChunk(AtmosChunkHandle chunk) SetProjectMessage($"Chunk {FormatChunkPosition(chunk.Position)} no longer exists.", true); } - private void SealProjectChunk(AtmosChunkHandle chunk) + private void SealProjectChunk(ChunkHandle chunk) { if (_simulation == null) return; @@ -269,7 +270,7 @@ private void SealProjectChunk(AtmosChunkHandle chunk) } } - private void UnsleepProjectChunk(AtmosChunkHandle chunk) + private void UnsleepProjectChunk(ChunkHandle chunk) { if (_simulation == null) return; @@ -346,7 +347,7 @@ private void RemoveProjectGas(int gasId) } private void InjectProjectGas( - AtmosChunkHandle chunk, + ChunkHandle chunk, int x, int y, int z, diff --git a/src/Numos.Viewer/SimulationViewer.ProjectUi.cs b/src/Numos.Viewer/SimulationViewer.ProjectUi.cs index c3271ca..6b92098 100644 --- a/src/Numos.Viewer/SimulationViewer.ProjectUi.cs +++ b/src/Numos.Viewer/SimulationViewer.ProjectUi.cs @@ -1,6 +1,7 @@ using System.Numerics; using ImGuiNET; using Numos.API; +using Numos.Chunks; using Numos.CoreSim; using Numos.Maths; using Numos.Viewer.Ui; @@ -38,8 +39,8 @@ public partial class SimulationViewer private string _newGasName = "New Gas"; private int _projectChunkDepthDraft = 1; - private int _projectChunkHeightDraft = AtmosChunkConstants.DefaultHeight; - private int _projectChunkWidthDraft = AtmosChunkConstants.DefaultWidth; + private int _projectChunkHeightDraft = ChunkConstants.DefaultHeight; + private int _projectChunkWidthDraft = ChunkConstants.DefaultWidth; private string _projectNameDraft = "Untitled Simulation"; private bool _requestOpenCloseProject; @@ -55,11 +56,11 @@ private void RequestCreateProject() _projectNameDraft = _world == null ? "Untitled Simulation" : $"{_projectName} Copy"; _projectChunkWidthDraft = _chunkDimensions.X > 0 ? _chunkDimensions.X - : AtmosChunkConstants.DefaultWidth; + : ChunkConstants.DefaultWidth; _projectChunkHeightDraft = _chunkDimensions.Y > 0 ? _chunkDimensions.Y - : AtmosChunkConstants.DefaultHeight; + : ChunkConstants.DefaultHeight; _projectChunkDepthDraft = _chunkDimensions.Z > 0 ? _chunkDimensions.Z : 1; _includeDefaultGasesDraft = true; @@ -300,9 +301,9 @@ private void RenderProjectChunkControls() ImGui.TextDisabled($"Fixed size: {_chunkDimensions.X} x {_chunkDimensions.Y} x {_chunkDimensions.Z}"); ImGui.TextDisabled("Right-click a chunk coordinate for options."); - AtmosChunkHandle? chunkToRemove = null; - AtmosChunkHandle? chunkToSeal = null; - AtmosChunkHandle? chunkToUnsleep = null; + ChunkHandle? chunkToRemove = null; + ChunkHandle? chunkToSeal = null; + ChunkHandle? chunkToUnsleep = null; foreach (var handle in _liveChunkHandles) { ImGui.PushID($"chunk-{handle.Position.X}-{handle.Position.Y}-{handle.Position.Z}"); @@ -509,7 +510,7 @@ private void RenderProjectInjectionControls() _injectionChunkPosition.HasValue) { InjectProjectGas( - new AtmosChunkHandle(_injectionChunkPosition.Value), + new ChunkHandle(_injectionChunkPosition.Value), _injectionX, _injectionY, _injectionZ, diff --git a/src/Numos.Viewer/SimulationViewer.RenderUi.cs b/src/Numos.Viewer/SimulationViewer.RenderUi.cs index 5bcdd36..d5ca1c6 100644 --- a/src/Numos.Viewer/SimulationViewer.RenderUi.cs +++ b/src/Numos.Viewer/SimulationViewer.RenderUi.cs @@ -1,6 +1,7 @@ using System.Numerics; using ImGuiNET; using Numos.API; +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.Datatypes.Snapshots; @@ -980,7 +981,7 @@ private void RenderVoxelTools() try { _simulation!.SetChunkClassification( - new AtmosChunkHandle(_toolChunkPosition.Value), + new ChunkHandle(_toolChunkPosition.Value), new VoxelClassification(_toolClassificationDraft)); SetProjectMessage( @@ -1503,7 +1504,7 @@ private bool TryGetVoxelDetails(VoxelAddress address, out AtmosVoxelSnapshot sna try { bool available = _simulation.TryGetVoxelSnapshot( - new AtmosChunkHandle(address.Chunk.Position), + new ChunkHandle(address.Chunk.Position), address.LocalIndex, presentedVersion, out snapshot); diff --git a/src/Numos.Viewer/SimulationViewer.TopologyUi.cs b/src/Numos.Viewer/SimulationViewer.TopologyUi.cs index cce66a2..6146ec5 100644 --- a/src/Numos.Viewer/SimulationViewer.TopologyUi.cs +++ b/src/Numos.Viewer/SimulationViewer.TopologyUi.cs @@ -1,6 +1,8 @@ using System.Numerics; using ImGuiNET; using Numos.API; +using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.Maths; using Numos.Viewer.Ui; using Raylib_cs; @@ -18,8 +20,8 @@ public partial class SimulationViewer private int _dockSecondChunk; private int _dockSecondFace; private int _dockSecondSimulation; - private AtmosCellRef? _portalFirst; - private AtmosCellRef? _portalSecond; + private VoxelRef? _portalFirst; + private VoxelRef? _portalSecond; private bool _removeTopologyModalOpen; private bool _requestRemoveTopology; private ExplicitLinkSetHandle? _selectedLinkSet; @@ -189,7 +191,7 @@ private void RenderTopologyControls() ImGui.EndTable(); } - ImGui.BeginDisabled(!_portalFirst.HasValue || !_portalSecond.HasValue || GetTopologyFlags() == ExplicitLinkFlags.None); + ImGui.BeginDisabled(!_portalFirst.HasValue || !_portalSecond.HasValue || GetTopologyFlags() == AtmosLinkFlags.None); if (ImGui.Button("Create Portal")) CreatePortalFromCapturedEndpoints(); @@ -218,12 +220,12 @@ private void RenderTopologyControls() if (dockInputChanged) _topologyFeedback = null; - ImGui.BeginDisabled(GetTopologyFlags() == ExplicitLinkFlags.None || _simulationSurfaces.Count == 0); + ImGui.BeginDisabled(GetTopologyFlags() == AtmosLinkFlags.None || _simulationSurfaces.Count == 0); if (ImGui.Button("Create Dock")) { try { - ExplicitLinkDefinition[] links = BuildDockLinks(); + ExplicitLinkDefinition[] links = BuildDockLinks(); var dock = _world.CreateDock(links); _selectedLinkSet = dock.Links; SetTopologyFeedback( @@ -274,7 +276,7 @@ private bool DrawDockEndpoint(string label, ref int simulationIndex, ref int chu ImGui.EndCombo(); } - AtmosChunkHandle[] chunks = _simulationSurfaces[simulationIndex].Simulation.GetChunkHandles().ToArray(); + ChunkHandle[] chunks = _simulationSurfaces[simulationIndex].Simulation.GetChunkHandles().ToArray(); chunkIndex = chunks.Length == 0 ? 0 : Math.Clamp(chunkIndex, 0, chunks.Length - 1); string chunkLabel = chunks.Length == 0 ? "No chunks" : FormatChunkPosition(chunks[chunkIndex].Position); if (ImGui.BeginCombo($"{label} chunk", chunkLabel)) @@ -359,12 +361,12 @@ private void DrawRemoveTopologyModal() ImGui.SetItemDefaultFocus(); } - private ExplicitLinkDefinition[] BuildDockLinks() + private ExplicitLinkDefinition[] BuildDockLinks() { var firstSurface = _simulationSurfaces[Math.Clamp(_dockFirstSimulation, 0, _simulationSurfaces.Count - 1)]; var secondSurface = _simulationSurfaces[Math.Clamp(_dockSecondSimulation, 0, _simulationSurfaces.Count - 1)]; - AtmosChunkHandle[] firstChunks = firstSurface.Simulation.GetChunkHandles().ToArray(); - AtmosChunkHandle[] secondChunks = secondSurface.Simulation.GetChunkHandles().ToArray(); + ChunkHandle[] firstChunks = firstSurface.Simulation.GetChunkHandles().ToArray(); + ChunkHandle[] secondChunks = secondSurface.Simulation.GetChunkHandles().ToArray(); if (firstChunks.Length == 0 || secondChunks.Length == 0) throw new InvalidOperationException("Each dock endpoint needs a chunk."); @@ -377,7 +379,7 @@ private ExplicitLinkDefinition[] BuildDockLinks() firstSize.Height != (quarterTurn ? secondSize.Width : secondSize.Height)) throw new InvalidOperationException("The selected faces do not have matching dimensions after rotation."); - var links = new ExplicitLinkDefinition[firstSize.Width * firstSize.Height]; + var links = new ExplicitLinkDefinition[firstSize.Width * firstSize.Height]; int destination = 0; for (int v = 0; v < firstSize.Height; v++) for (int u = 0; u < firstSize.Width; u++) @@ -392,30 +394,30 @@ private ExplicitLinkDefinition[] BuildDockLinks() secondU, secondV); - links[destination++] = new ExplicitLinkDefinition( - new AtmosCellRef(firstSurface.Simulation.Id, firstChunk, firstIndex), - new AtmosCellRef(secondSurface.Simulation.Id, secondChunk, secondIndex), + links[destination++] = new ExplicitLinkDefinition( + new VoxelRef(firstSurface.Simulation.Id, firstChunk, firstIndex), + new VoxelRef(secondSurface.Simulation.Id, secondChunk, secondIndex), GetTopologyFlags()); } return links; } - private AtmosCellRef? GetSelectedAtmosCell() + private VoxelRef? GetSelectedAtmosCell() { if (_simulation == null || !_selectedCell.HasValue) return null; - return new AtmosCellRef( + return new VoxelRef( _simulation.Id, - new AtmosChunkHandle(_selectedCell.Value.Chunk.Position), + new ChunkHandle(_selectedCell.Value.Chunk.Position), _selectedCell.Value.LocalIndex); } - private ExplicitLinkFlags GetTopologyFlags() + private AtmosLinkFlags GetTopologyFlags() { - return (_topologyGas ? ExplicitLinkFlags.GasTransport : ExplicitLinkFlags.None) | - (_topologyThermal ? ExplicitLinkFlags.ThermalTransport : ExplicitLinkFlags.None); + return (_topologyGas ? AtmosLinkFlags.GasTransport : AtmosLinkFlags.None) | + (_topologyThermal ? AtmosLinkFlags.ThermalTransport : AtmosLinkFlags.None); } private void CreatePortalFromCapturedEndpoints() @@ -464,7 +466,7 @@ private void DrawTopologyOverlay() } } - private bool TryGetCellCenter(AtmosCellRef cell, out Vector3 center) + private bool TryGetCellCenter(VoxelRef cell, out Vector3 center) { if (_drawData!.Chunks.TryGetValue(cell.Chunk.Position, out var chunk) && cell.LocalVoxelIndex < chunk.CellCount) { @@ -486,7 +488,7 @@ private static Color GetLinkColor(ExplicitLinkSetHandle handle, AtmosWorldLinkSe return color; } - private string FormatCell(AtmosCellRef cell) + private string FormatCell(VoxelRef cell) { return $"{GetSimulationName(cell.Simulation)}\n{FormatChunkPosition(cell.Chunk.Position)}, voxel {cell.LocalVoxelIndex}"; } diff --git a/src/Numos.Viewer/SimulationViewer.VoxelEditing.cs b/src/Numos.Viewer/SimulationViewer.VoxelEditing.cs index cd8e3f0..0f83ec8 100644 --- a/src/Numos.Viewer/SimulationViewer.VoxelEditing.cs +++ b/src/Numos.Viewer/SimulationViewer.VoxelEditing.cs @@ -1,6 +1,8 @@ using System.Numerics; using ImGuiNET; using Numos.API; +using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim.Datatypes.Primitives; using Numos.SimDrawer; using Numos.Viewer.Rendering.Viewport; @@ -468,7 +470,7 @@ private void RenderVoxelContextMenu() private void RenderContextPortalMenu() { - AtmosCellRef? selected = GetSelectedAtmosCell(); + VoxelRef? selected = GetSelectedAtmosCell(); if (!selected.HasValue || !ImGui.BeginMenu("Portal")) return; @@ -479,7 +481,7 @@ private void RenderContextPortalMenu() _topologyFeedback = null; } - bool canCreate = _portalFirst.HasValue && GetTopologyFlags() != ExplicitLinkFlags.None; + bool canCreate = _portalFirst.HasValue && GetTopologyFlags() != AtmosLinkFlags.None; ImGui.BeginDisabled(!canCreate); if (ImGui.MenuItem("Create Portal to Here")) { @@ -495,7 +497,7 @@ private void RenderContextPortalMenu() else ImGui.TextDisabled("Start a portal at its first endpoint."); - if (GetTopologyFlags() == ExplicitLinkFlags.None) + if (GetTopologyFlags() == AtmosLinkFlags.None) ImGui.TextDisabled("Enable a transport mode in World & Topology."); ImGui.EndMenu(); @@ -553,7 +555,7 @@ private void ApplyClassification(IEnumerable addresses, int classi ApplyVoxelMutation( addresses, address => _simulation!.SetVoxelClassification( - new AtmosChunkHandle(address.Chunk.Position), + new ChunkHandle(address.Chunk.Position), address.LocalIndex, new VoxelClassification(classification)), $"Set classification {classification}"); @@ -580,7 +582,7 @@ private void ApplyGasInjection( ApplyVoxelMutation( addresses, address => _simulation!.AddGasToVoxel( - new AtmosChunkHandle(address.Chunk.Position), + new ChunkHandle(address.Chunk.Position), address.LocalIndex, gasId, moles, @@ -593,7 +595,7 @@ private void ApplyClearGas(IEnumerable addresses) ApplyVoxelMutation( addresses, address => _simulation!.GetVoxelGasMixture( - new AtmosChunkHandle(address.Chunk.Position), + new ChunkHandle(address.Chunk.Position), address.LocalIndex).Clear(), "Cleared gas from"); } @@ -604,7 +606,7 @@ private void ApplyClearCell(IEnumerable addresses) addresses, address => { - var handle = new AtmosChunkHandle(address.Chunk.Position); + var handle = new ChunkHandle(address.Chunk.Position); _simulation!.GetVoxelGasMixture(handle, address.LocalIndex).Clear(); _simulation.SetVoxelClassification( handle, @@ -619,7 +621,7 @@ private void ApplyTemperature(IEnumerable addresses, float tempera ApplyVoxelMutation( addresses, address => _simulation!.SetVoxelTemperature( - new AtmosChunkHandle(address.Chunk.Position), + new ChunkHandle(address.Chunk.Position), address.LocalIndex, temperature), $"Set temperature to {temperature:F1} K for"); diff --git a/src/Numos.Viewer/SimulationViewer.World.cs b/src/Numos.Viewer/SimulationViewer.World.cs index fc0d106..eebb587 100644 --- a/src/Numos.Viewer/SimulationViewer.World.cs +++ b/src/Numos.Viewer/SimulationViewer.World.cs @@ -1,7 +1,8 @@ using System.Numerics; using ImGuiNET; using Numos.API; -using Numos.CoreSim; +using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim.Datatypes.Snapshots; using Numos.Maths; using Numos.SimDrawer; @@ -13,19 +14,19 @@ namespace Numos.Viewer; public partial class SimulationViewer { - private readonly Dictionary _simulationNames = []; + private readonly Dictionary _simulationNames = []; private readonly List _simulationSurfaces = []; - private AtmosSimulationId? _activeSimulationId; + private SimulationId? _activeSimulationId; private long _knownSimulationRevision = -1; private int _newSimulationDepth = 1; - private int _newSimulationHeight = AtmosChunkConstants.DefaultHeight; - private int _newSimulationWidth = AtmosChunkConstants.DefaultWidth; + private int _newSimulationHeight = ChunkConstants.DefaultHeight; + private int _newSimulationWidth = ChunkConstants.DefaultWidth; private bool _removeSimulationModalOpen; private bool _requestRemoveSimulation; private bool _showWorldPanel = true; private string? _simulationFeedback; private bool _simulationFeedbackIsError; - private AtmosSimulationId? _simulationPendingRemoval; + private SimulationId? _simulationPendingRemoval; private void ReconcileSimulationSurfaces() { @@ -33,7 +34,7 @@ private void ReconcileSimulationSurfaces() return; _knownSimulationRevision = revision; - HashSet liveIds = simulations.Select(static simulation => simulation.Id).ToHashSet(); + HashSet liveIds = simulations.Select(static simulation => simulation.Id).ToHashSet(); if (_portalFirst is { } firstPortal && !liveIds.Contains(firstPortal.Simulation)) _portalFirst = null; @@ -69,14 +70,14 @@ private void ReconcileSimulationSurfaces() } _simulationSurfaces.Sort(static (left, right) => left.Simulation.Id.CompareTo(right.Simulation.Id)); - AtmosSimulationId? nextActive = _activeSimulationId is { } active && liveIds.Contains(active) + SimulationId? nextActive = _activeSimulationId is { } active && liveIds.Contains(active) ? active : simulations.FirstOrDefault()?.Id; SetActiveSimulation(nextActive); } - private void SetActiveSimulation(AtmosSimulationId? id) + private void SetActiveSimulation(SimulationId? id) { if (_world == null || id == null || !_world.TryGetSimulation(id.Value, out var simulation)) { @@ -161,7 +162,7 @@ private void RefreshSimulationSurfaces() private void RefreshSimulationSurface(SimulationSurface surface) { var fields = _frameBuilder!.GetRequiredSnapshotFields(_currentVisualizationId); - if (surface.Simulation.TryGetChunkHandles(surface.ChunkRevision, out long revision, out AtmosChunkHandle[] handles)) + if (surface.Simulation.TryGetChunkHandles(surface.ChunkRevision, out long revision, out ChunkHandle[] handles)) { surface.ChunkRevision = revision; surface.Handles = handles; @@ -204,7 +205,7 @@ private void RenderSimulationViewports() if (!_show3DViewport) return; - AtmosSimulationId? requestedActive = null; + SimulationId? requestedActive = null; foreach (var surface in _simulationSurfaces) { if (surface.Viewport == null) @@ -433,7 +434,7 @@ _simulationPendingRemoval is not { } id || ImGui.SetItemDefaultFocus(); } - private string GetSimulationName(AtmosSimulationId id) + private string GetSimulationName(SimulationId id) { return _simulationNames.GetValueOrDefault(id, $"Simulation {id.Index + 1}"); } @@ -462,7 +463,7 @@ internal SimulationSurface(AtmosSimulation simulation) Projection = CameraProjection.Perspective }; internal long ChunkRevision { get; set; } = -1; - internal AtmosChunkHandle[] Handles { get; set; } = []; + internal ChunkHandle[] Handles { get; set; } = []; internal Dictionary Snapshots { get; } = []; internal SimulationDrawData? DrawData { get; set; } diff --git a/src/Numos.Viewer/SimulationViewer.cs b/src/Numos.Viewer/SimulationViewer.cs index 1990722..f8794d4 100644 --- a/src/Numos.Viewer/SimulationViewer.cs +++ b/src/Numos.Viewer/SimulationViewer.cs @@ -2,6 +2,7 @@ using System.Numerics; using ImGuiNET; using Numos.API; +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Snapshots; using Numos.Maths; @@ -25,7 +26,7 @@ public partial class SimulationViewer : IDisposable private readonly Action? _configureVisualizations; private readonly List _highlights = []; - private readonly List _liveChunkHandles = []; + private readonly List _liveChunkHandles = []; private readonly HashSet _liveChunkPositions = []; private readonly List _orderedSnapshots = []; private readonly HashSet _paintedCells = []; @@ -302,7 +303,7 @@ private bool RefreshSnapshotCache() if (_simulation.TryGetChunkHandles( _chunkCollectionRevision, out long collectionRevision, - out AtmosChunkHandle[] handles)) + out ChunkHandle[] handles)) { _chunkCollectionRevision = collectionRevision; _liveChunkHandles.Clear(); diff --git a/tests/Numos.API.Dangerous.Tests/AtmosDangerousApiTests.cs b/tests/Numos.API.Dangerous.Tests/AtmosDangerousApiTests.cs index b54810b..30fd121 100644 --- a/tests/Numos.API.Dangerous.Tests/AtmosDangerousApiTests.cs +++ b/tests/Numos.API.Dangerous.Tests/AtmosDangerousApiTests.cs @@ -1,3 +1,4 @@ +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.Maths; @@ -54,7 +55,7 @@ public void GetChunk_WithUnknownHandle_Throws() using var simulation = new AtmosSimulation(); Assert.That( - () => simulation.Dangerous().GetChunk(new AtmosChunkHandle(Int3.PosX)), + () => simulation.Dangerous().GetChunk(new ChunkHandle(Int3.PosX)), Throws.TypeOf()); } @@ -100,7 +101,7 @@ public void WorldSolver_CanResolvePortalNeighborStorage() world.Solvers.RegisterNeighborSolver( "dangerous-neighbor", - AtmosNeighborSelection.All("tests/dangerous-neighbor-v1"), + VoxelNeighborSelection.All("tests/dangerous-neighbor-v1"), context => { foreach (var neighbor in context.Topology.GetNeighbors(source)) @@ -160,7 +161,7 @@ public void StatefulDangerousSolver_RetainsEditableConfiguration() Assert.That(simulation.GetChunkSnapshot(chunk).Gases.Single().Moles[0], Is.EqualTo(3f)); } - private sealed class ConfiguredDangerousWriter(AtmosChunkHandle chunk) + private sealed class ConfiguredDangerousWriter(ChunkHandle chunk) { public DangerousWriterConfig Config { get; } = new(); diff --git a/tests/Numos.API.Tests/AtmosChunkVersionTests.cs b/tests/Numos.API.Tests/AtmosChunkVersionTests.cs index d8ead17..7e6fdc7 100644 --- a/tests/Numos.API.Tests/AtmosChunkVersionTests.cs +++ b/tests/Numos.API.Tests/AtmosChunkVersionTests.cs @@ -1,3 +1,4 @@ +using Numos.Chunks; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.Datatypes.Snapshots; using Numos.Maths; @@ -186,9 +187,9 @@ public void GetChunkHandles_DiscoversAdditionsAndRemovalsWithoutCallerRegistry() var first = simulation.CreateAndRegisterChunk(new Int3(2, 0, 0)); var second = simulation.CreateAndRegisterChunk(new Int3(-1, 0, 0)); - AtmosChunkHandle[] before = simulation.GetChunkHandles(); + ChunkHandle[] before = simulation.GetChunkHandles(); simulation.UnregisterChunk(first); - AtmosChunkHandle[] after = simulation.GetChunkHandles(); + ChunkHandle[] after = simulation.GetChunkHandles(); Assert.Multiple(() => { @@ -206,8 +207,8 @@ public void TryGetChunkHandles_UnchangedCollection_DoesNotAllocateAnotherHandleL using var simulation = new AtmosSimulation(1, 1, 1); simulation.CreateAndRegisterChunk(default); - bool firstCreated = simulation.TryGetChunkHandles(-1, out long revision, out AtmosChunkHandle[] first); - bool secondCreated = simulation.TryGetChunkHandles(revision, out long unchangedRevision, out AtmosChunkHandle[] second); + bool firstCreated = simulation.TryGetChunkHandles(-1, out long revision, out ChunkHandle[] first); + bool secondCreated = simulation.TryGetChunkHandles(revision, out long unchangedRevision, out ChunkHandle[] second); Assert.Multiple(() => { @@ -228,7 +229,7 @@ public void TryGetChunkHandles_RemoveAndRecreateSamePosition_StillAdvancesCollec simulation.UnregisterChunk(original); simulation.CreateAndRegisterChunk(default); - bool changed = simulation.TryGetChunkHandles(firstRevision, out long secondRevision, out AtmosChunkHandle[] handles); + bool changed = simulation.TryGetChunkHandles(firstRevision, out long secondRevision, out ChunkHandle[] handles); Assert.Multiple(() => { diff --git a/tests/Numos.API.Tests/AtmosReplayTests.cs b/tests/Numos.API.Tests/AtmosReplayTests.cs index 27eb505..a38871d 100644 --- a/tests/Numos.API.Tests/AtmosReplayTests.cs +++ b/tests/Numos.API.Tests/AtmosReplayTests.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.Replay; @@ -292,7 +293,7 @@ public void Hash_UsesCanonicalChunkOrderAndRawFloatingPointBits() second.CreateAndRegisterChunk(new Int3(2, 0, 0)); second.CreateAndRegisterChunk(default); Assert.That(first.ComputeStateHash(), Is.EqualTo(second.ComputeStateHash())); - second.SetVoxelTemperature(new AtmosChunkHandle(default), 0, BitConverter.Int32BitsToSingle(unchecked((int)0x80000000))); + second.SetVoxelTemperature(new ChunkHandle(default), 0, BitConverter.Int32BitsToSingle(unchecked((int)0x80000000))); Assert.That(first.ComputeStateHash(), Is.Not.EqualTo(second.ComputeStateHash())); } diff --git a/tests/Numos.API.Tests/AtmosSimulationContractTests.cs b/tests/Numos.API.Tests/AtmosSimulationContractTests.cs index da36151..a380b01 100644 --- a/tests/Numos.API.Tests/AtmosSimulationContractTests.cs +++ b/tests/Numos.API.Tests/AtmosSimulationContractTests.cs @@ -1,3 +1,4 @@ +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.Maths; @@ -186,7 +187,7 @@ public void CreateAndRegisterChunk_ReturnsPositionHandleAndUpdatesCount() Assert.Multiple(() => { - Assert.That(handle, Is.EqualTo(new AtmosChunkHandle(position))); + Assert.That(handle, Is.EqualTo(new ChunkHandle(position))); Assert.That(handle.Position, Is.EqualTo(position)); Assert.That(simulation.ChunkCount, Is.EqualTo(1)); Assert.That(simulation.GetChunkSnapshot(handle).GridPosition, Is.EqualTo(position)); @@ -653,7 +654,7 @@ public void GetChunkSnapshot_ReturnsDeepDetachedCopies() public void ChunkOperations_WithMissingPosition_ThrowKeyNotFoundException() { using var simulation = new AtmosSimulation(new TestAtmosConfig(), 2, 2, 1); - var missing = new AtmosChunkHandle(new Int3(91, -37, 12)); + var missing = new ChunkHandle(new Int3(91, -37, 12)); Assert.Multiple(() => { diff --git a/tests/Numos.API.Tests/AtmosSolverPipelineTests.cs b/tests/Numos.API.Tests/AtmosSolverPipelineTests.cs index a5451c3..8a62d36 100644 --- a/tests/Numos.API.Tests/AtmosSolverPipelineTests.cs +++ b/tests/Numos.API.Tests/AtmosSolverPipelineTests.cs @@ -1,5 +1,6 @@ using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; +using Numos.Chunks; namespace Numos.API.Tests; @@ -154,8 +155,8 @@ public void ResetToDefaults_RemovesCustomStagesAndRestoresBuiltIns() } private static AtmosWorld CreateTransportWorld( - out (AtmosSimulation Simulation, AtmosChunkHandle Chunk) source, - out (AtmosSimulation Simulation, AtmosChunkHandle Chunk) target) + out (AtmosSimulation Simulation, ChunkHandle Chunk) source, + out (AtmosSimulation Simulation, ChunkHandle Chunk) target) { var config = new AtmosConfig { @@ -179,7 +180,7 @@ private static AtmosWorld CreateTransportWorld( return world; } - private static float TotalMoles(AtmosSimulation simulation, AtmosChunkHandle chunk) + private static float TotalMoles(AtmosSimulation simulation, ChunkHandle chunk) { return simulation.GetVoxelSnapshot(chunk, 0).Gases.Sum(static gas => gas.Moles); } diff --git a/tests/Numos.API.Tests/AtmosSolverStorageTests.cs b/tests/Numos.API.Tests/AtmosSolverStorageTests.cs index bebc562..847fb30 100644 --- a/tests/Numos.API.Tests/AtmosSolverStorageTests.cs +++ b/tests/Numos.API.Tests/AtmosSolverStorageTests.cs @@ -1,3 +1,4 @@ +using Numos.Chunks; using Numos.Collections; using Numos.Maths; @@ -108,7 +109,7 @@ public void InvalidRequests_RejectNullKeysNegativeLengthsAndMissingChunks() using var simulation = new AtmosSimulation(1, 1, 1); var chunk = simulation.CreateAndRegisterChunk(default); object key = new(); - var missing = new AtmosChunkHandle(Int3.PosX); + var missing = new ChunkHandle(Int3.PosX); Assert.Multiple(() => { diff --git a/tests/Numos.API.Tests/AtmosWorldReplayTests.cs b/tests/Numos.API.Tests/AtmosWorldReplayTests.cs index e5c7518..184011f 100644 --- a/tests/Numos.API.Tests/AtmosWorldReplayTests.cs +++ b/tests/Numos.API.Tests/AtmosWorldReplayTests.cs @@ -1,3 +1,5 @@ +using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.GasReactions; @@ -197,7 +199,7 @@ private static AtmosConfig CreateConfig() }; } - private static AtmosChunkHandle CreateOpenChunk(AtmosSimulation simulation, Int3 position) + private static ChunkHandle CreateOpenChunk(AtmosSimulation simulation, Int3 position) { var chunk = simulation.CreateAndRegisterChunk(position); simulation.SetChunkClassification(chunk, new VoxelClassification(1)); diff --git a/tests/Numos.API.Tests/AtmosWorldSolverPipelineTests.cs b/tests/Numos.API.Tests/AtmosWorldSolverPipelineTests.cs index 9629109..73bbdb1 100644 --- a/tests/Numos.API.Tests/AtmosWorldSolverPipelineTests.cs +++ b/tests/Numos.API.Tests/AtmosWorldSolverPipelineTests.cs @@ -1,3 +1,5 @@ +using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.Maths; @@ -19,16 +21,16 @@ public void NeighborSolver_SeesCartesianAndPortalNeighborsThroughOneView() second.SetChunkClassification(secondChunk, new VoxelClassification(1)); var source = first.GetCellRef(firstChunk, 0); var target = second.GetCellRef(secondChunk, 0); - world.CreatePortal(source, target, ExplicitLinkFlags.GasTransport); + world.CreatePortal(source, target, AtmosLinkFlags.GasTransport); int neighborCount = -1; - AtmosNeighbor[] neighbors = []; + VoxelNeighbor[] neighbors = []; world.Solvers.RegisterNeighborSolver( "inspect-neighbors", - new AtmosNeighborSelection( + new VoxelNeighborSelection( "tests/gas-neighbors-v1", true, - static link => (link.Flags & ExplicitLinkFlags.GasTransport) != 0), + static link => (link.Flags & AtmosLinkFlags.GasTransport) != 0), context => { neighborCount = context.Topology.GetNeighborCount(source); @@ -40,9 +42,9 @@ public void NeighborSolver_SeesCartesianAndPortalNeighborsThroughOneView() Assert.Multiple(() => { Assert.That(neighborCount, Is.EqualTo(3)); - Assert.That(neighbors.Count(static neighbor => neighbor.Kind == AtmosNeighborKind.Cartesian), Is.EqualTo(2)); - Assert.That(neighbors.Count(static neighbor => neighbor.Kind == AtmosNeighborKind.Explicit), Is.EqualTo(1)); - Assert.That(neighbors.Single(static neighbor => neighbor.Kind == AtmosNeighborKind.Explicit).Cell, Is.EqualTo(target)); + Assert.That(neighbors.Count(static neighbor => neighbor.Kind == VoxelNeighborKind.Cartesian), Is.EqualTo(2)); + Assert.That(neighbors.Count(static neighbor => neighbor.Kind == VoxelNeighborKind.Explicit), Is.EqualTo(1)); + Assert.That(neighbors.Single(static neighbor => neighbor.Kind == VoxelNeighborKind.Explicit).Cell, Is.EqualTo(target)); }); } @@ -57,11 +59,11 @@ public void NeighborSolver_SeesCartesianNeighborAcrossChunkBoundary() simulation.SetChunkClassification(second, new VoxelClassification(1)); var source = simulation.GetCellRef(first, 0); var target = simulation.GetCellRef(second, 0); - AtmosNeighbor[] neighbors = []; + VoxelNeighbor[] neighbors = []; world.Solvers.RegisterNeighborSolver( "inspect-chunk-boundary", - AtmosNeighborSelection.All("tests/chunk-boundary-v1"), + VoxelNeighborSelection.All("tests/chunk-boundary-v1"), context => neighbors = context.Topology.GetNeighbors(source).ToArray()); world.Tick(); @@ -71,7 +73,7 @@ public void NeighborSolver_SeesCartesianNeighborAcrossChunkBoundary() Is.EqualTo( new[] { - new AtmosNeighbor(target, AtmosNeighborKind.Cartesian, ExplicitLinkFlags.None) + new VoxelNeighbor(target, VoxelNeighborKind.Cartesian, AtmosLinkFlags.None) })); } @@ -88,7 +90,7 @@ public void Selector_IsReevaluatedOnlyWhenCompiledTopologyChanges() world.Solvers.RegisterNeighborSolver( "compiled-selector", - new AtmosNeighborSelection( + new VoxelNeighborSelection( "tests/compiled-selector-v1", false, _ => @@ -121,7 +123,7 @@ public void SelectorFailure_LeavesTopologyBoundaryPendingForRetry() world.Solvers.RegisterNeighborSolver( "fallible-selector", - new AtmosNeighborSelection( + new VoxelNeighborSelection( "tests/fallible-selector-v1", false, _ => failCompilation @@ -131,9 +133,10 @@ public void SelectorFailure_LeavesTopologyBoundaryPendingForRetry() var links = world.CreateLinks( [ - new ExplicitLinkDefinition( + new ExplicitLinkDefinition( simulation.GetCellRef(first, 0), - simulation.GetCellRef(second, 0)) + simulation.GetCellRef(second, 0), + AtmosLinkFlags.All) ]); Assert.That(() => world.Tick(), Throws.InvalidOperationException); @@ -222,19 +225,19 @@ public void OwnedEdges_VisitsCartesianAndExplicitEdgesOnce() first.SetChunkClassification(firstChunk, new VoxelClassification(1)); second.SetChunkClassification(secondChunk, new VoxelClassification(1)); world.CreatePortal(first.GetCellRef(firstChunk, 0), second.GetCellRef(secondChunk, 0)); - AtmosNeighborEdge[] edges = []; + VoxelNeighborEdge[] edges = []; world.Solvers.RegisterNeighborSolver( "owned-edges", - AtmosNeighborSelection.All("tests/owned-edges-v1"), + VoxelNeighborSelection.All("tests/owned-edges-v1"), context => edges = context.Topology.GetOwnedEdges().ToArray()); world.Tick(); Assert.Multiple(() => { - Assert.That(edges.Count(static edge => edge.Kind == AtmosNeighborKind.Cartesian), Is.EqualTo(1)); - Assert.That(edges.Count(static edge => edge.Kind == AtmosNeighborKind.Explicit), Is.EqualTo(1)); + Assert.That(edges.Count(static edge => edge.Kind == VoxelNeighborKind.Cartesian), Is.EqualTo(1)); + Assert.That(edges.Count(static edge => edge.Kind == VoxelNeighborKind.Explicit), Is.EqualTo(1)); }); } @@ -244,7 +247,7 @@ public void WorldSolverMetadata_ParticipatesInCheckpointHashAndCompatibility() using var world = new AtmosWorld(CreateConfig()); world.Solvers.RegisterNeighborSolver( "authoritative-custom", - AtmosNeighborSelection.All("tests/authoritative-custom-v1"), + VoxelNeighborSelection.All("tests/authoritative-custom-v1"), _ => { }); var checkpoint = world.CaptureCheckpoint(); @@ -309,7 +312,7 @@ public void HostDefinedFlagBit_IsInvisibleToBuiltInTransportButVisibleToCustomSe // A capability bit outside GasTransport/ThermalTransport: Numos' built-in transport stages must ignore it, // while a host-registered selector can still pick the link out by it. - const ExplicitLinkFlags customCapability = (ExplicitLinkFlags)(1 << 2); + const AtmosLinkFlags customCapability = (AtmosLinkFlags)(1 << 2); world.CreatePortal( first.GetCellRef(firstChunk, 0), second.GetCellRef(secondChunk, 0), @@ -318,7 +321,7 @@ public void HostDefinedFlagBit_IsInvisibleToBuiltInTransportButVisibleToCustomSe bool customSolverSawLink = false; world.Solvers.RegisterNeighborSolver( "custom-capability-observer", - new AtmosNeighborSelection( + new VoxelNeighborSelection( "tests/custom-capability-v1", false, link => (link.Flags & customCapability) != 0), @@ -344,14 +347,14 @@ public void CreateLinksPortalAndDock_AcceptHostDefinedFlagBits() var simulation = world.CreateSimulation(2, 2, 1); var chunk = simulation.CreateAndRegisterChunk(default); simulation.SetChunkClassification(chunk, new VoxelClassification(1)); - const ExplicitLinkFlags customCapability = (ExplicitLinkFlags)(1 << 3); + const AtmosLinkFlags customCapability = (AtmosLinkFlags)(1 << 3); Assert.Multiple(() => { Assert.That( () => world.CreateLinks( [ - new ExplicitLinkDefinition( + new ExplicitLinkDefinition( simulation.GetCellRef(chunk, 0), simulation.GetCellRef(chunk, 3), customCapability) @@ -366,7 +369,7 @@ public void CreateLinksPortalAndDock_AcceptHostDefinedFlagBits() Throws.Nothing); Assert.That( - () => world.CreatePortal(simulation.GetCellRef(chunk, 0), simulation.GetCellRef(chunk, 1), ExplicitLinkFlags.None), + () => world.CreatePortal(simulation.GetCellRef(chunk, 0), simulation.GetCellRef(chunk, 1), AtmosLinkFlags.None), Throws.ArgumentException); }); } @@ -399,14 +402,14 @@ private static AtmosWorld CreateDeterministicPortalWorld(bool reversePortal) world.CreatePortal( reversePortal ? target : source, reversePortal ? source : target, - ExplicitLinkFlags.GasTransport); + AtmosLinkFlags.GasTransport); world.Solvers.RegisterNeighborSolver( "deterministic-observer", - new AtmosNeighborSelection( + new VoxelNeighborSelection( "tests/deterministic-observer-v1", true, - static link => (link.Flags & ExplicitLinkFlags.GasTransport) != 0), + static link => (link.Flags & AtmosLinkFlags.GasTransport) != 0), context => { // Traverse the complete view so ordering bugs are observable to this fixture without mutating state. @@ -421,9 +424,9 @@ private static AtmosWorld CreateDeterministicPortalWorld(bool reversePortal) internal static class AtmosNeighborTestExtensions { - internal static AtmosNeighbor[] ToArray(this AtmosNeighborEnumerable neighbors) + internal static VoxelNeighbor[] ToArray(this VoxelNeighborEnumerable neighbors) { - var result = new List(); + var result = new List>(); foreach (var neighbor in neighbors) result.Add(neighbor); diff --git a/tests/Numos.API.Tests/AtmosWorldTests.cs b/tests/Numos.API.Tests/AtmosWorldTests.cs index af1b1da..ccff90c 100644 --- a/tests/Numos.API.Tests/AtmosWorldTests.cs +++ b/tests/Numos.API.Tests/AtmosWorldTests.cs @@ -1,3 +1,5 @@ +using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.Maths; @@ -70,7 +72,7 @@ public void InterSimulationPortal_ActivatesAtTickBoundaryAndConservesGas() var portal = world.CreatePortal( first.GetCellRef(source, 0), second.GetCellRef(target, 0), - ExplicitLinkFlags.GasTransport); + AtmosLinkFlags.GasTransport); Assert.That(world.ActiveLinkCount, Is.Zero); world.Tick(); @@ -98,7 +100,7 @@ public void LinkRegistration_CanonicalizesEndpointsAndRejectsDuplicates() var first = simulation.GetCellRef(firstChunk, 0); var second = simulation.GetCellRef(secondChunk, 0); - var handle = world.CreateLinks([new ExplicitLinkDefinition(second, first, ExplicitLinkFlags.GasTransport)]); + var handle = world.CreateLinks([new ExplicitLinkDefinition(second, first, AtmosLinkFlags.GasTransport)]); var link = world.GetLinks(handle).Single(); Assert.Multiple(() => @@ -106,7 +108,7 @@ public void LinkRegistration_CanonicalizesEndpointsAndRejectsDuplicates() Assert.That(link.First, Is.EqualTo(first)); Assert.That(link.Second, Is.EqualTo(second)); Assert.That( - () => world.CreateLinks([new ExplicitLinkDefinition(first, second)]), + () => world.CreateLinks([new ExplicitLinkDefinition(first, second, AtmosLinkFlags.All)]), Throws.ArgumentException); }); } @@ -120,7 +122,13 @@ public void DestroyedLinkSet_BecomesStaleAndStopsFutureTransfer() var source = CreateOpenChunk(first); var target = CreateOpenChunk(second); first.AddGasToVoxel(source, 0, GasName, 1f, 300f); - var handle = world.CreateLinks([new ExplicitLinkDefinition(first.GetCellRef(source, 0), second.GetCellRef(target, 0))]); + var handle = world.CreateLinks( + [ + new ExplicitLinkDefinition( + first.GetCellRef(source, 0), + second.GetCellRef(target, 0), + AtmosLinkFlags.All) + ]); world.Tick(); world.DestroyLinks(handle); @@ -151,8 +159,8 @@ public void MultiplePortalsFromOneCell_CannotOverdrawAnySpecies() var sourceCell = simulation.GetCellRef(source, 0); world.CreateLinks( [ - new ExplicitLinkDefinition(sourceCell, simulation.GetCellRef(firstTarget, 0)), - new ExplicitLinkDefinition(sourceCell, simulation.GetCellRef(secondTarget, 0)) + new ExplicitLinkDefinition(sourceCell, simulation.GetCellRef(firstTarget, 0), AtmosLinkFlags.All), + new ExplicitLinkDefinition(sourceCell, simulation.GetCellRef(secondTarget, 0), AtmosLinkFlags.All) ]); world.Tick(); @@ -175,7 +183,7 @@ public void RemovingChunk_InvalidatesOnlyIncidentSparseTopology() var simulation = world.CreateSimulation(1, 1, 1); var first = CreateOpenChunk(simulation); var second = CreateOpenChunk(simulation, new Int3(2, 0, 0)); - var handle = world.CreateLinks([new ExplicitLinkDefinition(simulation.GetCellRef(first, 0), simulation.GetCellRef(second, 0))]); + var handle = world.CreateLinks([new ExplicitLinkDefinition(simulation.GetCellRef(first, 0), simulation.GetCellRef(second, 0), AtmosLinkFlags.All)]); world.Tick(); @@ -198,7 +206,7 @@ public void WorldCheckpoint_RestoresTopologyHandlesAndSimulationState() var source = CreateOpenChunk(first); var target = CreateOpenChunk(second); first.AddGasToVoxel(source, 0, GasName, 1f, 300f); - var handle = world.CreateLinks([new ExplicitLinkDefinition(first.GetCellRef(source, 0), second.GetCellRef(target, 0))]); + var handle = world.CreateLinks([new ExplicitLinkDefinition(first.GetCellRef(source, 0), second.GetCellRef(target, 0), AtmosLinkFlags.All)]); world.Tick(); var checkpoint = world.CaptureCheckpoint(); @@ -234,7 +242,7 @@ public void WorldCheckpoint_RestoresPendingLinkActivation() var source = CreateOpenChunk(first); var target = CreateOpenChunk(second); first.AddGasToVoxel(source, 0, GasName, 1f, 300f); - var handle = world.CreateLinks([new ExplicitLinkDefinition(first.GetCellRef(source, 0), second.GetCellRef(target, 0))]); + var handle = world.CreateLinks([new ExplicitLinkDefinition(first.GetCellRef(source, 0), second.GetCellRef(target, 0), AtmosLinkFlags.All)]); var checkpoint = world.CaptureCheckpoint(); @@ -266,7 +274,7 @@ public void WorldCheckpoint_RestoresPendingLinkRemoval() var source = CreateOpenChunk(first); var target = CreateOpenChunk(second); first.AddGasToVoxel(source, 0, GasName, 1f, 300f); - var handle = world.CreateLinks([new ExplicitLinkDefinition(first.GetCellRef(source, 0), second.GetCellRef(target, 0))]); + var handle = world.CreateLinks([new ExplicitLinkDefinition(first.GetCellRef(source, 0), second.GetCellRef(target, 0), AtmosLinkFlags.All)]); world.Tick(); world.DestroyLinks(handle); @@ -298,14 +306,14 @@ public void EquivalentRegistrationSequences_CompileToTheSameEdgeOrder() using var secondWorld = new AtmosWorld(CreateConfig()); var firstSimulation = firstWorld.CreateSimulation(1, 1, 1); var secondSimulation = secondWorld.CreateSimulation(1, 1, 1); - AtmosChunkHandle[] firstChunks = + ChunkHandle[] firstChunks = [ CreateOpenChunk(firstSimulation), CreateOpenChunk(firstSimulation, new Int3(2, 0, 0)), CreateOpenChunk(firstSimulation, new Int3(4, 0, 0)) ]; - AtmosChunkHandle[] secondChunks = + ChunkHandle[] secondChunks = [ CreateOpenChunk(secondSimulation), CreateOpenChunk(secondSimulation, new Int3(2, 0, 0)), @@ -314,18 +322,18 @@ public void EquivalentRegistrationSequences_CompileToTheSameEdgeOrder() firstWorld.CreateLinks( [ - new ExplicitLinkDefinition(firstSimulation.GetCellRef(firstChunks[1], 0), firstSimulation.GetCellRef(firstChunks[2], 0)), - new ExplicitLinkDefinition(firstSimulation.GetCellRef(firstChunks[0], 0), firstSimulation.GetCellRef(firstChunks[1], 0)) + new ExplicitLinkDefinition(firstSimulation.GetCellRef(firstChunks[1], 0), firstSimulation.GetCellRef(firstChunks[2], 0), AtmosLinkFlags.All), + new ExplicitLinkDefinition(firstSimulation.GetCellRef(firstChunks[0], 0), firstSimulation.GetCellRef(firstChunks[1], 0), AtmosLinkFlags.All) ]); secondWorld.CreateLinks( [ - new ExplicitLinkDefinition(secondSimulation.GetCellRef(secondChunks[0], 0), secondSimulation.GetCellRef(secondChunks[1], 0)) + new ExplicitLinkDefinition(secondSimulation.GetCellRef(secondChunks[0], 0), secondSimulation.GetCellRef(secondChunks[1], 0), AtmosLinkFlags.All) ]); secondWorld.CreateLinks( [ - new ExplicitLinkDefinition(secondSimulation.GetCellRef(secondChunks[2], 0), secondSimulation.GetCellRef(secondChunks[1], 0)) + new ExplicitLinkDefinition(secondSimulation.GetCellRef(secondChunks[2], 0), secondSimulation.GetCellRef(secondChunks[1], 0), AtmosLinkFlags.All) ]); firstWorld.Tick(); @@ -349,7 +357,7 @@ public void PortalAndOrdinaryNeighbor_DoNotOverdrawTheirSharedSource() world.CreatePortal( local.GetCellRef(localChunk, 0), remote.GetCellRef(remoteChunk, 0), - ExplicitLinkFlags.GasTransport); + AtmosLinkFlags.GasTransport); world.Tick(); @@ -384,8 +392,8 @@ public void DockBatch_TransfersAcrossEveryPairAndUndocksAsOneUnit() var dock = world.CreateDock( [ - new ExplicitLinkDefinition(first.GetCellRef(firstChunk, 0), second.GetCellRef(secondChunk, 0)), - new ExplicitLinkDefinition(first.GetCellRef(firstChunk, 1), second.GetCellRef(secondChunk, 1)) + new ExplicitLinkDefinition(first.GetCellRef(firstChunk, 0), second.GetCellRef(secondChunk, 0), AtmosLinkFlags.All), + new ExplicitLinkDefinition(first.GetCellRef(firstChunk, 1), second.GetCellRef(secondChunk, 1), AtmosLinkFlags.All) ]); world.Tick(); @@ -411,7 +419,7 @@ public void DestroyedSimulation_InvalidatesIncidentLinksAndReusedIdGeneration() var second = world.CreateSimulation(1, 1, 1); var firstChunk = CreateOpenChunk(first); var secondChunk = CreateOpenChunk(second); - var links = world.CreateLinks([new ExplicitLinkDefinition(first.GetCellRef(firstChunk, 0), second.GetCellRef(secondChunk, 0))]); + var links = world.CreateLinks([new ExplicitLinkDefinition(first.GetCellRef(firstChunk, 0), second.GetCellRef(secondChunk, 0), AtmosLinkFlags.All)]); world.Tick(); var removedId = second.Id; @@ -441,7 +449,7 @@ public void ExplicitTransport_RunsAtTheSharedAdvectionBarrierBeforeLaterStages() world.CreatePortal( first.GetCellRef(source, 0), second.GetCellRef(target, 0), - ExplicitLinkFlags.GasTransport); + AtmosLinkFlags.GasTransport); float observedMoles = 0f; world.Solvers.RegisterAfter( @@ -472,7 +480,7 @@ public void ThermalOnlyPortal_UsesThermodynamicsCadenceAndConservesEnergy() world.CreatePortal( first.GetCellRef(firstChunk, 0), second.GetCellRef(secondChunk, 0), - ExplicitLinkFlags.ThermalTransport); + AtmosLinkFlags.ThermalTransport); float heatCapacity = world.Config.GetMolarHeatCapacityAtConstantVolume(0); float initialEnergy = heatCapacity * 600f; @@ -515,7 +523,7 @@ public void DisabledExplicitThermalTransport_StopsThermalPortalTransport() world.CreatePortal( first.GetCellRef(firstChunk, 0), second.GetCellRef(secondChunk, 0), - ExplicitLinkFlags.ThermalTransport); + AtmosLinkFlags.ThermalTransport); world.Solvers.SetEnabled(AtmosBuiltInSolvers.ExplicitThermalTransport, false); @@ -547,7 +555,7 @@ public void DisabledThermodynamics_DoesNotStopThermalPortalTransport() world.CreatePortal( first.GetCellRef(firstChunk, 0), second.GetCellRef(secondChunk, 0), - ExplicitLinkFlags.ThermalTransport); + AtmosLinkFlags.ThermalTransport); // Disabling intra-chunk thermodynamics no longer disables portal thermal transport: the two stages are // independent now, unlike the pre-split fused domain. @@ -583,7 +591,7 @@ private static AtmosConfig CreateConfig() return config; } - private static AtmosChunkHandle CreateOpenChunk( + private static ChunkHandle CreateOpenChunk( AtmosSimulation simulation, Int3 position = default) { @@ -593,7 +601,7 @@ private static AtmosChunkHandle CreateOpenChunk( return chunk; } - private static float TotalMoles(AtmosSimulation simulation, AtmosChunkHandle chunk) + private static float TotalMoles(AtmosSimulation simulation, ChunkHandle chunk) { return simulation.GetChunkSnapshot(chunk).Gases.Sum(gas => gas.Moles.Sum()); } diff --git a/tests/Numos.CoreSim.IntegrationTests/CrossChunkFlowTests.cs b/tests/Numos.CoreSim.IntegrationTests/CrossChunkFlowTests.cs index 3d8df34..85ba916 100644 --- a/tests/Numos.CoreSim.IntegrationTests/CrossChunkFlowTests.cs +++ b/tests/Numos.CoreSim.IntegrationTests/CrossChunkFlowTests.cs @@ -1,4 +1,5 @@ using Numos.API; +using Numos.Chunks; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.Datatypes.Snapshots; using Numos.Maths; @@ -348,7 +349,7 @@ public void BoundaryFlow_ParallelInjectionBatchesRemainDeterministic() Int3[] sourcePositions = [Int3.NegX, Int3.PosX, Int3.NegY, Int3.PosY]; float[] sourceTemperatures = [250f, 300f, 350f, 400f]; - var sources = new AtmosChunkHandle[sourcePositions.Length]; + var sources = new ChunkHandle[sourcePositions.Length]; for (int sourceIndex = 0; sourceIndex < sourcePositions.Length; sourceIndex++) { sources[sourceIndex] = SimTestHelpers.CreateOpenChunk(simulation, sourcePositions[sourceIndex]); @@ -861,7 +862,7 @@ public void VacuumCleanup_UsesPressurizedNeighborAcrossChunkBoundary() Is.EqualTo(0.1f).Within(SimTestHelpers.Tolerance)); } - private static AtmosChunkHandle CreateIsolatedVoxel( + private static ChunkHandle CreateIsolatedVoxel( AtmosSimulation simulation, Int3 position, int x, int y, int z, VoxelClassification classification) { diff --git a/tests/Numos.CoreSim.IntegrationTests/SimTestHelpers.cs b/tests/Numos.CoreSim.IntegrationTests/SimTestHelpers.cs index 6e9c2f3..19da073 100644 --- a/tests/Numos.CoreSim.IntegrationTests/SimTestHelpers.cs +++ b/tests/Numos.CoreSim.IntegrationTests/SimTestHelpers.cs @@ -1,4 +1,5 @@ using Numos.API; +using Numos.Chunks; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.Datatypes.Snapshots; using Numos.Maths; @@ -40,7 +41,7 @@ internal static AtmosConfig CreateDeterministicConfig() }; } - internal static AtmosChunkHandle CreateOpenChunk( + internal static ChunkHandle CreateOpenChunk( AtmosSimulation simulation, Int3 position, VoxelClassification? classification = null) { @@ -50,7 +51,7 @@ internal static AtmosChunkHandle CreateOpenChunk( } internal static void SetAllTemperatures( - AtmosSimulation simulation, AtmosChunkHandle chunk, + AtmosSimulation simulation, ChunkHandle chunk, int width, int height, int depth, float temperature = DefaultTemperature) { for (int z = 0; z < depth; z++) diff --git a/tests/Numos.CoreSim.IntegrationTests/ThermodynamicsIntegrationTests.cs b/tests/Numos.CoreSim.IntegrationTests/ThermodynamicsIntegrationTests.cs index 31df70a..4e8abf7 100644 --- a/tests/Numos.CoreSim.IntegrationTests/ThermodynamicsIntegrationTests.cs +++ b/tests/Numos.CoreSim.IntegrationTests/ThermodynamicsIntegrationTests.cs @@ -1,4 +1,5 @@ using Numos.API; +using Numos.Chunks; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.Datatypes.Snapshots; using Numos.Maths; @@ -177,7 +178,7 @@ public void CrossChunkThermalDiffusion_MultipleFacesUseOneSymmetricSnapshot() using var simulation = new AtmosSimulation(config, 1, 1, 1); var center = SimTestHelpers.CreateOpenChunk(simulation, new Int3(0, 0, 0)); Int3[] neighborPositions = [Int3.NegX, Int3.PosX, Int3.NegY, Int3.PosY]; - AtmosChunkHandle[] neighbors = neighborPositions + ChunkHandle[] neighbors = neighborPositions .Select(position => SimTestHelpers.CreateOpenChunk(simulation, position)) .ToArray(); @@ -1086,7 +1087,7 @@ private static AtmosConfig CreateCondensationConfig() return config; } - private static AtmosChunkHandle CreateIsolatedVoxel( + private static ChunkHandle CreateIsolatedVoxel( AtmosSimulation simulation, Int3 position, int x, int y, int z, VoxelClassification classification, float temperature) { diff --git a/tests/Numos.CoreSim.Tests/AtmosChunkTopologyTests.cs b/tests/Numos.CoreSim.Tests/AtmosChunkTopologyTests.cs index 5339889..e050cfa 100644 --- a/tests/Numos.CoreSim.Tests/AtmosChunkTopologyTests.cs +++ b/tests/Numos.CoreSim.Tests/AtmosChunkTopologyTests.cs @@ -1,3 +1,4 @@ +using Numos.Chunks; using Numos.CoreSim.Datatypes.Primitives; using Numos.Maths; diff --git a/tests/Numos.Serialization.Tests/NumosReplaySerializerTests.cs b/tests/Numos.Serialization.Tests/NumosReplaySerializerTests.cs index 63a451b..442f9ce 100644 --- a/tests/Numos.Serialization.Tests/NumosReplaySerializerTests.cs +++ b/tests/Numos.Serialization.Tests/NumosReplaySerializerTests.cs @@ -1,4 +1,5 @@ using Numos.API; +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.Replay; @@ -98,7 +99,7 @@ public void RoundTrip_PreservesReplayAndBuildsImportedTimeline() operation.Sequence <= imported.Position.OperationSequence); imported.SimulateFromHere(); - replaySimulation.SetVoxelTemperature(new AtmosChunkHandle(default), 1, 350f); + replaySimulation.SetVoxelTemperature(new ChunkHandle(default), 1, 350f); var branch = imported.CaptureReplay(); Assert.Multiple(() => { diff --git a/tests/Numos.Serialization.Tests/ReplayWireFormatGoldenTests.cs b/tests/Numos.Serialization.Tests/ReplayWireFormatGoldenTests.cs index 4854636..18d1e01 100644 --- a/tests/Numos.Serialization.Tests/ReplayWireFormatGoldenTests.cs +++ b/tests/Numos.Serialization.Tests/ReplayWireFormatGoldenTests.cs @@ -1,4 +1,5 @@ using Numos.API; +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.Replay; @@ -213,7 +214,7 @@ private static NumosWorldReplayDocument BuildGoldenWorldReplayDocument() return new NumosWorldReplayDocument(metadata, archive); } - private static AtmosChunkHandle CreateOpenChunk(AtmosSimulation simulation, Int3 position) + private static ChunkHandle CreateOpenChunk(AtmosSimulation simulation, Int3 position) { var chunk = simulation.CreateAndRegisterChunk(position); simulation.SetChunkClassification(chunk, new VoxelClassification(1));