<feed xmlns='http://www.w3.org/2005/Atom'>
<title>esp32p4.git/tools/bench_main.zig, branch main</title>
<subtitle>ESP32-P4</subtitle>
<id>https://git.0x4200.cafe/esp32p4.git/atom?h=main</id>
<link rel='self' href='https://git.0x4200.cafe/esp32p4.git/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/esp32p4.git/'/>
<updated>2026-09-16T14:28:40Z</updated>
<entry>
<title>Make the toolchain a package another build can drive, and move the editor's glue to the editor</title>
<updated>2026-09-16T14:28:40Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-08-26T16:28:33Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/esp32p4.git/commit/?id=b42ecaed412be2e30b9e780eb7c9e46e1535f26f'/>
<id>urn:sha1:b42ecaed412be2e30b9e780eb7c9e46e1535f26f</id>
<content type='text'>
</content>
</entry>
<entry>
<title>Offer the editor the board's pads, and check one flips on the die</title>
<updated>2026-08-26T15:40:24Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-08-26T15:40:24Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/esp32p4.git/commit/?id=38bb891dd6bd0074894cbfedbf9185e303cc549e'/>
<id>urn:sha1:38bb891dd6bd0074894cbfedbf9185e303cc549e</id>
<content type='text'>
## The board half of Gpio

`pardes_p4_init` gained a `GpioFn` and the ABI version went to 2, which is what turns a mixed
pair of builds into a refusal to boot rather than five arguments read as six.

`gpioToggle` is four lines over `hal.gpio`: configure the pad as a readable output, read the level
it is driving, drive the other one, read it again. It is deliberately here and not in the editor.
A toggle is not a write to GPIO_OUT - `configureOutput` sets the IO MUX function, the GPIO matrix
route, the drive strength, the input buffer and the pulls, then the output enable, indexed by a
per-pin table - and that code is already in this repo, already the call `src/main.zig` blinks with,
and already checked against ESP-IDF's headers by `zig build diff`. The editor object gets a
function pointer instead of a second copy nobody tests.

`getDrivenLevel`, not `getLevel`: the answer is the level the board is driving, which is defined
for every pin. The pad's own level is what the outside world says, and on an unconnected header pin
that is noise. The input buffer is enabled anyway so `Peek` of GPIO_IN_REG can be compared to it.

## A fifth hardware check

`p4-bench --check` runs `Gpio 33` three times and requires 0-&gt;1, 1-&gt;0, 0-&gt;1.

The alternation is the oracle, not either answer. `0-&gt;1` alone is what a firmware printing a
hardcoded string would also say; two runs that disagree can only come from a level that was stored
and read again. Three, so the third rules out an ordering coincidence. GPIO33 because
`src/oracle/ledc_cases.zig` already documents it as a free pin on this board's JP1 header - pin 21
on the diagram the editor now draws. GPIO20 is the blink demo's pin and may have a wire on it.

This is the only check here that crosses the whole seam: editor word, C ABI, HAL, pad, and the
level back out through the message row. Nothing smaller exercises the ABI at all.

## -Dtheme-animation forwarded

Same path as the geometry, for the same reason: baked into the object, wanted from here.

All five checks pass on the die; host tests green; the board is flashed with md5 932898f34ca1dc31.
</content>
</entry>
<entry>
<title>Set the grid from the firmware build, and check Poke against Peek on the die</title>
<updated>2026-08-26T14:12:04Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-08-26T14:12:04Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/esp32p4.git/commit/?id=adb23572bc2d7aae09cf8605825f6d48ca23ba4a'/>
<id>urn:sha1:adb23572bc2d7aae09cf8605825f6d48ca23ba4a</id>
<content type='text'>
## -Dcols / -Drows

The geometry is baked into pardes's object, and that object is built by the other
repository - so changing the grid was two commands in two directories, and the second one
silently linked whatever the first had left behind. That is the shape of mistake that ends
with a firmware whose grid is not the grid you think you flashed.

`-Dcols` and `-Drows` now drive the editor's own build before linking it. A nested
`zig build`, not a package dependency: the seam between these repositories stays a file,
for the reasons the comment above it has always given. This only reaches across to ask for
the file to be made a particular way, and it stands aside entirely when `-Dpardes-obj`
names an object explicitly, because then the caller has already said which one they want.

Verified by the segment table: `-Dcols=80 -Drows=24` takes the loaded segment from 20,408
to 49,944 bytes, which is the shadow grid going from 784 cells to 1,920.

## Poke and Peek, checked against each other

Two more checks in `p4-bench --check`, and they are checked against each other because
that is the only way to check either one without a second debugger: a Peek alone cannot
tell a correct read from a stuck one, and a Poke alone cannot tell a write from a no-op.

Both oracles were wrong before they were right, and both failures are worth recording.

