2.9.16
Coherent GT
A modern user interface library for games
Time control

Coherent GT allows clients to control the time in the UISystem. Currently time control can only be applied across all views and not individual ones.

Enabling the feature has 3 steps:

The feature requires the client to return a monotonically increasing time point in seconds (could be fractional) whenever that's needed. Time cannot go back, it can only be slowed down or sped up. In case the client provides a time point in the past, the SDK will disregard it and use the last known time.

User-controlled time affects almost all subsystems with a few exceptions, such as timestamps for:

  • Cache expiration
  • Input event creation
  • GT Debugger marks

Most other subsystems are affected. For example, JavaScript garbage collection is affected, too. Care must be taken especially in the case where manual GC is used as the sweep duration is affected by the user time scale.

Another example is video playback. The rendered picture can be sped up or slowed down using the time control and that happens automatically. The audio playback rate must be adjusted in the client code, though, as Coherent GT provides the raw PCM bytes and is oblivious of the playback engine.

Time clamping module

We have added a module for clients that want to allow user-generated UI content to be ran inside their projects, which helps mitigate the effects of Spectre and Meltdown vulnerabilities. It works by reducing the precision of timers and adding jitter to them. It is located in Modules\TimeClamper\ThirdParty\chromium\TimeClamper.h. Use the TimeClamper::ClampTimeResolution method to modify the time you provide to Coherent::UIGT::UISystemListener::GetMonotonicallyIncreasingTime

Animations and VSync / Locked framerate

There are some specific details for different animation methods.

CSS Animations

CSS animations are basically unaffected by the framerate and will work as expected regardless of the application FPS.

JavaScript's setInterval/setTimeout

The two functions are very similar so we will only explore setInterval, which has a slightly more complex behaviour. Internally, setInterval works roughly as follows:

  • Compute the next timepoint at which the JS callback should be fired. The computation is Coherent::UIGT::UISystemListener::GetMonotonicallyIncreasingTime + interval.
  • Add the computed timepoint in a heap.
  • The next time Coherent::UIGT::UISystem::Advance is called, the timer heap is searched for timers which should fire before the current time. All timers that should fire have their callbacks executed. Consider the timer fired now.
  • Assuming clearInterval hasn't been called, start again with the first step for computing the next fire interval and repeat.

This leads to somewhat correct behaviour when slowing down time, but speeding it up has no effect if the application has its framerate locked.

Here's an example to illustrate this better:

The setup is an app locked at 60 FPS and a function that should be called every 20ms using setInterval. Time is slowed down to half speed. Here's the timeline for this case:

  • 0ms app (0ms real): next fire time is at 20ms app
  • 8ms app (16ms real): before fire time, do nothing
  • 16ms app (32ms real): before fire time, do nothing
  • 24ms app (48ms real): the 20ms mark is passed so fire the callback. Next fire time is 24+20 = 44ms app.
  • 32ms app (64ms real): before fire time, do nothing
  • 40ms app (80ms real): before fire time, do nothing
  • 48ms app (96ms real): the 44ms mark is passed so fire the callback. Next fire time is 48+20 = 68ms app.
  • 56ms app (112ms real): before fire time, do nothing
  • 64ms app (128ms real): before fire time, do nothing
  • 72ms app (144ms real): the 68ms mark is passed so fire the callback. Next fire time is 72+20 = 92ms app. ...

For the sample period of 128ms the callback was fired 3 times instead of the 7 that would have happened if using a 1x time scale. The half speed time scale produced roughly half of the expected ticks. The fire time differences have a slightly different pattern depending on the time scale, but in general when the fire interval is larger than the app delta time the result will be as expected.

Here's the case where time is sped up 10 times:

  • 0ms app (0ms real): next fire time is at 20ms app
  • 160ms app (16ms real): the 20ms mark is passed so fire the callback. Next fire time is 160+20 = 180ms app.
  • 320ms app (32ms real): the 180ms mark is passed so fire the callback.Next fire time is 320+20 = 340ms app.
  • 480ms app (48ms real): the 340ms mark is passed so fire the callback.Next fire time is 480+20 = 500ms app.
  • 640ms app (64ms real): the 500ms mark is passed so fire the callback.Next fire time is 640+20 = 660ms app. ...

The timer ticked only 4 times in a period where you'd expect it to tick 640/20 = 32 times. That's because the timer cannot tick faster than the app framerate.

Lower frame deltas have a positive effect on the execution and expected results, but that is not always possible/desirable. Extra care must be taken when using setInterval considering the notes above.

JavaScript's requestAnimationFrame

requestAnimationFrame is invoked every frame (unless Coherent::UIGT::ViewInfo::AnimationFrameDefer defers it). It isn't directly affected by the time control in the sense that if you have a fixed animation (e.g. moving a <div> 10 pixels to the right) it will always be executed every frame regardless of the monotonically increasing time passed. The requestAnimationFrame callback provides a timepoint as its first argument, which could be used for delta-time animation - e.g. move a <div> 10*dt pixels to the right. You only need to save the previous timepoint to be able to compute the difference.

Date extensions

A new function that provides the "application time" is introduced. You can get that time using new Date().appTime(), which corresponds to the new Date() call. It returns a Date object constructed from the number of milliseconds since the epoch in application time, not 'real' time.

The raw number of ticks from the epoch corrected by the application time is available via __coherentTicks(). Sometimes it might be useful to have Date() return the application time as well - for instance if you do not want to change already existing JavaScript code but you still want to be able to control time. This can be trivially achieved: just pass Date = Date.prototype.appTime as JavaScript code you want to be executed as soon as a view is created. This is done via the Coherent::UIGT::UISystem::CreateView overload which has a const char* scriptToExecute parameter. The string passed in that parameter is JavaScript code that will be executed when the view is created. Now even the default 'Date' object will work only in application time.