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
62 changes: 62 additions & 0 deletions bench/bench.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const std = @import("std");
const vaxis = @import("vaxis");

fn parseIterations(allocator: std.mem.Allocator) !usize {
var args = try std.process.argsWithAllocator(allocator);
defer args.deinit();
_ = args.next();
if (args.next()) |val| {
return std.fmt.parseUnsigned(usize, val, 10);
}
return 200;
}

fn printResults(writer: anytype, label: []const u8, iterations: usize, elapsed_ns: u64, total_bytes: usize) !void {
const ns_per_frame = elapsed_ns / @as(u64, @intCast(iterations));
const bytes_per_frame = total_bytes / iterations;
try writer.print(
"{s}: frames={d} total_ns={d} ns/frame={d} bytes={d} bytes/frame={d}\n",
.{ label, iterations, elapsed_ns, ns_per_frame, total_bytes, bytes_per_frame },
);
}

pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();

const iterations = try parseIterations(allocator);

var vx = try vaxis.init(allocator, .{});
var init_writer = std.io.Writer.Allocating.init(allocator);
defer init_writer.deinit();
defer vx.deinit(allocator, &init_writer.writer);

const winsize = vaxis.Winsize{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 };
try vx.resize(allocator, &init_writer.writer, winsize);

const stdout = std.fs.File.stdout().deprecatedWriter();
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method deprecatedWriter() does not exist on std.fs.File. This should be writer() to get a writer from stdout.

Suggested change
const stdout = std.fs.File.stdout().deprecatedWriter();
const stdout = std.fs.File.stdout().writer();

Copilot uses AI. Check for mistakes.

var idle_writer = std.io.Writer.Allocating.init(allocator);
defer idle_writer.deinit();
var timer = try std.time.Timer.start();
var i: usize = 0;
while (i < iterations) : (i += 1) {
try vx.render(&idle_writer.writer);
}
const idle_ns = timer.read();
const idle_bytes: usize = idle_writer.writer.end;
try printResults(stdout, "idle", iterations, idle_ns, idle_bytes);

var dirty_writer = std.io.Writer.Allocating.init(allocator);
defer dirty_writer.deinit();
Comment on lines +31 to +52
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The benchmark uses std.io.Writer.Allocating (lowercase 'io'), but the existing code in tty.zig uses std.Io.Writer.Allocating (capital 'I'). This inconsistency will cause a compilation error since std.io.Writer.Allocating does not exist in Zig's standard library. It should be std.Io.Writer.Allocating to match the existing pattern in the codebase.

Copilot uses AI. Check for mistakes.
timer.reset();
i = 0;
while (i < iterations) : (i += 1) {
vx.queueRefresh();
try vx.render(&dirty_writer.writer);
}
const dirty_ns = timer.read();
const dirty_bytes: usize = dirty_writer.writer.end;
Comment on lines +48 to +60
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code accesses idle_writer.writer.end but this is incorrect. If Writer.Allocating follows the pattern of a buffering writer, it likely has the byte count accessible directly on the struct (e.g., idle_writer.end or similar), not on the .writer field. The .writer field typically returns a writer interface that doesn't expose the buffer size.

