870 lines
31 KiB
C#

using System.Diagnostics;
using Sand.ChunkPrototype;
using Sand.Core;
var mode = ParseMode(args);
var headlessGasParticleId = ParseOptionalArgument(args, "--gas-particle", "--particle");
var contentRoot = Path.Combine(AppContext.BaseDirectory, "Content", "part");
var library = ParticleLibraryLoader.LoadFromDirectory(contentRoot);
var denseSizes = new[] { 512, 1024, 2048 };
var appWidth = 265;
var appHeight = 200;
var appWorkloads = BuildAppWorkloads();
Console.WriteLine($"Sand benchmarks | mode={mode}");
Console.WriteLine($"Definitions={library.Definitions.Count}");
Console.WriteLine("Columns: scenario | backend | world | particles | avg_fps | avg_step_ms | avg_frame_ms | step_core_ms | frame_build_ms | activation_ms | movement_ms | runtime_ms | field_decay_ms | loaded_chunks | active_chunks | stepped_chunks | dirty_chunks | field_cells | move_attempts | successful_moves | swap_attempts | stalled_movable | fast_path | full_runtime | estimated_mb");
Console.WriteLine();
switch (mode)
{
case "dense-baseline":
RunDenseBaseline(library, denseSizes);
break;
case "chunk-baseline":
RunChunkBaseline(library, denseSizes);
break;
case "app-sized":
RunAppSizedComparisons(library, appWidth, appHeight, appWorkloads);
break;
case "app-headless":
RunHeadlessAppComparisons(library, appWidth, appHeight, headlessGasParticleId);
break;
case "snapshot-scenes":
RunSnapshotScenes(library, appWidth, appHeight, appWorkloads);
break;
default:
RunDenseBaseline(library, denseSizes);
Console.WriteLine();
RunChunkBaseline(library, denseSizes);
Console.WriteLine();
RunAppSizedComparisons(library, appWidth, appHeight, appWorkloads);
Console.WriteLine();
RunHeadlessAppComparisons(library, appWidth, appHeight, headlessGasParticleId);
break;
}
static string ParseMode(string[] args)
{
for (var i = 0; i < args.Length; i++)
{
if ((args[i] == "--mode" || args[i] == "-m") && i + 1 < args.Length)
{
return args[i + 1];
}
}
return "all";
}
static string? ParseOptionalArgument(string[] args, params string[] optionNames)
{
for (var i = 0; i < args.Length; i++)
{
foreach (var optionName in optionNames)
{
if (args[i] == optionName && i + 1 < args.Length)
{
return args[i + 1];
}
}
}
return null;
}
void RunDenseBaseline(ParticleLibrary library, IReadOnlyList<int> sizes)
{
Console.WriteLine("Dense baseline");
foreach (var size in sizes)
{
foreach (var workload in BuildDenseScaleWorkloads())
{
var result = MeasureDense(library, size, size, workload, includePerStepAction: false);
PrintResult(result);
}
}
}
void RunChunkBaseline(ParticleLibrary library, IReadOnlyList<int> sizes)
{
Console.WriteLine("Chunk baseline");
foreach (var size in sizes)
{
foreach (var workload in BuildChunkScaleWorkloads())
{
var result = MeasureChunk(library, size, size, workload, includePerStepAction: false);
PrintResult(result);
}
}
}
void RunAppSizedComparisons(ParticleLibrary library, int width, int height, IReadOnlyList<AppWorkload> workloads)
{
Console.WriteLine("App-sized comparisons");
foreach (var workload in workloads)
{
PrintResult(MeasureDense(library, width, height, workload, includePerStepAction: true));
PrintResult(MeasureChunk(library, width, height, workload, includePerStepAction: true));
Console.WriteLine();
}
}
void RunHeadlessAppComparisons(ParticleLibrary library, int width, int height, string? preferredGasParticleId)
{
Console.WriteLine("Headless app-style comparisons");
var resolvedGasParticleId = ResolveHeadlessGasParticleId(library, preferredGasParticleId);
Console.WriteLine($"Headless gas particle={resolvedGasParticleId}");
foreach (var workload in BuildHeadlessAppWorkloads(library, resolvedGasParticleId))
{
PrintResult(MeasureDense(library, width, height, workload, includePerStepAction: true));
PrintResult(MeasureChunk(library, width, height, workload, includePerStepAction: true));
Console.WriteLine();
}
}
void RunSnapshotScenes(ParticleLibrary library, int width, int height, IReadOnlyList<AppWorkload> workloads)
{
Console.WriteLine("Snapshot scenes");
foreach (var workload in workloads.Where(static workload => workload.IncludeInSnapshots))
{
var dense120 = BuildDenseSnapshot(library, width, height, workload, 120);
var dense300 = BuildDenseSnapshot(library, width, height, workload, 300);
var chunk120 = BuildChunkSnapshot(library, width, height, workload, 120);
var chunk300 = BuildChunkSnapshot(library, width, height, workload, 300);
Console.WriteLine($"{workload.Name} | dense | step120={dense120} | step300={dense300}");
Console.WriteLine($"{workload.Name} | chunk | step120={chunk120} | step300={chunk300}");
}
}
static BenchmarkResult MeasureDense(ParticleLibrary library, int width, int height, AppWorkload workload, bool includePerStepAction)
{
var simulation = new SandSimulation(width, height, 1, library, new SimulationSettings { PauseSim = false, AmbientTemperature = 22f });
workload.SetupDense(simulation);
WarmupDense(simulation, workload, includePerStepAction);
var iterations = GetIterationCount(width, height);
var frameBuffer = new byte[width * height * 4];
var stepAverage = MeasureAverageMilliseconds(() =>
{
if (includePerStepAction)
{
workload.TickDense(simulation);
}
simulation.Step(1f / 60f);
}, iterations);
var frameAverage = MeasureAverageMilliseconds(() => simulation.BuildRgbaFrame(frameBuffer), GetFrameIterationCount(width, height));
return new BenchmarkResult(
workload.Name,
"dense",
$"{width}x{height}",
simulation.ParticleCount,
stepAverage + frameAverage <= 0.001 ? 0d : 1000d / (stepAverage + frameAverage),
stepAverage,
frameAverage,
stepAverage,
frameAverage,
0d,
0d,
0d,
0d,
0,
0,
0,
0,
0,
0d,
0d,
0d,
0d,
0d,
0d,
EstimateDenseBytes(simulation) / (1024d * 1024d));
}
static BenchmarkResult MeasureChunk(ParticleLibrary library, int width, int height, AppWorkload workload, bool includePerStepAction)
{
var adapter = new PrototypeSparseSandAdapter(width, height);
workload.SetupChunk(adapter, library);
WarmupChunk(adapter, library, workload, includePerStepAction);
var iterations = GetIterationCount(width, height);
var totalStepMs = 0d;
long totalLoadedChunks = 0;
long totalActiveChunks = 0;
long totalSteppedChunks = 0;
long totalDirtyChunks = 0;
long totalFieldCells = 0;
long totalMoveAttempts = 0;
long totalSuccessfulMoves = 0;
long totalSwapAttempts = 0;
long totalStalledMovable = 0;
long totalFastPath = 0;
long totalFullRuntime = 0;
long totalActivationMicros = 0;
long totalMovementMicros = 0;
long totalRuntimeMicros = 0;
long totalFieldDecayMicros = 0;
for (var i = 0; i < iterations; i++)
{
if (includePerStepAction)
{
workload.TickChunk(adapter, library);
}
adapter.World.ClearDirtyChunks();
var stepStart = Stopwatch.GetTimestamp();
adapter.Step();
totalStepMs += Stopwatch.GetElapsedTime(stepStart).TotalMilliseconds;
var stats = adapter.LastStepStats;
totalLoadedChunks += adapter.World.LoadedChunkCount;
totalActiveChunks += adapter.World.ActiveChunkCount;
totalSteppedChunks += stats.SteppedChunks;
totalDirtyChunks += adapter.World.DirtyChunkCount;
totalFieldCells += adapter.FieldCellCount;
totalMoveAttempts += stats.MoveAttempts;
totalSuccessfulMoves += stats.SuccessfulMoves;
totalSwapAttempts += stats.SwapAttempts;
totalStalledMovable += stats.StalledMovableCells;
totalFastPath += stats.MovementOnlyFastPathCount;
totalFullRuntime += stats.FullRuntimeStepCount;
totalActivationMicros += stats.ActivationTimeMicroseconds;
totalMovementMicros += stats.MovementTimeMicroseconds;
totalRuntimeMicros += stats.RuntimeTimeMicroseconds;
totalFieldDecayMicros += stats.FieldDecayTimeMicroseconds;
}
var stepAverage = totalStepMs / iterations;
var frameAverage = MeasureAverageMilliseconds(() => adapter.BuildRgbaFrame(), GetFrameIterationCount(width, height));
return new BenchmarkResult(
workload.Name,
"chunk",
$"{width}x{height}",
adapter.ParticleCount,
stepAverage + frameAverage <= 0.001 ? 0d : 1000d / (stepAverage + frameAverage),
stepAverage,
frameAverage,
stepAverage,
frameAverage,
totalActivationMicros / 1000d / iterations,
totalMovementMicros / 1000d / iterations,
totalRuntimeMicros / 1000d / iterations,
totalFieldDecayMicros / 1000d / iterations,
(int)Math.Round(totalLoadedChunks / (double)iterations),
(int)Math.Round(totalActiveChunks / (double)iterations),
(int)Math.Round(totalSteppedChunks / (double)iterations),
(int)Math.Round(totalDirtyChunks / (double)iterations),
(int)Math.Round(totalFieldCells / (double)iterations),
totalMoveAttempts / (double)iterations,
totalSuccessfulMoves / (double)iterations,
totalSwapAttempts / (double)iterations,
totalStalledMovable / (double)iterations,
totalFastPath / (double)iterations,
totalFullRuntime / (double)iterations,
adapter.EstimatedStorageBytes / (1024d * 1024d));
}
static void WarmupDense(SandSimulation simulation, AppWorkload workload, bool includePerStepAction)
{
for (var i = 0; i < 6; i++)
{
if (includePerStepAction)
{
workload.TickDense(simulation);
}
simulation.Step(1f / 60f);
}
}
static void WarmupChunk(PrototypeSparseSandAdapter adapter, ParticleLibrary library, AppWorkload workload, bool includePerStepAction)
{
for (var i = 0; i < 6; i++)
{
if (includePerStepAction)
{
workload.TickChunk(adapter, library);
}
adapter.Step();
}
}
static double MeasureAverageMilliseconds(Action action, int iterations)
{
var stopwatch = Stopwatch.StartNew();
for (var i = 0; i < iterations; i++)
{
action();
}
stopwatch.Stop();
return stopwatch.Elapsed.TotalMilliseconds / iterations;
}
static int GetIterationCount(int width, int height)
{
var area = width * height;
return area switch
{
<= 53000 => 120,
<= 262144 => 24,
<= 1048576 => 10,
_ => 5,
};
}
static int GetFrameIterationCount(int width, int height)
{
var area = width * height;
return area switch
{
<= 53000 => 120,
<= 262144 => 18,
<= 1048576 => 8,
_ => 4,
};
}
static void PrintResult(BenchmarkResult result)
{
Console.WriteLine($"{result.Scenario} | {result.Backend} | {result.World} | {result.Particles} | {result.AvgFpsEquivalent:F2} | {result.AvgStepMs:F3} | {result.AvgFrameMs:F3} | {result.StepCoreMs:F3} | {result.FrameBuildMs:F3} | {result.ActivationMs:F3} | {result.MovementMs:F3} | {result.RuntimeMs:F3} | {result.FieldDecayMs:F3} | {result.LoadedChunks} | {result.ActiveChunks} | {result.SteppedChunks} | {result.DirtyChunks} | {result.FieldCells} | {result.MoveAttempts:F1} | {result.SuccessfulMoves:F1} | {result.SwapAttempts:F1} | {result.StalledMovable:F1} | {result.MovementOnlyFastPathCount:F1} | {result.FullRuntimeStepCount:F1} | {result.EstimatedMb:F3}");
}
static string BuildDenseSnapshot(ParticleLibrary library, int width, int height, AppWorkload workload, int steps)
{
var simulation = new SandSimulation(width, height, 1, library, new SimulationSettings { PauseSim = false, AmbientTemperature = 22f });
workload.SetupDense(simulation);
for (var i = 0; i < steps; i++)
{
workload.TickDense(simulation);
simulation.Step(1f / 60f);
}
unchecked
{
uint hash = 2166136261;
for (var y = 0; y < simulation.Height; y++)
{
for (var x = 0; x < simulation.Width; x++)
{
var typeId = simulation.TypeId[x, y];
if (typeId == 0)
{
continue;
}
hash = (hash ^ (uint)((x * 397) ^ (y * 911) ^ typeId)) * 16777619;
}
}
return $"{hash:X8}";
}
}
static string BuildChunkSnapshot(ParticleLibrary library, int width, int height, AppWorkload workload, int steps)
{
var adapter = new PrototypeSparseSandAdapter(width, height);
workload.SetupChunk(adapter, library);
for (var i = 0; i < steps; i++)
{
workload.TickChunk(adapter, library);
adapter.Step();
}
unchecked
{
uint hash = 2166136261;
foreach (var ((x, y), particle) in adapter.ParticleEntries.OrderBy(static entry => entry.Key.Item2).ThenBy(static entry => entry.Key.Item1))
{
hash = (hash ^ (uint)((x * 397) ^ (y * 911) ^ particle.TypeId)) * 16777619;
}
return $"{hash:X8}";
}
}
static long EstimateDenseBytes(SandSimulation simulation)
{
var cells = (long)simulation.Width * simulation.Height;
const int typeBytes = sizeof(ushort);
const int floatBytes = sizeof(float);
const int byteBytes = sizeof(byte);
const int shortBytes = sizeof(short);
const int intBytes = sizeof(int);
var perCellBytes = typeBytes + (12 * floatBytes) + byteBytes + shortBytes + (2 * intBytes) + 3 + 4;
return cells * perCellBytes;
}
IReadOnlyList<AppWorkload> BuildDenseScaleWorkloads() =>
[
new AppWorkload("empty", _ => { }, (_, _) => { }, _ => { }, (_, _) => { }, IncludeInSnapshots: false),
new AppWorkload("sparse_rain", CreateSparseRainSceneDense, CreateSparseRainSceneChunk, _ => { }, (_, _) => { }, IncludeInSnapshots: true),
new AppWorkload("dense_pile", CreateDensePileSceneDense, CreateDensePileSceneChunk, _ => { }, (_, _) => { }, IncludeInSnapshots: false),
new AppWorkload("gas_pocket", CreateGasPocketSceneDense, CreateGasPocketSceneChunk, _ => { }, (_, _) => { }, IncludeInSnapshots: true),
new AppWorkload("tool_stress", CreateToolStressSceneDense, CreateToolStressSceneChunk, TickToolStressDense, TickToolStressChunk, IncludeInSnapshots: true),
];
IReadOnlyList<AppWorkload> BuildChunkScaleWorkloads() => BuildDenseScaleWorkloads();
IReadOnlyList<AppWorkload> BuildAppWorkloads() =>
[
new AppWorkload("active_sand_flood", _ => { }, (_, _) => { }, TickActiveSandFloodDense, TickActiveSandFloodChunk, IncludeInSnapshots: false),
new AppWorkload("mixed_pile", CreateMixedPileSceneDense, CreateMixedPileSceneChunk, _ => { }, (_, _) => { }, IncludeInSnapshots: true),
new AppWorkload("active_gas_burst", CreateGasPocketSceneDense, CreateGasPocketSceneChunk, TickActiveGasBurstDense, TickActiveGasBurstChunk, IncludeInSnapshots: false),
new AppWorkload("continuous_mixed_paint", _ => { }, (_, _) => { }, TickContinuousMixedPaintDense, TickContinuousMixedPaintChunk, IncludeInSnapshots: false),
new AppWorkload("tool_stress", CreateToolStressSceneDense, CreateToolStressSceneChunk, TickToolStressDense, TickToolStressChunk, IncludeInSnapshots: true),
];
IReadOnlyList<AppWorkload> BuildHeadlessAppWorkloads(ParticleLibrary library, string gasParticleId)
{
const int brushRadius = 16;
return
[
new AppWorkload(
"headless_gas_hold_paint",
_ => { },
(_, _) => { },
simulation => TickHeadlessHeldPaintDense(simulation, library, gasParticleId, brushRadius),
(adapter, particleLibrary) => TickHeadlessHeldPaintChunk(adapter, particleLibrary ?? library, gasParticleId, brushRadius),
IncludeInSnapshots: false),
new AppWorkload(
"headless_gas_paint_erase",
_ => { },
(_, _) => { },
simulation => TickHeadlessGasPaintEraseDense(simulation, library, gasParticleId, brushRadius),
(adapter, particleLibrary) => TickHeadlessGasPaintEraseChunk(adapter, particleLibrary ?? library, gasParticleId, brushRadius),
IncludeInSnapshots: false),
];
}
static void CreateSparseRainSceneDense(SandSimulation simulation)
{
foreach (var (x, y) in BuildSparseRainCells(simulation.Width, simulation.Height))
{
simulation.CreateParticleAtPixel(x, y, "sand");
}
}
static void CreateSparseRainSceneChunk(PrototypeSparseSandAdapter adapter, ParticleLibrary? _)
{
foreach (var (x, y) in BuildSparseRainCells(adapter.Width, adapter.Height))
{
adapter.AddParticle(x, y, PrototypeParticleType.Sand);
}
}
static void CreateDensePileSceneDense(SandSimulation simulation)
{
for (var x = 0; x < simulation.Width; x++)
{
simulation.CreateParticleAtPixel(x, simulation.Height - 2, "wall");
}
for (var y = simulation.Height / 2; y < simulation.Height - 2; y++)
{
for (var x = simulation.Width / 4; x < (simulation.Width * 3) / 4; x++)
{
simulation.CreateParticleAtPixel(x, y, ((x + y) & 1) == 0 ? "sand" : "water");
}
}
}
static void CreateDensePileSceneChunk(PrototypeSparseSandAdapter adapter, ParticleLibrary? _)
{
for (var x = 0; x < adapter.Width; x++)
{
adapter.AddParticle(x, adapter.Height - 2, PrototypeParticleType.Wall);
}
for (var y = adapter.Height / 2; y < adapter.Height - 2; y++)
{
for (var x = adapter.Width / 4; x < (adapter.Width * 3) / 4; x++)
{
adapter.AddParticle(x, y, ((x + y) & 1) == 0 ? PrototypeParticleType.Sand : PrototypeParticleType.Water);
}
}
}
static void CreateMixedPileSceneDense(SandSimulation simulation)
{
for (var x = 0; x < simulation.Width; x++)
{
simulation.CreateParticleAtPixel(x, simulation.Height - 2, "wall");
}
for (var x = simulation.Width / 4; x < (simulation.Width * 3) / 4; x++)
{
simulation.CreateParticleAtPixel(x, simulation.Height / 2, "sand");
simulation.CreateParticleAtPixel(x, simulation.Height / 2 - 18, "water");
}
}
static void CreateMixedPileSceneChunk(PrototypeSparseSandAdapter adapter, ParticleLibrary? _)
{
for (var x = 0; x < adapter.Width; x++)
{
adapter.AddParticle(x, adapter.Height - 2, PrototypeParticleType.Wall);
}
for (var x = adapter.Width / 4; x < (adapter.Width * 3) / 4; x++)
{
adapter.AddParticle(x, adapter.Height / 2, PrototypeParticleType.Sand);
adapter.AddParticle(x, adapter.Height / 2 - 18, PrototypeParticleType.Water);
}
}
static void CreateGasPocketSceneDense(SandSimulation simulation)
{
var left = simulation.Width / 3;
var right = (simulation.Width * 2) / 3;
var top = simulation.Height / 3;
var bottom = simulation.Height - 16;
for (var x = left; x <= right; x++)
{
simulation.CreateParticleAtPixel(x, bottom, "wall");
}
for (var y = top; y <= bottom; y++)
{
simulation.CreateParticleAtPixel(left, y, "wall");
simulation.CreateParticleAtPixel(right, y, "wall");
}
for (var x = left + 2; x < right - 1; x += 2)
{
for (var y = bottom - 22; y < bottom - 6; y += 2)
{
simulation.CreateParticleAtPixel(x, y, "steam");
}
}
}
static void CreateGasPocketSceneChunk(PrototypeSparseSandAdapter adapter, ParticleLibrary? _)
{
var left = adapter.Width / 3;
var right = (adapter.Width * 2) / 3;
var top = adapter.Height / 3;
var bottom = adapter.Height - 16;
for (var x = left; x <= right; x++)
{
adapter.AddParticle(x, bottom, PrototypeParticleType.Wall);
}
for (var y = top; y <= bottom; y++)
{
adapter.AddParticle(left, y, PrototypeParticleType.Wall);
adapter.AddParticle(right, y, PrototypeParticleType.Wall);
}
for (var x = left + 2; x < right - 1; x += 2)
{
for (var y = bottom - 22; y < bottom - 6; y += 2)
{
adapter.AddParticle(x, y, PrototypeParticleType.Steam);
}
}
}
static void CreateToolStressSceneDense(SandSimulation simulation)
{
CreateMixedPileSceneDense(simulation);
TickToolStressDense(simulation);
}
static void CreateToolStressSceneChunk(PrototypeSparseSandAdapter adapter, ParticleLibrary? _)
{
CreateMixedPileSceneChunk(adapter, null);
TickToolStressChunk(adapter, null);
}
static void TickActiveSandFloodDense(SandSimulation simulation)
{
var x = 8 + ((simulation.Frame * 5) % Math.Max(1, simulation.Width - 40));
for (var dx = 0; dx < 20; dx += 2)
{
simulation.CreateParticleAtPixel(x + dx, 4, "sand");
simulation.CreateParticleAtPixel(x + dx, 6, "sand");
}
}
static void TickActiveSandFloodChunk(PrototypeSparseSandAdapter adapter, ParticleLibrary? _)
{
var x = 8 + (((adapter.ParticleCount * 5) + (adapter.LastStepStats.MoveAttempts * 3)) % Math.Max(1, adapter.Width - 40));
for (var dx = 0; dx < 20; dx += 2)
{
adapter.AddParticle(x + dx, 4, PrototypeParticleType.Sand);
adapter.AddParticle(x + dx, 6, PrototypeParticleType.Sand);
}
}
static void TickContinuousMixedPaintDense(SandSimulation simulation)
{
var x = 8 + ((simulation.Frame * 7) % Math.Max(1, simulation.Width - 36));
for (var y = 4; y < 22; y += 3)
{
simulation.CreateParticleAtPixel(x, y, "sand");
simulation.CreateParticleAtPixel(x + 8, y, "water");
simulation.CreateParticleAtPixel(x + 16, y, "steam");
}
}
static void TickContinuousMixedPaintChunk(PrototypeSparseSandAdapter adapter, ParticleLibrary? _)
{
var x = 8 + (((adapter.ParticleCount * 7) + (adapter.LastStepStats.MoveAttempts * 5)) % Math.Max(1, adapter.Width - 36));
for (var y = 4; y < 22; y += 3)
{
adapter.AddParticle(x, y, PrototypeParticleType.Sand);
adapter.AddParticle(x + 8, y, PrototypeParticleType.Water);
adapter.AddParticle(x + 16, y, PrototypeParticleType.Steam);
}
}
static void TickActiveGasBurstDense(SandSimulation simulation)
{
var centerX = simulation.Width / 2;
var sourceY = simulation.Height - 26;
for (var dx = -10; dx <= 10; dx += 2)
{
simulation.CreateParticleAtPixel(centerX + dx, sourceY, "steam");
simulation.CreateParticleAtPixel(centerX + dx, sourceY - 2, "steam");
}
}
static void TickActiveGasBurstChunk(PrototypeSparseSandAdapter adapter, ParticleLibrary? _)
{
var centerX = adapter.Width / 2;
var sourceY = adapter.Height - 26;
for (var dx = -10; dx <= 10; dx += 2)
{
adapter.AddParticle(centerX + dx, sourceY, PrototypeParticleType.Steam);
adapter.AddParticle(centerX + dx, sourceY - 2, PrototypeParticleType.Steam);
}
}
static void TickHeadlessHeldPaintDense(SandSimulation simulation, ParticleLibrary library, string particleId, int brushRadius)
{
var particle = library.GetDefinition(library.GetTypeId(particleId));
var centerX = (simulation.Width / 2) * 4;
var centerY = (simulation.Height / 4) * 4;
PaintLikeAppDense(simulation, particleId, particle, centerX, centerY, brushRadius);
}
static void TickHeadlessHeldPaintChunk(PrototypeSparseSandAdapter adapter, ParticleLibrary library, string particleId, int brushRadius)
{
var particle = library.GetDefinition(library.GetTypeId(particleId));
var centerX = (adapter.Width / 2) * 4;
var centerY = (adapter.Height / 4) * 4;
PaintLikeAppChunk(adapter, library, particleId, particle, centerX, centerY, brushRadius);
}
static void TickHeadlessGasPaintEraseDense(SandSimulation simulation, ParticleLibrary library, string particleId, int brushRadius)
{
TickHeadlessHeldPaintDense(simulation, library, particleId, brushRadius);
if ((simulation.Frame & 1) == 0)
{
simulation.ClearParticleCircle((simulation.Width / 2) * 4, (simulation.Height / 2) * 4, Math.Max(4, brushRadius / 2));
}
}
static void TickHeadlessGasPaintEraseChunk(PrototypeSparseSandAdapter adapter, ParticleLibrary library, string particleId, int brushRadius)
{
TickHeadlessHeldPaintChunk(adapter, library, particleId, brushRadius);
if ((adapter.LastStepStats.SteppedChunks & 1) == 0)
{
ClearCircleChunk(adapter, adapter.Width / 2, adapter.Height / 2, Math.Max(4, brushRadius / 2));
}
}
static void PaintLikeAppDense(SandSimulation simulation, string particleId, ParticleDef particle, int centerX, int centerY, int brushRadius)
{
if (ShouldUseContinuousPaint(particle))
{
simulation.CreateParticlePourAtPixel(centerX, centerY, brushRadius, particleId, GetContinuousPaintCount(particle, brushRadius), simulation.Frame);
return;
}
simulation.CreateParticleCircle(centerX, centerY, brushRadius, particleId);
}
static void PaintLikeAppChunk(PrototypeSparseSandAdapter adapter, ParticleLibrary library, string particleId, ParticleDef particle, int centerX, int centerY, int brushRadius)
{
if (ShouldUseContinuousPaint(particle))
{
CreateParticlePourChunk(adapter, library, centerX, centerY, brushRadius, particleId, GetContinuousPaintCount(particle, brushRadius), adapter.LastStepStats.SteppedChunks + adapter.ParticleCount);
return;
}
CreateParticleCircleChunk(adapter, library, centerX, centerY, brushRadius, particleId);
}
static bool ShouldUseContinuousPaint(ParticleDef particle) =>
particle.Kind == ParticleKind.Gas ||
particle.Kind == ParticleKind.Liquid ||
(particle.Kind == ParticleKind.Solid && !particle.IsStatic);
static int GetContinuousPaintCount(ParticleDef particle, int brushRadius) => particle.Kind switch
{
ParticleKind.Gas => Math.Clamp(brushRadius + 1, 1, 8),
ParticleKind.Liquid => Math.Clamp((brushRadius * 2) + 1, 1, 24),
ParticleKind.Solid when !particle.IsStatic => Math.Clamp((brushRadius * 2) + 1, 1, 24),
_ => 0,
};
static string ResolveHeadlessGasParticleId(ParticleLibrary library, string? preferredParticleId)
{
if (!string.IsNullOrWhiteSpace(preferredParticleId) && library.TryGetTypeId(preferredParticleId, out var preferredId) && preferredId != 0)
{
return preferredParticleId;
}
return library.TryGetTypeId("steam", out var steamId) && steamId != 0
? "steam"
: library.TryGetTypeId("ultratanium", out var ultraId) && ultraId != 0
? "ultratanium"
: library.Definitions.First(static d => d.Kind == ParticleKind.Gas).Id;
}
static void CreateParticleCircleChunk(PrototypeSparseSandAdapter adapter, ParticleLibrary library, int centerX, int centerY, int brushRadius, string particleId)
{
var gridCenterX = centerX / 4;
var gridCenterY = centerY / 4;
var particle = ResolveChunkParticle(library, adapter, particleId);
for (var dx = -brushRadius; dx <= brushRadius; dx++)
{
for (var dy = -brushRadius; dy <= brushRadius; dy++)
{
if ((dx * dx) + (dy * dy) > brushRadius * brushRadius)
{
continue;
}
adapter.AddParticle(gridCenterX + dx, gridCenterY + dy, particle);
}
}
}
static void CreateParticlePourChunk(PrototypeSparseSandAdapter adapter, ParticleLibrary library, int centerX, int centerY, int brushRadius, string particleId, int maxParticles, int seed)
{
var gridCenterX = centerX / 4;
var gridCenterY = centerY / 4;
var diameter = (brushRadius * 2) + 1;
var placed = 0;
var particle = ResolveChunkParticle(library, adapter, particleId);
for (var i = 0; i < Math.Max(maxParticles * 6, diameter * diameter) && placed < maxParticles; i++)
{
var dx = ((seed + (i * 17)) % diameter) - brushRadius;
var dy = ((seed + (i * 31)) % diameter) - brushRadius;
if ((dx * dx) + (dy * dy) > brushRadius * brushRadius)
{
continue;
}
if (adapter.AddParticle(gridCenterX + dx, gridCenterY + dy, particle))
{
placed++;
}
}
}
static void ClearCircleChunk(PrototypeSparseSandAdapter adapter, int centerX, int centerY, int brushRadius)
{
for (var dx = -brushRadius; dx <= brushRadius; dx++)
{
for (var dy = -brushRadius; dy <= brushRadius; dy++)
{
if ((dx * dx) + (dy * dy) > brushRadius * brushRadius)
{
continue;
}
adapter.RemoveParticle(centerX + dx, centerY + dy);
}
}
}
static PrototypeParticle ResolveChunkParticle(ParticleLibrary library, PrototypeSparseSandAdapter adapter, string particleId)
{
var typeId = library.GetTypeId(particleId);
var def = library.GetDefinition(typeId);
var color = def.Color;
var motionType = def.Kind switch
{
ParticleKind.Solid => def.IsStatic ? PrototypeParticleType.Wall : PrototypeParticleType.Sand,
ParticleKind.Liquid => PrototypeParticleType.Water,
ParticleKind.Gas => PrototypeParticleType.Steam,
_ => PrototypeParticleType.Sand,
};
var particle = new PrototypeParticle(typeId, def.Id, motionType, def.Kind, ParticleBehaviorKind.None, color.R, color.G, color.B, MathF.Max(0.1f, def.Mass), MathF.Max(0.01f, def.Velocity), MathF.Max(0f, def.Friction), MathF.Max(0f, def.Viscosity), IsStatic: def.IsStatic);
adapter.RegisterParticleProfile(particle);
return particle;
}
static void TickToolStressDense(SandSimulation simulation)
{
var centerX = simulation.Width / 2;
var centerY = simulation.Height / 2;
simulation.ApplyWindBrushAtPixel(centerX, centerY, 8, 1f, 0.2f);
simulation.ApplyAirBrushAtPixel(centerX + 12, centerY - 6, 7, 0.75f, 0f);
simulation.ApplyGravityBrushAtPixel(centerX - 10, centerY + 4, 6, 2.5f);
}
static void TickToolStressChunk(PrototypeSparseSandAdapter adapter, ParticleLibrary? _)
{
var centerX = adapter.Width / 2;
var centerY = adapter.Height / 2;
adapter.ApplyWindBrush(centerX, centerY, 8, 1f, 0.2f);
adapter.ApplyAirBrush(centerX + 12, centerY - 6, 7, 0.75f, 0f);
adapter.ApplyGravityBrush(centerX - 10, centerY + 4, 6, 2.5f);
}
static List<(int X, int Y)> BuildSparseRainCells(int width, int height)
{
var cells = new List<(int X, int Y)>();
for (var x = 4; x < width - 4; x += Math.Max(10, width / 24))
{
cells.Add((x, 4 + ((x / 7) % 5)));
cells.Add((x, 12 + ((x / 11) % 7)));
if (height > 40)
{
cells.Add((x, 24 + ((x / 5) % 9)));
}
}
return cells;
}
internal readonly record struct AppWorkload(
string Name,
Action<SandSimulation> SetupDense,
Action<PrototypeSparseSandAdapter, ParticleLibrary?> SetupChunk,
Action<SandSimulation> TickDense,
Action<PrototypeSparseSandAdapter, ParticleLibrary?> TickChunk,
bool IncludeInSnapshots);
internal readonly record struct BenchmarkResult(
string Scenario,
string Backend,
string World,
int Particles,
double AvgFpsEquivalent,
double AvgStepMs,
double AvgFrameMs,
double StepCoreMs,
double FrameBuildMs,
double ActivationMs,
double MovementMs,
double RuntimeMs,
double FieldDecayMs,
int LoadedChunks,
int ActiveChunks,
int SteppedChunks,
int DirtyChunks,
int FieldCells,
double MoveAttempts,
double SuccessfulMoves,
double SwapAttempts,
double StalledMovable,
double MovementOnlyFastPathCount,
double FullRuntimeStepCount,
double EstimatedMb);