sandpypi/Sand.ChunkPrototype/PrototypeSparseSandAdapter.Step.cs

473 lines
15 KiB
C#

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<ChunkCoord> 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<ChunkCoord> 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;
}
}