Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/showcase.zig
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ const Model = struct {
\\const std = @import("std");
\\
\\pub fn main() !void {
\\ const stdout = std.io.getStdOut().writer();
\\ const stdout = std.Io.getStdOut().writer();
\\ try stdout.print("Hello, {s}!\n", .{"world"});
\\}
\\
Expand Down
1 change: 1 addition & 0 deletions src/core/message.zig
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub const WindowSize = struct {

/// Tick message for timer-based updates
pub const Tick = struct {
/// Monotonic timestamp in nanoseconds since program start.
timestamp: i64,
delta: u64, // nanoseconds since last tick
};
Expand Down
95 changes: 81 additions & 14 deletions src/core/program.zig
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ pub fn Program(comptime Model: type) type {
context: Context,
options: Options,
running: bool,
start_time: i128,
last_frame_time: i128,
clock: std.time.Timer,
start_time: u64,
last_frame_time: u64,
pending_tick: ?u64,
every_interval: ?u64,
last_every_tick: u64,
Expand All @@ -67,6 +68,8 @@ pub fn Program(comptime Model: type) type {
/// Initialize with custom options
pub fn initWithOptions(allocator: std.mem.Allocator, options: Options) !Self {
const arena = std.heap.ArenaAllocator.init(allocator);
var clock = try std.time.Timer.start();
const now = clock.read();
const self = Self{
.allocator = allocator,
.arena = arena,
Expand All @@ -77,8 +80,9 @@ pub fn Program(comptime Model: type) type {
.context = Context.init(allocator, allocator),
.options = options,
.running = false,
.start_time = std.time.nanoTimestamp(),
.last_frame_time = std.time.nanoTimestamp(),
.clock = clock,
.start_time = now,
.last_frame_time = now,
.pending_tick = null,
.every_interval = null,
.last_every_tick = 0,
Expand Down Expand Up @@ -173,6 +177,13 @@ pub fn Program(comptime Model: type) type {
self.context.kitty_text_sizing = width_caps.kitty_text_sizing;
unicode.setWidthStrategy(effective_width_strategy);

self.clock.reset();
self.start_time = self.clock.read();
self.last_frame_time = self.start_time;
self.context.elapsed = 0;
self.context.delta = 0;
self.context.frame = 0;

self.resetFrameAllocator();

// Initialize the model
Expand All @@ -189,8 +200,8 @@ pub fn Program(comptime Model: type) type {

/// 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));
const now = self.clock.read();
const delta = now - self.last_frame_time;

// Enforce framerate limit
const min_frame_time_ns: u64 = if (self.options.fps > 0)
Expand All @@ -199,14 +210,15 @@ pub fn Program(comptime Model: type) type {
16_666_666; // ~60fps default

if (delta < min_frame_time_ns) {
std.Thread.sleep(min_frame_time_ns - delta);
sleepNs(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))));
const frame_time = self.clock.read();
const actual_delta = frame_time - self.last_frame_time;
self.last_frame_time = frame_time;

self.context.delta = actual_delta;
self.context.elapsed = @intCast(self.last_frame_time - self.start_time);
self.context.elapsed = frame_time - self.start_time;
self.context.frame += 1;

self.resetFrameAllocator();
Expand Down Expand Up @@ -252,8 +264,8 @@ pub fn Program(comptime Model: type) type {
// Deliver tick to user's update if Model.Msg has a tick variant
if (@hasField(UserMsg, "tick")) {
const user_msg = UserMsg{ .tick = .{
.timestamp = @intCast(now),
.delta = delta,
.timestamp = @intCast(frame_time),
.delta = actual_delta,
} };
const cmd = self.dispatchToModel(user_msg);
try self.processCommand(cmd);
Expand All @@ -267,8 +279,8 @@ pub fn Program(comptime Model: type) type {
self.last_every_tick = self.context.elapsed;
if (@hasField(UserMsg, "tick")) {
const user_msg = UserMsg{ .tick = .{
.timestamp = @intCast(now),
.delta = delta,
.timestamp = @intCast(frame_time),
.delta = actual_delta,
} };
const cmd = self.dispatchToModel(user_msg);
try self.processCommand(cmd);
Expand Down Expand Up @@ -381,6 +393,9 @@ pub fn Program(comptime Model: type) type {
term.setup() catch {};
}

// Avoid a large post-resume frame delta.
self.last_frame_time = self.clock.read();

// Force re-render
self.last_view_hash = 0;

Expand Down Expand Up @@ -484,6 +499,58 @@ pub fn Program(comptime Model: type) type {
}
}

fn sleepNs(nanoseconds: u64) void {
if (nanoseconds == 0) return;
const ns_per_s: u64 = if (@hasDecl(std.time, "ns_per_s")) std.time.ns_per_s else 1_000_000_000;
const ns_per_ms: u64 = if (@hasDecl(std.time, "ns_per_ms")) std.time.ns_per_ms else 1_000_000;

// Zig 0.15 API path.
if (@hasDecl(std.Thread, "sleep")) {
std.Thread.sleep(nanoseconds);
return;
}

// Zig 0.16+ API path.
if (@hasDecl(std, "Io")) {
const Io = std.Io;
if (@hasDecl(Io, "Threaded") and
@hasDecl(Io, "Clock") and
@hasDecl(Io.Clock, "Duration") and
@hasDecl(Io.Clock.Duration, "fromNanoseconds") and
@hasDecl(Io.Clock.Duration, "sleep"))
{
var threaded_io: Io.Threaded = .init_single_threaded;
const io = threaded_io.io();
const duration = Io.Clock.Duration.fromNanoseconds(@intCast(nanoseconds));
duration.sleep(io) catch {};
return;
}
}

// Fallback for targets/environments where the above are unavailable.
if (@hasDecl(std, "os") and @hasDecl(std.os, "windows") and builtin.os.tag == .windows) {
const windows = std.os.windows;
const big_ms_from_ns = nanoseconds / ns_per_ms;
const ms = std.math.cast(windows.DWORD, big_ms_from_ns) orelse std.math.maxInt(windows.DWORD);
windows.kernel32.Sleep(ms);
return;
}

if (@hasDecl(std, "posix") and @hasDecl(std.posix, "nanosleep")) {
const seconds = nanoseconds / ns_per_s;
const rem_ns = nanoseconds % ns_per_s;
std.posix.nanosleep(seconds, rem_ns);
return;
}

// Last resort: spin for the requested duration.
var timer = std.time.Timer.start() catch return;
const start_ns = timer.read();
while (timer.read() - start_ns < nanoseconds) {
std.atomic.spinLoopHint();
}
}

fn resetFrameAllocator(self: *Self) void {
_ = self.arena.reset(.retain_capacity);
self.context.allocator = self.arena.allocator();
Expand Down
2 changes: 1 addition & 1 deletion src/terminal/platform/windows.zig
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ pub fn disableMouse(state: *State, writer: anytype) !void {
state.mouse_enabled = false;
}

/// Read available input (Windows uses std.io)
/// Read available input (Windows uses std.Io)
pub fn readInput(state: *State, buffer: []u8, timeout_ms: i32) !usize {
if (state.stdin_handle == windows.INVALID_HANDLE_VALUE) return 0;

Expand Down