using Sand.Core; using System.Diagnostics; namespace Sand.ChunkPrototype; public sealed partial class PrototypeSparseSandAdapter { private const int GasBandHaloRows = 2; private const int FallingBandHaloRows = 1; public int StepDown() => Step(); public int Step() { _stepCounter++; ResetStepMetrics(); if (_particleCount == 0) { var emptyFieldStart = Stopwatch.GetTimestamp(); DecayFields(); var emptyFieldDecayMicros = ToMicroseconds(emptyFieldStart, Stopwatch.GetTimestamp()); _lastStepStats = new ChunkStepStats( SteppedChunks: 0, SleepingChunks: 0, FieldPages: _fieldPages.Count, MoveAttempts: 0, VerticalMoveAttempts: 0, DiagonalMoveAttempts: 0, LateralMoveAttempts: 0, SuccessfulMoves: 0, SwapAttempts: 0, StalledMovableCells: 0, MovementOnlyFastPathCount: 0, FullRuntimeStepCount: 0, FullRuntimeSolidCount: 0, FullRuntimeLiquidCount: 0, FullRuntimeGasCount: 0, MovedParticles: 0, SwappedParticles: 0, VisualDirtyPages: _dirtyVisualChunks.Count, FrameBuildBytesTouched: _lastStepStats.FrameBuildBytesTouched, ActivationTimeMicroseconds: 0, MovementTimeMicroseconds: 0, RuntimeTimeMicroseconds: 0, FieldDecayTimeMicroseconds: emptyFieldDecayMicros, RenderTimeMicroseconds: _lastStepStats.RenderTimeMicroseconds); return 0; } var activationStart = Stopwatch.GetTimestamp(); var (activeChunks, sleepingChunks) = _scheduler.BuildSchedule(_cellPages); var steppedChunks = 0; for (var i = 0; i < activeChunks.Count; i++) { var coord = activeChunks[i]; if (!_cellPages.TryGetValue(coord, out var page)) { continue; } page.HasFieldActivity = false; page.IsActive = false; } var activationMicros = ToMicroseconds(activationStart, Stopwatch.GetTimestamp()); long movementMicros = 0; long runtimeMicros = 0; ProcessGasChunksInterleaved(activeChunks, ref movementMicros, ref runtimeMicros); for (var i = 0; i < activeChunks.Count; i++) { var coord = activeChunks[i]; if (!_cellPages.TryGetValue(coord, out var page)) { continue; } if (!TryGetRowRange(page, FallingBandHaloRows, out var minRow, out var maxRow)) { page.ClearDirtyBands(); page.DecayPendingSteps(); page.IsActive = page.HasFieldActivity || page.PendingActiveSteps > 0; steppedChunks++; continue; } page.ClearDirtyBands(); ProcessFallingPage(coord, page, minRow, maxRow, ref movementMicros, ref runtimeMicros); if (page.LastTouchedFrame == _stepCounter || page.HasFieldActivity) { page.IsActive = true; } else { page.DecayPendingSteps(); page.IsActive = page.PendingActiveSteps > 0; } steppedChunks++; } ResolveGasChunkRowSeams(activeChunks); var fieldStart = Stopwatch.GetTimestamp(); DecayFields(); var fieldDecayMicros = ToMicroseconds(fieldStart, Stopwatch.GetTimestamp()); _lastStepStats = new ChunkStepStats( SteppedChunks: steppedChunks, SleepingChunks: sleepingChunks, FieldPages: _fieldPages.Count, MoveAttempts: _moveAttemptCount, VerticalMoveAttempts: _verticalMoveAttemptCount, DiagonalMoveAttempts: _diagonalMoveAttemptCount, LateralMoveAttempts: _lateralMoveAttemptCount, SuccessfulMoves: _movedParticles, SwapAttempts: _swapAttemptCount, StalledMovableCells: _stalledMovableCount, MovementOnlyFastPathCount: _movementOnlyFastPathCount, FullRuntimeStepCount: _fullRuntimeStepCount, FullRuntimeSolidCount: _fullRuntimeSolidCount, FullRuntimeLiquidCount: _fullRuntimeLiquidCount, FullRuntimeGasCount: _fullRuntimeGasCount, MovedParticles: _movedParticles, SwappedParticles: _swappedParticles, VisualDirtyPages: _dirtyVisualChunks.Count, FrameBuildBytesTouched: _lastStepStats.FrameBuildBytesTouched, ActivationTimeMicroseconds: activationMicros, MovementTimeMicroseconds: movementMicros, RuntimeTimeMicroseconds: runtimeMicros, FieldDecayTimeMicroseconds: fieldDecayMicros, RenderTimeMicroseconds: _lastStepStats.RenderTimeMicroseconds); return _movedParticles + _swappedParticles; } private void ProcessGasChunksInterleaved(IReadOnlyList activeChunks, ref long movementMicros, ref long runtimeMicros) { if (activeChunks.Count == 0) { return; } var minChunkY = int.MaxValue; var maxChunkY = int.MinValue; for (var i = 0; i < activeChunks.Count; i++) { var coord = activeChunks[i]; if (!_cellPages.TryGetValue(coord, out var page) || page.GasCellCount == 0) { continue; } minChunkY = Math.Min(minChunkY, coord.Y); maxChunkY = Math.Max(maxChunkY, coord.Y); } if (minChunkY == int.MaxValue) { return; } for (var chunkY = minChunkY; chunkY <= maxChunkY; chunkY++) { for (var localY = 0; localY < _config.ChunkHeight; localY++) { var leftToRight = (((chunkY * _config.ChunkHeight) + localY + _stepCounter) & 1) == 0; if (leftToRight) { for (var i = 0; i < activeChunks.Count; i++) { ProcessGasRowIfNeeded(activeChunks[i], chunkY, localY, ref movementMicros, ref runtimeMicros); } continue; } for (var i = activeChunks.Count - 1; i >= 0; i--) { ProcessGasRowIfNeeded(activeChunks[i], chunkY, localY, ref movementMicros, ref runtimeMicros); } } } } private void ProcessGasRowIfNeeded(ChunkCoord coord, int chunkY, int localY, ref long movementMicros, ref long runtimeMicros) { if (coord.Y != chunkY || !_cellPages.TryGetValue(coord, out var page) || page.GasCellCount == 0) { return; } if (page.GetGasRowCount(localY) == 0) { return; } if (!ShouldProcessGasRow(page, localY)) { return; } ProcessGasRow(coord, page, localY, ref movementMicros, ref runtimeMicros); } private void ProcessGasRow(ChunkCoord coord, ChunkCellPage page, int localY, ref long movementMicros, ref long runtimeMicros) { var movementOnlyPage = page.RuntimeCellCount == 0; var moveCountBefore = _movedParticles + _swappedParticles; var moveAttemptsBefore = _moveAttemptCount; var rowGasCountAtStart = page.GetGasRowCount(localY); var start = Stopwatch.GetTimestamp(); for (var localX = 0; localX < page.Width; localX++) { if (page.IsProcessed(localX, localY, _stepCounter)) { continue; } var particle = page[localX, localY]; if (particle.MotionType != PrototypeParticleType.Steam) { continue; } page.MarkProcessed(localX, localY, _stepCounter); if (movementOnlyPage || !particle.RequiresFullRuntimeStep) { StepMovementOnlyParticle(coord, localX, localY, particle); continue; } TryStepParticle(coord, localX, localY, particle); } if (movementOnlyPage) { UpdateSparseGasRowCooldown(page, localY, rowGasCountAtStart, (_movedParticles + _swappedParticles) - moveCountBefore, _moveAttemptCount - moveAttemptsBefore); } else { page.ClearGasRowCooldown(localY); } var elapsedMicros = ToMicroseconds(start, Stopwatch.GetTimestamp()); if (movementOnlyPage) { movementMicros += elapsedMicros; } else { runtimeMicros += elapsedMicros; } } private void ResolveGasChunkRowSeams(IReadOnlyList activeChunks) { for (var i = activeChunks.Count - 1; i >= 0; i--) { var lowerCoord = activeChunks[i]; if (!_cellPages.TryGetValue(lowerCoord, out var lowerPage)) { continue; } for (var localX = 0; localX < lowerPage.Width; localX++) { var particle = lowerPage[localX, 0]; if (particle.MotionType != PrototypeParticleType.Steam) { continue; } var worldX = ToWorldX(lowerCoord, localX); var worldY = ToWorldY(lowerCoord, 0); if (!InBounds(worldX, worldY - 1)) { continue; } if (TryMoveEmpty(worldX, worldY, worldX, worldY - 1)) { CompactGasSeamColumn(worldX, worldY); } } } } private void CompactGasSeamColumn(int x, int vacancyY) { while (InBounds(x, vacancyY + 1) && !HasParticle(x, vacancyY)) { if (!TryGetParticle(x, vacancyY + 1, out var below) || below.MotionType != PrototypeParticleType.Steam) { break; } if (!MoveParticle(x, vacancyY + 1, x, vacancyY)) { break; } vacancyY++; } } private void ProcessFallingPage(ChunkCoord coord, ChunkCellPage page, int minRow, int maxRow, ref long movementMicros, ref long runtimeMicros) { var movementOnlyPage = page.RuntimeCellCount == 0; var start = Stopwatch.GetTimestamp(); for (var localY = maxRow; localY >= minRow; localY--) { if (page.GetOccupiedRowCount(localY) == 0) { continue; } for (var localX = 0; localX < page.Width; localX++) { if (page.IsProcessed(localX, localY, _stepCounter)) { continue; } var particle = page[localX, localY]; if (particle.TypeId == 0 || particle.MotionType == PrototypeParticleType.Steam) { continue; } page.MarkProcessed(localX, localY, _stepCounter); if (movementOnlyPage || !particle.RequiresFullRuntimeStep) { StepMovementOnlyParticle(coord, localX, localY, particle); continue; } TryStepParticle(coord, localX, localY, particle); } } var elapsedMicros = ToMicroseconds(start, Stopwatch.GetTimestamp()); if (movementOnlyPage) { movementMicros += elapsedMicros; } else { runtimeMicros += elapsedMicros; } } private bool ShouldProcessGasRow(ChunkCellPage page, int localY) { if (page.HasFieldActivity) { page.ClearGasRowCooldown(localY); return true; } if (page.RuntimeCellCount > 0) { page.ClearGasRowCooldown(localY); return true; } if (ShouldSkipSparseGasRow(page, localY)) { return false; } if (!page.HasDirtyBands) { return false; } var minRow = Math.Max(0, page.DirtyMinRow - GasBandHaloRows); var maxRow = Math.Min(page.Height - 1, page.DirtyMaxRow + GasBandHaloRows); return localY >= minRow && localY <= maxRow; } private bool ShouldSkipSparseGasRow(ChunkCellPage page, int localY) { var signature = ComputeSparseGasRowSignature(page, localY); if (signature == 0) { page.ClearGasRowCooldown(localY); return false; } if (page.GetGasRowCooldownUntilStep(localY) > _stepCounter && page.GetGasRowCooldownSignature(localY) == signature) { return true; } if (page.GetGasRowCooldownSignature(localY) != signature) { page.ClearGasRowCooldown(localY); } return false; } private void UpdateSparseGasRowCooldown(ChunkCellPage page, int localY, int rowGasCountAtStart, int moveDelta, int attemptDelta) { if (rowGasCountAtStart <= 0 || rowGasCountAtStart > 4) { page.ClearGasRowCooldown(localY); return; } if (moveDelta != 0 || attemptDelta <= 0) { page.ClearGasRowCooldown(localY); return; } var signature = ComputeSparseGasRowSignature(page, localY); if (signature == 0) { page.ClearGasRowCooldown(localY); return; } var cooldownFrames = attemptDelta > rowGasCountAtStart ? 2 : 1; page.SetGasRowCooldown(localY, signature, _stepCounter + cooldownFrames); } private static int ComputeSparseGasRowSignature(ChunkCellPage page, int localY) { var gasCount = page.GetGasRowCount(localY); if (gasCount <= 0 || gasCount > 4) { return 0; } var aboveRow = Math.Max(0, localY - 1); return HashCode.Combine( localY, page.GetRowRevision(localY), page.GetRowRevision(aboveRow), page.GetGasRowCount(localY), page.GetGasRowCount(aboveRow)); } private static bool TryGetRowRange(ChunkCellPage page, int haloRows, out int minRow, out int maxRow) { if (page.HasFieldActivity) { minRow = 0; maxRow = page.Height - 1; return true; } if (!page.HasDirtyBands) { if (page.RuntimeCellCount > 0) { minRow = 0; maxRow = page.Height - 1; return true; } minRow = 0; maxRow = -1; return false; } minRow = Math.Max(0, page.DirtyMinRow - haloRows); maxRow = Math.Min(page.Height - 1, page.DirtyMaxRow + haloRows); return minRow <= maxRow; } }