1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
# Simulation loop module
Simulation loop for games and graphics applications.
## Features
- Client retains control flow.
- Client-controlled time axis.
- Updates are frame-rate capped and use a fixed time step for determinism.
- Rendering is optionally frame-rate capped.
- Interpolation factor for smooth animation and rendering between frames.
Control flow: the client steps the loop and then checks whether the simulation
must be updated and/or the result rendered. Time readings are external to the
library and provided by the client.
## Invariants
- An initial render of the initial application state is always triggered.
- The same frame is not re-rendered if time does not advance.
- Animation interpolation factor in [0,1].
## Handling Time Spikes
Generally, the simulation's update logic should be able to keep up with the
requested frame rate; it is the application's responsibility to ensure this.
Specifically, the frequency with which the application loops must be higher
than the requested update frequency, given by the update delta time.
However, occasional time spikes may occur, for example when switching to the
desktop or when pausing the application in a debugger. The library handles this
simply by requesting an update from the application. Under the assumption that
the loop frequency is higher than the update frequency, the simulation will
catch up with the real-time clock.
### Time Spikes in Detail
When a time spike occurs, the simulation clock falls significantly behind the
real-time clock. Ideally, the simulation should be able to recover and catch up
to the real-time clock when this occurs.
Under a variable time delta, the loop could simply update the simulation with
a large delta that puts the simulation back into the current clock time.
Under a fixed time delta, this isn't possible, and we seem to have the
following choices instead:
- a) Queue as many updates as necessary to bring the simulation back to the
current clock time (time_difference / fixed_delta).
- b) Queue a single update.
- c) Some middle ground between the two.
The issue with (a) is that, if the simulation is never able to catch up, then
the number of requested updates at every loop iteration diverges and the
simulation eventually appears to freeze.
(b) only works if:
- clock time added per iter < desired update delta time
Where:
- clock time added per iter = update time + render time + vsync + etc
- desired delta time = 1 / update frequency
If the clock time added per iteration is greater or equal to the desired delta,
then the simulation can never "catch up" and recover from the spike.
The middle ground is to perform only some number of updates in each loop
iteration N. The simulation catches up only if:
- clock time added per iter < N * desired update delta time
The ideal value of N depends on how many frames the application can actually
render. For example, if the application is vsync'ed to a 240hz monitor and is
able to render that many frames, then:
- N = ceil(1/240hz / desired update delta time)
Realistically, the actual frame rate will be variable. Moreover, if we queued
as many frames as possible, then we would risk the freeze in option (a) if the
actual update time were too large for the application to catch up. So the
library can only guess the value of N.
The library picks a small constant value of N, implementation-defined, that the
application can override.
### Example: Spike Handling with Option (A)
- desired delta = 10ms (100 fps)
- actual delta = 20ms ( 50 fps)
| iter | sim time | clock time | comment |
|------|----------|------------|-------------------------------|
| 0 | 0 | 0 | initial state |
| 1 | 0 | 10 | queue 1 update |
| 2 | 10 | 30 | queue (30-10)/10 = 2 updates |
| 3 | 30 | 70 | queue (70-30)/10 = 4 updates |
| 4 | 70 | 150 | queue (150-70)/10 = 8 updates |
| ... |
|