Skip to content

Frame timings and non-monotonic clocks #33

@dmit

Description

@dmit

I was looking at using this library in my project that uses Zig 0.16 (nightly), so I set out to see how much effort it would take to update a local fork to the new Zig 0.16 std.Io interface. While that work is ongoing, I might have discovered an issue with the frame timing code in src/core/program.zig:

.start_time = std.time.nanoTimestamp(),
.last_frame_time = std.time.nanoTimestamp(),

zigzag/src/core/program.zig

Lines 190 to 210 in b2610e0

/// Execute a single frame: poll input, process events, render.
pub fn tick(self: *Self) !void {
const now = std.time.nanoTimestamp();
const delta = @as(u64, @intCast(now - self.last_frame_time));
// Enforce framerate limit
const min_frame_time_ns: u64 = if (self.options.fps > 0)
@divFloor(std.time.ns_per_s, self.options.fps)
else
16_666_666; // ~60fps default
if (delta < min_frame_time_ns) {
std.Thread.sleep(min_frame_time_ns - delta);
}
self.last_frame_time = std.time.nanoTimestamp();
const actual_delta = @as(u64, @intCast(self.last_frame_time - now + @as(i128, @intCast(delta))));
self.context.delta = actual_delta;
self.context.elapsed = @intCast(self.last_frame_time - self.start_time);
self.context.frame += 1;

In 0.15 std.time.nanoTimestamp uses the system's real clock, so it's not monotonic (may return an earlier timestamp than before after a daylight savings clock jump, or after an NTP adjustment), and will have large forward jumps in case of sleep/resume. The latter is not much of a problem (aside from tanking the reported frame rate?), but the former might cause an @intCast() from a negative signed integer value to an unsigned integer, which is Illegal Behavior.

It appears that this code should instead be using std.time.Timer in 0.15, and in the future std.Io.Clock.awake in 0.16.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions