Own every pixel each frame, and bring the cursor back as a blinking block
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 19s
akbasic CI Build / sanitizers (push) Failing after 13s
akbasic CI Build / coverage (push) Failing after 16s
akbasic CI Build / akgl_build (push) Failing after 18s
akbasic CI Build / mutation_test (push) Failing after 17s

The blinking backspace was not the backspace. The sink skipped rows it
believed were already clear, tracked in a drawn[] array, which assumes a frame
inherits the frame before it. SDL_RenderPresent swaps buffers, so a frame
inherits the one two back: a row erased once is clean in one buffer and dirty
in the other, and presenting alternates between them. The first character of a
backspaced wrap flickered forever.

It reproduces on X11 and never under the dummy driver or a software renderer,
which is why the suite stayed green through two rounds of fixing it. The sink
now repaints every row it owns every frame -- a frame either owns every pixel
it presents or inherits pixels it cannot reason about.

Confirmed on real hardware with the reported input: the wrapped tail's row
reads zero across five successive captures.

The cursor returns as a blinking block, half a second per cycle, off SDL's
clock. It sits in the cell after the text rather than under it, which is what
made the underscore unreadable, and it wraps to the next row when a line
exactly fills one -- where the next character actually lands.

The new regression test paints stale pixels by hand, which is what a swapped-in
buffer hands back, so the case is covered without real hardware. Every new
assertion was checked by reverting the fix and watching it fail.

Recorded honestly in TODO.md section 5: the same buffer swap means the
graphics verbs were never reliable across frames either. A one-shot DRAW lands
in one buffer and the next present shows the other. Making that work needs a
persistent surface, which is its own commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 15:23:41 -04:00
parent acd20eed9a
commit df50201121
4 changed files with 222 additions and 74 deletions

69
TODO.md
View File

@@ -844,38 +844,53 @@ deviations from the reference's *program*: `main.go` and the SDL half of
exactly, and the whole golden corpus is driven through this loop to prove it. What it
changes for a *user* is that the window closes when asked.
26. **The frame is not cleared, but the text layer erases its own rows.** The reference blits a
text surface onto the window surface and calls `UpdateSurface`. Here the graphics verbs draw
straight to the renderer rather than into a display list, so a `SDL_RenderClear` at the top
of each frame would erase every `DRAW` at the end of the frame it was issued in. The host
therefore does not clear.
26. **The text layer repaints every row it owns, every frame.** Not just the rows that changed.
**The sink erases row by row instead**, filling each row it is about to draw — and each row
that has emptied since it last drew — with its background colour first. That makes the text
layer authoritative over the rows it occupies and leaves every other pixel alone, which is
how a C128's text plane sits over its bitmap.
That was tried — a `drawn[]` array marking which rows had carried glyphs, so an untouched
row could be skipped — and it is **wrong on real hardware**. `SDL_RenderPresent` swaps
buffers, so a frame does not inherit the frame before it; it inherits the one *two* back,
and on some backends something undefined. A row erased once is clean in one buffer and still
dirty in the other, and presenting alternates between them.
**This was originally "the text layer is drawn over whatever is already there", and that was
a bug rather than a deviation.** The grid was always right and the screen kept the previous
frame, which surfaced as three separate-looking faults reported together: a backspaced
character that stayed on screen, a scroll that appeared to overwrite the display with
garbage, and the editing cursor smearing an underscore along every column it had passed
through. One cause, three symptoms. `tests/akgl_frontend.c` asserts the fix at pixel level —
grid assertions could never have caught it, because the grid was never wrong.
That is what a backspace across a line wrap looked like: the first character of the wrapped
tail flickering in and out forever after the rest had gone. It reproduces on X11 and never
under the dummy driver or a software renderer, which is exactly why the suite was green
through two rounds of fixing it. `tests/akgl_frontend.c` now paints stale pixels by hand —
which is what a swapped-in buffer hands back — so the case is covered without needing real
hardware.
**What it still costs:** a program that draws a picture and then prints loses the strip of
picture behind each text row it uses. That is the deliberate half of the trade — the
alternative was clearing the whole frame, which loses the picture entirely.
There is no cheaper correct answer available: a frame either owns every pixel it presents or
it inherits pixels it cannot reason about.
27. **There is no cursor.** The reference draws one as a literal underscore glyph (`drawCursor`,
`basicruntime_graphics.go:33`) and this did too. Removed on request: it sat under the text
being typed and made it hard to read.
**What this costs, and it is more than it used to.** The host still does not call
`SDL_RenderClear`, but the text area is now repainted in full every frame, so anything a
graphics verb drew underneath it is erased every frame rather than only where text lands. In
the standalone driver the text area is the whole window, so `DRAW` and `PRINT` no longer
coexist there.
Worth separating the two reasons it was unbearable, because only one of them is gone. It
*smeared* — an underscore left at every column the cursor passed through — and that was
deviation 26's missing erase, now fixed. It also simply sits under the text, which is what
an underscore cursor does in a font whose glyphs reach the baseline. A cursor could come back
and behave now; a block or a different colour would read better than an underscore.
**And the same buffer swap means the graphics verbs were never reliable across frames
anyway**, which is worth stating plainly rather than leaving as a surprise: a one-shot `DRAW`
lands in one buffer and the next present shows the other. It looked fine in a single
screenshot and would have flickered exactly as the text did. Making graphics survive needs a
persistent surface the frame is composited from, which is real work and its own commit —
filed here rather than half-done.
27. **The cursor is a blinking block.** The reference draws an underscore glyph (`drawCursor`,
`basicruntime_graphics.go:33`), and so did this until the underscore turned out to sit
*under* the text being typed and make it hard to read. A block occupies the cell after the
text instead of the space beneath it.
Half a second per blink (`AKBASIC_SINK_CURSOR_BLINK_MS`), which is about a Commodore's and
slow enough to read under. `akbasic_AkglSink.cursorperiodms` set to zero holds it solid,
which is what makes a frame deterministic for a test.
Two details that are decisions rather than accidents. The clock is **SDL's**, not the
host's: everywhere else the host owns the clock because the library owns no loop and must
not block, but this is an adaptor that already links SDL and threading a timestamp through
the sink interface to animate a cursor would be a poor trade. And a line that exactly fills
a row leaves the cursor one column past the end, because `putchar_at` wraps only when the
next character arrives — the cursor is drawn at the start of the next row instead, which is
where the next character will actually land and where a C128 puts it.
28. **The window is closed by the host, and that is not `QUIT`.** Closing the window stops the
frontend and leaves `obj->mode` alone; it does not set `AKBASIC_MODE_QUIT`. The distinction