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