//! Every board-shaped capacity in one table. //! //! These numbers used to be nine `platform == .esp32p4` tests scattered across //! nine files, each one a separate place to forget. They are not nine //! decisions: they are ONE decision — how much memory this build is allowed to //! spend — taken nine times, in nine files, where no reader could see the //! total. Here the whole budget is on one screen and every cap says what it is //! measured against. //! //! Two booleans derive all of it, and nothing outside this file tests the //! platform for a capacity again. //! //! WHAT DOES NOT BELONG HERE: capability switches. `terminal_panes`, //! `board_memory.enabled`, `hosted`, `font_picker` and the rest answer "does //! this build have the thing at all", which is a question about the platform //! and not about a budget — they stay next to the thing they gate. That //! division is also why a build option selecting the board's budget on a //! desktop does not work; the note on `board` below records the attempt. const std = @import("std"); const builtin = @import("builtin"); const config = @import("pardes_config"); /// `board` is the ESP32-P4 firmware's budget: a 384 KiB heap and a 240 KiB /// chunk of L2MEM shared between `.bss`, `.data` and the stack. `reduced` is /// any freestanding target with no OS under it — the browser's wasm linear /// memory grown on demand, megabytes rather than tens, but not a desktop's /// address space. /// /// TWO BOOLEANS AND NOT A PROFILE ENUM, and a build option was tried and /// removed. `-Dmem-profile=board` was meant to let a native test runner /// compile the board's capacities and boot the core under them; it does not /// work, and cannot. The dominant term in a boot is `@sizeOf(Pane)`, which /// carries the ghostty-vt Terminal — 1.1 MiB of it — and what removes that is /// `pardes.terminal_panes`, a CAPABILITY keyed on the platform rather than a /// capacity in this table. So the option shrank the rings and left the boot /// six times over budget, producing a configuration nothing was designed for: /// `zig build unit-test -Dmem-profile=board` deadlocked in a futex rather than /// failing, because a hosted build with the board's effect ring silently drops /// effects a hosted test is waiting on. /// /// What DOES test the board's memory pressure natively is in pardes.zig: the /// grid-scaled cost and the allocation-failure sweep, both of which are /// platform-independent and run on the ordinary build. See the comment block /// above `board_heap_bytes` there. const board = config.platform == .esp32p4; /// No OS means no address space to reserve megabytes out of, whatever the /// platform is called. `.web` is wasm32-freestanding and `.esp32p4` is /// riscv32-freestanding, so the target answers this for both. const reduced_target = builtin.os.tag == .freestanding; const KiB = 1024; const MiB = 1024 * KiB; /// THE NUMBER EVERY OTHER NUMBER HERE IS MEASURED AGAINST: the board's whole /// heap, the 384 KiB chunk of L2MEM at 0x4FF40000 (`05-zig-p4`'s linker script /// owns the split; the 128 KiB above it measured as L2 cache rather than /// memory). Unconditional and not profile-derived, because it is a fact about /// the silicon rather than a budget this build chose — a desktop build that /// wants to know what the board affords is asking exactly this question, which /// is what the memory tests in pardes.zig do with it. pub const board_heap_bytes = 384 * KiB; /// How many effects the ring holds. SHRUNK, not moved to the heap, on the /// board: `pump` drains this to empty on every iteration with an /// unconditional `while (nextEffect())` — including effects `perform` itself /// queues — so no capacity can deadlock the drain, and the only question a /// capacity answers is how big a single-pump BURST may be before `emit` /// refuses the overflow. The one producer that can burst is `emitWrite`, /// which chunks arbitrary bytes into 64-byte `.write` effects for a pty, and /// a build with `terminal_panes == false` has no pty to write to. Everything /// else queues O(1) effects per event, and `in_q` holds at most 64 events per /// pump, so 128 leaves two effects per queued event. /// /// A 1.0625 MiB inline ring cannot live in the board's 384 KiB heap at all; /// 128 entries is 34 KiB. NOTE THE BEHAVIOUR CHANGE: `emit` has always /// refused (not evicted) once full, so on the board a burst larger than 128 effects /// now drops its tail where 4096 would have held it — reachable only through /// `emitWrite`, i.e. only if a pty ever appears on this platform. pub const effect_cap = if (board) 128 else 4096; /// Bytes of a pane's pty write that may WAIT in the core when `effect_cap` /// chunks are already queued. `emitWrite` splits a burst into fixed 64-byte /// effects, so without this a paste larger than `effect_cap * 64` (256 KiB on /// a desktop) lost its tail silently — the ring refuses rather than evicts, /// which keeps queued bytes in order but cut the new ones off. The remainder /// parks here instead and `nextEffect` refills the ring as the host drains it, /// so a large paste is DELAYED rather than truncated. /// /// 4 MiB matches `tty.max_paste_bytes`, the largest burst a host can hand the /// core in one event, so the bound is the one the producer already enforces. /// Zero on the board: no ptys means no `emitWrite`, and the allocation this /// would justify cannot live in 384 KiB anyway. A zero cap parks nothing and /// restores the old refusal exactly. pub const pending_write_cap: usize = if (board) 0 else 4 << 20; /// Rows the per-pane soft-wrap map covers. `wrapWidth` refuses to wrap a pane /// taller than this (it reads the array's own length), so shrinking it cannot /// truncate a map — a taller pane renders unwrapped, exactly as documented on /// `Pane.wrap_line`. A serial console is not 128 rows tall. pub const wrap_rows = if (board) 128 else 256; /// A shell's reported working directory, owned inline by the pane. Zero-sized /// where there are no processes to report one: the `PdfSlot` rule, applied to /// a capacity whose sole producer (`Pardes.setCwd`, fed by a pty's prompt /// report) does not exist without terminal panes. `setOwnedCwd` clamps, so a /// zero cap reads as "no directory known" — which is the truth here. /// /// Keyed on the profile rather than on `pardes.terminal_panes`, which this /// file must not import (the core imports the table, not the other way round). /// The two agree by construction: the board is the only build with no ptys. pub const cwd_buf_cap = if (board) 0 else 1024; /// EDIT BOUNDARIES REMEMBERED PER FILE PANE. Every entry owns a gpa copy of /// the WHOLE file, so this number multiplies heap, not just the pane: 256 of /// them is not a bound a 384 KiB board could ever reach anyway. `pushHistory` /// evicts and frees the oldest once full, so the smaller ring loses the /// deepest undo steps and nothing else — no truncation, no dropped edit. pub const undo_max = if (board) 16 else 256; /// How many message-row lines the session keeps for `Messages`, and one of the /// bigger fixed costs on `Pardes`: an entry is 262 bytes, so 128 of them is /// 32.75 KiB that is allocated whether or not anybody ever reads it. That is /// 8.5% of the board's whole 384 KiB heap and about the size of its effect /// ring, so the board takes sixteen — enough that a failure you looked away /// from is still there, which is the whole point, and not enough to matter /// beside the panes. This belongs here rather than in config.zig for exactly /// the reason the file's header gives: it is a board-shaped capacity. pub const message_log = if (board) 16 else 128; /// Bounds the only user-editable, schema-owned tag fragment. It IS the storage /// bound: `Pane.tag_tail` is `[max_tag_tail]u8`, and every writer (appendTag, /// tagInsert, restoreDumpTail, the acmefs `tag` file) refuses input that does /// not fit rather than truncating it, so the schema limit and the buffer can /// never disagree — a dump reader can reject data before copying it into a /// pane. /// /// 512 on the P4 firmware. A tag is ONE line — a pane's path plus its command /// words — and 4 KiB of it is 4 KiB per pane out of a 384 KiB heap. A serial /// console is 80 columns; 512 is six of those. pub const max_tag_tail: usize = if (board) 512 else 4096; /// HOW LONG A HOST-SUPPLIED ABSOLUTE PATH MAY BE, and the only reason that /// record was ever kilobytes: the shell a native host resolved, the font file /// a native picker returned, and (in pardes.zig) the one watched theme file. /// All three name something on a FILESYSTEM, and all three are retained /// inline because the core has no allocator at the point they arrive. /// /// Fixed at 4095 wherever a filesystem exists — deliberately NOT derived from /// std.fs PATH_MAX, which web has no answer for, and 4095 rather than 4096 so /// the macOS C bridge's NUL fits without a second, subtly different limit at /// that boundary. Zero on the P4 firmware, which has no filesystem, no /// processes to spawn a shell for and no font picker: `Text(0)` is a /// zero-sized field whose `set` refuses every non-empty path, so the three /// producers report failure instead of storing 12 KiB nothing can fill. pub const host_path_cap: usize = if (board) 0 else 4095; /// WHETHER PARDES'S OWN SOURCE IS EMBEDDED — the source_manifest allowlist, /// which is a capacity spelled as rodata rather than as a number. /// /// ON THE P4 the allowlist is EMPTY, and that is the whole difference: the /// table is ~0.95 MiB of rodata against a 1.5 MiB flash partition, and the /// firmware's filesystem is the serial host's, reached through the Host /// vtable. The API is unchanged — `all` is a zero-length array and `find` /// answers null — so every caller compiles identically and simply finds /// nothing embedded. pub const embedded_sources = !board; /// Bytes per dumped row, and it is a different number on the board. /// /// `hexdump -C`'s sixteen is the layout everyone can already read, and it needs 79 columns: ten for /// the address, forty-eight for the hex, a gap, and the eighteen-column ASCII gutter. The P4 drives /// a 56-column grid of which seven go to the line-number gutter, so a sixteen-byte row wraps onto a /// second display line and the columns stop lining up - which is the entire value of the layout. /// /// Eight fits in 46 and keeps every property that matters: address on the left, fixed-width hex /// columns, ASCII on the right, and a gap at the halfway mark because the eye counts in fours and /// eights rather than in sixteens. /// /// NO `0x` ON WHAT THESE WORDS PRINT, which is where two of those columns came from. It reads no /// worse - every number here is hex, there is no other kind, and the words refuse a decimal one - and /// it buys something better than the width: an address in a dump can now be typed straight back into /// a `Peek` without editing it, because bare hex is exactly what the parser wants. Output that is /// valid input is worth more than a prefix restating what the whole file already says. pub const hexdump_row_bytes: u32 = if (board) 8 else 16; /// Three tiers, because the address space differs by four orders of magnitude. /// `reduced_target` is the browser: a wasm linear memory it grows on demand, so /// the static reservations are megabytes rather than tens. /// /// `board` is ESP32-P4 firmware, and its tier is deliberately ALL FALLBACK. Every /// capacity here is a `StackFallbackAllocator`'s buffer, which is a static and /// therefore lands in `.bss` — and on the P4 `.bss`, `.data` and the stack all /// share ONE 240 KiB chunk of L2MEM at 0x4FF03000, while the heap the fallback /// allocator hands out is the separate 384 KiB chunk at 0x4FF40000 - the 128 KiB /// above that measured as L2 cache rather than memory. A /// megabyte-shaped reservation here would not fit, and every byte that did fit /// would be taken from the stack's neighbourhood to duplicate memory the heap /// already has. So the buffers exist only because the type requires one: 4 KiB /// absorbs the small churn, and everything else spills to the real heap on the /// first allocation. pub const arena = struct { pub const pardes = if (board) 4 * KiB else if (reduced_target) 8 * MiB else 32 * MiB; pub const frame = if (board) 4 * KiB else if (reduced_target) 4 * MiB else 16 * MiB; // Zero is legal and always spills, which is exactly what an arena for a // compiled-out subsystem should do. `StackFallbackAllocator(0).buffer` is // `[0]u8`; `get()` inits the FixedBufferAllocator over an empty slice, so // `FixedBufferAllocator.alloc` fails every nonzero request and `alloc` // falls through to `self.fallback_allocator.rawAlloc`, while `ownsPtr` over // an empty range is false for every pointer so `resize`/`remap`/`free` // route to the fallback too. See lib/std/heap.zig, StackFallbackAllocator. pub const tree_sitter = if (board) 0 else if (reduced_target) 4 * MiB else 16 * MiB; pub const image = if (board) 0 else if (reduced_target) 64 * KiB else 32 * MiB; pub const pdf = if (board) 0 else if (reduced_target or !config.mupdf) 64 * KiB else 64 * MiB; }; // THE REGRESSION GUARD for the refactor that created this file: nine caps // moved out of nine files, and the one thing that must not have changed is // what a tty/gui/macos build gets. Spelling the historical desktop numbers // here as literals is the point — a derivation would agree with itself. test "board limits: a desktop build keeps exactly its historical capacities" { if (board or reduced_target) return error.SkipZigTest; try std.testing.expectEqual(4096, effect_cap); try std.testing.expectEqual(256, wrap_rows); try std.testing.expectEqual(1024, cwd_buf_cap); try std.testing.expectEqual(256, undo_max); try std.testing.expectEqual(@as(usize, 4096), max_tag_tail); try std.testing.expectEqual(@as(usize, 4095), host_path_cap); try std.testing.expect(embedded_sources); try std.testing.expectEqual(@as(u32, 16), hexdump_row_bytes); // The arena tier a desktop gets is the third one, so it is only the // historical desktop tier when the target is not itself reduced. if (reduced_target) return; try std.testing.expectEqual(32 * MiB, arena.pardes); try std.testing.expectEqual(16 * MiB, arena.frame); try std.testing.expectEqual(16 * MiB, arena.tree_sitter); try std.testing.expectEqual(32 * MiB, arena.image); try std.testing.expectEqual(if (config.mupdf) 64 * MiB else 64 * KiB, arena.pdf); }