procedure Bas_LoopEnter ();
var
StepsUpdate: TBas_SInt32 = 0;
StepsRender: TBas_SInt32 = 0;
StepsRefresh: TBas_SInt32 = 0;
CurrPaceUpdate: TBas_SInt32;
CurrPaceRender: TBas_SInt32;
CurrPaceRefresh: TBas_SInt32;
{$IFDEF BAS_FEATURE_FAST_FORWARD}
CurrPaceForward: TBas_SInt32;
{$ENDIF}
TimeCurr: TBas_UInt64;
TimePrev: TBas_UInt64;
TimePrevEvent: TBas_UInt64;
TimePrevClock: TBas_UInt64;
ToDoSteps: TBas_SInt32;
ToDoStepsMul: TBas_SInt32;
ToDoInterval: TBas_UInt64;
ToDoDrain: TBas_Bool8;
AllowRender: TBas_Bool8 = False;
begin
// Initialize frame update logic and rendering performance meters.
Bas_MeterInitialize(@MeterUpdate, BAS_METER_SAMPLES_LOAD_DEFAULT, 256);
Bas_MeterInitialize(@MeterRender, BAS_METER_SAMPLES_LOAD_DEFAULT, 256);
// Set all timestamps so that the first iteration will perform the step right away.
TimeCurr := SDL_GetTicksNS();
// Watch out for possible integer underflows. If enough time has passed since SDL initialization, calculate the timestamps
// for the step and clock. Otherwise, set the minimum timestamp and wait to perform the step until some time has passed.
if TimeCurr >= BAS_LOOP_INTERVAL then TimePrev := TimeCurr - BAS_LOOP_INTERVAL else TimePrev := 0;
if TimeCurr >= BAS_LOOP_INTERVAL then TimePrevEvent := TimeCurr - BAS_LOOP_INTERVAL else TimePrevEvent := 0;
if TimeCurr >= 1000000000 then TimePrevClock := TimeCurr - 1000000000 else TimePrevClock := 0;
// Start the main game loop.
repeat
// Copy the currently set activity paces to auxiliary variables and use them in a full iteration of the main loop. The
// game logic may change the pace of some activity in the meantime and then the loop would behave incorrectly or crash.
CurrPaceUpdate := PaceUpdate;
CurrPaceRender := PaceRender;
CurrPaceRefresh := PaceRefresh;
{$IFDEF BAS_FEATURE_FAST_FORWARD}
CurrPaceForward := PaceForward;
{$ENDIF}
// Get the current time to calculate how many steps need to be taken.
TimeCurr := SDL_GetTicksNS();
// Calculate how much time has elapsed since the previous iteration and how many iterations the main loop is behind. Based
// on the number of outstanding iterations, determine whether the event queue should be drained. The only situation in
// which part of the events must remain in the queue is when there has been a lag significant enough that the number of
// logic updates in the current iteration had to be limited.
ToDoSteps := (TimeCurr - TimePrev) div BAS_LOOP_INTERVAL;
ToDoDrain := (ToDoSteps <= BAS_LOOP_STEPS_BATCH) or (ToDoSteps >= BAS_LOOP_STEPS_CATCH);
{$IFDEF BAS_FEATURE_FAST_FORWARD}
// If the fast-forwarding is in progress, set the step multiplier to the current pace of this feature. It will be used to
// multiply the number of logic steps so that this iteration of the loop performs that many more logic updates.
if FastForward then
ToDoStepsMul := CurrPaceForward
else
{$ENDIF}
// Fast-forwarding is unavailable or disabled, so stick with the original number of logic steps.
ToDoStepsMul := 1;
// If there was a significant lag, either due to a massive CPU load or because the simulation has been stopped for
// debugging (or for any other reason), instead of running a batch of updates, catch up to real time immediately.
if ToDoSteps >= BAS_LOOP_STEPS_CATCH then
begin
// Set the step counters so that all activities are performed in this iteration of the main loop. The number of steps
// for the logic update must account for the calculated multiplier in order to accommodate optional fast-forwarding.
StepsUpdate := CurrPaceUpdate * ToDoStepsMul;
StepsRender := CurrPaceRender;
StepsRefresh := CurrPaceRefresh;
// Set the timestamp of the previous iteration to the real time.
TimePrev := TimeCurr;
end
else
begin
// Check if the lag is significant enough that, in this iteration of the main loop, the limited batch of logic updates
// must be performed, while the rest must be left for the next iteration. Since we aren't trying to catch up to real
// time right away, the game will still render at least a few frames per second, even if it's lagging massively.
if ToDoSteps > BAS_LOOP_STEPS_BATCH then
ToDoSteps := BAS_LOOP_STEPS_BATCH;
// Add the calculated number of outstanding iterations to the step counters for each activity. The number of steps for
// the logic update must account for the calculated multiplier in order to accommodate optional fast-forwarding.
StepsUpdate += ToDoSteps * ToDoStepsMul;
StepsRender += ToDoSteps;
StepsRefresh += ToDoSteps;
// Add the sum of the intervals for the outstanding iterations to the timestamp of the previous iteration.
TimePrev += ToDoSteps * BAS_LOOP_INTERVAL;
end;
// Calculate the simulation tempo based on the current settings of the logic update pace. Normally, the tempo is "1.0",
// but if the simulation has been slowed down (for debugging purposes), the tempo will be less than that.
TempoUpdate := BAS_LOOP_TICKRATE / CurrPaceUpdate / BAS_LOOP_TICKRATE;
{$IFDEF BAS_FEATURE_FAST_FORWARD}
// If the fast-forwarding is in progress, increase the tempo according to the current pace settings of this feature. By
// default the simulation tempo is "1.0" and the fast-forwarding pace is "3", so the result will be "3.0".
if FastForward then
TempoUpdate *= CurrPaceForward;
{$ENDIF}
// Now let's check whether the game logic needs to be updated in this iteration of the loop. The only case in which the
// logic isn't updated in some iterations of the main loop is when the simulation was slowed down (due to debugging).
// Here, we're just checking if the logic must be updated, no matter how many times.
if StepsUpdate >= CurrPaceUpdate then
begin
// Due to lag or fast-forwarding, the logic in this iteration of the main loop may need to be updated multiple times.
// So calculate the time window so that each logic update processes the corresponding batch of SDL events.
ToDoInterval := (TimePrev - TimePrevEvent) div (StepsUpdate div CurrPaceUpdate);
// Normally, one iteration of the main loop corresponds to one logic update, but in the case of short lags or active
// fast-forwarding, the logic may need to be updated multiple times.
repeat
// Start measuring the time of events processing and updating the game logic.
Bas_MeterAddLoadEnter(@MeterUpdate);
begin
// If this is the last logic update in this iteration of the main loop and the SDL event queue must be drained,
// process all events in the queue — this is a typical scenario where there are no lags or they are very short.
if (StepsUpdate div CurrPaceUpdate = 1) and ToDoDrain then
begin
// Updating the logic itself took some time, and the real time could drift so that we once again have at least
// one overdue logic update. We can drain the event queue only if there are no overdue updates.
if SDL_GetTicksNS() - (TimePrevEvent + ToDoInterval) < ToDoInterval then
Bas_EventsProcess(BAS_EVENT_TIMESTAMP_MAX)
else
// All the logic updates in this iteration of the main loop took so long that it caused an additional lag of at
// least one frame, so process only the batch of events from the time window corresponding to the last update.
Bas_EventsProcess(TimePrevEvent + ToDoInterval);
end
else
// This is not the last update in this iteration of the main loop, or it is the last one in a batch of updates
// during a massive lag, so process only the events from the time window corresponding to this update.
Bas_EventsProcess(TimePrevEvent + ToDoInterval);
// Events are processed, so now perform a single step of updating game logic.
Bas_LoopUpdate();
end;
Bas_MeterAddLoadLeave(@MeterUpdate);
Bas_MeterAddRateEvent(@MeterUpdate);
// Move the timestamp of the last event update by the size of the calculated time window.
TimePrevEvent += ToDoInterval;
// Reduce the number of remaining logic updates to perform, based on the current pace settings.
StepsUpdate -= CurrPaceUpdate;
until StepsUpdate < CurrPaceUpdate;
// Time window calculations are based on integer division, which means that if several updates have been performed, the
// event timestamp may be smaller than the iteration timestamp. The more iterations that perform several updates, the
// more these timestamps will diverge. So, align both timestamps to avoid accumulating rounding errors from division.
TimePrevEvent := TimePrev;
// As the game logic is being updated, set the flag allowing the frame to be rendered at the next opportunity. This flag
// must be set until a new frame is rendered, even for several iterations of the main loop, in case the rendering pace
// is lower than the pace of updating the game logic or the frequency of logic updates has been slowed down (debugging).
AllowRender := True;
end;
// Check whether in this iteration of the main loop, a new game frame must be rendered. If the render pace matches the
// loop tickrate, a new frame will be rendered in each loop iteration. If the logic update pace is lower than the render
// pace, frames will be rendered only in those iterations of the main loop in which the game logic has been updated.
if (StepsRender >= CurrPaceRender) and AllowRender then
begin
// Render a complete game frame onto the window swapchain texture and measure how long it took.
Bas_MeterAddLoadEnter(@MeterRender);
Bas_LoopRender();
Bas_MeterAddLoadLeave(@MeterRender);
// Display the image on the screen. This function must be called after the frame has been rendered and after the
// rendering measurement has finished, so that in the case of enabled VSync, the measurement does not include waiting
// time of the GPU buffer flip.
SDL_RenderPresent(Renderer.Handle);
// Add a new event to the meter and finish measuring rendering performance.
Bas_MeterAddRateEvent(@MeterRender);
// The game frame is rendered only for the most recent game state (only once in a given iteration of the main loop), so
// catch up to real time, leaving only excess steps in the counter.
StepsRender := StepsRender mod CurrPaceRender;
// As a new frame has been rendered, reset the flag and wait for the next logic update.
AllowRender := False;
end;
// Check if in this iteration of the main loop, the performance counters must be refreshed and if so, do it.
if StepsRefresh >= CurrPaceRefresh then
begin
// Recalculate performance results based on collected samples.
Bas_MeterRefreshLoad(@MeterUpdate);
Bas_MeterRefreshRate(@MeterUpdate);
Bas_MeterRefreshLoad(@MeterRender);
Bas_MeterRefreshRate(@MeterRender);
// Update the status of system performance counters.
Bas_CountersUpdate();
// Catch up to real time, leaving only excess steps in the counter.
StepsRefresh := StepsRefresh mod CurrPaceRefresh;
end;
// Get the real time and update the clock — as last, because that's the least important thing to do.
TimeCurr := SDL_GetTicksNS();
// If a full second has passed since the previous update, synchronize the clock now. The clock is designed solely for the
// purpose of rendering the local system time in the game window, so updating it once per second is perfectly sufficient.
if TimeCurr - TimePrevClock >= 1000000000 then
begin
Bas_ClockUpdate();
// Catch up with real time, but don't overtake it.
while TimePrevClock + 1000000000 < TimeCurr do
TimePrevClock += 1000000000;
end;
// Finally, check if there is still time left until the next iteration. If we have a lag and the next loop iteration is
// to be executed without any delay, skip thread sleep. In this case the freeze time will be negative, and since we are
// dealing with unsigned integers, the test must be protected against possible integer underflow.
TimeCurr := SDL_GetTicksNS();
if TimeCurr < TimePrev + BAS_LOOP_INTERVAL then
SDL_DelayPrecise(TimePrev + BAS_LOOP_INTERVAL - TimeCurr);
// Break the main loop only if the next game stage is the sentinel stage. This stage can be set due to a direct attempt to
// close the window or using a special option in the game's main menu UI, only inside the stage updater callback.
until Bas_StageGetNext() = BAS_STAGE_SENTINEL;
// Finalize update and rendering performance meter objects.
Bas_MeterFinalize(@MeterUpdate);
Bas_MeterFinalize(@MeterRender);
end;