summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-26 11:12:04 -0300
committerGabriel Schneider <[email protected]>2026-08-26 11:12:04 -0300
commitadb23572bc2d7aae09cf8605825f6d48ca23ba4a (patch)
tree1afa309350d0d730342ccacbbd19ca1f5af9b4df /tools
parent55743cc5d564f6ef6d6f8a0eb1a614a74b021d3a (diff)
downloadesp32p4-adb23572bc2d7aae09cf8605825f6d48ca23ba4a.tar.gz
esp32p4-adb23572bc2d7aae09cf8605825f6d48ca23ba4a.zip
Set the grid from the firmware build, and check Poke against Peek on the die
## -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
Diffstat (limited to 'tools')
-rw-r--r--tools/bench_main.zig104
1 files changed, 100 insertions, 4 deletions
diff --git a/tools/bench_main.zig b/tools/bench_main.zig
index 6e453a8..18b204e 100644
--- a/tools/bench_main.zig
+++ b/tools/bench_main.zig
@@ -800,6 +800,57 @@ fn lastCup(bytes: []const u8) ?Cursor {
return found;
}
+/// Everything in `src` that a terminal would have PRINTED, with the escape sequences removed.
+///
+/// Searching the raw stream for a string does not work against this firmware, and the reason is the
+/// renderer: it 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. `deadbeef` came back as
+/// `dead`, a CUP, then `beef`, and a check looking for the whole token called a working Peek broken.
+fn stripAnsi(dst: []u8, src: []const u8) []u8 {
+ var n: usize = 0;
+ var i: usize = 0;
+ while (i < src.len) {
+ if (src[i] == 0x1b) {
+ i += 1;
+ if (i < src.len and src[i] == '[') {
+ i += 1;
+ // parameters and intermediates, then one final byte in 0x40..0x7e
+ while (i < src.len and src[i] >= 0x20 and src[i] < 0x40) i += 1;
+ if (i < src.len) i += 1;
+ } else if (i < src.len) i += 1;
+ continue;
+ }
+ if (n < dst.len) {
+ dst[n] = src[i];
+ n += 1;
+ }
+ i += 1;
+ }
+ return dst[0..n];
+}
+
+/// Type one of the editor's words into a fresh line, select it, and execute it. Returns everything
+/// the board sent back.
+///
+/// `o` opens a line below rather than reusing one, so this leaves the boot buffer readable instead of
+/// overwriting whatever it landed on. `x` selects the line and Tab executes the selection - the same
+/// two keystrokes a person uses, which is the point: this drives the editor rather than reaching
+/// behind it.
+fn runWord(port: *serial.Port, buf: []u8, word: []const u8) ![]u8 {
+ try port.write("\x1b");
+ _ = soak(port, buf, 150, 1500);
+ try port.write("o");
+ _ = soak(port, buf, 150, 1500);
+ try port.write(word);
+ _ = soak(port, buf, 200, 3000);
+ try port.write("\x1b");
+ _ = soak(port, buf, 200, 2000);
+ try port.write("x");
+ _ = soak(port, buf, 200, 2000);
+ try port.write("\t");
+ return soak(port, buf, 350, 4000);
+}
+
fn check(port: *serial.Port, o: Options, r: *Report) !void {
try ready(port, o);
var buf: [8192]u8 = undefined;
@@ -811,14 +862,26 @@ fn check(port: *serial.Port, o: Options, r: *Report) !void {
// every escape sequence arrives alone. The hold must expire, or Escape stops working and the
// editor is unusable. Typing `abc`, pressing Escape, then `x` deletes a character in normal
// mode; if Escape had been swallowed, the `x` would be inserted instead and show up on the wire.
+ // The oracle is the CURSOR, not the text. The first version asked whether an `x` came back on
+ // the wire, which was true until the boot buffer gained a line beginning "x selects a line" - and
+ // then a passing check started failing for a reason that had nothing to do with Escape. A cursor
+ // column cannot be spelled by the document.
+ //
+ // After `abc`, `0` in NORMAL mode goes to the start of the line, which is the gutter's width plus
+ // one. In insert mode it would insert a `0` and leave the cursor three columns further right. The
+ // two are not close.
try port.write("abc");
_ = soak(port, &buf, 150, 1500);
try port.write("\x1b");
_ = soak(port, &buf, 150, 1500);
- try port.write("x");
- const after_x = soak(port, &buf, 200, 1500);
- const escaped = std.mem.indexOfScalar(u8, after_x, 'x') == null;
- r.print(" lone Escape still leaves insert mode {s}\n", .{if (escaped) "ok" else "FAILED"});
+ try port.write("0");
+ const after_zero = lastCup(soak(port, &buf, 200, 2000));
+ const escaped = after_zero != null and after_zero.?.col <= 9;
+ if (after_zero) |at| {
+ r.print(" lone Escape still leaves insert mode {s} (cursor col {d})\n", .{
+ if (escaped) "ok" else "FAILED", at.col,
+ });
+ } else r.print(" lone Escape still leaves insert mode FAILED (no reply)\n", .{});
if (!escaped) failures += 1;
// 2. AN ESCAPE SEQUENCE SPLIT ACROSS READS PARSES AS ONE EVENT.
@@ -865,6 +928,39 @@ fn check(port: *serial.Port, o: Options, r: *Report) !void {
} else r.print(" a click split byte-by-byte lands at {d} FAILED (no reply)\n", .{want_col});
if (!at_col) failures += 1;
+ // 3. POKE WRITES AND PEEK READS IT BACK.
+ //
+ // The two words that touch the bus, checked against each other, which 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. Together the pair is falsifiable.
+ //
+ // 0x5011002c is an LP register that holds a written word - verified by hand on this die before
+ // it went into the boot buffer - so a round trip through it exercises the whole path: the hex
+ // parse with no `0x`, the alignment guard, the volatile store, and the volatile load.
+ //
+ // Typed rather than selected out of the boot buffer, because a check that depends on which line
+ // a command sits on breaks every time that buffer is edited.
+ var plain: [8192]u8 = undefined;
+ const wrote = stripAnsi(&plain, try runWord(port, &buf, "Poke 5011002c deadbeef"));
+ // Poke reports on the message row: `0x5011002c: wrote 0xdeadbeef, reads 0xdeadbeef`. The
+ // read-back is the interesting half - on MMIO it is frequently NOT what was written.
+ // Asserted as two facts rather than one phrase: the message row is 49 columns on this grid, so
+ // `0x5011002c: wrote 0xdeadbeef, reads 0xdeadbeef` can wrap, and a wrapped line puts a row
+ // boundary inside whichever phrase happens to straddle it.
+ const poked = std.mem.indexOf(u8, wrote, "wrote") != null and
+ std.mem.indexOf(u8, wrote, "reads") != null and
+ std.mem.indexOf(u8, wrote, "deadbeef") != null;
+ r.print(" Poke writes a word and reads it back {s}\n", .{if (poked) "ok" else "FAILED"});
+ if (!poked) failures += 1;
+
+ var plain2: [8192]u8 = undefined;
+ const peeked = stripAnsi(&plain2, try runWord(port, &buf, "Peek 5011002c"));
+ // A separate command, a separate read, a separate render: this is what proves the word survived
+ // rather than that one function returned its own argument.
+ const still = std.mem.indexOf(u8, peeked, "deadbeef") != null;
+ r.print(" and a later Peek still finds it {s}\n", .{if (still) "ok" else "FAILED"});
+ if (!still) failures += 1;
+
// A BURST IS NOT CHECKED HERE, deliberately. The bug it would cover - input lost while the
// transmitter was full - has a deterministic host test in `src/pardes/input_rescue.zig` that
// loses 67 bytes with the rescue removed and needs no board at all. Every hardware oracle for it