public static void Test()
        {
            //const int bodyCount = 8;
            //SimulationSetup.BuildStackOfBodiesOnGround(bodyCount, false, true, out var bodies, out var solver, out var graph, out var bodyHandles, out var constraintHandles);

            SimulationSetup.BuildLattice(
                new RegularGridWithKinematicBaseBuilder(new Vector3(1), new Vector3()),
                new ContactManifoldConstraintBuilder(),
                32, 32, 32, out var simulation, out var bodyHandles, out var constraintHandles);

            SimulationScrambling.ScrambleBodies(simulation);
            SimulationScrambling.ScrambleConstraints(simulation.Solver);
            SimulationScrambling.ScrambleBodyConstraintLists(simulation);
            SimulationScrambling.AddRemoveChurn <Contact4>(simulation, 100000, bodyHandles, constraintHandles);

            var threadDispatcher = new SimpleThreadDispatcher(8);

            //var threadDispatcher = new NotQuiteAThreadDispatcher(8);

            simulation.ConstraintLayoutOptimizer.OptimizationFraction = 0.01f;
            int constraintOptimizationIterations = 8192;

            simulation.ConstraintLayoutOptimizer.Update(simulation.BufferPool, threadDispatcher);//prejit
            var timer = Stopwatch.StartNew();

            for (int i = 0; i < constraintOptimizationIterations; ++i)
            {
                simulation.ConstraintLayoutOptimizer.Update(simulation.BufferPool, threadDispatcher);
            }
            timer.Stop();
            Console.WriteLine($"Finished constraint optimizations, time (ms): {timer.Elapsed.TotalMilliseconds}" +
                              $", per iteration (us): {timer.Elapsed.TotalSeconds * 1e6 / constraintOptimizationIterations}");

            //threadDispatcher.Dispose();
            simulation.BufferPool.Clear();
        }
        public static SimulationTimeSamples Solve <TBodyBuilder, TConstraintBuilder, TConstraint>(TBodyBuilder bodyBuilder, TConstraintBuilder constraintBuilder,
                                                                                                  int width, int height, int length, int frameCount, int threadCount, IThreadDispatcher initializationThreadPool, IThreadDispatcher threadDispatcher)
            where TBodyBuilder : IBodyBuilder where TConstraintBuilder : IConstraintBuilder where TConstraint : IConstraintDescription <TConstraint>
        {
            //const int bodyCount = 8;
            //SimulationSetup.BuildStackOfBodiesOnGround(bodyCount, false, true, out var bodies, out var solver, out var graph, out var bodyHandles, out var constraintHandles);
            GC.Collect(3, GCCollectionMode.Forced, true);
            SimulationSetup.BuildLattice(
                bodyBuilder, constraintBuilder,
                width, height, length, out var simulation, out var bodyHandles, out var constraintHandles);

            SimulationScrambling.ScrambleBodies(simulation);
            SimulationScrambling.ScrambleConstraints(simulation.Solver);
            SimulationScrambling.ScrambleBodyConstraintLists(simulation);
            SimulationScrambling.AddRemoveChurn <TConstraint>(simulation, bodyHandles.Length * 2, bodyHandles, constraintHandles);

            const int batchCompressionIterations = 1000;

            simulation.SolverBatchCompressor.TargetCandidateFraction    = .005f;
            simulation.SolverBatchCompressor.MaximumCompressionFraction = 0.0005f;
            for (int i = 0; i < batchCompressionIterations; ++i)
            {
                simulation.SolverBatchCompressor.Compress(simulation.BufferPool, initializationThreadPool);
            }

            //Attempt cache optimization.
            int bodyOptimizationIterations = bodyHandles.Length / 4;

            simulation.BodyLayoutOptimizer.OptimizationFraction = 0.005f;
            for (int i = 0; i < bodyOptimizationIterations; ++i)
            {
                simulation.BodyLayoutOptimizer.IncrementalOptimize(simulation.BufferPool, initializationThreadPool);
            }

            simulation.ConstraintLayoutOptimizer.OptimizationFraction = 0.044f;
            int constraintOptimizationIterations = 1024;

            for (int i = 0; i < constraintOptimizationIterations; ++i)
            {
                simulation.ConstraintLayoutOptimizer.Update(simulation.BufferPool, initializationThreadPool);
            }

            var simulationTimeSamples = new SimulationTimeSamples(frameCount);

            const float dt             = 1 / 60f;
            const int   iterationCount = 8;

            simulation.Solver.IterationCount = iterationCount;

            for (int frameIndex = 0; frameIndex < frameCount; ++frameIndex)
            {
                CacheBlaster.Blast();
                simulation.Timestep(dt, threadDispatcher);
                simulationTimeSamples.RecordFrame(simulation);
            }

            simulation.Dispose();
            simulation.BufferPool.Clear();


            return(simulationTimeSamples);
        }