diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-25 13:01:13 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-25 17:13:54 -0300 |
| commit | 939e3a7288d6782139cac36f091ccd1d41cdfc0a (patch) | |
| tree | 6b7b86c2ba4ade9f34c8f174354425a5d31a150a /src/allocators.zig | |
| parent | e5f9e172330bc4500995ddd5954ed94f94e07af6 (diff) | |
| download | pardes-939e3a7288d6782139cac36f091ccd1d41cdfc0a.tar.gz pardes-939e3a7288d6782139cac36f091ccd1d41cdfc0a.zip | |
A fourth platform: pardes as ESP32-P4 firmware, bytes in and bytes out
`-Dplatform=p4 -Dtarget=riscv32-freestanding` emits a single freestanding OBJECT
exporting a seven-function C ABI, not an executable. The board's toolchain
(../05-zig-p4) owns `_start`, the linker script and the UART driver and links this
in. The seam is bytes rather than types, so neither side can accidentally depend
on the other's internals, and a signature that drifts fails at link time.
The serial line is the whole of the I/O. `src/p4.zig` drives vaxis unchanged over
it: the renderer is a byte writer and `queryTerminalSend` is a byte writer, so the
terminal emulator on the host answers the capability handshake and the firmware
sees a real terminal. Measured going out over the wire on attach: alt screen,
in-band resize, cursor report, kitty keyboard, kitty graphics, DA1.
THREE WORDS EXIST ONLY HERE. `src/board_memory.zig` implements `Peek`, `Poke` and
`Hexdump`, gated on `builtin.os.tag == .freestanding and !isWasm()` - derived from
the TARGET, because they are a property of running with no OS under you rather
than a product option, and because wasm is freestanding too and is exactly what
must be excluded: in a browser an address is an offset into the linear memory this
editor's own heap lives in. Every access goes through `*allowzero volatile`: a
peripheral register is not memory, and address 0 is an ordinary unmapped address
on this bus. One 4 KiB cap per command, set by the console rather than the memory -
an unbounded dump would wedge the only console the board has for eleven hours.
Measured on ESP32-P4 rev v1.3 silicon, driven from a host terminal:
Peek 0x501101a4 0x0e63ce71, then 0xaeaa6919 on a second read - the
RNG register, so the volatile loads are not folded
Poke 0x5011002c 0xdeadbeef LP_STORE0; a later Peek returned 0xdeadbeef
Hexdump 0x5011002c 32 16 bytes a row, hex columns and an ASCII gutter
Peek 0x50110001 `peek: MisalignedAddress` on the message row
That last line is the one that matters. A misaligned 32-bit access traps, and a
trap in firmware is a watchdog reset that takes the session with it, so the check
that turns it into a message is the reason the file is hand-written rather than a
generic reader.
BARE METAL BOOTS AN EMPTY OUTPUT BUFFER. Every other boot layout in `init` makes a
shell, and on this platform that is not a preference but an impossibility: nothing
to fork, no pty to give a terminal pane. Booting one anyway produced precisely what
that describes - a pane whose tag ends in `Filter`, no gutter, no buffer, and every
keystroke vanishing into the Fallback's silent pty. An output buffer is also what
the platform's own words want, since Peek, Poke and Hexdump each fill one.
Sized for the board rather than for a desktop:
* `allocators.zig` gains a p4 tier that is ALL fallback - every capacity is zero,
so each arena spills immediately to the 384 KiB heap the firmware hands over,
and no megabyte-shaped static reservation lands in `.bss`.
* `source_manifest.zig`'s allowlist is EMPTY on p4. The table is ~0.95 MiB of
rodata against a 1.5 MiB flash partition; the firmware's filesystem is the
serial host's, through the Host vtable.
* The grid is clamped and the clamp is measured, not guessed: every cell is paid
for four times (vaxis Screen + InternalScreen, pardes Surface + previous_cells),
so 40x12 fits and 80x24 exhausts the heap during `Pardes.init`.
* `Vaxis.resize` deinits both screens before allocating replacements, so a failed
resize leaves vaxis rendering nothing. The p4 shell keeps the previous geometry
on failure instead of leaving a half-applied one.
Also here: `output_pane_integration_test.zig` had an exhaustive switch over
`Platform` that adding `.p4` left unhandled, which broke `zig build unit-test`
outright - the native test binary is the one consumer no platform build compiles.
346 tests pass again.
Diffstat (limited to 'src/allocators.zig')
| -rw-r--r-- | src/allocators.zig | 33 |
1 files changed, 28 insertions, 5 deletions
diff --git a/src/allocators.zig b/src/allocators.zig index 7a8433e8..cf024a0b 100644 --- a/src/allocators.zig +++ b/src/allocators.zig @@ -4,16 +4,39 @@ const config = @import("pardes_config"); const Allocator = std.mem.Allocator; const debug_enabled = builtin.mode == .Debug; +/// 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. +/// +/// `p4` 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. +const p4 = config.platform == .p4; const reduced_target = config.platform == .web or builtin.os.tag == .freestanding; const KiB = 1024; const MiB = 1024 * KiB; const capacities = struct { - const pardes = if (reduced_target) 8 * MiB else 32 * MiB; - const frame = if (reduced_target) 4 * MiB else 16 * MiB; - const tree_sitter = if (reduced_target) 4 * MiB else 16 * MiB; - const image = if (reduced_target) 64 * KiB else 32 * MiB; - const pdf = if (reduced_target or !config.mupdf) 64 * KiB else 64 * MiB; + const pardes = if (p4) 4 * KiB else if (reduced_target) 8 * MiB else 32 * MiB; + const frame = if (p4) 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. + const tree_sitter = if (p4) 0 else if (reduced_target) 4 * MiB else 16 * MiB; + const image = if (p4) 0 else if (reduced_target) 64 * KiB else 32 * MiB; + const pdf = if (p4) 0 else if (reduced_target or !config.mupdf) 64 * KiB else 64 * MiB; }; pub const Allocators = struct { |
