Created
July 9, 2026 08:48
-
-
Save 1ceOcean/95a6a5727cc2284d0119b02af3cdff3f to your computer and use it in GitHub Desktop.
use `dotnet run -c Release --project benchmarks/Hshg.SingleFileBench -- --dimension 2` run c like bench
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.Diagnostics; | |
| using System.Globalization; | |
| using System.Numerics; | |
| using System.Runtime.CompilerServices; | |
| using System.Runtime.InteropServices; | |
| Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; | |
| BenchOptions options = BenchOptions.Parse(args); | |
| if (options.Dimension == 3) | |
| { | |
| ConsoleBench3D.Run(options); | |
| } | |
| else | |
| { | |
| ConsoleBench2D.Run(options); | |
| } | |
| internal readonly record struct BenchOptions(int Dimension, bool Lite, int? Seed, int Samples, int Windows) | |
| { | |
| public static BenchOptions Parse(string[] args) | |
| { | |
| int dimension = 2; | |
| bool lite = false; | |
| int? seed = null; | |
| int samples = 200; | |
| int windows = 0; | |
| for (int i = 0; i < args.Length; i++) | |
| { | |
| switch (args[i]) | |
| { | |
| case "--dimension": | |
| dimension = int.Parse(args[++i], CultureInfo.InvariantCulture); | |
| break; | |
| case "--lite": | |
| lite = true; | |
| break; | |
| case "--seed": | |
| seed = int.Parse(args[++i], CultureInfo.InvariantCulture); | |
| break; | |
| case "--samples": | |
| samples = int.Parse(args[++i], CultureInfo.InvariantCulture); | |
| break; | |
| case "--windows": | |
| windows = int.Parse(args[++i], CultureInfo.InvariantCulture); | |
| break; | |
| case "--help": | |
| case "-h": | |
| Console.WriteLine("Usage: dotnet run -c Release --project benchmarks/Hshg.SingleFileBench -- [--dimension 2|3] [--lite] [--seed N] [--samples 200] [--windows N]"); | |
| Environment.Exit(0); | |
| break; | |
| } | |
| } | |
| if (dimension is not 2 and not 3) | |
| { | |
| throw new ArgumentOutOfRangeException(nameof(args), "--dimension must be 2 or 3."); | |
| } | |
| if (samples <= 0) | |
| { | |
| throw new ArgumentOutOfRangeException(nameof(args), "--samples must be positive."); | |
| } | |
| return new BenchOptions(dimension, lite, seed, samples, windows); | |
| } | |
| } | |
| internal static class ConsoleBench2D | |
| { | |
| public static void Run(BenchOptions options) | |
| { | |
| int agents = options.Lite ? 100_000 : 500_000; | |
| int side = options.Lite ? 1024 : 2048; | |
| var sim = new Scenario2D(agents, side, 16, 7, options.Seed ?? Environment.TickCount); | |
| sim.InsertAll(); | |
| RunLoop(options, sim); | |
| } | |
| private static void RunLoop(BenchOptions options, Scenario2D sim) | |
| { | |
| Console.WriteLine($"took {sim.InsertMilliseconds}ms to insert {sim.AgentCount} entities"); | |
| Console.WriteLine($"{sim.Hshg.GridCount} grids\n"); | |
| Console.WriteLine("--- | average | sd | +- 0.1ms | +- 0.4ms | +- 0.7ms | +- 1.0ms | col stats |"); | |
| bool interrupted = false; | |
| Console.CancelKeyPress += (_, e) => | |
| { | |
| interrupted = true; | |
| e.Cancel = true; | |
| }; | |
| var stats = new LatencyStats(options.Samples); | |
| int windows = 0; | |
| while (!interrupted) | |
| { | |
| stats.Add(sim.Tick()); | |
| if (!stats.Full) | |
| { | |
| continue; | |
| } | |
| stats.PrintAndReset(sim.MaybeCollisions, sim.Collisions); | |
| sim.ResetCollisionStats(); | |
| windows++; | |
| if (options.Windows > 0 && windows >= options.Windows) | |
| { | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| internal static class ConsoleBench3D | |
| { | |
| public static void Run(BenchOptions options) | |
| { | |
| int agents = options.Lite ? 100_000 : 300_000; | |
| var sim = new Scenario3D(agents, 128, 16, 7, options.Seed ?? Environment.TickCount); | |
| sim.InsertAll(); | |
| RunLoop(options, sim); | |
| } | |
| private static void RunLoop(BenchOptions options, Scenario3D sim) | |
| { | |
| Console.WriteLine($"took {sim.InsertMilliseconds}ms to insert {sim.AgentCount} entities"); | |
| Console.WriteLine($"{sim.Hshg.GridCount} grids\n"); | |
| Console.WriteLine("--- | average | sd | +- 0.1ms | +- 0.4ms | +- 0.7ms | +- 1.0ms | col stats |"); | |
| bool interrupted = false; | |
| Console.CancelKeyPress += (_, e) => | |
| { | |
| interrupted = true; | |
| e.Cancel = true; | |
| }; | |
| var stats = new LatencyStats(options.Samples); | |
| int windows = 0; | |
| while (!interrupted) | |
| { | |
| stats.Add(sim.Tick()); | |
| if (!stats.Full) | |
| { | |
| continue; | |
| } | |
| stats.PrintAndReset(sim.MaybeCollisions, sim.Collisions); | |
| sim.ResetCollisionStats(); | |
| windows++; | |
| if (options.Windows > 0 && windows >= options.Windows) | |
| { | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| internal sealed class Scenario2D | |
| { | |
| private const int QueryWidth = 1920; | |
| private const int QueryHeight = 1080; | |
| private readonly float[] _initData; | |
| private Ball2D[] _balls; | |
| private Ball2D[] _ballsScratch; | |
| private int _queriedLen; | |
| private readonly int _queriesPerTick; | |
| private readonly int _arenaWidth; | |
| private readonly int _arenaHeight; | |
| private readonly float _agentRadius; | |
| private ulong _maybeCollisions; | |
| private ulong _collisions; | |
| public Scenario2D(int agentCount, int cellsSide, int cellSize, float agentRadius, int seed) | |
| { | |
| AgentCount = agentCount; | |
| _agentRadius = agentRadius; | |
| _arenaWidth = checked(cellsSide * cellSize); | |
| _arenaHeight = checked(cellsSide * cellSize); | |
| _queriesPerTick = 255 >> 2; | |
| _initData = GC.AllocateUninitializedArray<float>(agentCount * 3); | |
| _balls = GC.AllocateUninitializedArray<Ball2D>(agentCount); | |
| _ballsScratch = GC.AllocateUninitializedArray<Ball2D>(agentCount); | |
| Hshg = new Hshg2D(cellsSide, cellSize); | |
| Hshg.EnsureCapacity(agentCount + 1); | |
| Initialize(seed); | |
| } | |
| public int AgentCount { get; } | |
| public Hshg2D Hshg { get; } | |
| public long InsertMilliseconds { get; private set; } | |
| public ulong MaybeCollisions => _maybeCollisions; | |
| public ulong Collisions => _collisions; | |
| public void InsertAll() | |
| { | |
| long start = Stopwatch.GetTimestamp(); | |
| for (int i = 0; i < AgentCount; i++) | |
| { | |
| Hshg.Insert(_initData[i * 3], _initData[i * 3 + 2], _initData[i * 3 + 1], i); | |
| } | |
| InsertMilliseconds = (long)Stopwatch.GetElapsedTime(start).TotalMilliseconds; | |
| } | |
| public TickTimings Tick() | |
| { | |
| long optStart = Stopwatch.GetTimestamp(); | |
| Hshg.OptimizeAndRemapReferences(_balls, _ballsScratch); | |
| (_balls, _ballsScratch) = (_ballsScratch, _balls); | |
| long colStart = Stopwatch.GetTimestamp(); | |
| var collider = new Collider2D(_balls); | |
| Hshg.Collide(ref collider); | |
| _maybeCollisions += collider.MaybeCollisions; | |
| _collisions += collider.Collisions; | |
| long qryStart = Stopwatch.GetTimestamp(); | |
| QueryBatch(); | |
| long updStart = Stopwatch.GetTimestamp(); | |
| var updater = new Updater2D(this); | |
| Hshg.UpdateDenseAndMove(ref updater); | |
| long end = Stopwatch.GetTimestamp(); | |
| return new TickTimings( | |
| ElapsedMs(optStart, colStart), | |
| ElapsedMs(colStart, qryStart), | |
| ElapsedMs(qryStart, updStart), | |
| ElapsedMs(updStart, end)); | |
| } | |
| public void ResetCollisionStats() | |
| { | |
| _maybeCollisions = 0; | |
| _collisions = 0; | |
| } | |
| private void Initialize(int seed) | |
| { | |
| var random = new Random(seed); | |
| for (int i = 0; i < AgentCount; i++) | |
| { | |
| _initData[i * 3] = random.NextSingle() * _arenaWidth; | |
| _initData[i * 3 + 1] = _agentRadius; | |
| _initData[i * 3 + 2] = random.NextSingle() * _arenaHeight; | |
| _balls[i].Vx = random.NextSingle() - 0.5f; | |
| _balls[i].Vy = random.NextSingle() - 0.5f; | |
| } | |
| } | |
| private void QueryBatch() | |
| { | |
| var query = new Query2D(this); | |
| int ptr = _queriedLen * 3; | |
| for (int i = 0; i < _queriesPerTick; i++) | |
| { | |
| Hshg.Query( | |
| _initData[ptr] - QueryWidth * 0.5f, | |
| _initData[ptr + 2] - QueryHeight * 0.5f, | |
| _initData[ptr] + QueryWidth * 0.5f, | |
| _initData[ptr + 2] + QueryHeight * 0.5f, | |
| ref query); | |
| ptr++; | |
| if (ptr + 2 >= _initData.Length) | |
| { | |
| ptr = 0; | |
| } | |
| } | |
| } | |
| private static double ElapsedMs(long start, long end) => (end - start) * 1000.0 / Stopwatch.Frequency; | |
| private struct Updater2D(Scenario2D scenario) : IHshgDenseUpdater2D | |
| { | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void Update(ref HshgEntity2D entity) | |
| { | |
| ref Ball2D ball = ref scenario._balls[entity.Reference]; | |
| entity.X += ball.Vx; | |
| if (entity.X < entity.Radius) | |
| { | |
| ball.Vx *= 0.9f; | |
| ball.Vx += 1.0f; | |
| } | |
| else if (entity.X + entity.Radius >= scenario._arenaWidth) | |
| { | |
| ball.Vx *= 0.9f; | |
| ball.Vx -= 1.0f; | |
| } | |
| entity.Y += ball.Vy; | |
| if (entity.Y < entity.Radius) | |
| { | |
| ball.Vy *= 0.9f; | |
| ball.Vy += 1.0f; | |
| } | |
| else if (entity.Y + entity.Radius >= scenario._arenaHeight) | |
| { | |
| ball.Vy *= 0.9f; | |
| ball.Vy -= 1.0f; | |
| } | |
| } | |
| } | |
| private struct Collider2D(Ball2D[] balls) : IHshgCollider2D | |
| { | |
| public ulong MaybeCollisions; | |
| public ulong Collisions; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void Collide(Hshg2D hshg, in HshgEntity2D a, in HshgEntity2D b) | |
| { | |
| _ = hshg; | |
| MaybeCollisions++; | |
| float dx = a.X - b.X; | |
| float dy = a.Y - b.Y; | |
| float dr = a.Radius + b.Radius; | |
| float dist = dx * dx + dy * dy; | |
| if (dist <= dr * dr) | |
| { | |
| Collisions++; | |
| float mag = 1.0f / float.Sqrt(dist); | |
| dx *= mag; | |
| dy *= mag; | |
| ref Ball2D ballsBase = ref MemoryMarshal.GetArrayDataReference(balls); | |
| ref Ball2D ballA = ref Unsafe.Add(ref ballsBase, a.Reference); | |
| ref Ball2D ballB = ref Unsafe.Add(ref ballsBase, b.Reference); | |
| ballA.Vx *= 0.75f; | |
| ballA.Vx += dx; | |
| ballA.Vy *= 0.75f; | |
| ballA.Vy += dy; | |
| ballB.Vx *= 0.75f; | |
| ballB.Vx -= dx; | |
| ballB.Vy *= 0.75f; | |
| ballB.Vy -= dy; | |
| } | |
| } | |
| } | |
| private struct Query2D(Scenario2D scenario) : IHshgQuery2D | |
| { | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void Query(Hshg2D hshg, in HshgEntity2D entity) | |
| { | |
| _ = hshg; | |
| _ = entity.Reference; | |
| scenario._queriedLen++; | |
| scenario._queriedLen = (scenario._queriedLen + 1) % scenario.AgentCount; | |
| } | |
| } | |
| } | |
| internal sealed class Scenario3D | |
| { | |
| private const int QueryWidth = 1920; | |
| private const int QueryHeight = 1080; | |
| private readonly float[] _initData; | |
| private Ball3D[] _balls; | |
| private Ball3D[] _ballsScratch; | |
| private int _queriedLen; | |
| private readonly int _queriesPerTick; | |
| private readonly int _arenaWidth; | |
| private readonly int _arenaHeight; | |
| private readonly int _arenaDepth; | |
| private readonly float _agentRadius; | |
| private ulong _maybeCollisions; | |
| private ulong _collisions; | |
| public Scenario3D(int agentCount, int cellsSide, int cellSize, float agentRadius, int seed) | |
| { | |
| AgentCount = agentCount; | |
| _agentRadius = agentRadius; | |
| _arenaWidth = checked(cellsSide * cellSize); | |
| _arenaHeight = checked(cellsSide * cellSize); | |
| _arenaDepth = checked(cellsSide * cellSize); | |
| _queriesPerTick = 255 >> 4; | |
| _initData = GC.AllocateUninitializedArray<float>(agentCount * 4); | |
| _balls = GC.AllocateUninitializedArray<Ball3D>(agentCount); | |
| _ballsScratch = GC.AllocateUninitializedArray<Ball3D>(agentCount); | |
| Hshg = new Hshg3D(cellsSide, cellSize); | |
| Hshg.EnsureCapacity(agentCount + 1); | |
| Initialize(seed); | |
| } | |
| public int AgentCount { get; } | |
| public Hshg3D Hshg { get; } | |
| public long InsertMilliseconds { get; private set; } | |
| public ulong MaybeCollisions => _maybeCollisions; | |
| public ulong Collisions => _collisions; | |
| public void InsertAll() | |
| { | |
| long start = Stopwatch.GetTimestamp(); | |
| for (int i = 0; i < AgentCount; i++) | |
| { | |
| Hshg.Insert(_initData[i * 4], _initData[i * 4 + 2], _initData[i * 4 + 3], _initData[i * 4 + 1], i); | |
| } | |
| InsertMilliseconds = (long)Stopwatch.GetElapsedTime(start).TotalMilliseconds; | |
| } | |
| public TickTimings Tick() | |
| { | |
| long optStart = Stopwatch.GetTimestamp(); | |
| Hshg.OptimizeAndRemapReferences(_balls, _ballsScratch); | |
| (_balls, _ballsScratch) = (_ballsScratch, _balls); | |
| long colStart = Stopwatch.GetTimestamp(); | |
| var collider = new Collider3D(_balls); | |
| Hshg.Collide(ref collider); | |
| _maybeCollisions += collider.MaybeCollisions; | |
| _collisions += collider.Collisions; | |
| long qryStart = Stopwatch.GetTimestamp(); | |
| QueryBatch(); | |
| long updStart = Stopwatch.GetTimestamp(); | |
| var updater = new Updater3D(this); | |
| Hshg.UpdateDenseAndMove(ref updater); | |
| long end = Stopwatch.GetTimestamp(); | |
| return new TickTimings( | |
| ElapsedMs(optStart, colStart), | |
| ElapsedMs(colStart, qryStart), | |
| ElapsedMs(qryStart, updStart), | |
| ElapsedMs(updStart, end)); | |
| } | |
| public void ResetCollisionStats() | |
| { | |
| _maybeCollisions = 0; | |
| _collisions = 0; | |
| } | |
| private void Initialize(int seed) | |
| { | |
| var random = new Random(seed); | |
| for (int i = 0; i < AgentCount; i++) | |
| { | |
| _initData[i * 4] = random.NextSingle() * _arenaWidth; | |
| _initData[i * 4 + 1] = _agentRadius; | |
| _initData[i * 4 + 2] = random.NextSingle() * _arenaHeight; | |
| _initData[i * 4 + 3] = random.NextSingle() * _arenaDepth; | |
| _balls[i].Vx = random.NextSingle() - 0.5f; | |
| _balls[i].Vy = random.NextSingle() - 0.5f; | |
| _balls[i].Vz = random.NextSingle() - 0.5f; | |
| } | |
| } | |
| private void QueryBatch() | |
| { | |
| var query = new Query3D(this); | |
| int ptr = _queriedLen * 4; | |
| for (int i = 0; i < _queriesPerTick; i++) | |
| { | |
| Hshg.Query( | |
| _initData[ptr] - QueryWidth * 0.5f, | |
| _initData[ptr + 2] - QueryHeight * 0.5f, | |
| _initData[ptr + 3] - QueryHeight * 0.5f, | |
| _initData[ptr] + QueryWidth * 0.5f, | |
| _initData[ptr + 2] + QueryHeight * 0.5f, | |
| _initData[ptr + 3] + QueryHeight * 0.5f, | |
| ref query); | |
| ptr++; | |
| if (ptr + 3 >= _initData.Length) | |
| { | |
| ptr = 0; | |
| } | |
| } | |
| } | |
| private static double ElapsedMs(long start, long end) => (end - start) * 1000.0 / Stopwatch.Frequency; | |
| private struct Updater3D(Scenario3D scenario) : IHshgDenseUpdater3D | |
| { | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void Update(ref HshgEntity3D entity) | |
| { | |
| ref Ball3D ball = ref scenario._balls[entity.Reference]; | |
| entity.X += ball.Vx; | |
| if (entity.X < entity.Radius) | |
| { | |
| ball.Vx *= 0.9f; | |
| ball.Vx += 1.0f; | |
| } | |
| else if (entity.X + entity.Radius >= scenario._arenaWidth) | |
| { | |
| ball.Vx *= 0.9f; | |
| ball.Vx -= 1.0f; | |
| } | |
| entity.Y += ball.Vy; | |
| if (entity.Y < entity.Radius) | |
| { | |
| ball.Vy *= 0.9f; | |
| ball.Vy += 1.0f; | |
| } | |
| else if (entity.Y + entity.Radius >= scenario._arenaHeight) | |
| { | |
| ball.Vy *= 0.9f; | |
| ball.Vy -= 1.0f; | |
| } | |
| entity.Z += ball.Vz; | |
| if (entity.Z < entity.Radius) | |
| { | |
| ball.Vz *= 0.9f; | |
| ball.Vz += 1.0f; | |
| } | |
| else if (entity.Z + entity.Radius >= scenario._arenaDepth) | |
| { | |
| ball.Vz *= 0.9f; | |
| ball.Vz -= 1.0f; | |
| } | |
| } | |
| } | |
| private struct Collider3D(Ball3D[] balls) : IHshgCollider3D | |
| { | |
| public ulong MaybeCollisions; | |
| public ulong Collisions; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void Collide(Hshg3D hshg, in HshgEntity3D a, in HshgEntity3D b) | |
| { | |
| _ = hshg; | |
| MaybeCollisions++; | |
| float dx = a.X - b.X; | |
| float dy = a.Y - b.Y; | |
| float dz = a.Z - b.Z; | |
| float dr = a.Radius + b.Radius; | |
| float dist = dx * dx + dy * dy + dz * dz; | |
| if (dist <= dr * dr) | |
| { | |
| Collisions++; | |
| float mag = 1.0f / float.Sqrt(dist); | |
| dx *= mag; | |
| dy *= mag; | |
| dz *= mag; | |
| ref Ball3D ballsBase = ref MemoryMarshal.GetArrayDataReference(balls); | |
| ref Ball3D ballA = ref Unsafe.Add(ref ballsBase, a.Reference); | |
| ref Ball3D ballB = ref Unsafe.Add(ref ballsBase, b.Reference); | |
| ballA.Vx *= 0.75f; | |
| ballA.Vx += dx; | |
| ballA.Vy *= 0.75f; | |
| ballA.Vy += dy; | |
| ballA.Vz *= 0.75f; | |
| ballA.Vz += dz; | |
| ballB.Vx *= 0.75f; | |
| ballB.Vx -= dx; | |
| ballB.Vy *= 0.75f; | |
| ballB.Vy -= dy; | |
| ballB.Vz *= 0.75f; | |
| ballB.Vz -= dz; | |
| } | |
| } | |
| } | |
| private struct Query3D(Scenario3D scenario) : IHshgQuery3D | |
| { | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void Query(Hshg3D hshg, in HshgEntity3D entity) | |
| { | |
| _ = hshg; | |
| _ = entity.Reference; | |
| scenario._queriedLen++; | |
| scenario._queriedLen = (scenario._queriedLen + 1) % scenario.AgentCount; | |
| } | |
| } | |
| } | |
| internal readonly record struct TickTimings(double OptimizeMs, double CollideMs, double QueryMs, double UpdateMs); | |
| internal sealed class LatencyStats | |
| { | |
| private readonly TickTimings[] _samples; | |
| private int _index; | |
| public LatencyStats(int sampleCount) | |
| { | |
| _samples = new TickTimings[sampleCount]; | |
| } | |
| public bool Full => _index == _samples.Length; | |
| public void Add(TickTimings timings) | |
| { | |
| _samples[_index++] = timings; | |
| } | |
| public void PrintAndReset(ulong maybeCollisions, ulong collisions) | |
| { | |
| double optAvg = Average(static t => t.OptimizeMs); | |
| double colAvg = Average(static t => t.CollideMs); | |
| double qryAvg = Average(static t => t.QueryMs); | |
| double updAvg = Average(static t => t.UpdateMs); | |
| double colSd = StandardDeviation(static t => t.CollideMs, colAvg); | |
| double updSd = StandardDeviation(static t => t.UpdateMs, updAvg); | |
| Console.WriteLine("------------------------------------------------------------------------------"); | |
| Console.WriteLine($"opt | {optAvg,5:0.00}ms | ---- | -------- | -------- | -------- | -------- | attempted |"); | |
| Console.WriteLine($"col | {colAvg,5:0.00}ms | {colSd,4:0.00} | {Within(static t => t.CollideMs, colAvg, 0.1),5:0.0}% | {Within(static t => t.CollideMs, colAvg, 0.4),5:0.0}% | {Within(static t => t.CollideMs, colAvg, 0.7),5:0.0}% | {Within(static t => t.CollideMs, colAvg, 1.0),5:0.0}% | {maybeCollisions,9} |"); | |
| Console.WriteLine($"qry | {qryAvg,5:0.00}ms | ---- | -------- | -------- | -------- | -------- | |"); | |
| Console.WriteLine($"upd | {updAvg,5:0.00}ms | {updSd,4:0.00} | {Within(static t => t.UpdateMs, updAvg, 0.1),5:0.0}% | {Within(static t => t.UpdateMs, updAvg, 0.4),5:0.0}% | {Within(static t => t.UpdateMs, updAvg, 0.7),5:0.0}% | {Within(static t => t.UpdateMs, updAvg, 1.0),5:0.0}% | succeeded |"); | |
| Console.WriteLine($"all | {optAvg + colAvg + qryAvg + updAvg,5:0.00}ms | ---- | -------- | -------- | -------- | -------- | {collisions,9} |"); | |
| _index = 0; | |
| } | |
| private double Average(Func<TickTimings, double> selector) | |
| { | |
| double sum = 0; | |
| for (int i = 0; i < _samples.Length; i++) | |
| { | |
| sum += selector(_samples[i]); | |
| } | |
| return sum / _samples.Length; | |
| } | |
| private double StandardDeviation(Func<TickTimings, double> selector, double average) | |
| { | |
| double sum = 0; | |
| for (int i = 0; i < _samples.Length; i++) | |
| { | |
| double diff = selector(_samples[i]) - average; | |
| sum += diff * diff; | |
| } | |
| return Math.Sqrt(sum / _samples.Length); | |
| } | |
| private double Within(Func<TickTimings, double> selector, double average, double window) | |
| { | |
| int count = 0; | |
| for (int i = 0; i < _samples.Length; i++) | |
| { | |
| double value = selector(_samples[i]); | |
| if (value >= average - window && value <= average + window) | |
| { | |
| count++; | |
| } | |
| } | |
| return count * 100.0 / _samples.Length; | |
| } | |
| } | |
| [StructLayout(LayoutKind.Sequential)] | |
| internal struct HshgEntity2D | |
| { | |
| public int Cell; | |
| public byte Grid; | |
| public int Next; | |
| public int Prev; | |
| public int Reference; | |
| public float X; | |
| public float Y; | |
| public float Radius; | |
| } | |
| [StructLayout(LayoutKind.Sequential)] | |
| internal struct HshgEntity3D | |
| { | |
| public int Cell; | |
| public byte Grid; | |
| public int Next; | |
| public int Prev; | |
| public int Reference; | |
| public float X; | |
| public float Y; | |
| public float Z; | |
| public float Radius; | |
| } | |
| internal interface IHshgDenseUpdater2D | |
| { | |
| void Update(ref HshgEntity2D entity); | |
| } | |
| internal interface IHshgCollider2D | |
| { | |
| void Collide(Hshg2D hshg, in HshgEntity2D a, in HshgEntity2D b); | |
| } | |
| internal interface IHshgQuery2D | |
| { | |
| void Query(Hshg2D hshg, in HshgEntity2D entity); | |
| } | |
| internal interface IHshgDenseUpdater3D | |
| { | |
| void Update(ref HshgEntity3D entity); | |
| } | |
| internal interface IHshgCollider3D | |
| { | |
| void Collide(Hshg3D hshg, in HshgEntity3D a, in HshgEntity3D b); | |
| } | |
| internal interface IHshgQuery3D | |
| { | |
| void Query(Hshg3D hshg, in HshgEntity3D entity); | |
| } | |
| internal sealed class Hshg2D | |
| { | |
| private const int InvalidCell = int.MaxValue; | |
| private HshgEntity2D[] _entities; | |
| private HshgEntity2D[]? _optimizeBuffer; | |
| private readonly int[] _cellHeads; | |
| private readonly Grid[] _grids; | |
| private readonly int _cellLog; | |
| private readonly int _cellSize; | |
| private readonly int _gridSize; | |
| private readonly float _inverseGridSize; | |
| private int _entitiesUsed; | |
| private uint _oldCache; | |
| private uint _newCache; | |
| public Hshg2D(int side, int cellSize) | |
| { | |
| if (!IsPowerOfTwo(side) || !IsPowerOfTwo(cellSize)) | |
| { | |
| throw new ArgumentException("side and cellSize must be positive powers of two."); | |
| } | |
| int cellsLen = MaxCells(side); | |
| int gridsLen = MaxGrids(side); | |
| _entities = new HshgEntity2D[1]; | |
| _cellHeads = new int[cellsLen]; | |
| _grids = GC.AllocateUninitializedArray<Grid>(gridsLen); | |
| _cellLog = 31 - BitOperations.TrailingZeroCount((uint)cellSize); | |
| _cellSize = cellSize; | |
| _gridSize = checked(side * cellSize); | |
| _inverseGridSize = 1.0f / _gridSize; | |
| _entitiesUsed = 1; | |
| int cellOffset = 0; | |
| int gridSide = side; | |
| int gridCellSize = cellSize; | |
| for (int i = 0; i < gridsLen; i++) | |
| { | |
| _grids[i] = new Grid | |
| { | |
| CellOffset = cellOffset, | |
| CellsSide = gridSide, | |
| CellsMask = gridSide - 1, | |
| Cells2DLog = BitOperations.TrailingZeroCount((uint)gridSide), | |
| InverseCellSize = 1.0f / gridCellSize, | |
| }; | |
| cellOffset += checked(gridSide * gridSide); | |
| gridSide >>= 1; | |
| gridCellSize <<= 1; | |
| } | |
| } | |
| public int GridCount => _grids.Length; | |
| public void EnsureCapacity(int capacityIncludingSentinel) | |
| { | |
| if (capacityIncludingSentinel <= _entities.Length) | |
| { | |
| return; | |
| } | |
| HshgEntity2D[] entities = GC.AllocateUninitializedArray<HshgEntity2D>(capacityIncludingSentinel); | |
| Array.Copy(_entities, entities, _entitiesUsed); | |
| _entities = entities; | |
| if (_optimizeBuffer is not null && _optimizeBuffer.Length < capacityIncludingSentinel) | |
| { | |
| _optimizeBuffer = null; | |
| } | |
| } | |
| public void Insert(float x, float y, float radius, int reference) | |
| { | |
| int index = GetEntityIndex(); | |
| ref HshgEntity2D entity = ref _entities[index]; | |
| entity.Grid = GetGrid(radius); | |
| entity.Reference = reference; | |
| entity.X = x; | |
| entity.Y = y; | |
| entity.Radius = radius; | |
| Reinsert(index); | |
| } | |
| public void UpdateDenseAndMove<TUpdater>(ref TUpdater updater) | |
| where TUpdater : struct, IHshgDenseUpdater2D | |
| { | |
| for (int entityIndex = 1; entityIndex < _entitiesUsed; entityIndex++) | |
| { | |
| ref HshgEntity2D entity = ref _entities[entityIndex]; | |
| if (entity.Cell == InvalidCell) | |
| { | |
| continue; | |
| } | |
| updater.Update(ref entity); | |
| ref Grid grid = ref _grids[entity.Grid]; | |
| int newCell = GetCell(in grid, entity.X, entity.Y); | |
| if (entity.Cell != newCell) | |
| { | |
| RemoveLight(entityIndex); | |
| Reinsert(entityIndex); | |
| } | |
| } | |
| } | |
| public void Collide<TCollider>(ref TCollider collider) | |
| where TCollider : struct, IHshgCollider2D | |
| { | |
| UpdateCache(); | |
| ref HshgEntity2D entities = ref MemoryMarshal.GetArrayDataReference(_entities); | |
| ref int cellHeads = ref MemoryMarshal.GetArrayDataReference(_cellHeads); | |
| ref Grid grids = ref MemoryMarshal.GetArrayDataReference(_grids); | |
| int entitiesUsed = _entitiesUsed; | |
| for (int entityIndex = 1; entityIndex < entitiesUsed; entityIndex++) | |
| { | |
| ref HshgEntity2D entity = ref Unsafe.Add(ref entities, entityIndex); | |
| if (entity.Cell == InvalidCell) | |
| { | |
| continue; | |
| } | |
| int gridIndex = entity.Grid; | |
| ref Grid grid = ref Unsafe.Add(ref grids, gridIndex); | |
| int cellX = entity.Cell & grid.CellsMask; | |
| int cellY = entity.Cell >> grid.Cells2DLog; | |
| int head = entity.Next; | |
| while (head != 0) | |
| { | |
| ref HshgEntity2D other = ref Unsafe.Add(ref entities, head); | |
| collider.Collide(this, in entity, in other); | |
| head = other.Next; | |
| } | |
| if (cellX != grid.CellsMask) | |
| { | |
| head = Unsafe.Add(ref cellHeads, grid.CellOffset + entity.Cell + 1); | |
| while (head != 0) | |
| { | |
| ref HshgEntity2D other = ref Unsafe.Add(ref entities, head); | |
| collider.Collide(this, in entity, in other); | |
| head = other.Next; | |
| } | |
| } | |
| if (cellY != grid.CellsMask) | |
| { | |
| int cell = grid.CellOffset + entity.Cell + grid.CellsSide; | |
| if (cellX != 0) | |
| { | |
| head = Unsafe.Add(ref cellHeads, cell - 1); | |
| while (head != 0) | |
| { | |
| ref HshgEntity2D other = ref Unsafe.Add(ref entities, head); | |
| collider.Collide(this, in entity, in other); | |
| head = other.Next; | |
| } | |
| } | |
| head = Unsafe.Add(ref cellHeads, cell); | |
| while (head != 0) | |
| { | |
| ref HshgEntity2D other = ref Unsafe.Add(ref entities, head); | |
| collider.Collide(this, in entity, in other); | |
| head = other.Next; | |
| } | |
| if (cellX != grid.CellsMask) | |
| { | |
| head = Unsafe.Add(ref cellHeads, cell + 1); | |
| while (head != 0) | |
| { | |
| ref HshgEntity2D other = ref Unsafe.Add(ref entities, head); | |
| collider.Collide(this, in entity, in other); | |
| head = other.Next; | |
| } | |
| } | |
| } | |
| while (grid.Shift != 0) | |
| { | |
| int shift = grid.Shift; | |
| cellX >>= shift; | |
| cellY >>= shift; | |
| gridIndex += shift; | |
| grid = ref Unsafe.Add(ref grids, gridIndex); | |
| int minCellX = cellX != 0 ? cellX - 1 : 0; | |
| int minCellY = cellY != 0 ? cellY - 1 : 0; | |
| int maxCellX = cellX != grid.CellsMask ? cellX + 1 : cellX; | |
| int maxCellY = cellY != grid.CellsMask ? cellY + 1 : cellY; | |
| for (int y = minCellY; y <= maxCellY; y++) | |
| { | |
| for (int x = minCellX; x <= maxCellX; x++) | |
| { | |
| head = Unsafe.Add(ref cellHeads, grid.CellOffset + (x | (y << grid.Cells2DLog))); | |
| while (head != 0) | |
| { | |
| ref HshgEntity2D other = ref Unsafe.Add(ref entities, head); | |
| collider.Collide(this, in entity, in other); | |
| head = other.Next; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| public void Query<TQuery>(float minX, float minY, float maxX, float maxY, ref TQuery query) | |
| where TQuery : struct, IHshgQuery2D | |
| { | |
| UpdateCache(); | |
| (int xStart, int xEnd) = MapPosition(minX, maxX); | |
| (int yStart, int yEnd) = MapPosition(minY, maxY); | |
| int gridIndex = 0; | |
| int shift = 0; | |
| while (gridIndex < _grids.Length && _grids[gridIndex].EntitiesLen == 0) | |
| { | |
| gridIndex++; | |
| shift++; | |
| } | |
| if (gridIndex == _grids.Length) | |
| { | |
| return; | |
| } | |
| xStart >>= shift; | |
| yStart >>= shift; | |
| xEnd >>= shift; | |
| yEnd >>= shift; | |
| while (true) | |
| { | |
| ref Grid grid = ref _grids[gridIndex]; | |
| int sx = xStart != 0 ? xStart - 1 : 0; | |
| int sy = yStart != 0 ? yStart - 1 : 0; | |
| int ex = xEnd != grid.CellsMask ? xEnd + 1 : xEnd; | |
| int ey = yEnd != grid.CellsMask ? yEnd + 1 : yEnd; | |
| for (int y = sy; y <= ey; y++) | |
| { | |
| for (int x = sx; x <= ex; x++) | |
| { | |
| for (int j = _cellHeads[grid.CellOffset + (x | (y << grid.Cells2DLog))]; j != 0;) | |
| { | |
| ref HshgEntity2D entity = ref _entities[j]; | |
| if (entity.X + entity.Radius >= minX && | |
| entity.X - entity.Radius <= maxX && | |
| entity.Y + entity.Radius >= minY && | |
| entity.Y - entity.Radius <= maxY) | |
| { | |
| query.Query(this, in entity); | |
| } | |
| j = entity.Next; | |
| } | |
| } | |
| } | |
| if (grid.Shift == 0) | |
| { | |
| break; | |
| } | |
| shift = grid.Shift; | |
| xStart >>= shift; | |
| yStart >>= shift; | |
| xEnd >>= shift; | |
| yEnd >>= shift; | |
| gridIndex += shift; | |
| } | |
| } | |
| public void OptimizeAndRemapReferences<T>(T[] sourceReferences, T[] destinationReferences) | |
| { | |
| HshgEntity2D[] entities = _optimizeBuffer is { } buffer && buffer.Length >= _entities.Length | |
| ? buffer | |
| : GC.AllocateUninitializedArray<HshgEntity2D>(_entities.Length); | |
| HshgEntity2D[] oldEntities = _entities; | |
| ref int cellHeads = ref MemoryMarshal.GetArrayDataReference(_cellHeads); | |
| ref HshgEntity2D source = ref MemoryMarshal.GetArrayDataReference(oldEntities); | |
| ref HshgEntity2D destination = ref MemoryMarshal.GetArrayDataReference(entities); | |
| ref T destinationReference = ref MemoryMarshal.GetArrayDataReference(destinationReferences); | |
| int index = 1; | |
| int referenceIndex = 0; | |
| for (int cell = 0; cell < _cellHeads.Length; cell++) | |
| { | |
| ref int cellHead = ref Unsafe.Add(ref cellHeads, cell); | |
| int entityIndex = cellHead; | |
| if (entityIndex == 0) | |
| { | |
| continue; | |
| } | |
| cellHead = index; | |
| while (true) | |
| { | |
| ref HshgEntity2D entity = ref Unsafe.Add(ref destination, index); | |
| entity = Unsafe.Add(ref source, entityIndex); | |
| Unsafe.Add(ref destinationReference, referenceIndex) = sourceReferences[entity.Reference]; | |
| entity.Reference = referenceIndex++; | |
| if (entity.Prev != 0) | |
| { | |
| entity.Prev = index - 1; | |
| } | |
| index++; | |
| if (entity.Next != 0) | |
| { | |
| entityIndex = entity.Next; | |
| entity.Next = index; | |
| } | |
| else | |
| { | |
| break; | |
| } | |
| } | |
| } | |
| _optimizeBuffer = oldEntities; | |
| _entities = entities; | |
| _entitiesUsed = index; | |
| } | |
| private int GetEntityIndex() | |
| { | |
| if (_entitiesUsed == _entities.Length) | |
| { | |
| EnsureCapacity(_entities.Length << 1); | |
| } | |
| return _entitiesUsed++; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private void Reinsert(int index) | |
| { | |
| ref HshgEntity2D entity = ref _entities[index]; | |
| ref Grid grid = ref _grids[entity.Grid]; | |
| entity.Cell = GetCell(in grid, entity.X, entity.Y); | |
| int cellIndex = grid.CellOffset + entity.Cell; | |
| entity.Next = _cellHeads[cellIndex]; | |
| if (entity.Next != 0) | |
| { | |
| _entities[entity.Next].Prev = index; | |
| } | |
| entity.Prev = 0; | |
| _cellHeads[cellIndex] = index; | |
| if (grid.EntitiesLen == 0) | |
| { | |
| _newCache |= 1u << entity.Grid; | |
| } | |
| grid.EntitiesLen++; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private void RemoveLight(int index) | |
| { | |
| ref HshgEntity2D entity = ref _entities[index]; | |
| ref Grid grid = ref _grids[entity.Grid]; | |
| if (entity.Prev == 0) | |
| { | |
| _cellHeads[grid.CellOffset + entity.Cell] = entity.Next; | |
| } | |
| else | |
| { | |
| _entities[entity.Prev].Next = entity.Next; | |
| } | |
| _entities[entity.Next].Prev = entity.Prev; | |
| grid.EntitiesLen--; | |
| if (grid.EntitiesLen == 0) | |
| { | |
| _newCache ^= 1u << entity.Grid; | |
| } | |
| } | |
| private void UpdateCache() | |
| { | |
| if (_oldCache == _newCache) | |
| { | |
| return; | |
| } | |
| _oldCache = _newCache; | |
| for (int i = 0; i < _grids.Length; i++) | |
| { | |
| _grids[i].Shift = 0; | |
| } | |
| int oldGrid = 0; | |
| while (oldGrid < _grids.Length && _grids[oldGrid].EntitiesLen == 0) | |
| { | |
| oldGrid++; | |
| } | |
| if (oldGrid == _grids.Length) | |
| { | |
| return; | |
| } | |
| byte shift = 1; | |
| for (int newGrid = oldGrid + 1; newGrid < _grids.Length; newGrid++) | |
| { | |
| if (_grids[newGrid].EntitiesLen == 0) | |
| { | |
| shift++; | |
| continue; | |
| } | |
| _grids[oldGrid].Shift = shift; | |
| oldGrid = newGrid; | |
| shift = 1; | |
| } | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private byte GetGrid(float radius) | |
| { | |
| uint rounded = (uint)(radius + radius); | |
| if (rounded < (uint)_cellSize) | |
| { | |
| return 0; | |
| } | |
| int grid = _cellLog - BitOperations.LeadingZeroCount(rounded) + 1; | |
| return (byte)Math.Min(grid, _grids.Length - 1); | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private static int GetCell1D(in Grid grid, float x) | |
| { | |
| int cell = (int)(MathF.Abs(x) * grid.InverseCellSize); | |
| return (cell & grid.CellsSide) != 0 | |
| ? grid.CellsMask - (cell & grid.CellsMask) | |
| : cell & grid.CellsMask; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private static int GetCell(in Grid grid, float x, float y) => | |
| GetCell1D(in grid, x) | (GetCell1D(in grid, y) << grid.Cells2DLog); | |
| private (int Start, int End) MapPosition(float x1Raw, float x2Raw) | |
| { | |
| float x1; | |
| float x2; | |
| if (x1Raw < 0) | |
| { | |
| float shift = ((((int)(-x1Raw * _inverseGridSize)) << 1) + 2) * _gridSize; | |
| x1 = x1Raw + shift; | |
| x2 = x2Raw + shift; | |
| } | |
| else | |
| { | |
| x1 = x1Raw; | |
| x2 = x2Raw; | |
| } | |
| ref Grid grid = ref _grids[0]; | |
| int folds = (int)((x2 - ((int)(x1 * _inverseGridSize)) * _gridSize) * _inverseGridSize); | |
| switch (folds) | |
| { | |
| case 0: | |
| { | |
| int cell = GetCell1D(in grid, x1); | |
| int end = GetCell1D(in grid, x2); | |
| return (Math.Min(cell, end), Math.Max(cell, end)); | |
| } | |
| case 1: | |
| { | |
| int cell = (int)(MathF.Abs(x1) * grid.InverseCellSize); | |
| int end = GetCell1D(in grid, x2); | |
| return (cell & grid.CellsSide) != 0 | |
| ? (0, Math.Max(grid.CellsMask - (cell & grid.CellsMask), end)) | |
| : (Math.Min(cell & grid.CellsMask, end), grid.CellsMask); | |
| } | |
| default: | |
| return (0, grid.CellsMask); | |
| } | |
| } | |
| private static bool IsPowerOfTwo(int value) => value > 0 && (value & (value - 1)) == 0; | |
| private static int MaxGrids(int side) | |
| { | |
| int grids = 0; | |
| do | |
| { | |
| grids++; | |
| side >>= 1; | |
| } | |
| while (side >= 2); | |
| return grids; | |
| } | |
| private static int MaxCells(int side) | |
| { | |
| long cells = 0; | |
| do | |
| { | |
| cells += (long)side * side; | |
| side >>= 1; | |
| } | |
| while (side >= 2); | |
| return (int)cells; | |
| } | |
| private struct Grid | |
| { | |
| public int CellOffset; | |
| public int CellsSide; | |
| public int CellsMask; | |
| public int Cells2DLog; | |
| public byte Shift; | |
| public float InverseCellSize; | |
| public int EntitiesLen; | |
| } | |
| } | |
| internal sealed class Hshg3D | |
| { | |
| private const int InvalidCell = int.MaxValue; | |
| private HshgEntity3D[] _entities; | |
| private HshgEntity3D[]? _optimizeBuffer; | |
| private readonly int[] _cellHeads; | |
| private readonly Grid[] _grids; | |
| private readonly int _cellLog; | |
| private readonly int _cellSize; | |
| private readonly int _gridSize; | |
| private readonly float _inverseGridSize; | |
| private int _entitiesUsed; | |
| private uint _oldCache; | |
| private uint _newCache; | |
| public Hshg3D(int side, int cellSize) | |
| { | |
| if (!IsPowerOfTwo(side) || !IsPowerOfTwo(cellSize)) | |
| { | |
| throw new ArgumentException("side and cellSize must be positive powers of two."); | |
| } | |
| int cellsLen = MaxCells(side); | |
| int gridsLen = MaxGrids(side); | |
| _entities = new HshgEntity3D[1]; | |
| _cellHeads = new int[cellsLen]; | |
| _grids = GC.AllocateUninitializedArray<Grid>(gridsLen); | |
| _cellLog = 31 - BitOperations.TrailingZeroCount((uint)cellSize); | |
| _cellSize = cellSize; | |
| _gridSize = checked(side * cellSize); | |
| _inverseGridSize = 1.0f / _gridSize; | |
| _entitiesUsed = 1; | |
| int cellOffset = 0; | |
| int gridSide = side; | |
| int gridCellSize = cellSize; | |
| for (int i = 0; i < gridsLen; i++) | |
| { | |
| int cellsSq = checked(gridSide * gridSide); | |
| _grids[i] = new Grid | |
| { | |
| CellOffset = cellOffset, | |
| CellsSide = gridSide, | |
| CellsSq = cellsSq, | |
| CellsMask = gridSide - 1, | |
| Cells2DLog = BitOperations.TrailingZeroCount((uint)gridSide), | |
| Cells3DLog = BitOperations.TrailingZeroCount((uint)gridSide) << 1, | |
| InverseCellSize = 1.0f / gridCellSize, | |
| }; | |
| cellOffset += checked(cellsSq * gridSide); | |
| gridSide >>= 1; | |
| gridCellSize <<= 1; | |
| } | |
| } | |
| public int GridCount => _grids.Length; | |
| public void EnsureCapacity(int capacityIncludingSentinel) | |
| { | |
| if (capacityIncludingSentinel <= _entities.Length) | |
| { | |
| return; | |
| } | |
| HshgEntity3D[] entities = GC.AllocateUninitializedArray<HshgEntity3D>(capacityIncludingSentinel); | |
| Array.Copy(_entities, entities, _entitiesUsed); | |
| _entities = entities; | |
| if (_optimizeBuffer is not null && _optimizeBuffer.Length < capacityIncludingSentinel) | |
| { | |
| _optimizeBuffer = null; | |
| } | |
| } | |
| public void Insert(float x, float y, float z, float radius, int reference) | |
| { | |
| int index = GetEntityIndex(); | |
| ref HshgEntity3D entity = ref _entities[index]; | |
| entity.Grid = GetGrid(radius); | |
| entity.Reference = reference; | |
| entity.X = x; | |
| entity.Y = y; | |
| entity.Z = z; | |
| entity.Radius = radius; | |
| Reinsert(index); | |
| } | |
| public void UpdateDenseAndMove<TUpdater>(ref TUpdater updater) | |
| where TUpdater : struct, IHshgDenseUpdater3D | |
| { | |
| for (int entityIndex = 1; entityIndex < _entitiesUsed; entityIndex++) | |
| { | |
| ref HshgEntity3D entity = ref _entities[entityIndex]; | |
| if (entity.Cell == InvalidCell) | |
| { | |
| continue; | |
| } | |
| updater.Update(ref entity); | |
| ref Grid grid = ref _grids[entity.Grid]; | |
| int newCell = GetCell(in grid, entity.X, entity.Y, entity.Z); | |
| if (entity.Cell != newCell) | |
| { | |
| RemoveLight(entityIndex); | |
| Reinsert(entityIndex); | |
| } | |
| } | |
| } | |
| public void Collide<TCollider>(ref TCollider collider) | |
| where TCollider : struct, IHshgCollider3D | |
| { | |
| UpdateCache(); | |
| for (int entityIndex = 1; entityIndex < _entitiesUsed; entityIndex++) | |
| { | |
| ref HshgEntity3D entity = ref _entities[entityIndex]; | |
| if (entity.Cell == InvalidCell) | |
| { | |
| continue; | |
| } | |
| int gridIndex = entity.Grid; | |
| ref Grid grid = ref _grids[gridIndex]; | |
| int cellX = entity.Cell & grid.CellsMask; | |
| int cellY = (entity.Cell >> grid.Cells2DLog) & grid.CellsMask; | |
| int cellZ = entity.Cell >> grid.Cells3DLog; | |
| if (cellZ != 0) | |
| { | |
| if (cellY != 0) | |
| { | |
| int cell = grid.CellOffset + entity.Cell - grid.CellsSq - grid.CellsSide; | |
| if (cellX != 0) | |
| { | |
| CollideChain(ref collider, in entity, _cellHeads[cell - 1]); | |
| } | |
| CollideChain(ref collider, in entity, _cellHeads[cell]); | |
| if (cellX != grid.CellsMask) | |
| { | |
| CollideChain(ref collider, in entity, _cellHeads[cell + 1]); | |
| } | |
| } | |
| { | |
| int cell = grid.CellOffset + entity.Cell - grid.CellsSq; | |
| if (cellX != 0) | |
| { | |
| CollideChain(ref collider, in entity, _cellHeads[cell - 1]); | |
| } | |
| CollideChain(ref collider, in entity, _cellHeads[cell]); | |
| if (cellX != grid.CellsMask) | |
| { | |
| CollideChain(ref collider, in entity, _cellHeads[cell + 1]); | |
| } | |
| } | |
| if (cellY != grid.CellsMask) | |
| { | |
| int cell = grid.CellOffset + entity.Cell - grid.CellsSq + grid.CellsSide; | |
| if (cellX != 0) | |
| { | |
| CollideChain(ref collider, in entity, _cellHeads[cell - 1]); | |
| } | |
| CollideChain(ref collider, in entity, _cellHeads[cell]); | |
| if (cellX != grid.CellsMask) | |
| { | |
| CollideChain(ref collider, in entity, _cellHeads[cell + 1]); | |
| } | |
| } | |
| } | |
| CollideChain(ref collider, in entity, entity.Next); | |
| if (cellX != grid.CellsMask) | |
| { | |
| CollideChain(ref collider, in entity, _cellHeads[grid.CellOffset + entity.Cell + 1]); | |
| } | |
| if (cellY != grid.CellsMask) | |
| { | |
| int cell = grid.CellOffset + entity.Cell + grid.CellsSide; | |
| if (cellX != 0) | |
| { | |
| CollideChain(ref collider, in entity, _cellHeads[cell - 1]); | |
| } | |
| CollideChain(ref collider, in entity, _cellHeads[cell]); | |
| if (cellX != grid.CellsMask) | |
| { | |
| CollideChain(ref collider, in entity, _cellHeads[cell + 1]); | |
| } | |
| } | |
| while (grid.Shift != 0) | |
| { | |
| int shift = grid.Shift; | |
| cellX >>= shift; | |
| cellY >>= shift; | |
| cellZ >>= shift; | |
| gridIndex += shift; | |
| grid = ref _grids[gridIndex]; | |
| int minCellX = cellX != 0 ? cellX - 1 : 0; | |
| int minCellY = cellY != 0 ? cellY - 1 : 0; | |
| int minCellZ = cellZ != 0 ? cellZ - 1 : 0; | |
| int maxCellX = cellX != grid.CellsMask ? cellX + 1 : cellX; | |
| int maxCellY = cellY != grid.CellsMask ? cellY + 1 : cellY; | |
| int maxCellZ = cellZ != grid.CellsMask ? cellZ + 1 : cellZ; | |
| for (int z = minCellZ; z <= maxCellZ; z++) | |
| { | |
| for (int y = minCellY; y <= maxCellY; y++) | |
| { | |
| for (int x = minCellX; x <= maxCellX; x++) | |
| { | |
| int cell = x | (y << grid.Cells2DLog) | (z << grid.Cells3DLog); | |
| CollideChain(ref collider, in entity, _cellHeads[grid.CellOffset + cell]); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| public void Query<TQuery>(float minX, float minY, float minZ, float maxX, float maxY, float maxZ, ref TQuery query) | |
| where TQuery : struct, IHshgQuery3D | |
| { | |
| UpdateCache(); | |
| (int xStart, int xEnd) = MapPosition(minX, maxX); | |
| (int yStart, int yEnd) = MapPosition(minY, maxY); | |
| (int zStart, int zEnd) = MapPosition(minZ, maxZ); | |
| int gridIndex = 0; | |
| int shift = 0; | |
| while (gridIndex < _grids.Length && _grids[gridIndex].EntitiesLen == 0) | |
| { | |
| gridIndex++; | |
| shift++; | |
| } | |
| if (gridIndex == _grids.Length) | |
| { | |
| return; | |
| } | |
| xStart >>= shift; | |
| yStart >>= shift; | |
| zStart >>= shift; | |
| xEnd >>= shift; | |
| yEnd >>= shift; | |
| zEnd >>= shift; | |
| while (true) | |
| { | |
| ref Grid grid = ref _grids[gridIndex]; | |
| int sx = xStart != 0 ? xStart - 1 : 0; | |
| int sy = yStart != 0 ? yStart - 1 : 0; | |
| int sz = zStart != 0 ? zStart - 1 : 0; | |
| int ex = xEnd != grid.CellsMask ? xEnd + 1 : xEnd; | |
| int ey = yEnd != grid.CellsMask ? yEnd + 1 : yEnd; | |
| int ez = zEnd != grid.CellsMask ? zEnd + 1 : zEnd; | |
| for (int z = sz; z <= ez; z++) | |
| { | |
| for (int y = sy; y <= ey; y++) | |
| { | |
| for (int x = sx; x <= ex; x++) | |
| { | |
| int cell = x | (y << grid.Cells2DLog) | (z << grid.Cells3DLog); | |
| for (int j = _cellHeads[grid.CellOffset + cell]; j != 0;) | |
| { | |
| ref HshgEntity3D entity = ref _entities[j]; | |
| if (entity.X + entity.Radius >= minX && | |
| entity.X - entity.Radius <= maxX && | |
| entity.Y + entity.Radius >= minY && | |
| entity.Y - entity.Radius <= maxY && | |
| entity.Z + entity.Radius >= minZ && | |
| entity.Z - entity.Radius <= maxZ) | |
| { | |
| query.Query(this, in entity); | |
| } | |
| j = entity.Next; | |
| } | |
| } | |
| } | |
| } | |
| if (grid.Shift == 0) | |
| { | |
| break; | |
| } | |
| shift = grid.Shift; | |
| xStart >>= shift; | |
| yStart >>= shift; | |
| zStart >>= shift; | |
| xEnd >>= shift; | |
| yEnd >>= shift; | |
| zEnd >>= shift; | |
| gridIndex += shift; | |
| } | |
| } | |
| public void OptimizeAndRemapReferences<T>(T[] sourceReferences, T[] destinationReferences) | |
| { | |
| HshgEntity3D[] entities = _optimizeBuffer is { } buffer && buffer.Length >= _entities.Length | |
| ? buffer | |
| : GC.AllocateUninitializedArray<HshgEntity3D>(_entities.Length); | |
| HshgEntity3D[] oldEntities = _entities; | |
| ref int cellHeads = ref MemoryMarshal.GetArrayDataReference(_cellHeads); | |
| ref HshgEntity3D source = ref MemoryMarshal.GetArrayDataReference(oldEntities); | |
| ref HshgEntity3D destination = ref MemoryMarshal.GetArrayDataReference(entities); | |
| ref T destinationReference = ref MemoryMarshal.GetArrayDataReference(destinationReferences); | |
| int index = 1; | |
| int referenceIndex = 0; | |
| for (int cell = 0; cell < _cellHeads.Length; cell++) | |
| { | |
| ref int cellHead = ref Unsafe.Add(ref cellHeads, cell); | |
| int entityIndex = cellHead; | |
| if (entityIndex == 0) | |
| { | |
| continue; | |
| } | |
| cellHead = index; | |
| while (true) | |
| { | |
| ref HshgEntity3D entity = ref Unsafe.Add(ref destination, index); | |
| entity = Unsafe.Add(ref source, entityIndex); | |
| Unsafe.Add(ref destinationReference, referenceIndex) = sourceReferences[entity.Reference]; | |
| entity.Reference = referenceIndex++; | |
| if (entity.Prev != 0) | |
| { | |
| entity.Prev = index - 1; | |
| } | |
| index++; | |
| if (entity.Next != 0) | |
| { | |
| entityIndex = entity.Next; | |
| entity.Next = index; | |
| } | |
| else | |
| { | |
| break; | |
| } | |
| } | |
| } | |
| _optimizeBuffer = oldEntities; | |
| _entities = entities; | |
| _entitiesUsed = index; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private void CollideChain<TCollider>(ref TCollider collider, in HshgEntity3D entity, int head) | |
| where TCollider : struct, IHshgCollider3D | |
| { | |
| while (head != 0) | |
| { | |
| ref HshgEntity3D other = ref _entities[head]; | |
| collider.Collide(this, in entity, in other); | |
| head = other.Next; | |
| } | |
| } | |
| private int GetEntityIndex() | |
| { | |
| if (_entitiesUsed == _entities.Length) | |
| { | |
| EnsureCapacity(_entities.Length << 1); | |
| } | |
| return _entitiesUsed++; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private void Reinsert(int index) | |
| { | |
| ref HshgEntity3D entity = ref _entities[index]; | |
| ref Grid grid = ref _grids[entity.Grid]; | |
| entity.Cell = GetCell(in grid, entity.X, entity.Y, entity.Z); | |
| int cellIndex = grid.CellOffset + entity.Cell; | |
| entity.Next = _cellHeads[cellIndex]; | |
| if (entity.Next != 0) | |
| { | |
| _entities[entity.Next].Prev = index; | |
| } | |
| entity.Prev = 0; | |
| _cellHeads[cellIndex] = index; | |
| if (grid.EntitiesLen == 0) | |
| { | |
| _newCache |= 1u << entity.Grid; | |
| } | |
| grid.EntitiesLen++; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private void RemoveLight(int index) | |
| { | |
| ref HshgEntity3D entity = ref _entities[index]; | |
| ref Grid grid = ref _grids[entity.Grid]; | |
| if (entity.Prev == 0) | |
| { | |
| _cellHeads[grid.CellOffset + entity.Cell] = entity.Next; | |
| } | |
| else | |
| { | |
| _entities[entity.Prev].Next = entity.Next; | |
| } | |
| _entities[entity.Next].Prev = entity.Prev; | |
| grid.EntitiesLen--; | |
| if (grid.EntitiesLen == 0) | |
| { | |
| _newCache ^= 1u << entity.Grid; | |
| } | |
| } | |
| private void UpdateCache() | |
| { | |
| if (_oldCache == _newCache) | |
| { | |
| return; | |
| } | |
| _oldCache = _newCache; | |
| for (int i = 0; i < _grids.Length; i++) | |
| { | |
| _grids[i].Shift = 0; | |
| } | |
| int oldGrid = 0; | |
| while (oldGrid < _grids.Length && _grids[oldGrid].EntitiesLen == 0) | |
| { | |
| oldGrid++; | |
| } | |
| if (oldGrid == _grids.Length) | |
| { | |
| return; | |
| } | |
| byte shift = 1; | |
| for (int newGrid = oldGrid + 1; newGrid < _grids.Length; newGrid++) | |
| { | |
| if (_grids[newGrid].EntitiesLen == 0) | |
| { | |
| shift++; | |
| continue; | |
| } | |
| _grids[oldGrid].Shift = shift; | |
| oldGrid = newGrid; | |
| shift = 1; | |
| } | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private byte GetGrid(float radius) | |
| { | |
| uint rounded = (uint)(radius + radius); | |
| if (rounded < (uint)_cellSize) | |
| { | |
| return 0; | |
| } | |
| int grid = _cellLog - BitOperations.LeadingZeroCount(rounded) + 1; | |
| return (byte)Math.Min(grid, _grids.Length - 1); | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private static int GetCell1D(in Grid grid, float x) | |
| { | |
| int cell = (int)(MathF.Abs(x) * grid.InverseCellSize); | |
| return (cell & grid.CellsSide) != 0 | |
| ? grid.CellsMask - (cell & grid.CellsMask) | |
| : cell & grid.CellsMask; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private static int GetCell(in Grid grid, float x, float y, float z) => | |
| GetCell1D(in grid, x) | (GetCell1D(in grid, y) << grid.Cells2DLog) | (GetCell1D(in grid, z) << grid.Cells3DLog); | |
| private (int Start, int End) MapPosition(float x1Raw, float x2Raw) | |
| { | |
| float x1; | |
| float x2; | |
| if (x1Raw < 0) | |
| { | |
| float shift = ((((int)(-x1Raw * _inverseGridSize)) << 1) + 2) * _gridSize; | |
| x1 = x1Raw + shift; | |
| x2 = x2Raw + shift; | |
| } | |
| else | |
| { | |
| x1 = x1Raw; | |
| x2 = x2Raw; | |
| } | |
| ref Grid grid = ref _grids[0]; | |
| int folds = (int)((x2 - ((int)(x1 * _inverseGridSize)) * _gridSize) * _inverseGridSize); | |
| switch (folds) | |
| { | |
| case 0: | |
| { | |
| int cell = GetCell1D(in grid, x1); | |
| int end = GetCell1D(in grid, x2); | |
| return (Math.Min(cell, end), Math.Max(cell, end)); | |
| } | |
| case 1: | |
| { | |
| int cell = (int)(MathF.Abs(x1) * grid.InverseCellSize); | |
| int end = GetCell1D(in grid, x2); | |
| return (cell & grid.CellsSide) != 0 | |
| ? (0, Math.Max(grid.CellsMask - (cell & grid.CellsMask), end)) | |
| : (Math.Min(cell & grid.CellsMask, end), grid.CellsMask); | |
| } | |
| default: | |
| return (0, grid.CellsMask); | |
| } | |
| } | |
| private static bool IsPowerOfTwo(int value) => value > 0 && (value & (value - 1)) == 0; | |
| private static int MaxGrids(int side) | |
| { | |
| int grids = 0; | |
| do | |
| { | |
| grids++; | |
| side >>= 1; | |
| } | |
| while (side >= 2); | |
| return grids; | |
| } | |
| private static int MaxCells(int side) | |
| { | |
| long cells = 0; | |
| do | |
| { | |
| cells += (long)side * side * side; | |
| side >>= 1; | |
| } | |
| while (side >= 2); | |
| return (int)cells; | |
| } | |
| private struct Grid | |
| { | |
| public int CellOffset; | |
| public int CellsSide; | |
| public int CellsSq; | |
| public int CellsMask; | |
| public int Cells2DLog; | |
| public int Cells3DLog; | |
| public byte Shift; | |
| public float InverseCellSize; | |
| public int EntitiesLen; | |
| } | |
| } | |
| internal struct Ball2D | |
| { | |
| public float Vx; | |
| public float Vy; | |
| } | |
| internal struct Ball3D | |
| { | |
| public float Vx; | |
| public float Vy; | |
| public float Vz; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment