Keep what a program draws, instead of making it a sprite
A drawing lasted exactly one frame. The verbs are immediate, they went to the back buffer, SDL double-buffers and the frontend never clears -- so the only way to keep a picture was to capture it with `SSHAPE` and install it as a sprite, which is what `examples/breakout/sprites/breakout.bas` spends two of its eight sprites doing. That was TODO.md section 9 item 9. The drawing verbs now render into a layer texture the frame composites under the text and the sprites. Draw once; it is there on every frame after. **Bracketed around the step phase, not around each verb.** One pair of `SDL_SetRenderTarget` calls a frame instead of one per `DRAW`, and it is also what makes `SSHAPE` read back what the program has just drawn rather than whatever the last frame left. **The layer is transparent where nothing was drawn.** It covers the whole window and composites underneath, so an opaque one would black out the frame the moment a program issued a single `DRAW`. And a fresh SDL target texture's contents are undefined, so it is cleared on creation -- skipping that puts uninitialised memory under the first frame's text and looks like a driver bug rather than a missing memset. **The line editor forced a wrinkle worth naming.** `akbasic_frontend_akgl_pump()` is called from two places with different answers to "is a render target current": the frame loop calls it between steps, and the sink's editor calls it from *inside* a step, borrowing a frame while it waits for a typed line. SDL refuses to present while a target is current, so the pump ends the layer, presents, and puts it back only if it was the one that ended it. `akgl_frontend` caught this -- it drives a REPL session, and it failed with "You can't present on a render target" the first time the brackets went in. This does not make a drawing *visible* on its own. The text layer still repaints every row it owns, opaque, every frame, and by default it owns the whole window; `WINDOW` shrinks it and that half was already fixed. The two together are what a picture needed, and the tests assert both -- a pixel still there a frame later with nothing redrawn, and a pixel below a shrunk text area surviving the text repaint. The second assertion wipes to a non-black colour first, because against black it could not tell a transparent layer from an opaque one. The tests found two of their own bugs on the way: `stop_runtime()` was not tearing the graphics backend down, so re-initialising it dropped a live texture on the floor; and a first draft called `begin()` before `start_runtime()`, which re-inits the backend, so the assertion read back off an orphaned render target and passed while proving nothing. Chapters 6 and 13 stop saying a drawing has to be redrawn every frame, because it does not. The batch-boundary tear stays documented -- it bites an `SSHAPE` capture, which matters much less now that capturing is not the only way to keep a picture. Both games still run clean. 111 with akgl, 110 without. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
This commit is contained in:
@@ -129,6 +129,12 @@ static void stop_runtime(void)
|
||||
* A host releases what it took. So does this.
|
||||
*/
|
||||
akbasic_sprite_akgl_shutdown(&SPRITES);
|
||||
/*
|
||||
* And the drawing layer, for the same reason: akbasic_graphics_init_akgl()
|
||||
* zeroes the state it is given, so a second start_runtime() would drop the
|
||||
* pointer to a live SDL texture on the floor.
|
||||
*/
|
||||
akbasic_graphics_akgl_shutdown(&GRAPHICS);
|
||||
if ( OUT != NULL ) {
|
||||
fclose(OUT);
|
||||
OUT = NULL;
|
||||
@@ -937,6 +943,132 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_static_geometry(void)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A drawing survives the frame it was made in, and the next one.
|
||||
*
|
||||
* The defect this closes, quoted from TODO.md section 9 item 3: "in the
|
||||
* standalone SDL build, nothing the graphics verbs draw can be seen unless the
|
||||
* program shrinks the text area first." The verbs are immediate and go to the
|
||||
* back buffer, SDL double-buffers, and the frontend never clears -- so a picture
|
||||
* lasted exactly one frame and the only way to keep one was to capture it with
|
||||
* SSHAPE and install it as a sprite.
|
||||
*
|
||||
* **The second half is the assertion that matters.** Anything can put a pixel on
|
||||
* the target once. Compositing the layer a second time, with the program having
|
||||
* drawn nothing in between, is what says the picture is *kept* rather than
|
||||
* merely issued.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_drawing_layer_persists(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
|
||||
PASS(errctx, clear_target());
|
||||
|
||||
/*
|
||||
* The frontend's order exactly, and it has to be: devices up once, then
|
||||
* frames. start_runtime() cannot be used here because it re-initializes the
|
||||
* graphics backend, which zeroes the layer pointer -- and a test that called
|
||||
* begin() first would then read its assertion back off a render target that
|
||||
* happened to still be the orphaned layer, and pass while proving nothing.
|
||||
*/
|
||||
memset(OUTPUT, 0, sizeof(OUTPUT));
|
||||
OUT = fmemopen(OUTPUT, sizeof(OUTPUT), "w");
|
||||
FAIL_ZERO_RETURN(errctx, (OUT != NULL), AKERR_IO, "could not open the output buffer");
|
||||
setvbuf(OUT, NULL, _IONBF, 0);
|
||||
PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, OUT, NULL));
|
||||
PASS(errctx, akbasic_runtime_init(&RUNTIME, &SINK));
|
||||
PASS(errctx, akbasic_graphics_init_akgl(&GRAPHICS, &GRAPHICSSTATE, akgl_renderer));
|
||||
PASS(errctx, akbasic_runtime_set_devices(&RUNTIME, &GRAPHICS, NULL, NULL, NULL));
|
||||
PASS(errctx, akbasic_runtime_load(&RUNTIME, "10 COLOR 1, 3\n20 DRAW 1, 40, 40\n"));
|
||||
PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN));
|
||||
|
||||
/* Frame one: steps run with the layer current, then it is composited. */
|
||||
PASS(errctx, akbasic_graphics_akgl_begin(&GRAPHICS));
|
||||
PASS(errctx, akbasic_runtime_run(&RUNTIME, 0));
|
||||
PASS(errctx, akbasic_graphics_akgl_end(&GRAPHICS));
|
||||
TEST_REQUIRE_STR(OUTPUT, "");
|
||||
PASS(errctx, akbasic_graphics_akgl_render(&GRAPHICS));
|
||||
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
TEST_REQUIRE(shot != NULL, "could not read the render target back");
|
||||
TEST_REQUIRE(pixel_is(shot, 40, 40, 0x88, 0x39, 0x32),
|
||||
"the drawing should have reached the frame through the layer");
|
||||
SDL_DestroySurface(shot);
|
||||
|
||||
/*
|
||||
* **Frame two is the assertion that matters.** Anything can put a pixel on
|
||||
* the target once. Wiping the frame the way a present does, running no
|
||||
* program at all, and compositing again is what says the picture is *kept*
|
||||
* rather than merely issued -- which is the whole of TODO.md section 9
|
||||
* item 3.
|
||||
*/
|
||||
PASS(errctx, clear_target());
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
TEST_REQUIRE(shot != NULL, "could not read the render target back");
|
||||
TEST_REQUIRE(!pixel_is(shot, 40, 40, 0x88, 0x39, 0x32),
|
||||
"the clear should have wiped the frame, or the next assertion proves nothing");
|
||||
SDL_DestroySurface(shot);
|
||||
|
||||
PASS(errctx, akbasic_graphics_akgl_render(&GRAPHICS));
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
TEST_REQUIRE(shot != NULL, "could not read the render target back");
|
||||
TEST_REQUIRE(pixel_is(shot, 40, 40, 0x88, 0x39, 0x32),
|
||||
"the drawing should still be there a frame later without being redrawn");
|
||||
SDL_DestroySurface(shot);
|
||||
|
||||
/*
|
||||
* **And transparent where nothing was drawn.** The layer covers the whole
|
||||
* window and composites *under* the text and the sprites, so an opaque one
|
||||
* would black out the frame the moment a program issued a single DRAW. Wipe
|
||||
* to a colour that is not black, composite, and the untouched pixel has to
|
||||
* still be that colour -- against black this assertion could not tell an
|
||||
* opaque layer from a transparent one.
|
||||
*/
|
||||
FAIL_ZERO_RETURN(errctx,
|
||||
SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0x11, 0x22, 0x33, 0xff),
|
||||
AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
FAIL_ZERO_RETURN(errctx, SDL_RenderClear(akgl_renderer->sdl_renderer),
|
||||
AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
PASS(errctx, akbasic_graphics_akgl_render(&GRAPHICS));
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
TEST_REQUIRE(shot != NULL, "could not read the render target back");
|
||||
TEST_REQUIRE(pixel_is(shot, 100, 100, 0x11, 0x22, 0x33),
|
||||
"the layer should be transparent where nothing was drawn");
|
||||
TEST_REQUIRE(pixel_is(shot, 40, 40, 0x88, 0x39, 0x32),
|
||||
"and still opaque where something was");
|
||||
SDL_DestroySurface(shot);
|
||||
|
||||
/*
|
||||
* **And the two halves together are what makes a drawing usable.**
|
||||
*
|
||||
* The text layer repaints every row it owns, opaque, every frame -- for the
|
||||
* reason akbasic_sink_akgl_render() gives at length -- and by default it
|
||||
* owns the whole window, so a drawing composited underneath is covered.
|
||||
* `WINDOW` shrinks it, and that half was already fixed. What was missing was
|
||||
* persistence: a program could uncover the drawing and then had nothing to
|
||||
* put there, because the picture was gone by the next frame.
|
||||
*
|
||||
* So: shrink the text area to two rows, render the text layer over the
|
||||
* composited drawing, and check that a pixel below the text area survived.
|
||||
*/
|
||||
PASS(errctx, akbasic_sink_init_akgl(&AKGLSINK, &AKGLSINKSTATE, akgl_renderer, font,
|
||||
TARGET_SIZE, TARGET_SIZE));
|
||||
TEST_REQUIRE_OK(AKGLSINK.window(&AKGLSINK, 0, 0, 15, 1));
|
||||
PASS(errctx, clear_target());
|
||||
PASS(errctx, akbasic_graphics_akgl_render(&GRAPHICS));
|
||||
PASS(errctx, akbasic_sink_akgl_render(&AKGLSINK));
|
||||
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
TEST_REQUIRE(shot != NULL, "could not read the render target back");
|
||||
TEST_REQUIRE(pixel_is(shot, 40, 40, 0x88, 0x39, 0x32),
|
||||
"with the text area shrunk, the drawing below it should survive the frame");
|
||||
SDL_DestroySurface(shot);
|
||||
|
||||
stop_runtime();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -1007,6 +1139,7 @@ int main(void)
|
||||
CATCH(errctx, test_collision_cross_shape());
|
||||
CATCH(errctx, test_sprite_shapes());
|
||||
CATCH(errctx, test_static_geometry());
|
||||
CATCH(errctx, test_drawing_layer_persists());
|
||||
} CLEANUP {
|
||||
if ( font != NULL ) {
|
||||
TTF_CloseFont(font);
|
||||
|
||||
Reference in New Issue
Block a user