Skip to main content
Time, Dr. Freeman? Is it really that… time again?

Overview

A compositor deals with one or more monitors on mostly fixed refresh cycles. For example, a 170 Hz monitor can draw a frame every ~5.88 ms. Most of the time, the compositor doesn’t actually redraw the monitor: when nothing changes on screen (e.g. you’re reading a document and aren’t moving your cursor), it would be wasteful to wake up the GPU to composite the same image.
During an animation however, screen contents do change every frame. Niri will generally start drawing the next frame as soon as the previous one shows up on screen.

Frame-Perfect Timing

Since the monitor refresh cycle is fixed in most cases (even with VRR, there’s a maximum refresh rate), the compositor can predict when the next frame will show up on the monitor, and render ongoing animations for that exact moment in time.

Benefits of predictive timing

All animation frames are perfectly timed with no jitter, regardless of when exactly the rendering code had a chance to run.For example, even if the compositor has to process new window events, delaying the rendering by a few ms, the animation timing will remain exactly aligned to the monitor refresh cycle.

Design Requirements

There are several properties that a compositor wants from its timing system:
1

Future time queries

It should be possible to get the state of the animations at a specific time in the near future, for rendering a frame exactly timed to when the monitor will show it.
This time override ability should be usable in tests to advance the time in a fully controlled fashion.
2

Action-synchronized animation start

Animations in response to user actions should begin at the moment when the action happens.For example, pressing a workspace switch key should start the animation at the instant when the user pressed the key (rather than, say, slightly in the future where we predicted the next monitor frame, which we had already rendered by now).
3

Consistent time during processing

During the processing of a single action, querying the current time should return the exact same value.Even if the processing finishes a few microseconds after it started, querying the time in the end should return the same thing.
This generally makes writing code much more sane; otherwise you’d need to for example avoid reading the position of some element twice in a row, since it could have moved by one pixel in-between, screwing with the logic.Also, fetching the current system time can be quite expensive in terms of overhead.
4

Animation speed control

It should be reasonably easy to implement an animation slow-down preference, so all animations can be slowed down or sped up by the same factor.

The LazyClock Solution

The solution in niri is a LazyClock, a clock that remembers one timestamp.

How it works

Initially, the timestamp is empty, so when you ask LazyClock for the current time, it will:
  1. Fetch and return the system time
  2. Remember it for future queries

Event Loop Behavior

// Pseudocode of event loop integration
loop {
    // Wait for events
    wait_for_events();
    
    // Process events - LazyClock will fetch time on first query
    process_events();
    
    // Clear the timestamp before next iteration
    lazy_clock.clear();
}
This way, anything that happens next (like a user key press) will fetch and use the most up-to-date timestamp as soon as one is needed, but then the processing code will keep getting the exact same timestamp, since LazyClock stores it.

AdjustableClock Wrapper

Finally, there’s an AdjustableClock wrapper on top that provides the ability to control the slow-down rate by modifying the timestamps returned by the clock.
Important detail: With rate changes, timestamps from the AdjustableClock will drift away and become unrelated to the system time.However, our target timestamp (for rendering) comes from the system time, so the override works directly on the underlying LazyClock.

API Design

Overriding the timestamp and then querying the AdjustableClock will return a different timestamp that is correct and consistent with the adjustments made by AdjustableClock. This is reflected in the API by naming:
  • Clock::set_unadjusted() - Set the raw timestamp
  • Clock::now_unadjusted() - Get the raw timestamp

Sharing Across Animations

Reference-counted clock

The clock is shared among all animations in niri through passing around and storing a reference-counted pointer.Benefits:
  • Overriding the time automatically applies to everything
  • In tests we can use a separate clock per test so that they don’t interfere with each other

Build docs developers (and LLMs) love