Copilot uses AI. Check for mistakes.
try printResults(stdout, "dirty", iterations, dirty_ns, dirty_bytes);
}
19 changes: 19 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,25 @@ pub fn build(b: *std.Build) void {
const example_run = b.addRunArtifact(example);
example_step.dependOn(&example_run.step);

// Benchmarks
const bench_step = b.step("bench", "Run benchmarks");
const bench = b.addExecutable(.{
.name = "bench",
.root_module = b.createModule(.{
.root_source_file = b.path("bench/bench.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "vaxis", .module = vaxis_mod },
},
}),
});
const bench_run = b.addRunArtifact(bench);
if (b.args) |args| {
bench_run.addArgs(args);
}
bench_step.dependOn(&bench_run.step);

// Tests
const tests_step = b.step("test", "Run tests");

Expand Down
97 changes: 70 additions & 27 deletions src/Vaxis.zig
Original file line number Diff line number Diff line change
Expand Up @@ -360,48 +360,72 @@ pub fn render(self: *Vaxis, tty: *IoWriter) !void {
assert(self.screen.buf.len == @as(usize, @intCast(self.screen.width)) * self.screen.height); // correct size
assert(self.screen.buf.len == self.screen_last.buf.len); // same size

// Set up sync before we write anything
// TODO: optimize sync so we only sync _when we have changes_. This
// requires a smarter buffered writer, we'll probably have to write
// our own
try tty.writeAll(ctlseqs.sync_set);
errdefer tty.writeAll(ctlseqs.sync_reset) catch {};

// Send the cursor to 0,0
// TODO: this needs to move after we optimize writes. We only do
// this if we have an update to make. We also need to hide cursor
// and then reshow it if needed
try tty.writeAll(ctlseqs.hide_cursor);
if (self.state.alt_screen)
try tty.writeAll(ctlseqs.home)
else {
try tty.writeByte('\r');
for (0..self.state.cursor.row) |_| {
try tty.writeAll(ctlseqs.ri);
}
}
try tty.writeAll(ctlseqs.sgr_reset);
var started: bool = false;
var sync_active: bool = false;
errdefer if (sync_active) tty.writeAll(ctlseqs.sync_reset) catch {};

const cursor_vis_changed = self.screen.cursor_vis != self.screen_last.cursor_vis;
const cursor_shape_changed = self.screen.cursor_shape != self.screen_last.cursor_shape;
const mouse_shape_changed = self.screen.mouse_shape != self.screen_last.mouse_shape;
const cursor_pos_changed = self.screen.cursor_vis and
(self.screen.cursor_row != self.state.cursor.row or
self.screen.cursor_col != self.state.cursor.col);
Comment on lines +371 to +372
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cursor position change detection compares against self.state.cursor instead of self.screen_last.cursor_row/cursor_col. However, self.screen_last.cursor_row and self.screen_last.cursor_col are never updated anywhere in the code (unlike cursor_vis at line 792 and cursor_shape at line 805). This means position changes won't be properly detected on subsequent renders unless self.state.cursor differs. You should add lines to update self.screen_last.cursor_row and self.screen_last.cursor_col after positioning the cursor (around line 792), and then use those fields for comparison in the change detection.

Suggested change
(self.screen.cursor_row != self.state.cursor.row or
self.screen.cursor_col != self.state.cursor.col);
(self.screen.cursor_row != self.screen_last.cursor_row or
self.screen.cursor_col != self.screen_last.cursor_col);

Copilot uses AI. Check for mistakes.
const needs_render = self.refresh or cursor_vis_changed or cursor_shape_changed or mouse_shape_changed or cursor_pos_changed;

// initialize some variables
var reposition: bool = false;
var row: u16 = 0;
var col: u16 = 0;
var cursor: Style = .{};
var link: Hyperlink = .{};
var cursor_pos: struct {
const CursorPos = struct {
row: u16 = 0,
col: u16 = 0,
} = .{};

// Clear all images
if (self.caps.kitty_graphics)
try tty.writeAll(ctlseqs.kitty_graphics_clear);
};
var cursor_pos: CursorPos = .{};

const startRender = struct {
fn run(
vx: *Vaxis,
io: *IoWriter,
cursor_pos_ptr: *CursorPos,
reposition_ptr: *bool,
started_ptr: *bool,
sync_active_ptr: *bool,
) !void {
if (started_ptr.*) return;
started_ptr.* = true;
sync_active_ptr.* = true;
// Set up sync before we write anything
try io.writeAll(ctlseqs.sync_set);
// Send the cursor to 0,0
try io.writeAll(ctlseqs.hide_cursor);
if (vx.state.alt_screen)
try io.writeAll(ctlseqs.home)
else {
try io.writeByte('\r');
for (0..vx.state.cursor.row) |_| {
try io.writeAll(ctlseqs.ri);
}
}
try io.writeAll(ctlseqs.sgr_reset);
cursor_pos_ptr.* = .{};
reposition_ptr.* = true;
// Clear all images
if (vx.caps.kitty_graphics)
try io.writeAll(ctlseqs.kitty_graphics_clear);
}
};

// Reset skip flag on all last_screen cells
for (self.screen_last.buf) |*last_cell| {
last_cell.skip = false;
}

if (needs_render) {
try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active);
}

var i: usize = 0;
while (i < self.screen.buf.len) {
const cell = self.screen.buf[i];
Expand Down Expand Up @@ -447,6 +471,9 @@ pub fn render(self: *Vaxis, tty: *IoWriter) !void {
}
continue;
}
if (!started) {
try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active);
}
self.screen_last.buf[i].skipped = false;
defer {
cursor = cell.style;
Expand Down Expand Up @@ -730,6 +757,7 @@ pub fn render(self: *Vaxis, tty: *IoWriter) !void {
cursor_pos.col = col + w;
cursor_pos.row = row;
}
if (!started) return;
if (self.screen.cursor_vis) {
if (self.state.alt_screen) {
try tty.print(
Expand Down Expand Up @@ -761,6 +789,7 @@ pub fn render(self: *Vaxis, tty: *IoWriter) !void {
self.state.cursor.row = cursor_pos.row;
self.state.cursor.col = cursor_pos.col;
}
self.screen_last.cursor_vis = self.screen.cursor_vis;
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After updating screen_last.cursor_vis at line 792, the code should also update screen_last.cursor_row and screen_last.cursor_col to track the last rendered cursor position. This is necessary for the cursor position change detection at lines 370-372 to work correctly. Add:

self.screen_last.cursor_row = self.screen.cursor_row;
self.screen_last.cursor_col = self.screen.cursor_col;
Suggested change
self.screen_last.cursor_vis = self.screen.cursor_vis;
self.screen_last.cursor_vis = self.screen.cursor_vis;
self.screen_last.cursor_row = self.screen.cursor_row;
self.screen_last.cursor_col = self.screen.cursor_col;

Copilot uses AI. Check for mistakes.
if (self.screen.mouse_shape != self.screen_last.mouse_shape) {
try tty.print(
ctlseqs.osc22_mouse_shape,
Expand Down Expand Up @@ -1409,3 +1438,17 @@ pub fn setTerminalWorkingDirectory(_: *Vaxis, tty: *IoWriter, path: []const u8)
try tty.print(ctlseqs.osc7, .{uri.fmt(.{ .scheme = true, .authority = true, .path = true })});
try tty.flush();
}

test "render: no output when no changes" {
var vx = try Vaxis.init(std.testing.allocator, .{});
var deinit_writer = std.io.Writer.Allocating.init(std.testing.allocator);
defer deinit_writer.deinit();
defer vx.deinit(std.testing.allocator, &deinit_writer.writer);

var render_writer = std.io.Writer.Allocating.init(std.testing.allocator);
defer render_writer.deinit();
Comment on lines +1444 to +1449
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses std.io.Writer.Allocating (lowercase 'io'), but the existing code in tty.zig uses std.Io.Writer.Allocating (capital 'I'). This inconsistency will cause a compilation error since std.io.Writer.Allocating does not exist in Zig's standard library. It should be std.Io.Writer.Allocating to match the existing pattern in the codebase.

Copilot uses AI. Check for mistakes.
try vx.render(&render_writer.writer);
const output = try render_writer.toOwnedSlice();
defer std.testing.allocator.free(output);
try std.testing.expectEqual(@as(usize, 0), output.len);
}