870 lines
31 KiB
C#
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);
|