From 7c333656cf0992eb6e75ee3f57a36858632b3b0a Mon Sep 17 00:00:00 2001 From: Roudenn Date: Sun, 20 Sep 2026 17:18:05 +0300 Subject: [PATCH 01/11] `Numos.Chunks` & split into `ChunkConstants` --- Numos.slnx | 1 + .../Infrastructure/SimulationWorkload.cs | 3 +- eng/package_manifest.py | 5 +++- src/Numos.API/AtmosSimulation.cs | 25 ++++++++-------- src/Numos.API/AtmosWorld.cs | 7 +++-- src/Numos.Chunks/ChunkConstants.cs | 16 ++++++++++ src/Numos.Chunks/Numos.Chunks.csproj | 25 ++++++++++++++++ src/Numos.CoreSim/AtmosChunk.cs | 29 ++++++++++--------- src/Numos.CoreSim/AtmosChunkConstants.cs | 13 --------- src/Numos.CoreSim/AtmosKernel.cs | 7 +++-- src/Numos.CoreSim/Numos.CoreSim.csproj | 1 + .../SimulationViewer.ProjectUi.cs | 9 +++--- src/Numos.Viewer/SimulationViewer.World.cs | 6 ++-- .../AtmosChunkTopologyTests.cs | 1 + 14 files changed, 94 insertions(+), 54 deletions(-) create mode 100644 src/Numos.Chunks/ChunkConstants.cs create mode 100644 src/Numos.Chunks/Numos.Chunks.csproj diff --git a/Numos.slnx b/Numos.slnx index eaf3258..5b04e3e 100644 --- a/Numos.slnx +++ b/Numos.slnx @@ -22,6 +22,7 @@ + diff --git a/benchmarks/Numos.CoreSim.Benchmarks/Infrastructure/SimulationWorkload.cs b/benchmarks/Numos.CoreSim.Benchmarks/Infrastructure/SimulationWorkload.cs index c0dccb9..3f59f3b 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; 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/AtmosSimulation.cs b/src/Numos.API/AtmosSimulation.cs index 066bf30..877ec7a 100644 --- a/src/Numos.API/AtmosSimulation.cs +++ b/src/Numos.API/AtmosSimulation.cs @@ -1,4 +1,5 @@ using JetBrains.Annotations; +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.Datatypes.Snapshots; @@ -47,9 +48,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 +72,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) { } @@ -93,24 +94,24 @@ internal AtmosSimulation( 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; diff --git a/src/Numos.API/AtmosWorld.cs b/src/Numos.API/AtmosWorld.cs index da1ad99..40627c5 100644 --- a/src/Numos.API/AtmosWorld.cs +++ b/src/Numos.API/AtmosWorld.cs @@ -1,5 +1,6 @@ using System.Buffers; using JetBrains.Annotations; +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Replay; using Numos.CoreSim.Solvers; @@ -330,9 +331,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) { diff --git a/src/Numos.Chunks/ChunkConstants.cs b/src/Numos.Chunks/ChunkConstants.cs new file mode 100644 index 0000000..7ff2a76 --- /dev/null +++ b/src/Numos.Chunks/ChunkConstants.cs @@ -0,0 +1,16 @@ +namespace 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/Numos.Chunks.csproj b/src/Numos.Chunks/Numos.Chunks.csproj new file mode 100644 index 0000000..52bbbbb --- /dev/null +++ b/src/Numos.Chunks/Numos.Chunks.csproj @@ -0,0 +1,25 @@ + + + + + + net10.0 + enable + enable + Numos.Chunks + Numos.Chunks + Numos Chunks + Voxel Chunk utilities used by Numos. + simulation;collections;voxel;nativeaot + true + + + + + + + + + + + diff --git a/src/Numos.CoreSim/AtmosChunk.cs b/src/Numos.CoreSim/AtmosChunk.cs index 377ca72..39debe5 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; @@ -162,12 +163,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; @@ -223,7 +224,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 +233,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; @@ -749,24 +750,24 @@ private static int GetValidatedVoxelCount(int width, int height, int depth) ArgumentOutOfRangeException.ThrowIfNegativeOrZero(width); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(height); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(depth); - if (width > AtmosChunkConstants.MaximumVoxelCount || - height > AtmosChunkConstants.MaximumVoxelCount || - depth > AtmosChunkConstants.MaximumVoxelCount) + if (width > ChunkConstants.MaximumVoxelCount || + height > ChunkConstants.MaximumVoxelCount || + depth > ChunkConstants.MaximumVoxelCount) { throw new ArgumentOutOfRangeException( nameof(width), width, - $"No chunk dimension may exceed {AtmosChunkConstants.MaximumVoxelCount}."); + $"No chunk dimension may exceed {ChunkConstants.MaximumVoxelCount}."); } long voxelCount = (long)width * height * depth; - if (voxelCount > AtmosChunkConstants.MaximumVoxelCount) + if (voxelCount > ChunkConstants.MaximumVoxelCount) { throw new ArgumentOutOfRangeException( nameof(width), width, $"Chunk dimensions contain {voxelCount} voxels, but at most " + - $"{AtmosChunkConstants.MaximumVoxelCount} are supported."); + $"{ChunkConstants.MaximumVoxelCount} are supported."); } return (int)voxelCount; 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.cs b/src/Numos.CoreSim/AtmosKernel.cs index 5a7f91d..2ccde2c 100644 --- a/src/Numos.CoreSim/AtmosKernel.cs +++ b/src/Numos.CoreSim/AtmosKernel.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Diagnostics; +using Numos.Chunks; using Numos.CoreSim.Replay; using Numos.CoreSim.Solvers; using Numos.Maths; @@ -41,9 +42,9 @@ internal sealed partial class AtmosKernel : IDisposable 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); _defaultSolvers = new DefaultAtmosSolvers(chunkWidth, chunkHeight, chunkDepth); diff --git a/src/Numos.CoreSim/Numos.CoreSim.csproj b/src/Numos.CoreSim/Numos.CoreSim.csproj index 143e277..a880883 100644 --- a/src/Numos.CoreSim/Numos.CoreSim.csproj +++ b/src/Numos.CoreSim/Numos.CoreSim.csproj @@ -27,6 +27,7 @@ + 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; diff --git a/src/Numos.Viewer/SimulationViewer.World.cs b/src/Numos.Viewer/SimulationViewer.World.cs index fc0d106..bfb3e21 100644 --- a/src/Numos.Viewer/SimulationViewer.World.cs +++ b/src/Numos.Viewer/SimulationViewer.World.cs @@ -1,7 +1,7 @@ using System.Numerics; using ImGuiNET; using Numos.API; -using Numos.CoreSim; +using Numos.Chunks; using Numos.CoreSim.Datatypes.Snapshots; using Numos.Maths; using Numos.SimDrawer; @@ -18,8 +18,8 @@ public partial class SimulationViewer private AtmosSimulationId? _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; 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; From 3d02c66d5f73b3b59023127870e0cde93b4a4076 Mon Sep 17 00:00:00 2001 From: Roudenn Date: Sun, 20 Sep 2026 17:37:43 +0300 Subject: [PATCH 02/11] Abstract `Chunk` class and universal `ChunkHandle` --- .../Scaling/ExplicitTopologyBenchmarks.cs | 3 +- benchmarks/Numos.Replay.Benchmarks/Program.cs | 3 +- src/Numos.API.Dangerous/AtmosDangerousApi.cs | 4 +- src/Numos.API/AtmosChunkHandle.cs | 8 -- src/Numos.API/AtmosSimulation.GasMixtures.cs | 5 +- .../AtmosSimulation.SolverStorage.cs | 5 +- src/Numos.API/AtmosSimulation.cs | 52 +++---- src/Numos.API/AtmosWorld.Replay.cs | 3 +- src/Numos.API/AtmosWorld.cs | 8 +- src/Numos.API/AtmosWorldNeighborTopology.cs | 13 +- src/Numos.API/ExplicitAtmosTopology.cs | 3 +- src/Numos.Chunks/Chunk.cs | 133 ++++++++++++++++++ src/Numos.Chunks/ChunkHandle.cs | 8 ++ src/Numos.Collections/FlatArray.cs | 52 +++++-- src/Numos.CoreSim/AtmosChunk.cs | 133 +----------------- .../Diagnostics/SimulationStateAnalyzer.cs | 7 +- src/Numos.Headless/SimulationSession.cs | 5 +- .../NumosWorldReplaySerializer.cs | 3 +- src/Numos.Viewer/SimulationViewer.Project.cs | 9 +- .../SimulationViewer.ProjectUi.cs | 8 +- src/Numos.Viewer/SimulationViewer.RenderUi.cs | 5 +- .../SimulationViewer.TopologyUi.cs | 9 +- .../SimulationViewer.VoxelEditing.cs | 11 +- src/Numos.Viewer/SimulationViewer.World.cs | 4 +- src/Numos.Viewer/SimulationViewer.cs | 5 +- .../AtmosDangerousApiTests.cs | 5 +- .../Numos.API.Tests/AtmosChunkVersionTests.cs | 11 +- tests/Numos.API.Tests/AtmosReplayTests.cs | 3 +- .../AtmosSimulationContractTests.cs | 5 +- .../AtmosSolverPipelineTests.cs | 7 +- .../AtmosSolverStorageTests.cs | 3 +- .../Numos.API.Tests/AtmosWorldReplayTests.cs | 3 +- tests/Numos.API.Tests/AtmosWorldTests.cs | 9 +- .../NumosReplaySerializerTests.cs | 3 +- .../ReplayWireFormatGoldenTests.cs | 3 +- .../CrossChunkFlowTests.cs | 5 +- .../SimTestHelpers.cs | 5 +- .../ThermodynamicsIntegrationTests.cs | 5 +- 38 files changed, 313 insertions(+), 253 deletions(-) delete mode 100644 src/Numos.API/AtmosChunkHandle.cs create mode 100644 src/Numos.Chunks/Chunk.cs create mode 100644 src/Numos.Chunks/ChunkHandle.cs diff --git a/benchmarks/Numos.CoreSim.Benchmarks/Scaling/ExplicitTopologyBenchmarks.cs b/benchmarks/Numos.CoreSim.Benchmarks/Scaling/ExplicitTopologyBenchmarks.cs index 9b7b18a..df833e9 100644 --- a/benchmarks/Numos.CoreSim.Benchmarks/Scaling/ExplicitTopologyBenchmarks.cs +++ b/benchmarks/Numos.CoreSim.Benchmarks/Scaling/ExplicitTopologyBenchmarks.cs @@ -1,5 +1,6 @@ using BenchmarkDotNet.Attributes; using Numos.API; +using Numos.Chunks; using Numos.CoreSim.Datatypes.Primitives; namespace Numos.CoreSim.Benchmarks.Scaling; @@ -15,7 +16,7 @@ namespace Numos.CoreSim.Benchmarks.Scaling; [BenchmarkCategory("Scaling", "ExplicitTopology")] public class ExplicitTopologyBenchmarks { - private AtmosChunkHandle _chunk; + private ChunkHandle _chunk; private ExplicitLinkDefinition[] _definitions = []; private ExplicitLinkSetHandle _links; private AtmosSimulation _simulation = null!; 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/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/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 877ec7a..d01cb3d 100644 --- a/src/Numos.API/AtmosSimulation.cs +++ b/src/Numos.API/AtmosSimulation.cs @@ -426,11 +426,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); + return new ChunkHandle(position); } /// @@ -445,7 +445,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); @@ -465,7 +465,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 AtmosCellRef GetCellRef(ChunkHandle chunk, ushort localVoxelIndex) { ThrowIfDisposed(); if (!_kernel.TryResolveExplicitEndpoint(chunk.Position, localVoxelIndex, out _)) @@ -488,7 +488,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(); @@ -506,7 +506,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)) @@ -519,7 +519,7 @@ public bool TryGetChunkHandles( return true; } - private static AtmosChunkHandle[] CreateSortedHandles(Int3[] positions) + private static ChunkHandle[] CreateSortedHandles(Int3[] positions) { Array.Sort( positions, @@ -533,9 +533,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; } @@ -551,7 +551,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); @@ -565,7 +565,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(); @@ -587,7 +587,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) @@ -609,7 +609,7 @@ public bool TryGetVoxelSnapshot( /// when a new snapshot was created; otherwise . [PublicAPI] public bool TryGetChunkSnapshot( - AtmosChunkHandle chunk, + ChunkHandle chunk, AtmosChunkVersion knownVersion, out AtmosChunkSnapshot snapshot) { @@ -634,7 +634,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) @@ -672,7 +672,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); @@ -692,7 +692,7 @@ public void SetChunkClassification(AtmosChunkHandle chunk, VoxelClassification c /// The simulation has been disposed. [PublicAPI] public void SetChunkBoundaryClassification( - AtmosChunkHandle chunk, + ChunkHandle chunk, VoxelClassification classification) { ThrowIfDisposed(); @@ -711,7 +711,7 @@ public void SetChunkBoundaryClassification( /// The simulation has been disposed. [PublicAPI] public void SetVoxelClassification( - AtmosChunkHandle chunk, ushort localVoxelIndex, + ChunkHandle chunk, ushort localVoxelIndex, VoxelClassification classification) { ThrowIfDisposed(); @@ -731,7 +731,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(); @@ -754,7 +754,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); @@ -778,7 +778,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); @@ -812,7 +812,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) @@ -852,7 +852,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) @@ -878,7 +878,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) @@ -904,7 +904,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) @@ -920,7 +920,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); @@ -934,7 +934,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/AtmosWorld.Replay.cs b/src/Numos.API/AtmosWorld.Replay.cs index cb0c296..11b79a2 100644 --- a/src/Numos.API/AtmosWorld.Replay.cs +++ b/src/Numos.API/AtmosWorld.Replay.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using Numos.Chunks; using Numos.CoreSim.Replay; namespace Numos.API; @@ -381,7 +382,7 @@ private void ApplyWorldOperation(AtmosWorldOperation operation) simulation.Kernel.ApplyRecordedOperation(simulationOperation.Operation); if (simulationOperation.Operation is RemoveChunkOperation removed) - InvalidateLinksForChunk(simulation, new AtmosChunkHandle(removed.Position)); + InvalidateLinksForChunk(simulation, new ChunkHandle(removed.Position)); break; } diff --git a/src/Numos.API/AtmosWorld.cs b/src/Numos.API/AtmosWorld.cs index 40627c5..88a6a46 100644 --- a/src/Numos.API/AtmosWorld.cs +++ b/src/Numos.API/AtmosWorld.cs @@ -23,8 +23,8 @@ 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)); +/// ChunkHandle stationChunk = station.CreateAndRegisterChunk(new Int3(0, 0, 0)); +/// ChunkHandle 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); @@ -589,7 +589,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) @@ -938,7 +938,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) { diff --git a/src/Numos.API/AtmosWorldNeighborTopology.cs b/src/Numos.API/AtmosWorldNeighborTopology.cs index efcb9b4..0e65bf9 100644 --- a/src/Numos.API/AtmosWorldNeighborTopology.cs +++ b/src/Numos.API/AtmosWorldNeighborTopology.cs @@ -1,3 +1,4 @@ +using Numos.Chunks; using Numos.Maths; namespace Numos.API; @@ -78,7 +79,7 @@ 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 AtmosChunkNeighborView GetChunk(AtmosSimulation simulation, ChunkHandle chunk) { ArgumentNullException.ThrowIfNull(simulation); var world = GetWorld(); @@ -97,7 +98,7 @@ public AtmosChunkNeighborView GetChunk(AtmosSimulation simulation, AtmosChunkHan var adjacentPosition = chunk.Position + GetDirection(direction); if (world.TryResolveCell( - new AtmosCellRef(simulation.Id, new AtmosChunkHandle(adjacentPosition), 0), + new AtmosCellRef(simulation.Id, new ChunkHandle(adjacentPosition), 0), out _)) { adjacentChunkMask |= checked((byte)(1 << direction)); @@ -303,13 +304,13 @@ public readonly struct AtmosChunkNeighborView 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; internal AtmosChunkNeighborView( AtmosSimulationId simulation, - AtmosChunkHandle chunk, + ChunkHandle chunk, Int3 dimensions, bool includeCartesian, CompiledChunkAdjacency? explicitAdjacency, @@ -466,7 +467,7 @@ internal bool TryGetCartesianNeighbor( } ushort targetIndex = checked((ushort)(x + y * _dimensions.X + z * plane)); - neighbor = new AtmosCellRef(_simulation, new AtmosChunkHandle(chunkPosition), targetIndex); + neighbor = new AtmosCellRef(_simulation, new ChunkHandle(chunkPosition), targetIndex); return true; } @@ -571,7 +572,7 @@ public bool MoveNext() internal readonly record struct CompiledChunkKey( AtmosSimulationId Simulation, - AtmosChunkHandle Chunk); + ChunkHandle Chunk); internal readonly record struct CompiledNeighborEntry( ushort SourceIndex, diff --git a/src/Numos.API/ExplicitAtmosTopology.cs b/src/Numos.API/ExplicitAtmosTopology.cs index b1c8d0b..6a24580 100644 --- a/src/Numos.API/ExplicitAtmosTopology.cs +++ b/src/Numos.API/ExplicitAtmosTopology.cs @@ -1,3 +1,4 @@ +using Numos.Chunks; using Numos.Maths; namespace Numos.API; @@ -45,7 +46,7 @@ public int CompareTo(AtmosSimulationId other) /// public readonly record struct AtmosCellRef( AtmosSimulationId Simulation, - AtmosChunkHandle Chunk, + ChunkHandle Chunk, ushort LocalVoxelIndex) : IComparable { /// diff --git a/src/Numos.Chunks/Chunk.cs b/src/Numos.Chunks/Chunk.cs new file mode 100644 index 0000000..12bd82f --- /dev/null +++ b/src/Numos.Chunks/Chunk.cs @@ -0,0 +1,133 @@ +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 such as gas channels (because... you know.... they aren't physical). +/// +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); + + /// + /// 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); + } + + 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); + } + + 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/ChunkHandle.cs b/src/Numos.Chunks/ChunkHandle.cs new file mode 100644 index 0000000..f50bb6e --- /dev/null +++ b/src/Numos.Chunks/ChunkHandle.cs @@ -0,0 +1,8 @@ +using Numos.Maths; + +namespace Numos.Chunks; + +/// +/// Identifies a chunk owned by an . +/// +public readonly record struct ChunkHandle(Int3 Position); diff --git a/src/Numos.Collections/FlatArray.cs b/src/Numos.Collections/FlatArray.cs index c1c3a0b..49ee182 100644 --- a/src/Numos.Collections/FlatArray.cs +++ b/src/Numos.Collections/FlatArray.cs @@ -101,10 +101,7 @@ public T this[Int3 position] /// public int GetIndex(Int3 position) { - if (!position.IsWithin(_dimensions)) - throw new IndexOutOfRangeException(); - - return position.X + position.Y * _dimensions.X + position.Z * _dimensions.X * _dimensions.Y; + return FlatArrayHelpers.GetIndex(position, _dimensions); } /// @@ -112,10 +109,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 +119,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); } /// @@ -188,4 +181,37 @@ public T[] ToArray() { return [.. _data]; } -} \ No newline at end of file +} + +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 39debe5..edf4c8a 100644 --- a/src/Numos.CoreSim/AtmosChunk.cs +++ b/src/Numos.CoreSim/AtmosChunk.cs @@ -12,15 +12,7 @@ namespace Numos.CoreSim; -/// -/// 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 such as gas channels (because... you know.... they aren't physical). -/// -internal class AtmosChunk +internal class AtmosChunk : Chunk { private static long _nextGeneration; @@ -57,7 +49,7 @@ internal class AtmosChunk /// /// /// 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 + /// 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. @@ -66,21 +58,6 @@ internal class AtmosChunk /// 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. @@ -127,11 +104,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. /// @@ -146,11 +118,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; @@ -186,11 +153,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. /// @@ -509,7 +471,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. @@ -545,7 +506,6 @@ public void SetChunkToVacuum() IsVacuum.Fill(true); } - /// /// Sets a specific voxel classification. Solid and void classifications clear the voxel to vacuum. /// @@ -574,7 +534,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. /// @@ -588,7 +547,6 @@ public void SetChunkClassification(int roomId) VoxelRoomMap.Fill(roomId); } - /// /// Sets the entire chunk classification. Solid and void classifications clear the chunk to vacuum. /// @@ -685,91 +643,4 @@ public AtmosChunkSnapshot GetNetworkSnapshot( snapshot.Version = Version; 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) - { - 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; - } } \ No newline at end of file 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.Serialization/NumosWorldReplaySerializer.cs b/src/Numos.Serialization/NumosWorldReplaySerializer.cs index 4f4cbdc..2274464 100644 --- a/src/Numos.Serialization/NumosWorldReplaySerializer.cs +++ b/src/Numos.Serialization/NumosWorldReplaySerializer.cs @@ -1,4 +1,5 @@ using Numos.API; +using Numos.Chunks; using Numos.CoreSim.Replay; using Numos.Replay.SourceGen; @@ -540,7 +541,7 @@ private static AtmosCellRef ReadCell(BinaryReader reader) { return new AtmosCellRef( ReadSimulationId(reader), - new AtmosChunkHandle(NumosReplaySerializer.ReadInt3(reader)), + new ChunkHandle(NumosReplaySerializer.ReadInt3(reader)), reader.ReadUInt16()); } 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 865150f..6b92098 100644 --- a/src/Numos.Viewer/SimulationViewer.ProjectUi.cs +++ b/src/Numos.Viewer/SimulationViewer.ProjectUi.cs @@ -301,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}"); @@ -510,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..006c721 100644 --- a/src/Numos.Viewer/SimulationViewer.TopologyUi.cs +++ b/src/Numos.Viewer/SimulationViewer.TopologyUi.cs @@ -1,6 +1,7 @@ using System.Numerics; using ImGuiNET; using Numos.API; +using Numos.Chunks; using Numos.Maths; using Numos.Viewer.Ui; using Raylib_cs; @@ -274,7 +275,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)) @@ -363,8 +364,8 @@ 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."); @@ -408,7 +409,7 @@ private ExplicitLinkDefinition[] BuildDockLinks() return new AtmosCellRef( _simulation.Id, - new AtmosChunkHandle(_selectedCell.Value.Chunk.Position), + new ChunkHandle(_selectedCell.Value.Chunk.Position), _selectedCell.Value.LocalIndex); } diff --git a/src/Numos.Viewer/SimulationViewer.VoxelEditing.cs b/src/Numos.Viewer/SimulationViewer.VoxelEditing.cs index cd8e3f0..3444c36 100644 --- a/src/Numos.Viewer/SimulationViewer.VoxelEditing.cs +++ b/src/Numos.Viewer/SimulationViewer.VoxelEditing.cs @@ -1,6 +1,7 @@ using System.Numerics; using ImGuiNET; using Numos.API; +using Numos.Chunks; using Numos.CoreSim.Datatypes.Primitives; using Numos.SimDrawer; using Numos.Viewer.Rendering.Viewport; @@ -553,7 +554,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 +581,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 +594,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 +605,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 +620,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 bfb3e21..f6dfb0a 100644 --- a/src/Numos.Viewer/SimulationViewer.World.cs +++ b/src/Numos.Viewer/SimulationViewer.World.cs @@ -161,7 +161,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; @@ -462,7 +462,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..cd95dc0 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()); } @@ -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..723f936 100644 --- a/tests/Numos.API.Tests/AtmosWorldReplayTests.cs +++ b/tests/Numos.API.Tests/AtmosWorldReplayTests.cs @@ -1,3 +1,4 @@ +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.GasReactions; @@ -197,7 +198,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/AtmosWorldTests.cs b/tests/Numos.API.Tests/AtmosWorldTests.cs index af1b1da..8f07b5c 100644 --- a/tests/Numos.API.Tests/AtmosWorldTests.cs +++ b/tests/Numos.API.Tests/AtmosWorldTests.cs @@ -1,3 +1,4 @@ +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.Maths; @@ -298,14 +299,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)), @@ -583,7 +584,7 @@ private static AtmosConfig CreateConfig() return config; } - private static AtmosChunkHandle CreateOpenChunk( + private static ChunkHandle CreateOpenChunk( AtmosSimulation simulation, Int3 position = default) { @@ -593,7 +594,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.API.Tests/NumosReplaySerializerTests.cs b/tests/Numos.API.Tests/NumosReplaySerializerTests.cs index 2408fd2..5c54b33 100644 --- a/tests/Numos.API.Tests/NumosReplaySerializerTests.cs +++ b/tests/Numos.API.Tests/NumosReplaySerializerTests.cs @@ -1,3 +1,4 @@ +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.Replay; @@ -97,7 +98,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.API.Tests/ReplayWireFormatGoldenTests.cs b/tests/Numos.API.Tests/ReplayWireFormatGoldenTests.cs index 740ab8d..d9c6708 100644 --- a/tests/Numos.API.Tests/ReplayWireFormatGoldenTests.cs +++ b/tests/Numos.API.Tests/ReplayWireFormatGoldenTests.cs @@ -1,3 +1,4 @@ +using Numos.Chunks; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.Replay; @@ -212,7 +213,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)); diff --git a/tests/Numos.CoreSim.IntegrationTests/CrossChunkFlowTests.cs b/tests/Numos.CoreSim.IntegrationTests/CrossChunkFlowTests.cs index 52c5c65..5b13933 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; @@ -305,7 +306,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]); @@ -661,7 +662,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 3d2ee47..6b6eb2f 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(); @@ -1076,7 +1077,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) { From 1c419910968203630fc6f0c1ed1f43f269db2647 Mon Sep 17 00:00:00 2001 From: Roudenn Date: Sun, 20 Sep 2026 17:51:16 +0300 Subject: [PATCH 03/11] Docs polishing --- src/Numos.Chunks/Chunk.cs | 17 ++++++++++++++++- src/Numos.Chunks/ChunkHandle.cs | 2 +- src/Numos.CoreSim/AtmosChunk.cs | 8 ++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Numos.Chunks/Chunk.cs b/src/Numos.Chunks/Chunk.cs index 12bd82f..521920d 100644 --- a/src/Numos.Chunks/Chunk.cs +++ b/src/Numos.Chunks/Chunk.cs @@ -71,7 +71,6 @@ public ushort GetIndexUnsafe(Int3 vec) return (ushort)FlatArrayHelpers.GetIndexUnsafe(vec, Dimensions); } - /// /// Converts a flat voxel index to local x, y, and z coordinates. /// @@ -95,6 +94,12 @@ public Int3 GetXyzInt3(ushort index) return FlatArrayHelpers.GetPosition(index, Dimensions); } + /// + /// 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) @@ -103,6 +108,16 @@ protected void EnsureInitialized(ref FlatArray array, Int3 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); diff --git a/src/Numos.Chunks/ChunkHandle.cs b/src/Numos.Chunks/ChunkHandle.cs index f50bb6e..497ba69 100644 --- a/src/Numos.Chunks/ChunkHandle.cs +++ b/src/Numos.Chunks/ChunkHandle.cs @@ -3,6 +3,6 @@ namespace Numos.Chunks; /// -/// Identifies a chunk owned by an . +/// Identifies a chunk owned by a chunk map. /// public readonly record struct ChunkHandle(Int3 Position); diff --git a/src/Numos.CoreSim/AtmosChunk.cs b/src/Numos.CoreSim/AtmosChunk.cs index edf4c8a..81d5385 100644 --- a/src/Numos.CoreSim/AtmosChunk.cs +++ b/src/Numos.CoreSim/AtmosChunk.cs @@ -12,6 +12,14 @@ namespace Numos.CoreSim; +/// +/// 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 such as gas channels (because... you know.... they aren't physical). +/// internal class AtmosChunk : Chunk { private static long _nextGeneration; From cffecbf2e4ad1787e0145a6a5240454c69b204d4 Mon Sep 17 00:00:00 2001 From: Roudenn Date: Sun, 20 Sep 2026 18:27:47 +0300 Subject: [PATCH 04/11] huh? --- src/Numos.Chunks/Numos.Chunks.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Numos.Chunks/Numos.Chunks.csproj b/src/Numos.Chunks/Numos.Chunks.csproj index 52bbbbb..3eeb39c 100644 --- a/src/Numos.Chunks/Numos.Chunks.csproj +++ b/src/Numos.Chunks/Numos.Chunks.csproj @@ -15,6 +15,7 @@ + From 9f4d67cd35218d5dad14bcad315a8075058bfde4 Mon Sep 17 00:00:00 2001 From: Roudenn Date: Sun, 20 Sep 2026 20:52:52 +0300 Subject: [PATCH 05/11] fix wrong tag --- src/Numos.Chunks/Numos.Chunks.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Numos.Chunks/Numos.Chunks.csproj b/src/Numos.Chunks/Numos.Chunks.csproj index 3eeb39c..7a9a10a 100644 --- a/src/Numos.Chunks/Numos.Chunks.csproj +++ b/src/Numos.Chunks/Numos.Chunks.csproj @@ -10,7 +10,7 @@ Numos.Chunks Numos Chunks Voxel Chunk utilities used by Numos. - simulation;collections;voxel;nativeaot + simulation;chunks;voxel;nativeaot true From bc5cae6d309e94f48fc04104c1b2c6824b080885 Mon Sep 17 00:00:00 2001 From: Roudenn Date: Sat, 26 Sep 2026 11:13:21 +0300 Subject: [PATCH 06/11] Abstract chunk map --- .../Infrastructure/SimulationWorkload.cs | 2 +- src/Numos.API/AtmosSimulation.cs | 2 +- src/Numos.API/AtmosWorld.Replay.cs | 2 +- src/Numos.Chunks/Chunk.cs | 19 ++ src/Numos.Chunks/ChunkMap.cs | 170 ++++++++++++++++++ src/Numos.Chunks/IChunkInitializer.cs | 11 ++ src/Numos.CoreSim/AtmosChunk.cs | 37 ++-- src/Numos.CoreSim/AtmosKernel.API.cs | 94 +++------- src/Numos.CoreSim/AtmosKernel.GasMixtures.cs | 7 +- src/Numos.CoreSim/AtmosKernel.Replay.cs | 34 ++-- src/Numos.CoreSim/AtmosKernel.cs | 26 +-- 11 files changed, 269 insertions(+), 135 deletions(-) create mode 100644 src/Numos.Chunks/ChunkMap.cs create mode 100644 src/Numos.Chunks/IChunkInitializer.cs diff --git a/benchmarks/Numos.CoreSim.Benchmarks/Infrastructure/SimulationWorkload.cs b/benchmarks/Numos.CoreSim.Benchmarks/Infrastructure/SimulationWorkload.cs index 3f59f3b..779db3f 100644 --- a/benchmarks/Numos.CoreSim.Benchmarks/Infrastructure/SimulationWorkload.cs +++ b/benchmarks/Numos.CoreSim.Benchmarks/Infrastructure/SimulationWorkload.cs @@ -160,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/src/Numos.API/AtmosSimulation.cs b/src/Numos.API/AtmosSimulation.cs index d01cb3d..3bd9cb8 100644 --- a/src/Numos.API/AtmosSimulation.cs +++ b/src/Numos.API/AtmosSimulation.cs @@ -429,7 +429,7 @@ public AtmosRecording StopRecording() public ChunkHandle CreateAndRegisterChunk(Int3 position) { ThrowIfDisposed(); - _kernel.CreateAndRegisterChunk(position, _chunkWidth, _chunkHeight, _chunkDepth); + _kernel.CreateAndRegisterChunk(position); return new ChunkHandle(position); } diff --git a/src/Numos.API/AtmosWorld.Replay.cs b/src/Numos.API/AtmosWorld.Replay.cs index bdb3672..70354bb 100644 --- a/src/Numos.API/AtmosWorld.Replay.cs +++ b/src/Numos.API/AtmosWorld.Replay.cs @@ -386,7 +386,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) diff --git a/src/Numos.Chunks/Chunk.cs b/src/Numos.Chunks/Chunk.cs index 521920d..800f3ae 100644 --- a/src/Numos.Chunks/Chunk.cs +++ b/src/Numos.Chunks/Chunk.cs @@ -44,6 +44,19 @@ public abstract class Chunk /// 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. /// @@ -93,6 +106,12 @@ 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. diff --git a/src/Numos.Chunks/ChunkMap.cs b/src/Numos.Chunks/ChunkMap.cs new file mode 100644 index 0000000..919e329 --- /dev/null +++ b/src/Numos.Chunks/ChunkMap.cs @@ -0,0 +1,170 @@ +using System.Collections.Concurrent; +using Numos.Maths; + +namespace Numos.Chunks; + +/// +/// Represents a voxel chunk map. +/// +/// +public sealed class ChunkMap(int x, int y, int z) : IDisposable where T : Chunk, IChunkInitializer +{ + public readonly Int3 Dimensions = new Int3(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; + } + + 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 void CreateAndRegisterChunk(Int3 position) + { + if (_chunkMap.ContainsKey(position)) + throw new InvalidOperationException($"A chunk is already registered at {position}."); + + var chunk = T.CreateInitializeChunk(position, Dimensions.X, Dimensions.Y, Dimensions.Z); + RegisterChunk(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})."); + } + + 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(); + } + + public ConcurrentDictionary UnsafeGetStorage() + { + return _chunkMap; + } + + public void UnsafeReplaceStorage(ConcurrentDictionary replacement) + { + _chunkMap = replacement; + } + + 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})."); + } + } + + 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..218d8a0 --- /dev/null +++ b/src/Numos.Chunks/IChunkInitializer.cs @@ -0,0 +1,11 @@ +using Numos.Maths; + +namespace Numos.Chunks; + +public interface IChunkInitializer where T : Chunk +{ + abstract static T CreateInitializeChunk(Int3 position, + int width = ChunkConstants.DefaultWidth, + int height = ChunkConstants.DefaultHeight, + int depth = ChunkConstants.DefaultDepth); +} diff --git a/src/Numos.CoreSim/AtmosChunk.cs b/src/Numos.CoreSim/AtmosChunk.cs index 81d5385..a8f2eed 100644 --- a/src/Numos.CoreSim/AtmosChunk.cs +++ b/src/Numos.CoreSim/AtmosChunk.cs @@ -20,7 +20,7 @@ namespace Numos.CoreSim; /// Use and when converting indices /// for scalar-indexed storage such as gas channels (because... you know.... they aren't physical). /// -internal class AtmosChunk : Chunk +internal sealed class AtmosChunk : Chunk, IChunkInitializer { private static long _nextGeneration; @@ -51,21 +51,7 @@ internal class AtmosChunk : Chunk /// 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; - + /// /// Whether this chunk is eligible to be processed by the simulation. /// A sleeping chunk is skipped during simulation ticks. @@ -248,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) @@ -352,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; @@ -384,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(); @@ -651,4 +637,15 @@ public AtmosChunkSnapshot GetNetworkSnapshot( snapshot.Version = Version; return snapshot; } -} \ No newline at end of file + + public static AtmosChunk CreateInitializeChunk( + Int3 position, + int width = ChunkConstants.DefaultWidth, + int height = ChunkConstants.DefaultHeight, + int depth = ChunkConstants.DefaultDepth) + { + var chunk = new AtmosChunk(width, height, depth); + chunk.Initialize(position, width, height, depth); + return chunk; + } +} diff --git a/src/Numos.CoreSim/AtmosKernel.API.cs b/src/Numos.CoreSim/AtmosKernel.API.cs index d8d7458..6160093 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,21 +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); - RegisterChunk(chunk); + _chunkMap.CreateAndRegisterChunk(position); } } @@ -370,7 +353,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 +444,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 +529,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 +554,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 +571,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 +596,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 +618,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 +651,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 +691,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 +722,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 +742,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 2ccde2c..3c19dd5 100644 --- a/src/Numos.CoreSim/AtmosKernel.cs +++ b/src/Numos.CoreSim/AtmosKernel.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using System.Diagnostics; using Numos.Chunks; using Numos.CoreSim.Replay; @@ -13,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(); @@ -28,16 +24,14 @@ 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; @@ -46,7 +40,7 @@ internal AtmosKernel( 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); } @@ -66,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(); @@ -78,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!); } /// @@ -95,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; @@ -132,7 +124,7 @@ internal AtmosWorldTickExecution BeginWorldTick() { lock (StateGate) { - return new AtmosWorldTickExecution(BeginTickSimulation(OrderedChunks())); + return new AtmosWorldTickExecution(BeginTickSimulation(_chunkMap.OrderedChunks())); } } @@ -215,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 +} From 14dd4d1bc4fd2fcc69946bb9240cd8e00f3eb0af Mon Sep 17 00:00:00 2001 From: Roudenn Date: Sat, 26 Sep 2026 11:26:42 +0300 Subject: [PATCH 07/11] Summaries --- src/Numos.Chunks/Chunk.cs | 2 +- src/Numos.Chunks/ChunkConstants.cs | 3 +++ src/Numos.Chunks/ChunkMap.cs | 9 ++++++--- src/Numos.Chunks/IChunkInitializer.cs | 5 +++++ src/Numos.CoreSim/Solvers/BoundaryFlowSolver.cs | 5 +++-- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Numos.Chunks/Chunk.cs b/src/Numos.Chunks/Chunk.cs index 800f3ae..8b07dd8 100644 --- a/src/Numos.Chunks/Chunk.cs +++ b/src/Numos.Chunks/Chunk.cs @@ -10,7 +10,7 @@ namespace Numos.Chunks; /// /// Chunk-owned per-voxel data supports both flat-index and coordinate access. /// Use and when converting indices -/// for scalar-indexed storage such as gas channels (because... you know.... they aren't physical). +/// for scalar-indexed storage. /// public abstract class Chunk { diff --git a/src/Numos.Chunks/ChunkConstants.cs b/src/Numos.Chunks/ChunkConstants.cs index 7ff2a76..0a9c04f 100644 --- a/src/Numos.Chunks/ChunkConstants.cs +++ b/src/Numos.Chunks/ChunkConstants.cs @@ -1,5 +1,8 @@ namespace Numos.Chunks; +/// +/// Default constants for Numos chunks. +/// public static class ChunkConstants { /// Default number of voxels along a chunk's x-axis. diff --git a/src/Numos.Chunks/ChunkMap.cs b/src/Numos.Chunks/ChunkMap.cs index 919e329..4da5990 100644 --- a/src/Numos.Chunks/ChunkMap.cs +++ b/src/Numos.Chunks/ChunkMap.cs @@ -4,12 +4,15 @@ namespace Numos.Chunks; /// -/// Represents a voxel chunk map. +/// 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 { - public readonly Int3 Dimensions = new Int3(x, y, z); + public readonly Int3 Dimensions = new(x, y, z); private ConcurrentDictionary _chunkMap = new(); diff --git a/src/Numos.Chunks/IChunkInitializer.cs b/src/Numos.Chunks/IChunkInitializer.cs index 218d8a0..1bbe54e 100644 --- a/src/Numos.Chunks/IChunkInitializer.cs +++ b/src/Numos.Chunks/IChunkInitializer.cs @@ -2,6 +2,11 @@ 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 { abstract static T CreateInitializeChunk(Int3 position, diff --git a/src/Numos.CoreSim/Solvers/BoundaryFlowSolver.cs b/src/Numos.CoreSim/Solvers/BoundaryFlowSolver.cs index c2e06dc..3c108b0 100644 --- a/src/Numos.CoreSim/Solvers/BoundaryFlowSolver.cs +++ b/src/Numos.CoreSim/Solvers/BoundaryFlowSolver.cs @@ -1,4 +1,5 @@ using CommunityToolkit.HighPerformance.Helpers; +using Numos.Chunks; using Numos.CoreSim.Datatypes.Events; using Numos.CoreSim.Datatypes.Primitives; using Numos.Maths; @@ -388,7 +389,7 @@ private sealed class InjectionBuffer private readonly List _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 = []; From fd94429e5c5e922aa78c830960a8e54c9fce2e9d Mon Sep 17 00:00:00 2001 From: Roudenn Date: Sun, 27 Sep 2026 17:53:05 +0300 Subject: [PATCH 08/11] Fix chunk waking being bugged --- src/Numos.Chunks/ChunkMap.cs | 5 ++--- src/Numos.CoreSim/AtmosKernel.API.cs | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Numos.Chunks/ChunkMap.cs b/src/Numos.Chunks/ChunkMap.cs index 4da5990..7c04f77 100644 --- a/src/Numos.Chunks/ChunkMap.cs +++ b/src/Numos.Chunks/ChunkMap.cs @@ -99,13 +99,12 @@ public bool UnregisterChunk(Int3 position) /// /// The chunk's position in the chunk grid. /// A chunk is already registered at . - public void CreateAndRegisterChunk(Int3 position) + public T CreateAndRegisterChunk(Int3 position) { if (_chunkMap.ContainsKey(position)) throw new InvalidOperationException($"A chunk is already registered at {position}."); - var chunk = T.CreateInitializeChunk(position, Dimensions.X, Dimensions.Y, Dimensions.Z); - RegisterChunk(chunk); + return T.CreateInitializeChunk(position, Dimensions.X, Dimensions.Y, Dimensions.Z); } public T GetChunk(Int3 position) diff --git a/src/Numos.CoreSim/AtmosKernel.API.cs b/src/Numos.CoreSim/AtmosKernel.API.cs index 6160093..5b0266e 100644 --- a/src/Numos.CoreSim/AtmosKernel.API.cs +++ b/src/Numos.CoreSim/AtmosKernel.API.cs @@ -295,7 +295,8 @@ internal void CreateAndRegisterChunk(Int3 position) lock (StateGate) { ThrowIfTickExecuting("register a chunk during the current tick"); - _chunkMap.CreateAndRegisterChunk(position); + var chunk = _chunkMap.CreateAndRegisterChunk(position); + RegisterChunk(chunk); } } From 0887badad97f8b481cd7fcc7c46707131327cc1d Mon Sep 17 00:00:00 2001 From: Roudenn Date: Sun, 27 Sep 2026 17:58:24 +0300 Subject: [PATCH 09/11] Part 2: port topology types --- .../Scaling/ExplicitTopologyBenchmarks.cs | 9 +- docs/deterministic_replay.md | 2 +- .../AtmosWorldSolverDangerousExtensions.cs | 4 +- src/Numos.API/AtmosSimulation.cs | 9 +- src/Numos.API/AtmosSolver.cs | 4 +- src/Numos.API/AtmosWorld.Replay.cs | 5 +- src/Numos.API/AtmosWorld.cs | 89 +++++------ src/Numos.API/AtmosWorldCheckpoint.cs | 7 +- src/Numos.API/AtmosWorldNeighborTopology.cs | 53 ++++--- src/Numos.API/AtmosWorldRecording.cs | 13 +- src/Numos.API/AtmosWorldSolver.cs | 7 +- src/Numos.API/AtmosWorldSolverPipeline.cs | 5 +- src/Numos.API/ExplicitAtmosTopology.cs | 148 +----------------- .../Topology/ExplicitLinkDefinition.cs | 12 ++ .../Topology/ExplicitLinkHandles.cs | 29 ++++ .../Topology/ExplicitLinkSetKind.cs | 26 +++ src/Numos.Chunks/Topology/SimulationId.cs | 31 ++++ src/Numos.Chunks/Topology/VoxelRef.cs | 48 ++++++ .../ReplayCodecGenerator.cs | 4 +- .../NumosWorldReplaySerializer.cs | 23 +-- .../SimulationViewer.TopologyUi.cs | 35 +++-- .../SimulationViewer.VoxelEditing.cs | 7 +- src/Numos.Viewer/SimulationViewer.World.cs | 17 +- .../Numos.API.Tests/AtmosWorldReplayTests.cs | 1 + .../AtmosWorldSolverPipelineTests.cs | 24 +-- tests/Numos.API.Tests/AtmosWorldTests.cs | 51 +++--- 26 files changed, 347 insertions(+), 316 deletions(-) create mode 100644 src/Numos.Chunks/Topology/ExplicitLinkDefinition.cs create mode 100644 src/Numos.Chunks/Topology/ExplicitLinkHandles.cs create mode 100644 src/Numos.Chunks/Topology/ExplicitLinkSetKind.cs create mode 100644 src/Numos.Chunks/Topology/SimulationId.cs create mode 100644 src/Numos.Chunks/Topology/VoxelRef.cs diff --git a/benchmarks/Numos.CoreSim.Benchmarks/Scaling/ExplicitTopologyBenchmarks.cs b/benchmarks/Numos.CoreSim.Benchmarks/Scaling/ExplicitTopologyBenchmarks.cs index df833e9..462e25a 100644 --- a/benchmarks/Numos.CoreSim.Benchmarks/Scaling/ExplicitTopologyBenchmarks.cs +++ b/benchmarks/Numos.CoreSim.Benchmarks/Scaling/ExplicitTopologyBenchmarks.cs @@ -1,6 +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; @@ -17,7 +18,7 @@ namespace Numos.CoreSim.Benchmarks.Scaling; public class ExplicitTopologyBenchmarks { private ChunkHandle _chunk; - private ExplicitLinkDefinition[] _definitions = []; + private ExplicitLinkDefinition[] _definitions = []; private ExplicitLinkSetHandle _links; private AtmosSimulation _simulation = null!; private AtmosWorld _world = null!; @@ -57,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/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/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/AtmosSimulation.cs b/src/Numos.API/AtmosSimulation.cs index 3bd9cb8..f914e90 100644 --- a/src/Numos.API/AtmosSimulation.cs +++ b/src/Numos.API/AtmosSimulation.cs @@ -1,5 +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; @@ -88,7 +89,7 @@ internal AtmosSimulation( int chunkWidth, int chunkHeight, int chunkDepth, - AtmosSimulationId? requestedRegistration = null) + SimulationId? requestedRegistration = null) { ArgumentNullException.ThrowIfNull(world); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(chunkWidth); @@ -138,7 +139,7 @@ internal AtmosSimulation( /// 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. @@ -465,7 +466,7 @@ public bool UnregisterChunk(ChunkHandle chunk) /// The local voxel index is outside the chunk. /// The simulation has been disposed. [PublicAPI] - public AtmosCellRef GetCellRef(ChunkHandle chunk, ushort localVoxelIndex) + public VoxelRef GetCellRef(ChunkHandle chunk, ushort localVoxelIndex) { ThrowIfDisposed(); if (!_kernel.TryResolveExplicitEndpoint(chunk.Position, localVoxelIndex, out _)) @@ -476,7 +477,7 @@ public AtmosCellRef GetCellRef(ChunkHandle chunk, ushort localVoxelIndex) throw new ArgumentOutOfRangeException(nameof(localVoxelIndex)); } - return new AtmosCellRef(Id, chunk, localVoxelIndex); + return new VoxelRef(Id, chunk, localVoxelIndex); } /// 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 70354bb..a67dbe0 100644 --- a/src/Numos.API/AtmosWorld.Replay.cs +++ b/src/Numos.API/AtmosWorld.Replay.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim.Replay; namespace Numos.API; @@ -242,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. @@ -534,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 88a6a46..1ed7d67 100644 --- a/src/Numos.API/AtmosWorld.cs +++ b/src/Numos.API/AtmosWorld.cs @@ -1,6 +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; @@ -25,8 +26,8 @@ namespace Numos.API; /// /// ChunkHandle stationChunk = station.CreateAndRegisterChunk(new Int3(0, 0, 0)); /// ChunkHandle shuttleChunk = shuttle.CreateAndRegisterChunk(new Int3(0, 0, 0)); -/// AtmosCellRef stationCell = station.GetCellRef(stationChunk, 0); -/// AtmosCellRef shuttleCell = shuttle.GetCellRef(shuttleChunk, 0); +/// VoxelRef stationCell = station.GetCellRef(stationChunk, 0); +/// VoxelRef shuttleCell = shuttle.GetCellRef(shuttleChunk, 0); /// AtmosPortalHandle portal = world.CreatePortal(stationCell, shuttleCell); /// world.Tick(); // Activates the portal, then advances both simulations. /// @@ -38,7 +39,7 @@ public sealed partial class AtmosWorld : IDisposable 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 = []; @@ -306,7 +307,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) { @@ -404,13 +405,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) @@ -501,13 +502,13 @@ public void DestroyLinks(ExplicitLinkSetHandle handle) /// The world has been disposed. [PublicAPI] public AtmosPortalHandle CreatePortal( - AtmosCellRef first, - AtmosCellRef second, - ExplicitLinkFlags flags = ExplicitLinkFlags.All) + VoxelRef first, + VoxelRef second, + AtmosLinkFlags flags = AtmosLinkFlags.All) { - ExplicitLinkDefinition definition = new(first, second, flags); + ExplicitLinkDefinition definition = new(first, second, flags); return new AtmosPortalHandle( - CreateLinksCore(new ReadOnlySpan(in definition), ExplicitLinkSetKind.Portal)); + CreateLinksCore(new ReadOnlySpan>(in definition), ExplicitLinkSetKind.Portal)); } /// @@ -536,7 +537,7 @@ public void DestroyPortal(AtmosPortalHandle portal) /// /// The world has been disposed. [PublicAPI] - public AtmosDockHandle CreateDock(ReadOnlySpan surfaceLinks) + public AtmosDockHandle CreateDock(ReadOnlySpan> surfaceLinks) { return new AtmosDockHandle(CreateLinksCore(surfaceLinks, ExplicitLinkSetKind.Dock)); } @@ -562,17 +563,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; @@ -634,7 +635,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, @@ -852,9 +853,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) { @@ -900,7 +901,7 @@ internal AtmosSimulationId RegisterSimulation( _simulationSlots[index] = slot; RebuildOrderedSimulations(); IncrementSimulationCollectionRevision(); - return new AtmosSimulationId(index, slot.Generation); + return new SimulationId(index, slot.Generation); } } @@ -1127,8 +1128,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 || @@ -1410,7 +1411,7 @@ private void WakeEndpoints(ExplicitAtmosEdge[] edges) } } - private void WakeEndpoint(AtmosCellRef cell) + private void WakeEndpoint(VoxelRef cell) { var simulation = TryGetSimulationCore(cell.Simulation); if (simulation != null && @@ -1512,7 +1513,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); @@ -1544,7 +1545,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; @@ -1554,7 +1555,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 || @@ -1567,12 +1568,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' // 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); } @@ -1603,7 +1604,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) { @@ -1613,7 +1614,7 @@ public bool TryGetSimulation(AtmosSimulationId id, out AtmosSimulation? simulati } } - private AtmosSimulation? TryGetSimulationCore(AtmosSimulationId id) + private AtmosSimulation? TryGetSimulationCore(SimulationId id) { if (!id.IsValid || (uint)id.Index >= (uint)_simulationSlots.Count) return null; @@ -1622,7 +1623,7 @@ public bool TryGetSimulation(AtmosSimulationId id, out AtmosSimulation? simulati return slot.Generation == id.Generation ? slot.Simulation : null; } - internal bool TryResolveCell(AtmosCellRef cell, out AtmosSimulation? simulation) + internal bool TryResolveCell(VoxelRef cell, out AtmosSimulation? simulation) { lock (Gate) { @@ -1635,7 +1636,7 @@ internal bool TryResolveCell(AtmosCellRef cell, out AtmosSimulation? simulation) } } - internal IReadOnlyList GetActiveLinksCore() + internal IReadOnlyList> GetActiveLinksCore() { return ToDefinitions(_activeEdges); } @@ -1654,7 +1655,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), @@ -1666,7 +1667,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; @@ -1728,7 +1729,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); @@ -1736,8 +1737,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) && @@ -1756,13 +1757,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; @@ -1822,14 +1823,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/AtmosWorldNeighborTopology.cs b/src/Numos.API/AtmosWorldNeighborTopology.cs index 0e65bf9..05bfe1d 100644 --- a/src/Numos.API/AtmosWorldNeighborTopology.cs +++ b/src/Numos.API/AtmosWorldNeighborTopology.cs @@ -1,4 +1,5 @@ using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.Maths; namespace Numos.API; @@ -24,11 +25,11 @@ public enum AtmosNeighborKind : byte /// /// The neighboring cell. /// How the adjacency was discovered. -/// The explicit-link capabilities, or for Cartesian adjacency. +/// The explicit-link capabilities, or for Cartesian adjacency. public readonly record struct AtmosNeighbor( - AtmosCellRef Cell, + VoxelRef Cell, AtmosNeighborKind Kind, - ExplicitLinkFlags Flags); + AtmosLinkFlags Flags); /// /// Describes one canonically owned solver edge. @@ -36,12 +37,12 @@ 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. +/// The explicit-link capabilities, or for Cartesian adjacency. public readonly record struct AtmosNeighborEdge( - AtmosCellRef First, - AtmosCellRef Second, + VoxelRef First, + VoxelRef Second, AtmosNeighborKind Kind, - ExplicitLinkFlags Flags); + AtmosLinkFlags Flags); /// /// Provides a solver-specific, immutable view of Cartesian and compiled explicit topology. @@ -49,7 +50,7 @@ public readonly record struct AtmosNeighborEdge( public sealed class AtmosWorldNeighborTopology { private readonly Dictionary _explicitByChunk; - private readonly ExplicitLinkDefinition[] _explicitEdges; + private readonly ExplicitLinkDefinition[] _explicitEdges; private readonly bool _includeCartesian; private readonly AtmosWorld? _world; @@ -57,7 +58,7 @@ private AtmosWorldNeighborTopology( AtmosWorld? world, bool includeCartesian, Dictionary explicitByChunk, - ExplicitLinkDefinition[] explicitEdges) + ExplicitLinkDefinition[] explicitEdges) { _world = world; _includeCartesian = includeCartesian; @@ -84,7 +85,7 @@ public AtmosChunkNeighborView GetChunk(AtmosSimulation simulation, ChunkHandle c ArgumentNullException.ThrowIfNull(simulation); var world = GetWorld(); if (!ReferenceEquals(simulation.World, world) || - !world.TryResolveCell(new AtmosCellRef(simulation.Id, chunk, 0), out _)) + !world.TryResolveCell(new VoxelRef(simulation.Id, chunk, 0), out _)) { throw new ArgumentException("The chunk is not registered in this world.", nameof(chunk)); } @@ -98,7 +99,7 @@ public AtmosChunkNeighborView GetChunk(AtmosSimulation simulation, ChunkHandle c var adjacentPosition = chunk.Position + GetDirection(direction); if (world.TryResolveCell( - new AtmosCellRef(simulation.Id, new ChunkHandle(adjacentPosition), 0), + new VoxelRef(simulation.Id, new ChunkHandle(adjacentPosition), 0), out _)) { adjacentChunkMask |= checked((byte)(1 << direction)); @@ -120,7 +121,7 @@ public AtmosChunkNeighborView GetChunk(AtmosSimulation simulation, ChunkHandle c /// 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) @@ -134,7 +135,7 @@ 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 AtmosNeighborEnumerable GetNeighbors(VoxelRef cell) { var world = GetWorld(); if (!world.TryGetSimulation(cell.Simulation, out var simulation) || simulation == null) @@ -174,7 +175,7 @@ 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)) @@ -184,7 +185,7 @@ public IEnumerable GetOwnedEdges() first, second, AtmosNeighborKind.Cartesian, - ExplicitLinkFlags.None); + AtmosLinkFlags.None); } } } @@ -204,9 +205,9 @@ public IEnumerable GetOwnedEdges() internal static AtmosWorldNeighborTopology Compile( AtmosWorld world, AtmosNeighborSelection selection, - IReadOnlyList links) + 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))) @@ -259,9 +260,9 @@ internal static AtmosWorldNeighborTopology Compile( private static void Add( Dictionary> entries, - AtmosCellRef source, - AtmosCellRef neighbor, - ExplicitLinkFlags flags) + VoxelRef source, + VoxelRef neighbor, + AtmosLinkFlags flags) { var key = new CompiledChunkKey(source.Simulation, source.Chunk); if (!entries.TryGetValue(key, out List? values)) @@ -306,10 +307,10 @@ public readonly struct AtmosChunkNeighborView private readonly bool _includeCartesian; private readonly ChunkHandle _chunk; private readonly Int3 _dimensions; - private readonly AtmosSimulationId _simulation; + private readonly SimulationId _simulation; internal AtmosChunkNeighborView( - AtmosSimulationId simulation, + SimulationId simulation, ChunkHandle chunk, Int3 dimensions, bool includeCartesian, @@ -366,7 +367,7 @@ public AtmosNeighborEnumerable GetNeighbors(ushort localVoxelIndex) internal bool TryGetCartesianNeighbor( ushort localVoxelIndex, int direction, - out AtmosCellRef neighbor) + out VoxelRef neighbor) { if (!_includeCartesian) { @@ -467,7 +468,7 @@ internal bool TryGetCartesianNeighbor( } ushort targetIndex = checked((ushort)(x + y * _dimensions.X + z * plane)); - neighbor = new AtmosCellRef(_simulation, new ChunkHandle(chunkPosition), targetIndex); + neighbor = new VoxelRef(_simulation, new ChunkHandle(chunkPosition), targetIndex); return true; } @@ -558,7 +559,7 @@ public bool MoveNext() if (!_view.TryGetCartesianNeighbor(_localVoxelIndex, direction, out var cell)) continue; - Current = new AtmosNeighbor(cell, AtmosNeighborKind.Cartesian, ExplicitLinkFlags.None); + Current = new AtmosNeighbor(cell, AtmosNeighborKind.Cartesian, AtmosLinkFlags.None); return true; } @@ -571,7 +572,7 @@ public bool MoveNext() } internal readonly record struct CompiledChunkKey( - AtmosSimulationId Simulation, + SimulationId Simulation, ChunkHandle Chunk); internal readonly record struct CompiledNeighborEntry( 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..6502fe6 100644 --- a/src/Numos.API/AtmosWorldSolver.cs +++ b/src/Numos.API/AtmosWorldSolver.cs @@ -1,3 +1,4 @@ +using Numos.Chunks.Topology; using Numos.CoreSim; namespace Numos.API; @@ -42,9 +43,9 @@ public enum AtmosWorldSolverKind : byte /// The canonical second endpoint. /// The interactions enabled by the link. public readonly record struct AtmosExplicitLinkInfo( - AtmosCellRef First, - AtmosCellRef Second, - ExplicitLinkFlags Flags); + VoxelRef First, + VoxelRef Second, + AtmosLinkFlags Flags); /// /// Configures the neighborhood compiled for one custom world solver. diff --git a/src/Numos.API/AtmosWorldSolverPipeline.cs b/src/Numos.API/AtmosWorldSolverPipeline.cs index 454cfaa..aed6210 100644 --- a/src/Numos.API/AtmosWorldSolverPipeline.cs +++ b/src/Numos.API/AtmosWorldSolverPipeline.cs @@ -1,4 +1,5 @@ using JetBrains.Annotations; +using Numos.Chunks.Topology; using Numos.CoreSim.Replay; using Numos.CoreSim.Solvers; @@ -215,7 +216,7 @@ 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]; for (int index = 0; index < _steps.Count; index++) @@ -391,7 +392,7 @@ internal void Execute(AtmosWorldExecutionContext context) internal AtmosWorldNeighborTopology BuildTopology( AtmosWorld world, - IReadOnlyList links) + IReadOnlyList> links) { return Selection == null ? AtmosWorldNeighborTopology.EmptyFor(world) diff --git a/src/Numos.API/ExplicitAtmosTopology.cs b/src/Numos.API/ExplicitAtmosTopology.cs index 6a24580..55b766b 100644 --- a/src/Numos.API/ExplicitAtmosTopology.cs +++ b/src/Numos.API/ExplicitAtmosTopology.cs @@ -1,83 +1,7 @@ -using Numos.Chunks; -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, - 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(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. /// @@ -85,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 +/// 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. @@ -115,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. /// @@ -194,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/Topology/ExplicitLinkDefinition.cs b/src/Numos.Chunks/Topology/ExplicitLinkDefinition.cs new file mode 100644 index 0000000..56a062f --- /dev/null +++ b/src/Numos.Chunks/Topology/ExplicitLinkDefinition.cs @@ -0,0 +1,12 @@ +namespace Numos.Chunks.Topology; + +/// +/// 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( + 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..81bc0e5 --- /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 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); 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.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 2274464..b1b8137 100644 --- a/src/Numos.Serialization/NumosWorldReplaySerializer.cs +++ b/src/Numos.Serialization/NumosWorldReplaySerializer.cs @@ -1,5 +1,6 @@ using Numos.API; using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim.Replay; using Numos.Replay.SourceGen; @@ -337,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); @@ -511,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 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.TopologyUi.cs b/src/Numos.Viewer/SimulationViewer.TopologyUi.cs index 006c721..6146ec5 100644 --- a/src/Numos.Viewer/SimulationViewer.TopologyUi.cs +++ b/src/Numos.Viewer/SimulationViewer.TopologyUi.cs @@ -2,6 +2,7 @@ using ImGuiNET; using Numos.API; using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.Maths; using Numos.Viewer.Ui; using Raylib_cs; @@ -19,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; @@ -190,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(); @@ -219,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( @@ -360,7 +361,7 @@ 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)]; @@ -378,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++) @@ -393,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 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() @@ -465,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) { @@ -487,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 3444c36..0f83ec8 100644 --- a/src/Numos.Viewer/SimulationViewer.VoxelEditing.cs +++ b/src/Numos.Viewer/SimulationViewer.VoxelEditing.cs @@ -2,6 +2,7 @@ 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; @@ -469,7 +470,7 @@ private void RenderVoxelContextMenu() private void RenderContextPortalMenu() { - AtmosCellRef? selected = GetSelectedAtmosCell(); + VoxelRef? selected = GetSelectedAtmosCell(); if (!selected.HasValue || !ImGui.BeginMenu("Portal")) return; @@ -480,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")) { @@ -496,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(); diff --git a/src/Numos.Viewer/SimulationViewer.World.cs b/src/Numos.Viewer/SimulationViewer.World.cs index f6dfb0a..eebb587 100644 --- a/src/Numos.Viewer/SimulationViewer.World.cs +++ b/src/Numos.Viewer/SimulationViewer.World.cs @@ -2,6 +2,7 @@ using ImGuiNET; using Numos.API; using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim.Datatypes.Snapshots; using Numos.Maths; using Numos.SimDrawer; @@ -13,9 +14,9 @@ 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 = ChunkConstants.DefaultHeight; @@ -25,7 +26,7 @@ public partial class SimulationViewer 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)) { @@ -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}"); } diff --git a/tests/Numos.API.Tests/AtmosWorldReplayTests.cs b/tests/Numos.API.Tests/AtmosWorldReplayTests.cs index 723f936..184011f 100644 --- a/tests/Numos.API.Tests/AtmosWorldReplayTests.cs +++ b/tests/Numos.API.Tests/AtmosWorldReplayTests.cs @@ -1,4 +1,5 @@ using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.CoreSim.GasReactions; diff --git a/tests/Numos.API.Tests/AtmosWorldSolverPipelineTests.cs b/tests/Numos.API.Tests/AtmosWorldSolverPipelineTests.cs index 9629109..71f3ed2 100644 --- a/tests/Numos.API.Tests/AtmosWorldSolverPipelineTests.cs +++ b/tests/Numos.API.Tests/AtmosWorldSolverPipelineTests.cs @@ -1,3 +1,4 @@ +using Numos.Chunks.Topology; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.Maths; @@ -19,7 +20,7 @@ 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 = []; @@ -28,7 +29,7 @@ public void NeighborSolver_SeesCartesianAndPortalNeighborsThroughOneView() new AtmosNeighborSelection( "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); @@ -71,7 +72,7 @@ public void NeighborSolver_SeesCartesianNeighborAcrossChunkBoundary() Is.EqualTo( new[] { - new AtmosNeighbor(target, AtmosNeighborKind.Cartesian, ExplicitLinkFlags.None) + new AtmosNeighbor(target, AtmosNeighborKind.Cartesian, AtmosLinkFlags.None) })); } @@ -131,9 +132,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); @@ -309,7 +311,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), @@ -344,14 +346,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 +368,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 +401,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( "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. diff --git a/tests/Numos.API.Tests/AtmosWorldTests.cs b/tests/Numos.API.Tests/AtmosWorldTests.cs index 8f07b5c..ccff90c 100644 --- a/tests/Numos.API.Tests/AtmosWorldTests.cs +++ b/tests/Numos.API.Tests/AtmosWorldTests.cs @@ -1,4 +1,5 @@ using Numos.Chunks; +using Numos.Chunks.Topology; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; using Numos.Maths; @@ -71,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(); @@ -99,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(() => @@ -107,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); }); } @@ -121,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); @@ -152,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(); @@ -176,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(); @@ -199,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(); @@ -235,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(); @@ -267,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); @@ -315,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(); @@ -350,7 +357,7 @@ public void PortalAndOrdinaryNeighbor_DoNotOverdrawTheirSharedSource() world.CreatePortal( local.GetCellRef(localChunk, 0), remote.GetCellRef(remoteChunk, 0), - ExplicitLinkFlags.GasTransport); + AtmosLinkFlags.GasTransport); world.Tick(); @@ -385,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(); @@ -412,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; @@ -442,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( @@ -473,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; @@ -516,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); @@ -548,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. From 227077e3fdfffd1f5fe898f6c1a02abdec0d599f Mon Sep 17 00:00:00 2001 From: Roudenn Date: Sun, 27 Sep 2026 18:13:22 +0300 Subject: [PATCH 10/11] Fix summaries for part 2 --- src/Numos.API/AtmosWorld.cs | 14 ++++---- src/Numos.Chunks/Chunk.cs | 4 +-- src/Numos.Chunks/ChunkConstants.cs | 2 +- src/Numos.Chunks/ChunkHandle.cs | 2 +- src/Numos.Chunks/ChunkMap.cs | 35 +++++++++++++++++++ src/Numos.Chunks/IChunkInitializer.cs | 12 +++++-- .../Topology/ExplicitLinkDefinition.cs | 2 +- .../Topology/ExplicitLinkHandles.cs | 4 +-- 8 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/Numos.API/AtmosWorld.cs b/src/Numos.API/AtmosWorld.cs index 1ed7d67..fb0321f 100644 --- a/src/Numos.API/AtmosWorld.cs +++ b/src/Numos.API/AtmosWorld.cs @@ -28,7 +28,7 @@ namespace Numos.API; /// ChunkHandle shuttleChunk = shuttle.CreateAndRegisterChunk(new Int3(0, 0, 0)); /// VoxelRef stationCell = station.GetCellRef(stationChunk, 0); /// VoxelRef shuttleCell = shuttle.GetCellRef(shuttleChunk, 0); -/// AtmosPortalHandle portal = world.CreatePortal(stationCell, shuttleCell); +/// PortalHandle portal = world.CreatePortal(stationCell, shuttleCell); /// world.Tick(); // Activates the portal, then advances both simulations. /// /// @@ -501,13 +501,13 @@ public void DestroyLinks(ExplicitLinkSetHandle handle) /// /// The world has been disposed. [PublicAPI] - public AtmosPortalHandle CreatePortal( + public PortalHandle CreatePortal( VoxelRef first, VoxelRef second, AtmosLinkFlags flags = AtmosLinkFlags.All) { ExplicitLinkDefinition definition = new(first, second, flags); - return new AtmosPortalHandle( + return new PortalHandle( CreateLinksCore(new ReadOnlySpan>(in definition), ExplicitLinkSetKind.Portal)); } @@ -519,7 +519,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); } @@ -537,9 +537,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)); } /// @@ -550,7 +550,7 @@ public AtmosDockHandle CreateDock(ReadOnlySpanRemoval 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); } diff --git a/src/Numos.Chunks/Chunk.cs b/src/Numos.Chunks/Chunk.cs index 8b07dd8..f79a964 100644 --- a/src/Numos.Chunks/Chunk.cs +++ b/src/Numos.Chunks/Chunk.cs @@ -108,8 +108,8 @@ public Int3 GetXyzInt3(ushort index) } /// - /// Method that is called before the chunk is released from the chunk map. - /// Here it must release all of its disposable resources. + /// 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() { } diff --git a/src/Numos.Chunks/ChunkConstants.cs b/src/Numos.Chunks/ChunkConstants.cs index 0a9c04f..4465ab3 100644 --- a/src/Numos.Chunks/ChunkConstants.cs +++ b/src/Numos.Chunks/ChunkConstants.cs @@ -1,7 +1,7 @@ namespace Numos.Chunks; /// -/// Default constants for Numos chunks. +/// Default constants for Numos chunks. /// public static class ChunkConstants { diff --git a/src/Numos.Chunks/ChunkHandle.cs b/src/Numos.Chunks/ChunkHandle.cs index 497ba69..2ad524a 100644 --- a/src/Numos.Chunks/ChunkHandle.cs +++ b/src/Numos.Chunks/ChunkHandle.cs @@ -3,6 +3,6 @@ namespace Numos.Chunks; /// -/// Identifies a chunk owned by a chunk map. +/// 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 index 7c04f77..b81c1b1 100644 --- a/src/Numos.Chunks/ChunkMap.cs +++ b/src/Numos.Chunks/ChunkMap.cs @@ -66,6 +66,12 @@ public bool TryGetChunkPositions( 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) @@ -107,6 +113,12 @@ public T CreateAndRegisterChunk(Int3 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)) @@ -115,6 +127,9 @@ public T GetChunk(Int3 position) 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 @@ -123,16 +138,33 @@ public T[] OrderedChunks() .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) @@ -164,6 +196,9 @@ public static void ValidateVoxelIndex(T chunk, ushort localVoxelIndex) } } + /// + /// Calls on all chunks on the map. + /// public void Dispose() { foreach (var chunk in _chunkMap.Values) diff --git a/src/Numos.Chunks/IChunkInitializer.cs b/src/Numos.Chunks/IChunkInitializer.cs index 1bbe54e..80a9075 100644 --- a/src/Numos.Chunks/IChunkInitializer.cs +++ b/src/Numos.Chunks/IChunkInitializer.cs @@ -3,12 +3,20 @@ namespace Numos.Chunks; /// -/// Interface implemented by chunk types so that they can create -/// and initialize new instances in static context. +/// 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, diff --git a/src/Numos.Chunks/Topology/ExplicitLinkDefinition.cs b/src/Numos.Chunks/Topology/ExplicitLinkDefinition.cs index 56a062f..7b71f51 100644 --- a/src/Numos.Chunks/Topology/ExplicitLinkDefinition.cs +++ b/src/Numos.Chunks/Topology/ExplicitLinkDefinition.cs @@ -1,7 +1,7 @@ namespace Numos.Chunks.Topology; /// -/// Defines one undirected sparse atmospheric adjacency. +/// Defines one undirected sparse voxel adjacency. /// /// One endpoint. Registration canonicalizes endpoint orientation. /// The other endpoint. diff --git a/src/Numos.Chunks/Topology/ExplicitLinkHandles.cs b/src/Numos.Chunks/Topology/ExplicitLinkHandles.cs index 81bc0e5..4c405bf 100644 --- a/src/Numos.Chunks/Topology/ExplicitLinkHandles.cs +++ b/src/Numos.Chunks/Topology/ExplicitLinkHandles.cs @@ -20,10 +20,10 @@ public readonly record struct ExplicitLinkSetHandle(int Index, uint Generation) /// Identifies a one-edge portal in the world's explicit topology. /// /// The underlying generic link set. -public readonly record struct AtmosPortalHandle(ExplicitLinkSetHandle Links); +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 AtmosDockHandle(ExplicitLinkSetHandle Links); +public readonly record struct DockHandle(ExplicitLinkSetHandle Links); From 20f8aed578cd57174854897bce087ca3b1abdf99 Mon Sep 17 00:00:00 2001 From: Roudenn Date: Sun, 27 Sep 2026 22:01:18 +0300 Subject: [PATCH 11/11] Part 3 - World Solver generalized --- src/Numos.API/AtmosSimulation.cs | 8 +- src/Numos.API/AtmosWorld.cs | 23 +- src/Numos.API/AtmosWorldSolver.cs | 79 +------ src/Numos.API/AtmosWorldSolverPipeline.cs | 29 +-- src/Numos.API/ExplicitAtmosTopology.cs | 2 +- src/Numos.Chunks/ChunkMap.cs | 3 + src/Numos.Chunks/IChunkSimulation.cs | 15 ++ src/Numos.Chunks/IChunkWorld.cs | 16 ++ .../Topology/ExplicitLinkSelector.cs | 12 + .../WorldNeighborTopology.cs} | 218 +++++++++++------- .../AtmosDangerousApiTests.cs | 2 +- .../AtmosWorldSolverPipelineTests.cs | 39 ++-- 12 files changed, 248 insertions(+), 198 deletions(-) create mode 100644 src/Numos.Chunks/IChunkSimulation.cs create mode 100644 src/Numos.Chunks/IChunkWorld.cs create mode 100644 src/Numos.Chunks/Topology/ExplicitLinkSelector.cs rename src/{Numos.API/AtmosWorldNeighborTopology.cs => Numos.Chunks/WorldNeighborTopology.cs} (68%) diff --git a/src/Numos.API/AtmosSimulation.cs b/src/Numos.API/AtmosSimulation.cs index f914e90..4fc9162 100644 --- a/src/Numos.API/AtmosSimulation.cs +++ b/src/Numos.API/AtmosSimulation.cs @@ -22,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. @@ -134,6 +134,12 @@ 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 . diff --git a/src/Numos.API/AtmosWorld.cs b/src/Numos.API/AtmosWorld.cs index fb0321f..39f8b3e 100644 --- a/src/Numos.API/AtmosWorld.cs +++ b/src/Numos.API/AtmosWorld.cs @@ -32,7 +32,7 @@ namespace Numos.API; /// 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 = []; @@ -133,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. /// @@ -1571,7 +1578,7 @@ private void ValidateCell(VoxelRef cell, 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 == AtmosLinkFlags.None) throw new ArgumentException("An explicit link must select at least one transport capability.", parameterName); @@ -1613,6 +1620,13 @@ public bool TryGetSimulation(SimulationId id, out AtmosSimulation? simulation) 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(SimulationId id) { @@ -1623,6 +1637,11 @@ public bool TryGetSimulation(SimulationId id, out AtmosSimulation? simulation) return slot.Generation == id.Generation ? slot.Simulation : null; } + public bool TryResolveCell(VoxelRef cell) + { + return TryResolveCell(cell, out _); + } + internal bool TryResolveCell(VoxelRef cell, out AtmosSimulation? simulation) { lock (Gate) diff --git a/src/Numos.API/AtmosWorldSolver.cs b/src/Numos.API/AtmosWorldSolver.cs index 6502fe6..f62d3d7 100644 --- a/src/Numos.API/AtmosWorldSolver.cs +++ b/src/Numos.API/AtmosWorldSolver.cs @@ -1,3 +1,4 @@ +using Numos.Chunks; using Numos.Chunks.Topology; using Numos.CoreSim; @@ -9,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. /// @@ -36,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( - VoxelRef First, - VoxelRef Second, - AtmosLinkFlags 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. /// @@ -109,7 +38,7 @@ public sealed class AtmosWorldSolverContext internal AtmosWorldSolverContext( AtmosWorld world, IReadOnlyList simulations, - AtmosWorldNeighborTopology topology) + WorldNeighborTopology topology) { World = world; Simulations = simulations; @@ -145,7 +74,7 @@ internal AtmosWorldSolverContext( /// neighbors, no explicit edges. Use whenever /// the callback needs this property. /// - public AtmosWorldNeighborTopology Topology { get; } + public WorldNeighborTopology Topology { get; } } /// @@ -159,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 aed6210..168e75e 100644 --- a/src/Numos.API/AtmosWorldSolverPipeline.cs +++ b/src/Numos.API/AtmosWorldSolverPipeline.cs @@ -1,4 +1,5 @@ using JetBrains.Annotations; +using Numos.Chunks; using Numos.Chunks.Topology; using Numos.CoreSim.Replay; using Numos.CoreSim.Solvers; @@ -71,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); @@ -101,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); @@ -131,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); @@ -218,7 +219,7 @@ internal void Execute(WorldSolverRegistration[] steps, AtmosWorldExecutionContex 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); @@ -234,7 +235,7 @@ internal AtmosSolverCheckpoint[] CaptureCheckpointSteps() private void RegisterCore( string name, AtmosWorldSolver solver, - AtmosNeighborSelection? selection, + VoxelNeighborSelection? selection, int insertionIndex) { ArgumentException.ThrowIfNullOrWhiteSpace(name); @@ -258,7 +259,7 @@ private void RegisterRelative( string existingName, string name, AtmosWorldSolver solver, - AtmosNeighborSelection? selection, + VoxelNeighborSelection? selection, bool before) { ArgumentException.ThrowIfNullOrWhiteSpace(existingName); @@ -343,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; @@ -361,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( @@ -374,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); } @@ -390,16 +391,16 @@ internal void Execute(AtmosWorldExecutionContext context) _builtInSolver!(context); } - internal AtmosWorldNeighborTopology BuildTopology( + internal WorldNeighborTopology BuildTopology( AtmosWorld world, 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 55b766b..854ffd5 100644 --- a/src/Numos.API/ExplicitAtmosTopology.cs +++ b/src/Numos.API/ExplicitAtmosTopology.cs @@ -10,7 +10,7 @@ namespace Numos.API; /// / /// 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 (AtmosLinkFlags)(1 << 2)) purely so a -/// host-registered can pick it out. Numos' built-in stages ignore bits +/// 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. diff --git a/src/Numos.Chunks/ChunkMap.cs b/src/Numos.Chunks/ChunkMap.cs index b81c1b1..6503553 100644 --- a/src/Numos.Chunks/ChunkMap.cs +++ b/src/Numos.Chunks/ChunkMap.cs @@ -12,6 +12,9 @@ namespace Numos.Chunks; /// 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(); 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/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.API/AtmosWorldNeighborTopology.cs b/src/Numos.Chunks/WorldNeighborTopology.cs similarity index 68% rename from src/Numos.API/AtmosWorldNeighborTopology.cs rename to src/Numos.Chunks/WorldNeighborTopology.cs index 05bfe1d..0c055ba 100644 --- a/src/Numos.API/AtmosWorldNeighborTopology.cs +++ b/src/Numos.Chunks/WorldNeighborTopology.cs @@ -1,13 +1,12 @@ -using Numos.Chunks; 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. @@ -25,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( +/// The explicit-link capabilities, or default enum value for Cartesian adjacency. +public readonly record struct VoxelNeighbor( VoxelRef Cell, - AtmosNeighborKind Kind, - AtmosLinkFlags Flags); + VoxelNeighborKind Kind, + T Flags) where T : struct, Enum; /// /// Describes one canonically owned solver edge. @@ -37,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( +/// The explicit-link capabilities, or default enum value for Cartesian adjacency. +public readonly record struct VoxelNeighborEdge( VoxelRef First, VoxelRef Second, - AtmosNeighborKind Kind, - AtmosLinkFlags Flags); + 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; @@ -66,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, [], []); } /// @@ -80,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, ChunkHandle chunk) + public VoxelChunkNeighborView GetChunk(IChunkSimulation simulation, ChunkHandle chunk) { ArgumentNullException.ThrowIfNull(simulation); var world = GetWorld(); - if (!ReferenceEquals(simulation.World, world) || - !world.TryResolveCell(new VoxelRef(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)); } @@ -99,14 +148,13 @@ public AtmosChunkNeighborView GetChunk(AtmosSimulation simulation, ChunkHandle c var adjacentPosition = chunk.Position + GetDirection(direction); if (world.TryResolveCell( - new VoxelRef(simulation.Id, new ChunkHandle(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, @@ -124,7 +172,7 @@ public AtmosChunkNeighborView GetChunk(AtmosSimulation simulation, ChunkHandle c 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); @@ -135,10 +183,10 @@ public int GetNeighborCount(VoxelRef cell) /// /// A live cell in the callback's world. /// An allocation-free incident-neighbor enumerable. - public AtmosNeighborEnumerable GetNeighbors(VoxelRef 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); @@ -154,16 +202,16 @@ public AtmosNeighborEnumerable GetNeighbors(VoxelRef 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()) { @@ -181,11 +229,11 @@ public IEnumerable GetOwnedEdges() if (!view.TryGetCartesianNeighbor(voxelIndex, direction, out var second)) continue; - yield return new AtmosNeighborEdge( + yield return new VoxelNeighborEdge( first, second, - AtmosNeighborKind.Cartesian, - AtmosLinkFlags.None); + VoxelNeighborKind.Cartesian, + default); } } } @@ -194,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); } @@ -221,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( @@ -239,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++) { @@ -252,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, VoxelRef source, VoxelRef neighbor, - AtmosLinkFlags flags) + T flags) { var key = new CompiledChunkKey(source.Simulation, source.Chunk); if (!entries.TryGetValue(key, out List? values)) @@ -274,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."); } @@ -295,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 ChunkHandle _chunk; private readonly Int3 _dimensions; private readonly SimulationId _simulation; - internal AtmosChunkNeighborView( + internal VoxelChunkNeighborView( SimulationId simulation, ChunkHandle chunk, Int3 dimensions, bool includeCartesian, - CompiledChunkAdjacency? explicitAdjacency, + CompiledChunkAdjacency? explicitAdjacency, byte adjacentChunkMask) { _simulation = simulation; @@ -356,12 +412,12 @@ 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( @@ -472,7 +528,7 @@ internal bool TryGetCartesianNeighbor( return true; } - internal AtmosNeighbor GetExplicitNeighbor(int index) + internal VoxelNeighbor GetExplicitNeighbor(int index) { return _explicitAdjacency!.Neighbors[index]; } @@ -488,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) @@ -511,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) @@ -545,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. @@ -559,7 +615,7 @@ public bool MoveNext() if (!_view.TryGetCartesianNeighbor(_localVoxelIndex, direction, out var cell)) continue; - Current = new AtmosNeighbor(cell, AtmosNeighborKind.Cartesian, AtmosLinkFlags.None); + Current = new VoxelNeighbor(cell, VoxelNeighborKind.Cartesian, default); return true; } @@ -571,23 +627,15 @@ public bool MoveNext() } } -internal readonly record struct CompiledChunkKey( - SimulationId Simulation, - ChunkHandle 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/tests/Numos.API.Dangerous.Tests/AtmosDangerousApiTests.cs b/tests/Numos.API.Dangerous.Tests/AtmosDangerousApiTests.cs index cd95dc0..30fd121 100644 --- a/tests/Numos.API.Dangerous.Tests/AtmosDangerousApiTests.cs +++ b/tests/Numos.API.Dangerous.Tests/AtmosDangerousApiTests.cs @@ -101,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)) diff --git a/tests/Numos.API.Tests/AtmosWorldSolverPipelineTests.cs b/tests/Numos.API.Tests/AtmosWorldSolverPipelineTests.cs index 71f3ed2..73bbdb1 100644 --- a/tests/Numos.API.Tests/AtmosWorldSolverPipelineTests.cs +++ b/tests/Numos.API.Tests/AtmosWorldSolverPipelineTests.cs @@ -1,3 +1,4 @@ +using Numos.Chunks; using Numos.Chunks.Topology; using Numos.CoreSim; using Numos.CoreSim.Datatypes.Primitives; @@ -23,10 +24,10 @@ public void NeighborSolver_SeesCartesianAndPortalNeighborsThroughOneView() 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 & AtmosLinkFlags.GasTransport) != 0), @@ -41,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)); }); } @@ -58,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(); @@ -72,7 +73,7 @@ public void NeighborSolver_SeesCartesianNeighborAcrossChunkBoundary() Is.EqualTo( new[] { - new AtmosNeighbor(target, AtmosNeighborKind.Cartesian, AtmosLinkFlags.None) + new VoxelNeighbor(target, VoxelNeighborKind.Cartesian, AtmosLinkFlags.None) })); } @@ -89,7 +90,7 @@ public void Selector_IsReevaluatedOnlyWhenCompiledTopologyChanges() world.Solvers.RegisterNeighborSolver( "compiled-selector", - new AtmosNeighborSelection( + new VoxelNeighborSelection( "tests/compiled-selector-v1", false, _ => @@ -122,7 +123,7 @@ public void SelectorFailure_LeavesTopologyBoundaryPendingForRetry() world.Solvers.RegisterNeighborSolver( "fallible-selector", - new AtmosNeighborSelection( + new VoxelNeighborSelection( "tests/fallible-selector-v1", false, _ => failCompilation @@ -224,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)); }); } @@ -246,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(); @@ -320,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), @@ -405,7 +406,7 @@ private static AtmosWorld CreateDeterministicPortalWorld(bool reversePortal) world.Solvers.RegisterNeighborSolver( "deterministic-observer", - new AtmosNeighborSelection( + new VoxelNeighborSelection( "tests/deterministic-observer-v1", true, static link => (link.Flags & AtmosLinkFlags.GasTransport) != 0), @@ -423,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);