public void PhysicsSimulateCycle(float dTime)
        {
            // maximum number of static counts
            var staticCnts = PhysicsConstants.StaticCnts;

            // it's okay to have this code outside of the inner loop, as the
            // ball hitrects already include the maximum distance they can
            // travel in that timespan
            _hitOcTreeDynamic.Update();

            while (dTime > 0)
            {
                var hitTime = dTime;

                // find earliest time where a flipper collides with its stop
                foreach (var flipperMover in _flipperMovers)
                {
                    var flipperHitTime = flipperMover.GetHitTime();
                    if (flipperHitTime > 0 && flipperHitTime < hitTime)
                    {
                        //!! >= 0.F causes infinite loop
                        hitTime = flipperHitTime;
                    }
                }

                RecordContacts = true;
                Contacts.Clear();

                foreach (var ball in Balls)
                {
                    var ballHit = ball.Hit;

                    // don't play with frozen balls
                    if (!ball.State.IsFrozen)
                    {
                        // search upto current hit time
                        ballHit.Coll.HitTime = hitTime;
                        ballHit.Coll.Clear();

                        // always check for playfield and top glass
                        if (!_table.HasMeshAsPlayfield)
                        {
                            _hitPlayfield.DoHitTest(ball, ball.Coll, this);
                        }

                        _hitTopGlass.DoHitTest(ball, ball.Coll, this);

                        // swap order of dynamic and static obj checks randomly
                        if (MathF.Random() < 0.5)
                        {
                            _hitOcTreeDynamic.HitTestBall(ball, ball.Coll, this);                             // dynamic objects
                            _hitOcTree.HitTestBall(ball, ball.Coll, this);                                    // find the hit objects and hit times
                        }
                        else
                        {
                            _hitOcTree.HitTestBall(ball, ball.Coll, this);                                    // find the hit objects and hit times
                            _hitOcTreeDynamic.HitTestBall(ball, ball.Coll, this);                             // dynamic objects
                        }

                        // this ball's hit time
                        var htz = ball.Coll.HitTime;

                        if (htz < 0)
                        {
                            // no negative time allowed
                            ball.Coll.Clear();
                        }

                        if (ball.Coll.HasHit)
                        {
                            // smaller hit time?
                            if (htz <= hitTime)
                            {
                                // record actual event time
                                hitTime = htz;

                                // less than static time interval
                                if (htz < PhysicsConstants.StaticTime)
                                {
                                    if (--staticCnts < 0)
                                    {
                                        staticCnts = 0;                                         // keep from wrapping
                                        hitTime    = PhysicsConstants.StaticTime;
                                    }
                                }
                            }
                        }
                    }
                }                 // end loop over all balls

                RecordContacts = false;

                // hit time now set... or full frame if no hit
                // now update displacements to collide-contact or end of physics frame

                if (hitTime > PhysicsConstants.StaticTime)
                {
                    // allow more zeros next round
                    staticCnts = PhysicsConstants.StaticCnts;
                }

                foreach (var mover in _movers)
                {
                    // step 2: move the objects about according to velocities
                    // (spinner, gate, flipper, plunger, ball)
                    mover.UpdateDisplacements(hitTime);
                }

                // find balls that need to be collided and scripted (generally
                // there will be one, but more are possible)
                for (var i = 0; i < Balls.Count; i++)
                {
                    var ball      = Balls[i];
                    var hitObject = ball.Coll.Obj;                     // object that ball hit in trials

                    // find balls with hit objects and minimum time
                    if (hitObject != null && ball.Coll.HitTime <= hitTime)
                    {
                        // now collision, contact and script reactions on active ball (object)
                        ActiveBall = ball;                                            // For script that wants the ball doing the collision
                        hitObject.Collide(ball.Coll, this);                           // collision on active ball
                        ball.Coll.Clear();                                            // remove trial hit object pointer

                        // Collide may have changed the velocity of the ball,
                        // and therefore the bounding box for the next hit cycle
                        if (Balls[i] != ball)                                                // Ball still exists? may have been deleted from list

                        // collision script deleted the ball, back up one count
                        {
                            --i;
                        }
                        else
                        {
                            ball.Hit.CalcHitBBox();                             // do new boundings
                        }
                    }
                }

                // Now handle contacts.
                //
                // At this point UpdateDisplacements() was already called, so the state is different
                // from that at HitTest(). However, contacts have zero relative velocity, so
                // hopefully nothing catastrophic has happened in the meanwhile.
                //
                // Maybe a two-phase setup where we first process only contacts, then only collisions
                // could also work.
                if (MathF.Random() < 0.5)                    // swap order of contact handling randomly
                {
                    foreach (var ce in Contacts)
                    {
                        ce.Obj.Contact(ce, hitTime, this);
                    }
                }
                else
                {
                    for (var i = Contacts.Count - 1; i != -1; --i)
                    {
                        Contacts[i].Obj.Contact(Contacts[i], hitTime, this);
                    }
                }

                Contacts.Clear();

                // TODO C_BALL_SPIN_HACK

                // new delta .. i.e. time remaining
                dTime -= hitTime;

                // swap order of ball-ball collisions
                SwapBallCollisionHandling = !SwapBallCollisionHandling;
            }
        }