summaryrefslogtreecommitdiff
path: root/build.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-26 01:13:28 -0300
committerGabriel Schneider <[email protected]>2026-08-26 01:13:28 -0300
commite1526395233aad15dc9e2fdb49d5842de3e7e77b (patch)
treea5ff0bdaf693ab2189ab7ea4a0219aa9262e26af /build.zig
parent110eeeb80181b990901c22d28fa4fd41dd10cdd7 (diff)
downloadesp32p4-e1526395233aad15dc9e2fdb49d5842de3e7e77b.tar.gz
esp32p4-e1526395233aad15dc9e2fdb49d5842de3e7e77b.zip
Drain the receiver while the transmitter is full: 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 - it was gone, and a later frame repainting those cells is what made it look like it arrived eventually. The loop is read, apply, render, write, and `uart.write` blocks while the transmit FIFO is full. That wait is real backpressure and should stay: dropping 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 it, and that FIFO is 128 bytes - 11 ms of wire at 115200. Measured on the die, typing a burst in one host write and counting what the firmware's loop actually took off the UART: burst before after after + chunked input 128 128 128 128 200 197 200 200 300 257 300 300 600 478 600 600 1200 - 1200 1200 2400 - 2316 2400 4096 - 3611 4096 Two windows had to close, and the second was only visible once the first was shut. `input_rescue.pump` drains the receiver on every iteration of the wait for transmitter room. That is the big one, and it is the whole reason this policy lives in its own file: `uart.zig` cannot be tested without the chip because every line of it is an MMIO access, while `pump` takes its port as `anytype` and runs against a fake with a two-byte transmit FIFO and an eight-byte receive FIFO in `zig build test`. The fake models the receive FIFO the way the hardware behaves - a byte arriving into a full FIFO is simply gone - so the test fails by 67 lost bytes with the rescue removed, which is the die's 88-of-200 in miniature. It also caught a flaw in its own first draft: a fake whose transmit FIFO drains as fast as it fills never blocks, so `pump` never waits and the test proves nothing. The second window was APPLYING the input. A keystroke costs 44 us on an empty line and 63 us at 640 characters, so handing the editor a full 128-byte batch is up to 8 ms in which nothing drains the receiver - against 11 ms of FIFO. The loop now feeds the editor eight bytes at a time and rescues between chunks. Splitting a burst at an arbitrary byte is already safe, because `pardes_p4_input` keeps whatever it could not parse; that is how it survives an escape sequence split across two UART reads. One render still happens per loop iteration, so this costs no extra wire. Eight rather than thirty-two by measurement: 32 left 2400 and 4096 lossy, 8 does not. Beyond 4096 bytes in one burst the editor genuinely cannot keep up, and the ring reports what it abandoned instead of losing it silently - `rxdrop` in the PROF line, alongside a running count of received bytes. That counter is the other lesson here: the first attempt at measuring this counted characters on the reconstructed screen, which cannot distinguish "never arrived" from "arrived but off the edge of the viewport", and it disagreed with the hardware in both directions. No cost to latency: round trip median 3687 us over 60 trials against 3687 before, maximum 3866, screen byte-identical to the vaxis reference, host tests green.
Diffstat (limited to 'build.zig')
-rw-r--r--build.zig16
1 files changed, 14 insertions, 2 deletions
diff --git a/build.zig b/build.zig
index 37156ca..a7e9edb 100644
--- a/build.zig
+++ b/build.zig
@@ -99,7 +99,6 @@ pub fn build(b: *std.Build) void {
if (oracle) readPeripheralsLd(b, registers.idf_path) else null,
));
-
// ---------------------------------------------------------------- the application
const options = b.addOptions();
options.addOption(u8, "led_pin", led_pin);
@@ -330,7 +329,6 @@ pub fn build(b: *std.Build) void {
app.link_function_sections = true;
app.link_data_sections = true;
-
// One install step for the ELF, reachable two ways: `zig build elf` on its own (handy when
// debugging the image builder) and `-Delf` to get it alongside the image.
const elf_only = b.addInstallArtifact(app, .{});
@@ -502,6 +500,20 @@ pub fn build(b: *std.Build) void {
});
test_step.dependOn(&b.addRunArtifact(console_tests).step);
+ // The input-rescue policy: drain the receiver while spinning on a full transmitter. This is a
+ // decision rather than a register access, and it was a measured bug - a 200-byte burst typed
+ // into a long frame lost 88 bytes on the die - so it is worth a test that fails without the
+ // fix. `pump` takes its port as `anytype` precisely so the same code can run against a fake
+ // with a two-byte FIFO here and against UART0 on the board.
+ const rescue_tests = b.addTest(.{
+ .root_module = b.createModule(.{
+ .root_source_file = b.path("src/pardes/input_rescue.zig"),
+ .target = b.graph.host,
+ .optimize = .Debug,
+ }),
+ });
+ test_step.dependOn(&b.addRunArtifact(rescue_tests).step);
+
// The measurement protocol. These are the tests that keep a throughput number honest: that a
// frame round-trips, that a short read is "incomplete" rather than "invalid", that a lost byte
// mid-stream changes the CRC, and that the pattern generator does not repeat on a 256-byte