473 lines
15 KiB
C#
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;
|
|
}
|
|
}
|