aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/datastruct/wasm_page_pool.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/datastruct/wasm_page_pool.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/datastruct/wasm_page_pool.zig')
-rw-r--r--vendor/libghostty-vt/src/datastruct/wasm_page_pool.zig162
1 files changed, 162 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/datastruct/wasm_page_pool.zig b/vendor/libghostty-vt/src/datastruct/wasm_page_pool.zig
new file mode 100644
index 00000000..0f73eec6
--- /dev/null
+++ b/vendor/libghostty-vt/src/datastruct/wasm_page_pool.zig
@@ -0,0 +1,162 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+
+/// A memory pool of wasm-page-multiple-sized items backed directly by
+/// wasm linear memory, for wasm targets only.
+///
+/// This is necessary for two reasons: the std.heap.MemoryPool grows 1.5x
+/// at each growth point. The backing allocator for that is usually a GPA
+/// which is the BrkAllocator for wasm. This grows by power-of-two
+/// big-allocation slots. If you pair these together you get a massive
+/// permanent linear memory growth. Native (non-wasm) targets don't care
+/// because unused virtual mappings are effectively free, but this isn't
+/// exactly true for Wasm runtimes.
+///
+/// This pool instead grows exactly @sizeOf(Item) bytes of fresh linear
+/// memory per item with memory.grow (which also guarantees wasm-page
+/// alignment and zeroing) and recycles freed items through a free list
+/// that is container-level, i.e. shared by every pool of the same Item
+/// type in the module instance. Every freed item is immediately
+/// reusable by every pool, and each item costs exactly @sizeOf(Item)
+/// reserved bytes, ever.
+///
+/// The "shared by every pool of the same Item type" is really important:
+/// the normal Ghostty memory pool is per-terminal. This one is per-module.
+/// The Ghostty wasm modules are not multi-threaded so this doesn't require
+/// any synchronization. But this means that all terminals share one pool
+/// so memory doesn't balloon like crazy that way either.
+///
+/// Requirements on Item:
+///
+/// - @sizeOf(Item) must be a nonzero multiple of the wasm page size
+/// (64KiB), since memory.grow allocates in whole pages. This is
+/// also what makes the pool exact-fit: any other size would strand
+/// the remainder of the last page.
+/// - Natural alignment must be at most the wasm page size.
+///
+/// Free-list items are dirty except that their first pointer-size bytes
+/// hold the intrusive free-list node, exactly like the std MemoryPool.
+/// Callers that need zeroed items must zero them (fresh items from
+/// memory.grow are zero by the wasm spec; recycled items retain
+/// whatever the caller left, so zero either on destroy or on create).
+///
+/// The API mirrors the native page pool (datastruct.UntouchedPool:
+/// initCapacity/deinit/reset/create/destroy plus the allocator field),
+/// so PageList can swap them at comptime.
+pub fn WasmPagePool(comptime Item: type) type {
+ return struct {
+ const Self = @This();
+
+ comptime {
+ if (builtin.target.cpu.arch.isWasm()) {
+ // The shared free list (and wasm's single linear
+ // memory) requires a single-threaded target.
+ assert(builtin.single_threaded);
+ // Items are whole wasm pages
+ assert(item_size > 0);
+ assert(item_size % std.heap.page_size_min == 0);
+ assert(@alignOf(Item) <= std.heap.page_size_min);
+ // Free items store the intrusive node in their bytes.
+ assert(item_size >= @sizeOf(std.SinglyLinkedList.Node));
+ }
+ }
+
+ /// The allocator this managed pool was initialized with,
+ /// carried for interface compatibility with the std managed
+ /// memory pools. Pool items never come from it (they come
+ /// from memory.grow), but callers may use it for related
+ /// allocations that don't fit the pool.
+ allocator: Allocator,
+
+ pub const item_size = @sizeOf(Item);
+ pub const ItemPtr = *align(std.heap.page_size_min) Item;
+
+ /// Freed items, shared by every pool of this Item type in the
+ /// module instance.
+ var free_list: std.SinglyLinkedList = .{};
+
+ pub fn initCapacity(
+ gpa: Allocator,
+ allocator: Allocator,
+ preheat: usize,
+ ) Allocator.Error!Self {
+ // The free list is intrusive so we never allocate from the
+ // general purpose allocator.
+ _ = gpa;
+
+ // Preheating is intentionally a no-op: item creation is a
+ // cheap memory.grow or free-list pop. And we want to avoid
+ // the preallocation mem cost.
+ _ = preheat;
+ return .{ .allocator = allocator };
+ }
+
+ pub fn deinit(self: *Self) void {
+ // Items are global to the instance and outlive the pool so
+ // other pools can reuse them. Callers must destroy() any
+ // items they still hold before deinit or they leak.
+ self.* = undefined;
+ }
+
+ pub fn reset(
+ self: *Self,
+ mode: std.heap.ArenaAllocator.ResetMode,
+ ) bool {
+ // There is nothing to trim: linear memory cannot shrink,
+ // so retaining every freed item for reuse is always the
+ // right policy on wasm. As with deinit, live items must be
+ // destroyed by the caller first.
+ _ = self;
+ _ = mode;
+ return true;
+ }
+
+ pub fn create(self: *Self) Allocator.Error!ItemPtr {
+ _ = self;
+
+ // If we have a free item, use it.
+ if (free_list.popFirst()) |node| return @ptrCast(@alignCast(node));
+
+ // Grow linear memory by exactly one item.
+ const pages = comptime item_size / std.heap.page_size_min;
+ const page_index = @wasmMemoryGrow(0, pages);
+ if (page_index == -1) return error.OutOfMemory;
+ return @ptrFromInt(@as(usize, @intCast(page_index)) * std.heap.page_size_min);
+ }
+
+ pub fn destroy(self: *Self, ptr: ItemPtr) void {
+ _ = self;
+ const node: *std.SinglyLinkedList.Node = @ptrCast(ptr);
+ node.* = .{};
+ free_list.prepend(node);
+ }
+ };
+}
+
+test WasmPagePool {
+ if (!comptime builtin.target.cpu.arch.isWasm()) return error.SkipZigTest;
+
+ const testing = std.testing;
+ const Pool = WasmPagePool([64 * 1024]u8);
+ var pool: Pool = try .initCapacity(testing.allocator, testing.allocator, 0);
+ defer pool.deinit();
+
+ const a = try pool.create();
+ const b = try pool.create();
+ try testing.expect(a != b);
+
+ // Freed items are recycled, most recent first.
+ pool.destroy(b);
+ try testing.expectEqual(b, try pool.create());
+
+ // The free list is shared across pools of the same Item type.
+ var pool2: Pool = try .initCapacity(testing.allocator, testing.allocator, 0);
+ defer pool2.deinit();
+ pool.destroy(a);
+ try testing.expectEqual(a, try pool2.create());
+
+ pool.destroy(a);
+ pool.destroy(b);
+}