The Escape check asked whether an `x` came back on the wire. That was true until the boot
buffer gained a line beginning "x selects a line", at which point a passing check began
failing for a reason that had nothing to do with Escape. It now reads the CURSOR COLUMN
after `0`, which the document cannot spell: 8 in normal mode, three further right if the
`0` had been inserted instead.

The Poke and Peek checks searched the raw byte stream for `deadbeef` and could not find it -
because this renderer emits only the cells that CHANGED, jumping between runs with absolute
cursor positioning, so a word on screen is frequently not a word on the wire. `stripAnsi`
removes the escapes first. The Poke assertion then had to be loosened from one phrase to
three facts, because the message row is 49 columns here and a wrapped line puts a row
boundary inside whichever phrase straddles it.

Four checks, all passing on the die:

    lone Escape still leaves insert mode      ok (cursor col 8)
    a click split byte-by-byte lands at 18    ok (cursor 3,18)
    Poke writes a word and reads it back      ok
    and a later Peek still finds it           ok
</content>
</entry>
<entry>
<title>Send mouse events over the wire, thinned, and check the two things only hardware can</title>
<updated>2026-08-26T04:42:35Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-08-26T04:42:35Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/esp32p4.git/commit/?id=cea01b8c08c30fa68fcbf18266937274a080507e'/>
<id>urn:sha1:cea01b8c08c30fa68fcbf18266937274a080507e</id>
<content type='text'>
## The console thins mouse reports

The board asks for DEC 1002, so the terminal reports presses, releases and motion while a
button is held. A press is one report; a DRAG is one report per cell crossed, each about a
dozen bytes. A hand crosses forty cells in a tenth of a second, which is ~500 bytes, which
is 43 ms of a 115200 line - and every one of those bytes is input the board must parse
while it is trying to paint the result of the previous one. Unthinned, a drag makes the
editor unusable for as long as the drag lasts and for a while after.

`MouseFilter` holds the newest motion report and drops the ones it supersedes, on a 40 ms
window: ~25 reports a second, about 2.6% of the line. Newest-wins is right for motion and
only for motion - where the pointer PASSED THROUGH is not information the editor can use,
since a selection is defined by where the drag began and where it is now, so an intermediate
report already stale by the time it reaches the wire is pure cost. Presses, releases and
wheel notches are never held: each one means something different and dropping one loses a
click.

Ordering is the part worth testing. A held report is released before any non-motion byte
that follows it, so a release cannot overtake the motion it ends, and a drag that stops
moving still delivers its final position when the window expires. The seven tests cover
those, a report split across a read boundary, a non-mouse escape sequence passing through
untouched, and - the one that would hurt most - a lone ESC not being swallowed, because
that is how you leave insert mode.

## Two hardware checks: p4-bench --check

Both are regressions that no host test can see and no latency number can show.

A lone Escape still leaves insert mode. The shell now holds a solitary ESC for 10 ms
because on this wire the first byte of every sequence arrives alone; if that hold ever
stops expiring, Escape stops working and the editor is unusable.

A click split byte-by-byte lands at the column clicked. This is the bug that made the mouse
look unimplemented, and it only appears when the bytes arrive separately - which the wire
does anyway, 87 us apart. The check sends them as ten separate writes with no gap, because
a gap longer than the hold would expire it and the check would be exercising nothing.

Two things this file deliberately does NOT check, both because a check that cannot fail
honestly is worse than no check. Input loss during transmit belongs to the deterministic
host test in `src/pardes/input_rescue.zig`, which loses 67 bytes with the fix removed and
needs no board. And every hardware oracle for it that was tried here was worse: the cursor
stops being reported past 160 characters because the wrapped line outgrows the viewport, and
a screen reconstruction cannot be rebuilt mid-session because the board only sends what
changed. Both false starts are recorded in the file so the next person does not repeat them.

The check also found its own bugs before it found any of the firmware's: 12 ms gaps between
the click's bytes expired the very hold it meant to test, `$` produces no frame when the
cursor is already at the end of the line, and reading the cursor after a press alone
measures the revert rather than the click.
</content>
</entry>
<entry>
<title>Bound the edit path's document scans, and find out they were never the problem</title>
<updated>2026-08-25T22:16:25Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-08-25T21:49:57Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/esp32p4.git/commit/?id=1935944a0352e9d176f0718063327e686ece1948'/>
<id>urn:sha1:1935944a0352e9d176f0718063327e686ece1948</id>
<content type='text'>
The board half: the -Dprof attribution that overturned the conclusion, its data, and
the report correction.

`-Dprof` adds two cycle-counter reads around `pardes_p4_input` and
`pardes_p4_render` and prints both. Off by default: it puts a line on the wire per
frame, which is the very resource being measured, so it answers "where did the 15 ms
go" and not "how fast is it".

It answered. Input is flat at ~220 us regardless of document size - 1.5% of a
keystroke - and the entire ~15 ms floor plus every microsecond of the per-character
slope live in `render`. The edit-path fix that the source reading implied (committed
next door in 02-pardes-code) is worth 20% on a 19 MB file and, measured here over 5
conditions x 7 trials, exactly 0% on this board.

