diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-26 02:06:49 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-26 02:06:49 -0300 |
| commit | ddee44e2a70e8b9be7b2778390059de6279dd454 (patch) | |
| tree | b146419e9d13b550bfd850327275b4fafca4e59d /experiments | |
| parent | c64012e35c6370804f8588a62964e358631323b9 (diff) | |
| download | esp32p4-ddee44e2a70e8b9be7b2778390059de6279dd454.tar.gz esp32p4-ddee44e2a70e8b9be7b2778390059de6279dd454.zip | |
Report: Experiment 5, the three bugs, and a resize that fixed itself
Adds the geometry curve - 0.87 us per cell, measured across nine grids from 40x12 to
140x42 - and the two ways it ends: 200x60 refused by the linker because '.bss' cannot hold
the shadow grid, 160x48 linking and then trapping. Neither is a heap problem any more,
which is the finding: the heap has 336 KB spare while '.bss' runs out, because vaxis's two
unread grids stopped being allocated.
Also the three bugs the latency work walked straight past, none of which any number in this
document could see: keystrokes lost while the transmitter was full, every escape sequence
shredded because a lone ESC arrives alone on a 115200 line, and a mouse that was never
enabled. The middle one had been there since the port began and explains the other two -
a click arriving as ten key presses, with the '0' among them moving the cursor to column
zero, which is what made it look like a coordinate bug.
And Table 15 changes from 'not fixed' to 'fixed, and not on purpose'. The vx.resize failure
that three experiments characterised and left alone was an allocation of vaxis's two grids;
the emitter stopped needing them, so the path that failed is gone rather than repaired.
Verified on the die - an in-band report re-lays the screen from 56 columns to 30 and back.
Recorded as a consequence rather than as a fix, because nobody set out to repair it.
Eleven pages.
Diffstat (limited to 'experiments')
| -rw-r--r-- | experiments/pg-09.png | bin | 303363 -> 302306 bytes | |||
| -rw-r--r-- | experiments/pg-10.png | bin | 66687 -> 220988 bytes | |||
| -rw-r--r-- | experiments/pg-11.png | bin | 0 -> 255697 bytes | |||
| -rw-r--r-- | experiments/report.typ | 105 |
4 files changed, 97 insertions, 8 deletions
diff --git a/experiments/pg-09.png b/experiments/pg-09.png Binary files differindex 704b63d..d076622 100644 --- a/experiments/pg-09.png +++ b/experiments/pg-09.png diff --git a/experiments/pg-10.png b/experiments/pg-10.png Binary files differindex 9a76b56..ec8372b 100644 --- a/experiments/pg-10.png +++ b/experiments/pg-10.png diff --git a/experiments/pg-11.png b/experiments/pg-11.png Binary files differnew file mode 100644 index 0000000..26069e3 --- /dev/null +++ b/experiments/pg-11.png diff --git a/experiments/report.typ b/experiments/report.typ index 1601cef..0403a1a 100644 --- a/experiments/report.typ +++ b/experiments/report.typ @@ -839,23 +839,112 @@ here — the grapheme walk, the per-cell writes, the fill — returned between n 21 µs, because the cost is not in any one of those but in doing the whole frame again. Nothing in this repository can avoid work pardes has already done. += Experiment 5 --- how big can the window be + +The 40×12 grid was never a decision about the screen. It was memory, and the constraint was +four copies of every cell: vaxis keeps a `Screen` and an `InternalScreen`, pardes keeps its +`Surface`, and the shell keeps a shadow of it to diff against. Two of those four became dead +weight the moment the emitter started writing the wire itself — allocated every session, +never read. Sizing them to *one cell* removes the memory ceiling outright: the heap now +reports 336 KB free at every geometry tried, including ones that used to fail outright. + +What is left is the honest limit. Every frame walks the whole grid, so the round trip is +linear in the cell count at *0.87 µs per cell*, measured. + +#figure( + table( + columns: (auto, auto, auto, 1fr), + align: (left, right, right, left), + stroke: none, + table.hline(), + table.header([geometry], [cells], [round trip], []), + table.hline(stroke: 0.5pt), + [40×12], [480], [3 628 µs], [the old ceiling], + [*56×14*], [*784*], [*3 930 µs*], [*the new default: +63% area, +40% width*], + [56×16], [896], [3 965 µs], [over 4 ms on the phase-randomised instrument], + [60×18], [1 080], [4 114 µs], [], + [64×20], [1 280], [4 281 µs], [], + [80×24], [1 920], [4 809 µs], [the classic terminal, one flag away], + [100×30], [3 000], [5 743 µs], [], + [120×36], [4 320], [6 923 µs], [], + [140×42], [5 880], [8 310 µs], [the largest that runs at all], + [160×48], [7 680], [---], [links, then traps at boot], + [200×60], [12 000], [---], [does not link], + table.hline(), + ), + caption: [`-Dp4-cols` / `-Dp4-rows`, because none of this is a constant. The two failures at + the bottom are different failures and both are worth naming: 200×60 is refused by the + linker — `.bss will not fit in region l2mem, overflowed by 76 036 bytes`, that `.bss` being + the shadow grid, sized at compile time — while 160×48 links and then traps, the same + pressure arriving at runtime as a collision instead of as a diagnostic. Neither is a heap + problem any more, which is the surprise: the heap has 336 KB spare while `.bss` runs out.], +) + += Three bugs the latency work walked straight past + +All three were invisible to every number in this document, and two of them had been there +since the port began. + +*Keystrokes were being lost.* Reported as "a key is stuck and is only sent when I send a new +event". It was neither stuck nor late. The loop is read, apply, render, write, and the write +blocks while the transmit FIFO is full — real backpressure, and it should stay, because half +an escape sequence leaves the host terminal in the wrong colour for the rest of the session. +But nothing drained the RECEIVE FIFO during that wait, and that FIFO is 128 bytes, or 11 ms +of wire. Measured by counting what the firmware's loop actually took off the UART: a 200-byte +burst arrived as 197, a 300 as 257, a 600 as 478. Draining the receiver inside the wait fixes +the first window; a second one — applying the input, which costs 44 µs a keystroke on an empty +line and 63 µs at 640 characters — needed the loop to hand the editor eight bytes at a time +instead of a hundred and twenty-eight. With both, 4 096 bytes in a single write arrive intact, +and past that the loss is *counted* rather than silent. + +*Every escape sequence was being shredded.* `vaxis.Parser` resolves a buffer containing +nothing but `0x1b` as the Escape key — deliberately, and correctly for a terminal, where the +kernel hands over a whole sequence in one read. This wire hands over one byte at a time, 87 µs +apart, so the first byte of every sequence arrived alone and was resolved as Escape and the +rest arrived as ordinary keys. A mouse click came through as ten key presses, and the `0` +among them is "go to column zero" in normal mode — which is exactly where the cursor kept +landing, and why this looked at first like a coordinate bug. Arrow keys, function keys and the +host's own in-band resize reports were all being taken apart the same way. The shell now holds +a lone ESC for 10 ms, two orders of magnitude longer than the wire needs and imperceptible to +the person pressing it. + +Finding it took instrumenting the ABI to print the tag of every event the shell applied. Ten +`key_press` where one `mouse` belonged is not something any amount of reading the coordinate +arithmetic would have shown, and it had been read twice. + +*The mouse was never enabled.* With the sequences intact, that was one line: the shell already +handled mouse events, because it mirrors the tty shell. Spelled out rather than taken from +`vx.setMouseMode`, which asks for `1002;1003;1004;1006` — 1003 being any-motion tracking, a +report per cell the pointer crosses with no button held. On this line that is dozens of +15-byte reports for one sweep, arriving as input the editor must parse while it paints, and +arriving whether anyone wants it or not. The console thins drag reports to 25 a second on the +way in, newest-wins, which is right for motion and only for motion: where the pointer passed +through is not information an editor can use. + + #figure( table( columns: (auto, 1fr), align: (left, left), stroke: none, table.hline(), - table.header([found], [not fixed]), + table.header([found], [outcome]), table.hline(stroke: 0.5pt), - [`vx.resize` fails], [A runtime geometry change takes its allocation-failure path, - restores the previous size and returns: 80 bytes go out where 1 392 should, and - the screen keeps its old shape. Reproduced with the shadow grid compiled out, so - it predates it. The board has one geometry per session — which is also why the - staleness test compares two firmwares rather than forcing a repaint by resizing.], + [`vx.resize` fails], [A runtime geometry change took its allocation-failure path, restored + the previous size and returned: 80 bytes went out where 1 392 should, and the screen kept + its old shape. Reproduced with the shadow grid compiled out, so it predated it, and left + unrepaired through three experiments --- which is also why the staleness test compares two + firmwares rather than forcing a repaint by resizing. + + *Fixed, and not on purpose.* The failing allocation was vaxis's two grids, and the emitter + stopped needing them; there is nothing left to reallocate, so the path that failed is gone + rather than repaired. Verified on the die: an in-band report re-lays the screen from 56 + columns to 30 and back. The pleasant kind of consequence, and worth recording as a + consequence rather than as a fix --- nobody set out to repair it.], table.hline(), ), - caption: [A bug the optimisation work walked into and characterised but did not - repair.], + caption: [A bug that three experiments characterised and did not repair, removed by a change + aimed at something else entirely.], ) = Ranked by measured benefit |
