diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 23:28:05 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 23:29:28 +0300 |
| commit | 4a3302d19a2baa3a33e910e0088c24e780905b90 (patch) | |
| tree | dcaf76fae1c82102d933c9225b17ca7ac5d3a847 /vendor/libghostty-vt/src/terminal/mem.zig | |
| parent | 550e9f8cc085f428429f5cf2da0bb2dba897e8b8 (diff) | |
chore: update vendored libghostty-vt
refs #276
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/mem.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/mem.zig | 222 |
1 files changed, 222 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/mem.zig b/vendor/libghostty-vt/src/terminal/mem.zig new file mode 100644 index 00000000..7fa31480 --- /dev/null +++ b/vendor/libghostty-vt/src/terminal/mem.zig @@ -0,0 +1,222 @@ +//! Virtual-memory operations shared by terminal page owners. +//! +//! Terminal pages use page-aligned, page-multiple mappings. This module can +//! discard the physical pages behind one of those mappings without releasing +//! its virtual address range, then prepare the same range for reuse. It does +//! not allocate memory or decide which terminal pages should be discarded. +const std = @import("std"); +const builtin = @import("builtin"); +const assert = @import("../quirks.zig").inlineAssert; + +const log = std.log.scoped(.terminal_mem); + +/// What guarantee decommit must provide when the OS cannot discard a mapping. +pub const DecommitMode = enum { + /// The dirty prefix must read as zero after this call, even when physical + /// reclamation is unavailable. Bytes after the prefix must already be zero. + zero, + + /// Physical-memory reclamation is required. Do not touch the mapping when + /// reclamation is unsupported or fails; report the failure to the caller. + strict, +}; + +/// Return whether this target can reclaim physical memory for `mode` while +/// retaining the mapping's virtual address range. +/// +/// Test builds support both modes because `decommit` simulates reclamation by +/// clearing the supplied range. Runtime reclamation is intentionally limited +/// to 64-bit Linux and Darwin. Other targets must leave strict callers' memory +/// resident; zero mode still provides its documented memset fallback through +/// `decommit` even when this function returns false. +pub inline fn canReclaim(comptime mode: DecommitMode) bool { + // Both modes use the same retained-mapping primitives. Keeping the switch + // exhaustive makes additions to DecommitMode choose target support + // explicitly rather than inheriting it accidentally. + return switch (mode) { + .zero, .strict => supported: { + // Tests never call into the OS because their allocator ranges can + // share mappings with unrelated allocations. `decommit` simulates + // successful reclamation by zeroing the requested range instead, + // so both modes are always available to tests on every target. + if (builtin.is_test) break :supported true; + + // Compression currently retains complete page mappings for its + // lifetime. Limit the initial runtime support to 64-bit address + // spaces where that virtual-memory cost is negligible and where + // the retained-mapping behavior has been validated. + if (builtin.target.ptrBitWidth() != 64) break :supported false; + + // Linux provides MADV_DONTNEED, which immediately discards pages + // from a private anonymous mapping and faults them back as zeroes. + // Zig reaches this through the raw syscall path without libc. + if (builtin.target.os.tag == .linux) break :supported true; + + // Darwin provides the paired MADV_FREE_REUSABLE/FREE_REUSE + // operations used below. Darwin requires libc independently of + // this feature, so using its madvise entry point adds no new + // dependency to libghostty-vt. + if (builtin.target.os.tag.isDarwin()) break :supported true; + + // Other targets have no retained-mapping reclamation contract in + // this module. Zero mode can still clear through its memset + // fallback, but strict callers must leave their mapping resident. + break :supported false; + }, + }; +} + +/// Discard physical pages while retaining a mapping's virtual address range. +/// +/// The complete mapping must be page-aligned and a multiple of the minimum +/// system page size. `dirty_len` identifies the prefix whose contents may be +/// nonzero. Strict mode requires the complete mapping to be dirty because a +/// successful discard invalidates all of its contents. +/// +/// The return value reports whether the OS accepted the reclamation request. +/// Test builds return true after simulating reclamation by zeroing dirty bytes. +/// In zero mode, the requested bytes are guaranteed to be zero regardless of +/// the return value. +pub fn decommit( + comptime mode: DecommitMode, + memory: []align(std.heap.page_size_min) u8, + dirty_len: usize, +) bool { + assert(memory.len > 0); + assert(@intFromPtr(memory.ptr) % std.heap.page_size_min == 0); + assert(memory.len % std.heap.page_size_min == 0); + assert(dirty_len <= memory.len); + if (comptime mode == .strict) assert(dirty_len == memory.len); + + // Testing allocator ranges may share an OS mapping with unrelated memory, + // so madvise is not safe. Zeroing models the only content guarantee callers + // have after a successful discard. + if (comptime builtin.is_test) { + @memset(memory[0..dirty_len], 0); + return true; + } + + // DONTNEED immediately reclaims private anonymous pages on Linux and + // faults them back as zero-filled pages. We deliberately avoid MADV_FREE: + // it does not reduce RSS until memory pressure and does not guarantee that + // the next read is zero. + if (comptime builtin.os.tag == .linux) { + if (std.posix.madvise( + memory.ptr, + memory.len, + std.posix.MADV.DONTNEED, + )) |_| return true else |err| { + log.warn("madvise(DONTNEED) failed err={}", .{err}); + if (comptime mode == .strict) return false; + // Zero mode falls through to the memset below. + } + } + + // FREE_REUSABLE removes the range from the Darwin process footprint while + // retaining its mapping. Zero mode clears its dirty prefix first because + // the kernel may preserve the contents. Strict mode avoids that write + // because its caller will replace the entire mapping after recommit. + if (comptime builtin.os.tag.isDarwin()) { + if (comptime mode == .zero) @memset(memory[0..dirty_len], 0); + + if (std.posix.madvise( + memory.ptr, + memory.len, + std.posix.MADV.FREE_REUSABLE, + )) |_| return true else |err| { + switch (mode) { + .strict => { + log.warn("madvise(FREE_REUSABLE) failed err={}", .{err}); + return false; + }, + + .zero => { + // Plain FREE can still reclaim the already-zero mapping + // under pressure and does not require a reuse pairing. + std.posix.madvise( + memory.ptr, + memory.len, + std.posix.MADV.FREE, + ) catch {}; + return false; + }, + } + } + } + + if (comptime mode == .zero) @memset(memory[0..dirty_len], 0); + return false; +} + +/// Prepare a mapping previously passed to decommit for reuse. +/// +/// Linux and test builds need no explicit operation. Darwin pairs +/// FREE_REUSABLE with FREE_REUSE so pages touched by the caller are accounted +/// to the process again. Failure does not invalidate the retained mapping, so +/// reuse can continue after logging the accounting failure. +pub fn recommit(memory: []align(std.heap.page_size_min) u8) void { + assert(memory.len > 0); + assert(@intFromPtr(memory.ptr) % std.heap.page_size_min == 0); + assert(memory.len % std.heap.page_size_min == 0); + + if (comptime builtin.is_test) return; + if (comptime builtin.os.tag.isDarwin()) { + std.posix.madvise( + memory.ptr, + memory.len, + std.posix.MADV.FREE_REUSE, + ) catch |err| { + log.warn("madvise(FREE_REUSE) failed err={}", .{err}); + }; + } +} + +test "decommit with zero fallback clears the dirty prefix" { + const testing = std.testing; + const memory_len = 2 * std.heap.page_size_min; + const memory = try testing.allocator.alignedAlloc( + u8, + .fromByteUnits(std.heap.page_size_min), + memory_len, + ); + defer testing.allocator.free(memory); + + @memset(memory, 0xAA); + _ = decommit(.zero, memory, memory.len); + try testing.expect(std.mem.allEqual(u8, memory, 0)); + + // The tail is already zero by contract, so a partially dirty mapping only + // needs its dirty prefix cleared. + @memset(memory[0..1024], 0xAA); + _ = decommit(.zero, memory, 1024); + try testing.expect(std.mem.allEqual(u8, memory, 0)); +} + +test "strict decommit retains the mapping for recommit" { + const testing = std.testing; + const memory_len = 2 * std.heap.page_size_min; + const memory = try testing.allocator.alignedAlloc( + u8, + .fromByteUnits(std.heap.page_size_min), + memory_len, + ); + defer testing.allocator.free(memory); + @memset(memory, 0xAA); + + const original_ptr = memory.ptr; + const original_len = memory.len; + try testing.expect(decommit(.strict, memory, memory.len)); + try testing.expectEqual(original_ptr, memory.ptr); + try testing.expectEqual(original_len, memory.len); + try testing.expect(std.mem.allEqual(u8, memory, 0)); + + recommit(memory); + @memset(memory, 0xBB); + try testing.expect(std.mem.allEqual(u8, memory, 0xBB)); +} + +test "test builds can reclaim retained mappings" { + const testing = std.testing; + try testing.expect(canReclaim(.zero)); + try testing.expect(canReclaim(.strict)); +} |
