The Clocks Thread (Time Keeps On Ticking)

(Kel) #1

Since we’ve previously had a Discord discussion about the design of clocks in Amethyst, and currently a relevant GH issue is going stale with no action, I figured I’d make a central topic for discussing the management of Father Time in Amethyst. There’s no single aim for this thread I have in mind, except to start with talking about previous discussion, our current design, and at least some possible improvements, and thereafter we could also discuss usecases for Father Time’s Amethyst API.

Current Implementation

Currently, Amethyst handles the passage of time with the (wait for it…) Time resource, which (via simplification) only tracks the following:

  • Frames/time accumulated since application start
    • frame_number
    • absolute_time (scaled by time_scale)
    • absolute_real_time
  • Delta time between frames
    • delta_time (scaled by time_scale)
    • delta_real_time
  • Universal frame delta scaling: time_scale
  • Rate/delta of the universal fixed update: fixed_time
  • Remainder accumulator for fixed update: fixed_time_accumulator

Additionally, there is the FrameLimiter resource, which contains a frame limiting strategy, the interval to limit the framerate to, and a timestamp to track when the last frame ended and the current one began.

The way this is all managed is with the following generalized pseudocode for what happens in the CoreApplication::run loop.

Start frame_timer

Handle State Transitions
Run CallbackQueue
Drain all events into active State's event handler 

Increment Time::fixed_time_accumulator with Time::delta_time

While Time::fixed_time_accumulator >= Time::fixed_time
  Decrement Time::fixed_time_accumulator with Time::fixed_time
  Call active state's fixed_update method

Call active state's update method (which also usually calls the main dispatcher)

Maintain World

Unless the Unlimited FrameLimiter strategy is chosen, 
  Wait with the FrameLimiter for (hopefully around) the duration of... 
    FrameLimiter::frame_duration - (Instant::now - FrameLimiter::last_call)

Update the FrameLimiter::last_call timestamp to Instant::now

Increment Time::frame_number
Update Time::delta_time with result of frame_timer

Current Issues

Drawbacks with this implementation include:

  1. fixed_update cannot be parallelized as normal alongside the other, normal updates, except in the case of using a worker thread for calculation outside the ECS, and synchronizing the results briefly within the fixed_update.
  2. As mentioned in @AndreaCatania’s Github issue above, there is no failsafe against a spiral of death in certain conditions where frame deadlines are missed causing the number of required fixed update cycles to escalate every frame as the accumulator chases its tail.
  3. There’s only a single, simple, regular timer available for dispatch (the one fixed timer).

The first issue may depend on how a certain elephant’s cards fall. There are possibilities here, including the usage of Batch Systems, but I’ll leave it to subsequent replies to explore these possibilities, although I recommend each possibility (no matter the issue) is accompanied with a use-case and an informed prediction for the commonality of that use-case.

Resolving the second issue is completely dependent on how the application handles its fixed step code, although I could argue we should at least provide a sensible default which may be switched off in place of more complete and solution-specific strategies @jaynus had mentioned such as invalidating & resyncing (“rubber banding”).

The third issue is brought up by @kabergstrom, who brought up the post “Determinism in League of Legends: Unified Clock” by… the people who make League of Legends. The post mentions the issues of dealing with a multiplicity of clocks, each being initialized at different times, some clocks being implemented with floating point numbers, and many clocks relying on completely isolated individual synchronization implementations. While thankfully in Rust we have great integer-based time primitives (Duration & Instant [1][2]), it’s up to Amethyst to provide a nice platform for clock management, something that could possibly also integrate into the networking systems for both a common-case implementation and a structure to hoist special-cases.

In Summation

This post is getting quite long, and I’ve hit the character limit for posts on here before (ahem Legion thread). So I’m going to cut this off here for now. If you come up with timing API use-cases, issues with the current one, or solutions to either of the previous two items, please post them below. Also, if anybody wants to know the call order of State methods now you know where to direct them!

(Andrea Catania) #2

This is a really important discussion, which I could also attach another problem that is quite related but let’s first try to fix these three issues without flood this topic.


Have a fixed delta time is mostly useful and required for the physics stability, all the other cases that come to my mind are really tiny corner cases.

Considering the above statement, the fixed delta time is related to the physics engine.

Batch System

This consideration removes the need to have a fixed_update, coded directly in Amethyst, resolving the three problem in a shot.

Once the physics is finally integrated in amethyst there will be a mechanism to step the physics and all its related systems with a fixed delta time.

The actual amethyst_physics use a Batch System (more here) which is a dispatcher. This System takes the Amethyst dynamic Delta Time and dispatch the physics and its systems N times (Check it here).

This mechanism doesn’t suffer of spiral of death and fully support parallelism.

Legion or Shred, the fixed delta time processing must be supported by the dispatcher itself, like it is done with the Batch System, because in this way we can take 100% advantages of parallelism keeping the ECS design pattern.

(Kae) #3

A perhaps strange idea: what if there is no built-in fixed_update, but you could register dispatching that happen at ticks of clocks? Clocks can be hierarchical (with modifiers relative to parent), non-linear, and with fixed or variable length time steps. What is commonly called “fixed update” is then a child clock to the “global” real-time render clock (which is driven by sampling real elapsed time at each display sync) with a fixed-length time step.

This would also tie in well with dispatching simulation updates from a clock that is defined by an offset to the global render clock, where the offset is controlled by a networked state replication layer to facilitate “deterministic” simulation updates.

1 Like
(Andrea Catania) #4

The idea of the batch dispatcher is exactly this

(Kae) #5

Batch dispatcher seems to deal with the specific case of dispatching N times, which does not really cover all of what I mentioned

Mechanism to sub-dispatch group of systems
(Andrea Catania) #6

Yep, correct; basically the Batch is able to dispatch N times but the user is able to define the logic that determines the value of N.

So you can define your batch controller and dispatch the systems based on a customized timer (that can be delayed, slowdown, per event, hierarchical, dilated, fixed, pausable) or based on the gameplay events, or based on anything basically.

So we don’t really need to provide “special use cases” directly on the clock management, because the user is already able to control it.

By the way, it’s also possible to nest batch systems one under the other.

(Kae) #7

An issue that arises that makes it relatively complex is that, if the physics clock should tick twice in a frame, and gameplay sim or something else ticks on the same frame, you may need to tick the gameplay sim in-between the physics ticks. It’s not as simple as finding out how many times you should tick and then dispatch that many times without controlling for dependencies.

This complexity I think warrants a built-in system for solving the problem.;

(Andrea Catania) #8

You can have multiple batch systems, and you can nest them as you like, and you can decide how to tick these (So it’s not necessary add a batch under the physics batch, but you can have your own timer). A batch is a system with a bunch of dependencies that are derived from its child; check the system dependencies is already a solved problem in shred.

Not sure that I understand the problem that you are arising, but if you think to have better solutions, please go ahead I’ll support you.