`experiments/report.typ` gains Experiment 3 and a correction: Experiment 2's
mechanism claim was wrong, says so, and carries the disproof beside it. The ranked
recommendations are reordered with the renderer at #1.

Also here: `--sweep position` in p4-bench, which holds the document fixed at one
320-character line and moves only the cursor. Column 320 costs 33.9 ms and emits 28
bytes; column 0 costs 26.0 ms and emits 81. Latency and output size are inverted on
this board - the signature of a walk from the start of a line.
</content>
</entry>
<entry>
<title>A measuring instrument, and what it says about where the latency goes</title>
<updated>2026-08-25T21:41:18Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-08-25T21:41:18Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/esp32p4.git/commit/?id=1cef9c2e4bd873ebe13f5df635899231bcc467d2'/>
<id>urn:sha1:1cef9c2e4bd873ebe13f5df635899231bcc467d2</id>
<content type='text'>
"Too slow for interactive use" is a real complaint and not a number. This adds the
number, and the number says the wire is innocent.

## The instrument

`tools/perfproto.zig` is a small framed protocol - "P4", op, length, CRC-32 of the
payload, payload - shared VERBATIM by the host tool and `examples/uartperf.zig`, so
a frame one writes and the other parses cannot drift. It is imported as a module by
both, not copied.

The checksum is the whole point. RX overrun on this UART is undetected in hardware
and uncounted in the driver, so a byte that never arrived is indistinguishable from
a late one; a throughput figure that is not checksummed is a guess about how fast
data was corrupted. `sink` accumulates a CRC over every payload byte the board
received and `report` hands it back, so the host can prove that what arrived is
what it sent.

`tools/rtt.zig` is the two timing functions: `roundTrip` and `measure`. Round trip
is to the FIRST response byte, deliberately. A renderer that starts drawing in 8 ms
and finishes in 130 ms feels immediate; one that thinks for 130 ms and then draws in
8 ms feels broken; waiting for the wire to fall quiet cannot tell them apart. Time
to the last byte is recorded separately as `settle`. Microseconds, because at 115200
one byte is 87 us and a millisecond clock quantises the answer into buckets eleven
bytes wide.

`tools/bench_main.zig` is `p4-bench`: `--link` for the ceiling, `--editor` for how
much of it the editor uses, `--sweep` for one controlled variable at a time with
`--csv` raw per-trial output.

## What it measured

The link is essentially perfect: 11,496 B/s up and 11,413 B/s down, 99.8% of
capacity in both directions, CRC verified over 32,768 B each way, zero corruption.
Typing at 6 to 100 keys/s loses nothing and never uses more than 9% of the wire, so
H5 - "typing loses input" - is refuted.

Latency is compute per input event, not transmission. A 40-byte motion and a
206-byte insert-and-escape cost the SAME round trip to within 0.3 ms, across a
five-fold range of output. That is why raising the baud cannot fix typing: there is
almost no wire in it.

And an edit costs the whole document. Round trip against characters already in the
line is a straight line at 54.3 us per character per keystroke - 17.0 ms at an empty
line, 25.6 ms at 160. On a ~90 MHz core that is ~5,000 cycles per character, far
more than a copy alone, so the full-buffer copy the source does is accompanied by at
least one more full pass.

One controlled intervention: building the editor object ReleaseFast instead of
ReleaseSmall cuts the fixed cost 13% and the per-character cost 36%, for 35% more
flash (809,536 B of a 1,536,000 B partition). Its advantage grows with the document.
Nothing else measured comes close to that ratio.

## Three bugs found while building it

The responder printed garbage and looked dead: it read `.rodata` before evicting the
bootloader's stale cache lines. `flushFlashCache` moved from `src/pardes/app.zig` to
`soc.zig` with its measured evidence, since every application that touches `.rodata`
after hand-over needs it and exactly one file knew that.

Then it booted, printed its marker and went silent after ten seconds:
`rst:0x10 (CHIP_LP_WDT_RESET)`. The bootloader arms the RTC watchdog and expects the
application to take it over. Only the editor ever did.

`serial.Port.drain()` drains INPUT, not output - so timing a transfer to it reported
202% of the wire's capacity and ate the reply. Added `flushOutput` (tcdrain), named
so the two cannot be confused again.

Also: Zig 0.16 emits an explicit `+` for a non-negative SIGNED integer whenever a
width is given (std/Io/Writer.zig:1548-1559), which put a `+` in front of every
number in the first tables.

## The report

`experiments/report.typ` reads the raw CSVs and computes its own figures, so a
re-run changes the document instead of contradicting it. It states five hypotheses,
settles each against one experiment, and is explicit about the one that failed: the
geometry sweep is confounded, because characters accumulated across conditions and
the length experiment then proved that matters. It is reported as unsupported rather
than dressed up as a result.
</content>
</entry>
</feed>
