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
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:
@@ -29,6 +29,7 @@
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/controller.h>
|
||||
#include <akgl/draw.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
#include <akbasic/error.h>
|
||||
@@ -666,7 +667,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_backspace_across_a_wrap(void)
|
||||
* cursor had passed through, which read as an underline beneath the whole line
|
||||
* and made typed text hard to read. Removed outright; §5 records it.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_no_cursor_underline(void)
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_block_cursor_blinks(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
@@ -678,32 +679,119 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_no_cursor_underline(void)
|
||||
PASS(errctx, start_frontend(NULL));
|
||||
sink = &FRONTEND.akglstate;
|
||||
|
||||
/*
|
||||
* Rendered *while editing*, which is the only state the cursor was ever
|
||||
* drawn in -- an earlier version of this test rendered after readline had
|
||||
* returned, by which time `editing` is false and a re-added cursor would
|
||||
* sail straight through it.
|
||||
*/
|
||||
PASS(errctx, FRONTEND.akglsink.write(&FRONTEND.akglsink, "ab"));
|
||||
sink->editing = true;
|
||||
PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));
|
||||
|
||||
/*
|
||||
* The bottom scanline of each cell the text occupies and the one after it.
|
||||
* An underscore lands there; "a" and "b" in this font do not, so anything
|
||||
* lit on that line is the cursor and nothing else.
|
||||
*/
|
||||
/* Period zero holds it solid, so a frame is deterministic. */
|
||||
sink->cursorperiodms = 0;
|
||||
PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));
|
||||
shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
|
||||
TEST_REQUIRE(shot != NULL, "could not read the render target back");
|
||||
|
||||
/*
|
||||
* A *block*: every scanline of the cursor cell is lit, including the top
|
||||
* one. An underscore lights only the bottom, and it was the underscore
|
||||
* sitting under the text that made typed text hard to read.
|
||||
*/
|
||||
TEST_REQUIRE(anything_drawn(shot, sink->x + (2 * sink->cellw), sink->y, sink->cellw, 1),
|
||||
"the cursor cell's top scanline should be lit: a block, not an underscore");
|
||||
TEST_REQUIRE(anything_drawn(shot, sink->x + (2 * sink->cellw),
|
||||
sink->y + sink->cellh - 1, sink->cellw, 1),
|
||||
"the cursor cell's bottom scanline should be lit too");
|
||||
|
||||
/* And nothing under the text itself, which is what was reported. */
|
||||
y = sink->y + sink->cellh - 1;
|
||||
for ( col = 0; col < 4; col++ ) {
|
||||
for ( col = 0; col < 2; col++ ) {
|
||||
if ( anything_drawn(shot, sink->x + (col * sink->cellw), y, sink->cellw, 1) ) {
|
||||
lit += 1;
|
||||
}
|
||||
}
|
||||
TEST_REQUIRE_INT(lit, 0);
|
||||
SDL_DestroySurface(shot);
|
||||
|
||||
/*
|
||||
* It blinks. A short period and a wait past half of it: the same cell must
|
||||
* come out differently, which is the whole of what blinking is.
|
||||
*/
|
||||
sink->cursorperiodms = 200;
|
||||
while ( (SDL_GetTicks() % 200) >= 100 ) {
|
||||
SDL_Delay(5);
|
||||
}
|
||||
PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));
|
||||
shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
|
||||
TEST_REQUIRE(anything_drawn(shot, sink->x + (2 * sink->cellw), sink->y,
|
||||
sink->cellw, sink->cellh),
|
||||
"the cursor should be visible in the first half of its period");
|
||||
SDL_DestroySurface(shot);
|
||||
|
||||
while ( (SDL_GetTicks() % 200) < 100 ) {
|
||||
SDL_Delay(5);
|
||||
}
|
||||
PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));
|
||||
shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
|
||||
TEST_REQUIRE(!anything_drawn(shot, sink->x + (2 * sink->cellw), sink->y,
|
||||
sink->cellw, sink->cellh),
|
||||
"the cursor should be hidden in the second half of its period");
|
||||
SDL_DestroySurface(shot);
|
||||
|
||||
sink->editing = false;
|
||||
sink->cursorperiodms = 0;
|
||||
stop_frontend();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A frame owns every pixel it presents, including ones it did not write.
|
||||
*
|
||||
* **This is the regression test for the blinking backspace, and it is the only
|
||||
* one that could have caught it without real hardware.** The sink used to skip
|
||||
* rows it believed were already clear, tracked in a `drawn[]` array. That
|
||||
* 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 reproduced on X11 and never under the dummy driver, because a software
|
||||
* renderer with a single target does preserve. Rather than require real
|
||||
* hardware, this paints the stale pixels by hand -- which is exactly what the
|
||||
* other buffer hands back -- and asserts the sink erases them anyway.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_frame_owns_every_pixel(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
akbasic_AkglSink *sink = NULL;
|
||||
SDL_FRect stale;
|
||||
|
||||
PASS(errctx, start_frontend(NULL));
|
||||
sink = &FRONTEND.akglstate;
|
||||
|
||||
/* One line of text, and the grid otherwise empty. */
|
||||
PASS(errctx, FRONTEND.akglsink.writeln(&FRONTEND.akglsink, "KEPT"));
|
||||
PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));
|
||||
|
||||
/*
|
||||
* Now dirty row 1 behind the sink's back, the way a swapped-in buffer
|
||||
* arrives already dirty. The sink has never drawn there and its grid says
|
||||
* the row is empty, so nothing in its own bookkeeping suggests any work.
|
||||
*/
|
||||
stale.x = (float)sink->x;
|
||||
stale.y = (float)(sink->y + sink->cellh);
|
||||
stale.w = (float)sink->cellw;
|
||||
stale.h = (float)sink->cellh;
|
||||
PASS(errctx, akgl_draw_filled_rect(FRONTEND.renderer, &stale, sink->color));
|
||||
|
||||
shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
|
||||
TEST_REQUIRE(!cell_is_blank(shot, sink, 0, 1), "the stale pixels should be there to start");
|
||||
SDL_DestroySurface(shot);
|
||||
|
||||
PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));
|
||||
shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
|
||||
TEST_REQUIRE(shot != NULL, "could not read the render target back");
|
||||
TEST_REQUIRE(cell_is_blank(shot, sink, 0, 1),
|
||||
"a row the grid says is empty must be erased even if this sink never drew it");
|
||||
TEST_REQUIRE(!cell_is_blank(shot, sink, 0, 0), "and the row that has text is still drawn");
|
||||
SDL_DestroySurface(shot);
|
||||
|
||||
stop_frontend();
|
||||
SUCCEED_RETURN(errctx);
|
||||
@@ -789,7 +877,8 @@ int main(void)
|
||||
CATCH(errctx, test_render_erases_what_it_drew());
|
||||
CATCH(errctx, test_scroll_reaches_the_screen());
|
||||
CATCH(errctx, test_backspace_across_a_wrap());
|
||||
CATCH(errctx, test_no_cursor_underline());
|
||||
CATCH(errctx, test_block_cursor_blinks());
|
||||
CATCH(errctx, test_frame_owns_every_pixel());
|
||||
CATCH(errctx, test_repl_session());
|
||||
} CLEANUP {
|
||||
stop_frontend();
|
||||
|
||||
Reference in New Issue
Block a user