From 9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 9 Aug 2026 10:41:33 -0300 Subject: replace ArrayLists with bounded storage --- src/web.zig | 167 ++++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 123 insertions(+), 44 deletions(-) (limited to 'src/web.zig') diff --git a/src/web.zig b/src/web.zig index 43b6c96d..e17e147e 100644 --- a/src/web.zig +++ b/src/web.zig @@ -96,14 +96,45 @@ comptime { const State = struct { core: *pardes.Pardes, arena: std.heap.ArenaAllocator, - cells: std.ArrayList(WebCell) = .empty, - effect_data: std.ArrayList(u8) = .empty, + cells: []WebCell = &.{}, + frame_len: usize = 0, + frame_cols: u16 = 0, + frame_rows: u16 = 0, + effect_data: []u8 = &.{}, + effect_len: usize = 0, + + fn init(cols: u16, rows: u16) !State { + const allocs = pardes.allocators.init(gpa); + errdefer pardes.allocators.deinit(); + pardes.image.start(std.Io.failing, allocs.image); + errdefer pardes.image.stop(); + if (comptime pardes.pdf_enabled) pardes.pdf.start(allocs.pdf); + errdefer if (comptime pardes.pdf_enabled) pardes.pdf.stop(); + pardes.syntax.start(allocs.tree_sitter); + errdefer pardes.syntax.stop(); + + const core = try pardes.Pardes.initFromDump(allocs.pardes, .{ + .cols = cols, + .rows = rows, + .image_allocator = allocs.image, + .pdf_allocator = allocs.pdf, + .tree_sitter_allocator = allocs.tree_sitter, + }, embedded_dump); + return .{ + .core = core, + .arena = .init(allocs.frame), + }; + } fn deinit(s: *State) void { - s.core.deinit(); + if (s.cells.len > 0) gpa.free(s.cells); + if (s.effect_data.len > 0) gpa.free(s.effect_data); s.arena.deinit(); - s.cells.deinit(gpa); - s.effect_data.deinit(gpa); + s.core.deinit(); + pardes.image.stop(); + if (comptime pardes.pdf_enabled) pardes.pdf.stop(); + pardes.syntax.stop(); + pardes.allocators.deinit(); } }; @@ -120,12 +151,11 @@ export fn pardes_init(cols_arg: u32, rows_arg: u32) u32 { const cols: u16 = @intCast(std.math.clamp(cols_arg, 1, std.math.maxInt(u16))); const rows: u16 = @intCast(std.math.clamp(rows_arg, 1, std.math.maxInt(u16))); - const core = pardes.Pardes.initFromDump(gpa, .{ .cols = cols, .rows = rows }, embedded_dump) catch |err| { + state = State.init(cols, rows) catch |err| { last_error = @intFromError(err); return last_error; }; - state = .{ .core = core, .arena = .init(gpa) }; - core.update(.{ .resize = .{ .cols = cols, .rows = rows } }); + state.?.core.update(.{ .resize = .{ .cols = cols, .rows = rows } }); return 0; } @@ -284,16 +314,33 @@ export fn pardes_tick() void { export fn pardes_frame() u32 { const s = &(state orelse return 0); _ = s.arena.reset(.retain_capacity); + s.frame_len = 0; + s.frame_cols = 0; + s.frame_rows = 0; const surface = s.core.render(s.arena.allocator()) catch |err| { last_error = @intFromError(err); return 0; }; const count: usize = @as(usize, surface.cols) * surface.rows; - s.cells.resize(gpa, count) catch |err| { - last_error = @intFromError(err); - return 0; - }; - for (surface.cells, s.cells.items) |cell, *out| { + if (count != s.cells.len) { + if (count == 0) { + if (s.cells.len > 0) gpa.free(s.cells); + s.cells = &.{}; + } else { + const resized = if (s.cells.len == 0) + gpa.alloc(WebCell, count) + else + gpa.realloc(s.cells, count); + s.cells = resized catch |err| { + last_error = @intFromError(err); + return 0; + }; + } + } + s.frame_len = count; + s.frame_cols = surface.cols; + s.frame_rows = surface.rows; + for (surface.cells, s.cells[0..count]) |cell, *out| { out.* = .{ .text = @splat(0), .fg = encodeColor(cell.style.fg), @@ -309,17 +356,17 @@ export fn pardes_frame() u32 { export fn pardes_frame_ptr() u32 { const s = &(state orelse return 0); - return if (s.cells.items.len == 0) 0 else @intCast(@intFromPtr(s.cells.items.ptr)); + return if (s.frame_len == 0) 0 else @intCast(@intFromPtr(s.cells.ptr)); } export fn pardes_frame_cols() u32 { const s = &(state orelse return 0); - return s.core.screen_w; + return s.frame_cols; } export fn pardes_frame_rows() u32 { const s = &(state orelse return 0); - return s.core.screen_h; + return s.frame_rows; } export fn pardes_cursor_x() i32 { @@ -353,7 +400,7 @@ export fn pardes_chrome_target(col_arg: u32, row_arg: u32) u32 { // process-capable host does not require another wasm boundary. export fn pardes_effect_next() u32 { const s = &(state orelse return 0); - s.effect_data.clearRetainingCapacity(); + clearEffect(s); effect_aux0 = 0; effect_aux1 = 0; const effect = s.core.nextEffect() orelse return 0; @@ -379,11 +426,8 @@ export fn pardes_effect_next() u32 { }, .save_file => |e| blk: { effect_aux0 = e.pane; - if (s.core.panes[e.pane]) |pane| if (pane.file) |file| { - putU32(s, @intCast(file.path.len)); - appendEffect(s, file.path); - appendEffect(s, file.content); - }; + if (s.core.panes[e.pane]) |pane| if (pane.file) |file| + putSaveEffect(s, file.path, file.content); break :blk 5; }, .write_dump => blk: { @@ -402,18 +446,10 @@ export fn pardes_effect_next() u32 { }, // Process-capable embeddings receive exact command/cwd/input bytes: // [command_len][cwd_len][count][input lengths...][all payloads...]. - // The ordinary static browser surfaces this as `pardes-io` and stays - // inert because it has no process authority. .pipe => |e| blk: { effect_aux0 = e.id; const request = s.core.pipeRequest(e.id) orelse break :blk 12; - putU32(s, @intCast(request.command.len)); - putU32(s, @intCast(request.cwd.len)); - putU32(s, @intCast(request.inputs.len)); - for (request.inputs) |selection| putU32(s, @intCast(selection.bytes.len)); - appendEffect(s, request.command); - appendEffect(s, request.cwd); - for (request.inputs) |selection| appendEffect(s, selection.bytes); + putPipeEffect(s, request); break :blk 12; }, .watch => |e| blk: { @@ -422,8 +458,6 @@ export fn pardes_effect_next() u32 { if (e.on) if (s.core.panes[e.pane]) |pane| if (pane.file) |file| putEffect(s, file.path); break :blk 9; }, - // Browser hosts cannot create this file, but surface the request like - // the other host-specific IO effects instead of performing IO here. .new_file => |e| blk: { effect_aux0 = e.pane; effect_aux1 = e.serial; @@ -435,12 +469,12 @@ export fn pardes_effect_next() u32 { export fn pardes_effect_ptr() u32 { const s = &(state orelse return 0); - return if (s.effect_data.items.len == 0) 0 else @intCast(@intFromPtr(s.effect_data.items.ptr)); + return if (s.effect_len == 0) 0 else @intCast(@intFromPtr(s.effect_data.ptr)); } export fn pardes_effect_len() u32 { const s = &(state orelse return 0); - return @intCast(s.effect_data.items.len); + return @intCast(s.effect_len); } export fn pardes_effect_aux0() u32 { @@ -472,17 +506,62 @@ fn encodeAttrs(style: pardes.CellStyle) u16 { return attrs; } -fn putEffect(s: *State, bytes: []const u8) void { - s.effect_data.clearRetainingCapacity(); - appendEffect(s, bytes); +fn clearEffect(s: *State) void { + if (s.effect_data.len > 0) gpa.free(s.effect_data); + s.effect_data = &.{}; + s.effect_len = 0; } -fn appendEffect(s: *State, bytes: []const u8) void { - s.effect_data.appendSlice(gpa, bytes) catch {}; +fn allocEffect(s: *State, len: usize) ?[]u8 { + std.debug.assert(s.effect_data.len == 0 and s.effect_len == 0); + if (len == 0) return s.effect_data; + s.effect_data = gpa.alloc(u8, len) catch return null; + s.effect_len = len; + return s.effect_data; } -fn putU32(s: *State, value: u32) void { - var bytes: [4]u8 = undefined; - std.mem.writeInt(u32, &bytes, value, .little); - appendEffect(s, &bytes); +fn putEffect(s: *State, bytes: []const u8) void { + const out = allocEffect(s, bytes.len) orelse return; + @memcpy(out, bytes); +} + +fn writeEffectU32(out: []u8, at: *usize, value: u32) void { + const word: *[4]u8 = @ptrCast(out[at.*..].ptr); + std.mem.writeInt(u32, word, value, .little); + at.* += 4; +} + +fn putSaveEffect(s: *State, path: []const u8, content: []const u8) void { + var total = std.math.add(usize, 4, path.len) catch return; + total = std.math.add(usize, total, content.len) catch return; + const out = allocEffect(s, total) orelse return; + var at: usize = 0; + writeEffectU32(out, &at, @intCast(path.len)); + @memcpy(out[at..][0..path.len], path); + at += path.len; + @memcpy(out[at..][0..content.len], content); +} + +fn putPipeEffect(s: *State, request: anytype) void { + var total = std.math.mul(usize, request.inputs.len, 4) catch return; + total = std.math.add(usize, total, 12) catch return; + total = std.math.add(usize, total, request.command.len) catch return; + total = std.math.add(usize, total, request.cwd.len) catch return; + for (request.inputs) |selection| + total = std.math.add(usize, total, selection.bytes.len) catch return; + const out = allocEffect(s, total) orelse return; + var at: usize = 0; + writeEffectU32(out, &at, @intCast(request.command.len)); + writeEffectU32(out, &at, @intCast(request.cwd.len)); + writeEffectU32(out, &at, @intCast(request.inputs.len)); + for (request.inputs) |selection| writeEffectU32(out, &at, @intCast(selection.bytes.len)); + @memcpy(out[at..][0..request.command.len], request.command); + at += request.command.len; + @memcpy(out[at..][0..request.cwd.len], request.cwd); + at += request.cwd.len; + for (request.inputs) |selection| { + @memcpy(out[at..][0..selection.bytes.len], selection.bytes); + at += selection.bytes.len; + } + std.debug.assert(at == out.len); } -- cgit v1.3