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:
2026-08-02 10:46:10 -04:00
parent 802bbcc17a
commit 5709dc160c
7 changed files with 451 additions and 24 deletions

23
TODO.md
View File

@@ -3083,8 +3083,27 @@ reduced against `build/basic`, the stdio build, unless it says otherwise.
routine above it that ends in a fall-through rather than an `END`, and a table of angles or routine above it that ends in a fall-through rather than an `END`, and a table of angles or
offsets is exactly where negative numbers live. `tests/read_data.c`. offsets is exactly where negative numbers live. `tests/read_data.c`.
9. **A program that wants a picture on the screen has to spend a sprite slot on it, and 9. ~~**A program that wants a picture on the screen has to spend a sprite slot on it, and
`examples/breakout/sprites/breakout.bas` spends two of its eight on the screen itself.** `examples/breakout/sprites/breakout.bas` spends two of its eight on the screen itself.**~~
**Done for the interpreter; the listing has not been converted yet -- that is §6 item 39.**
The drawing verbs now render into a layer the frame composites under the text and the
sprites (`akbasic_graphics_akgl_begin`/`_end`/`_render`, `src/graphics_akgl.c`), so a
drawing survives the frame it was made in. With `WINDOW` already reachable to shrink the
text area, the two halves together are what a picture needed: draw it once, uncover it,
and it stays. `tests/akgl_backends.c` asserts both -- a pixel still present a frame later
with nothing redrawn, and a pixel below a shrunk text area surviving the text repaint.
**The layer is transparent where nothing was drawn**, which is not a detail: 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`. A fresh SDL target texture's contents are
undefined, so it is cleared on creation; skipping that put uninitialised memory under the
first frame's text and looked like a driver bug.
The remaining half of item 3 -- whether the text layer *should* own the whole window by
default -- is untouched and is still a real question rather than a defect.
The original report:
That listing draws its play field -- walls, sixty stamped bricks, the lettering -- captures That listing draws its play field -- walls, sixty stamped bricks, the lettering -- captures
the whole 800 by 540 region with `SSHAPE`, and installs the capture as sprite 2. It does the the whole 800 by 540 region with `SSHAPE`, and installs the capture as sprite 2. It does the

View File

@@ -240,10 +240,23 @@ does with one, but you cannot store it or measure it.
`FILTER` parses and then refuses: there is no filter stage to configure. See Chapter 7. `FILTER` parses and then refuses: there is no filter stage to configure. See Chapter 7.
The graphics verbs draw straight to the renderer rather than into a display list, so **A drawing stays.** The verbs render into a layer the frame composites underneath the
anything drawn is overwritten by the text layer on the next frame. A program that wants text and the sprites, so a program draws its picture once and it is there on every frame
its drawing to persist has to redraw it. This is recorded as a defect rather than a after. It does not have to redraw it, and it does not have to capture it into a sprite.
design; see Chapter 13.
What covers it is the text layer, which repaints every row it owns — opaque, every frame,
for a reason `akbasic_sink_akgl_render()` explains — and by default it owns the whole
window. `WINDOW` shrinks it:
```basic norun
10 WINDOW 0, 0, 39, 1
20 GRAPHIC 1, 1
30 COLOR 1, 3
40 BOX 1, 20, 40, 300, 180
```
Two rows of text at the top, the rest of the window for drawing, and the box is still
there a thousand frames later.
**A redraw also has to fit inside one batch.** The host runs a fixed number of source **A redraw also has to fit inside one batch.** The host runs a fixed number of source
lines and then presents, and presenting throws the drawing buffer away — so a run of lines and then presents, and presenting throws the drawing buffer away — so a run of

View File

@@ -142,19 +142,21 @@ interpreter's error code, which bears no relation to a Commodore error number. P
- **`SSHAPE` puts a handle in the string, not the pixels.** You can pass it to `GSHAPE` - **`SSHAPE` puts a handle in the string, not the pixels.** You can pass it to `GSHAPE`
and `SPRSAV`; you cannot save it or take its `LEN`. and `SPRSAV`; you cannot save it or take its `LEN`.
- **`WIDTH` is emulated** by drawing parallel passes. - **`WIDTH` is emulated** by drawing parallel passes.
- **Drawing does not persist across frames.** The verbs draw straight to the renderer, - **Drawing persists, and the text layer covers it.** A drawing goes into a layer that
so anything drawn is overwritten on the next frame unless the program redraws it. survives the frame, so a program draws its picture once and it stays — no redrawing, no
This is a defect, not a design. capturing it into a sprite. What is still true is that the text layer repaints every row
- **And a redraw has to *fit*.** The host runs a fixed number of source lines and then it owns, opaque, every frame, and by default it owns the whole window. `WINDOW` shrinks
presents the frame, and presenting discards the drawing buffer — so a sequence of it and hands the rest over. The two together are what makes a picture usable:
drawing verbs longer than one batch comes back **half drawn**, with an `SSHAPE` at the `WINDOW 0, 0, 39, 1` keeps a one-row status line and gives the drawing verbs everything
end capturing only what was issued since the present. "Redraw it every frame" is below it.
therefore not sufficient advice on its own. Measured against the standalone frontend's - **A drawing still has to fit in one batch.** The host runs a fixed number of source lines
256 lines a batch: after synchronising to a jiffy edge, 220 lines of drawing survive a and then presents, and an `SSHAPE` capture spanning that boundary comes back **half
capture and 250 do not. The only way a program can see the boundary is to watch `TI#`, drawn** — the part issued since the present, over whatever was there before. Measured
which is refreshed once per batch — so the step on which it changes is the first step against the standalone frontend's 256 lines a batch: after synchronising to a jiffy edge,
of one. [Chapter 18](18-tutorial-breakout-artwork.md#step-5-find-the-frame-boundary) 220 lines of drawing survive a capture and 250 do not. This bites a *capture*, not the
builds a pacing routine out of exactly that. drawing itself, so it matters far less than it did when capturing was the only way to
keep a picture. The only way a program can see the boundary is to watch `TI#`, which is
refreshed once per batch.
- **`CHAR` ignores its colour argument** and needs a text device with a cursor. - **`CHAR` ignores its colour argument** and needs a text device with a cursor.
## Sound ## Sound

View File

@@ -207,6 +207,30 @@ typedef struct
akgl_RenderBackend *renderer; akgl_RenderBackend *renderer;
SDL_Surface *shapes[AKBASIC_AKGL_MAX_SHAPES]; SDL_Surface *shapes[AKBASIC_AKGL_MAX_SHAPES];
int shapecount; int shapecount;
/**
* The drawing layer: a texture the drawing verbs render into and the frame
* composites, so a drawing stays on screen without the program redrawing it.
*
* **Without this a drawing lasts exactly one frame.** The verbs are
* immediate and go to the renderer's back buffer, SDL double-buffers, and
* the frontend never clears -- so what a program drew is gone the moment the
* frame it was issued in is presented. 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 and what TODO.md section 9 item 9 was filed about.
*
* NULL until the first drawing verb runs. A program that never draws pays
* for no texture, which matters: this is the size of the window.
*/
SDL_Texture *layer;
/**
* What the target was before the layer was made current, so it can be put
* back. The frontend brackets the whole step phase rather than each verb --
* one pair of SDL_SetRenderTarget calls a frame instead of one per DRAW.
*/
SDL_Texture *savedtarget;
bool layeractive;
} akbasic_AkglGraphics; } akbasic_AkglGraphics;
/** /**
@@ -414,6 +438,63 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_sink_akgl_set_pump(akbasic_TextSink *
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akbasic_AkglGraphics *state, akgl_RenderBackend *renderer); akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akbasic_AkglGraphics *state, akgl_RenderBackend *renderer);
/**
* @brief Make the drawing layer current, so what the program draws is kept.
*
* Called by a host **before** running the interpreter's steps, and paired with
* akbasic_graphics_akgl_end(). Between the two, every drawing verb renders into
* a texture that survives the frame instead of into a back buffer that does not.
*
* Bracketing the whole step phase rather than each verb is deliberate: it is 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 previous frame left.
*
* A host that never calls these gets the old behaviour -- drawings live one
* frame -- which is what `tools/screenshot.c` wants, since it presents nothing.
*
* @param obj The graphics backend, or NULL to do nothing.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL When the layer texture cannot be created or made current.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_akgl_begin(akbasic_GraphicsBackend *obj);
/**
* @brief Put the previous render target back. Pairs with akbasic_graphics_akgl_begin().
*
* @param obj The graphics backend, or NULL to do nothing.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL When the previous target cannot be restored.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_akgl_end(akbasic_GraphicsBackend *obj);
/**
* @brief Draw the layer onto the frame, under the text and the sprites.
*
* Called once a frame by a host, before the sink renders. Does nothing when no
* program has drawn anything, so a text-only program composites nothing.
*
* @param obj The graphics backend, or NULL to do nothing.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL When the layer cannot be drawn.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_akgl_render(akbasic_GraphicsBackend *obj);
/**
* @brief Is the drawing layer currently the render target?
*
* A host needs this because SDL refuses to present while a target is current,
* and the sink's line editor calls a host's pump from *inside* a step -- when
* the layer is on -- while the frame loop calls it between steps, when it is
* not. Answering the question is what lets one pump serve both.
*
* @param obj The graphics backend, or NULL for false.
*/
bool akbasic_graphics_akgl_layer_active(akbasic_GraphicsBackend *obj);
/** @brief Destroy the layer texture. Safe on a backend that never made one. */
void akbasic_graphics_akgl_shutdown(akbasic_GraphicsBackend *obj);
/** /**
* @brief Point a sprite backend at a renderer the host already has. * @brief Point a sprite backend at a renderer the host already has.
* *

View File

@@ -251,6 +251,7 @@ akerr_ErrorContext *akbasic_frontend_akgl_pump(void *self, bool *running)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akbasic_AkglFrontend *obj = (akbasic_AkglFrontend *)self; akbasic_AkglFrontend *obj = (akbasic_AkglFrontend *)self;
bool resume = false;
FAIL_ZERO_RETURN(errctx, (obj != NULL && running != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (obj != NULL && running != NULL), AKERR_NULLPOINTER,
"NULL argument in frontend_akgl_pump"); "NULL argument in frontend_akgl_pump");
@@ -262,12 +263,37 @@ akerr_ErrorContext *akbasic_frontend_akgl_pump(void *self, bool *running)
} }
/* /*
* No clear. The graphics verbs draw straight to the renderer rather than * **Take the drawing layer off the target before presenting.**
* into a display list, so clearing here would wipe every DRAW the moment the *
* frame it was issued in ended. The text layer is drawn over whatever is * SDL refuses to present while a render target is current, and this function
* already there, which is also how a C128 stacks its text plane on its * is called from two places with different answers to "is one current". The
* bitmap plane. * frame loop calls it between steps, with the layer off. The sink's *line
* editor* calls it from inside a step -- borrowing a frame while it waits
* for a typed line, which is how a REPL redraws without owning a loop -- and
* there the layer is very much on.
*
* So end it here and put it back afterwards. `resume` remembers which case
* this was, because unconditionally beginning would leave the layer current
* after a frame-loop pump and every subsequent draw would land in it at the
* wrong time.
*/ */
resume = akbasic_graphics_akgl_layer_active(&obj->graphics);
PASS(errctx, akbasic_graphics_akgl_end(&obj->graphics));
/*
* No clear. The text layer is drawn over whatever is already there, which is
* also how a C128 stacks its text plane on its bitmap plane -- and the
* drawing layer beneath it is composited rather than accumulated, so there
* is nothing here that a clear would fix.
*/
/*
* The drawing layer first, under everything. A drawing verb rendered into a
* texture rather than into the back buffer -- see
* akbasic_graphics_akgl_begin() -- so this is where what the program drew
* reaches the frame, and it is why a picture no longer has to be redrawn
* every frame or captured into a sprite to be kept.
*/
PASS(errctx, akbasic_graphics_akgl_render(&obj->graphics));
PASS(errctx, akbasic_sink_akgl_render(&obj->akglsink)); PASS(errctx, akbasic_sink_akgl_render(&obj->akglsink));
/* /*
* Sprites last, so they are over the text as well as over the drawing. That * Sprites last, so they are over the text as well as over the drawing. That
@@ -278,6 +304,9 @@ akerr_ErrorContext *akbasic_frontend_akgl_pump(void *self, bool *running)
PASS(errctx, akbasic_sprite_akgl_render(&obj->sprites)); PASS(errctx, akbasic_sprite_akgl_render(&obj->sprites));
FAIL_ZERO_RETURN(errctx, SDL_RenderPresent(obj->renderer->sdl_renderer), FAIL_ZERO_RETURN(errctx, SDL_RenderPresent(obj->renderer->sdl_renderer),
AKGL_ERR_SDL, "Couldn't present the frame: %s", SDL_GetError()); AKGL_ERR_SDL, "Couldn't present the frame: %s", SDL_GetError());
if ( resume ) {
PASS(errctx, akbasic_graphics_akgl_begin(&obj->graphics));
}
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -297,7 +326,16 @@ static akerr_ErrorContext AKERR_NOIGNORE *drive_loop(akbasic_AkglFrontend *obj,
* clock_gettime and a _POSIX_C_SOURCE. * clock_gettime and a _POSIX_C_SOURCE.
*/ */
PASS(errctx, akbasic_runtime_settime(rt, (int64_t)SDL_GetTicks())); PASS(errctx, akbasic_runtime_settime(rt, (int64_t)SDL_GetTicks()));
/*
* The steps run with the drawing layer current, so a DRAW lands in a
* texture that outlives the frame. Bracketed here rather than inside
* each verb: one pair of SDL_SetRenderTarget calls a frame instead of
* one per statement, and it is also what makes SSHAPE read back what the
* program has just drawn.
*/
PASS(errctx, akbasic_graphics_akgl_begin(&obj->graphics));
PASS(errctx, akbasic_runtime_run(rt, AKBASIC_FRONTEND_STEPS_PER_FRAME)); PASS(errctx, akbasic_runtime_run(rt, AKBASIC_FRONTEND_STEPS_PER_FRAME));
PASS(errctx, akbasic_graphics_akgl_end(&obj->graphics));
PASS(errctx, akbasic_frontend_akgl_pump(obj, &running)); PASS(errctx, akbasic_frontend_akgl_pump(obj, &running));
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -320,6 +358,7 @@ void akbasic_frontend_akgl_shutdown(akbasic_AkglFrontend *obj)
} }
/* Before the renderer goes: these are textures it created. */ /* Before the renderer goes: these are textures it created. */
akbasic_sprite_akgl_shutdown(&obj->sprites); akbasic_sprite_akgl_shutdown(&obj->sprites);
akbasic_graphics_akgl_shutdown(&obj->graphics);
if ( obj->font != NULL ) { if ( obj->font != NULL ) {
TTF_CloseFont(obj->font); TTF_CloseFont(obj->font);
obj->font = NULL; obj->font = NULL;

View File

@@ -262,3 +262,143 @@ akerr_ErrorContext *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akb
obj->free_shapes = gfx_free_shapes; obj->free_shapes = gfx_free_shapes;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/* ----------------------------------------------------------- the layer -- */
/**
* @brief Make sure the layer texture exists and is the size of the output.
*
* Created on demand rather than at init, because it is the size of the window
* and a program that never draws should not pay for one. Recreated if the
* output has since changed size, which is what a resized window looks like from
* here.
*/
static akerr_ErrorContext AKERR_NOIGNORE *ensure_layer(akbasic_AkglGraphics *state)
{
PREPARE_ERROR(errctx);
SDL_Texture *previous = NULL;
int w = 0;
int h = 0;
float texw = 0.0f;
float texh = 0.0f;
FAIL_ZERO_RETURN(errctx, SDL_GetCurrentRenderOutputSize(state->renderer->sdl_renderer, &w, &h),
AKGL_ERR_SDL, "Couldn't measure the render output: %s", SDL_GetError());
if ( state->layer != NULL ) {
if ( SDL_GetTextureSize(state->layer, &texw, &texh)
&& (int)texw == w && (int)texh == h ) {
SUCCEED_RETURN(errctx);
}
SDL_DestroyTexture(state->layer);
state->layer = NULL;
}
state->layer = SDL_CreateTexture(state->renderer->sdl_renderer,
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_TARGET, w, h);
FAIL_ZERO_RETURN(errctx, (state->layer != NULL), AKGL_ERR_SDL,
"Couldn't create the drawing layer: %s", SDL_GetError());
/*
* Blended, and cleared to fully transparent. The layer covers the whole
* window and is composited *under* the text and the sprites, so anywhere the
* program has not drawn has to let what is behind it through -- otherwise a
* single DRAW would black out the entire frame.
*/
FAIL_ZERO_RETURN(errctx, SDL_SetTextureBlendMode(state->layer, SDL_BLENDMODE_BLEND),
AKGL_ERR_SDL, "%s", SDL_GetError());
/*
* **And clear it, because a fresh target texture's contents are undefined.**
* Skipping this puts whatever was in that memory on the screen under the
* first frame's text, which is the kind of defect that looks like a driver
* bug and is not.
*/
previous = SDL_GetRenderTarget(state->renderer->sdl_renderer);
FAIL_ZERO_RETURN(errctx, SDL_SetRenderTarget(state->renderer->sdl_renderer, state->layer),
AKGL_ERR_SDL, "%s", SDL_GetError());
if ( !SDL_SetRenderDrawColor(state->renderer->sdl_renderer, 0, 0, 0, 0)
|| !SDL_RenderClear(state->renderer->sdl_renderer) ) {
/* Best effort: we are already failing, and leaving the target on the
layer would make the next unrelated draw land in the wrong place. */
(void)SDL_SetRenderTarget(state->renderer->sdl_renderer, previous);
FAIL_RETURN(errctx, AKGL_ERR_SDL, "Couldn't clear the drawing layer: %s", SDL_GetError());
}
FAIL_ZERO_RETURN(errctx, SDL_SetRenderTarget(state->renderer->sdl_renderer, previous),
AKGL_ERR_SDL, "%s", SDL_GetError());
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_graphics_akgl_begin(akbasic_GraphicsBackend *obj)
{
PREPARE_ERROR(errctx);
akbasic_AkglGraphics *state = NULL;
if ( obj == NULL || obj->self == NULL ) {
SUCCEED_RETURN(errctx);
}
state = (akbasic_AkglGraphics *)obj->self;
PASS(errctx, ensure_layer(state));
state->savedtarget = SDL_GetRenderTarget(state->renderer->sdl_renderer);
FAIL_ZERO_RETURN(errctx, SDL_SetRenderTarget(state->renderer->sdl_renderer, state->layer),
AKGL_ERR_SDL, "Couldn't make the drawing layer current: %s", SDL_GetError());
state->layeractive = true;
SUCCEED_RETURN(errctx);
}
bool akbasic_graphics_akgl_layer_active(akbasic_GraphicsBackend *obj)
{
if ( obj == NULL || obj->self == NULL ) {
return false;
}
return ((akbasic_AkglGraphics *)obj->self)->layeractive;
}
akerr_ErrorContext *akbasic_graphics_akgl_end(akbasic_GraphicsBackend *obj)
{
PREPARE_ERROR(errctx);
akbasic_AkglGraphics *state = NULL;
if ( obj == NULL || obj->self == NULL ) {
SUCCEED_RETURN(errctx);
}
state = (akbasic_AkglGraphics *)obj->self;
if ( !state->layeractive ) {
SUCCEED_RETURN(errctx);
}
state->layeractive = false;
FAIL_ZERO_RETURN(errctx,
SDL_SetRenderTarget(state->renderer->sdl_renderer, state->savedtarget),
AKGL_ERR_SDL, "Couldn't restore the render target: %s", SDL_GetError());
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_graphics_akgl_render(akbasic_GraphicsBackend *obj)
{
PREPARE_ERROR(errctx);
akbasic_AkglGraphics *state = NULL;
if ( obj == NULL || obj->self == NULL ) {
SUCCEED_RETURN(errctx);
}
state = (akbasic_AkglGraphics *)obj->self;
/* Nothing drawn, nothing to composite: a text-only program pays nothing. */
if ( state->layer == NULL ) {
SUCCEED_RETURN(errctx);
}
FAIL_ZERO_RETURN(errctx,
SDL_RenderTexture(state->renderer->sdl_renderer, state->layer, NULL, NULL),
AKGL_ERR_SDL, "Couldn't draw the drawing layer: %s", SDL_GetError());
SUCCEED_RETURN(errctx);
}
void akbasic_graphics_akgl_shutdown(akbasic_GraphicsBackend *obj)
{
akbasic_AkglGraphics *state = NULL;
if ( obj == NULL || obj->self == NULL ) {
return;
}
state = (akbasic_AkglGraphics *)obj->self;
if ( state->layer != NULL ) {
SDL_DestroyTexture(state->layer);
state->layer = NULL;
}
}

View File

@@ -129,6 +129,12 @@ static void stop_runtime(void)
* A host releases what it took. So does this. * A host releases what it took. So does this.
*/ */
akbasic_sprite_akgl_shutdown(&SPRITES); 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 ) { if ( OUT != NULL ) {
fclose(OUT); fclose(OUT);
OUT = NULL; OUT = NULL;
@@ -937,6 +943,132 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_static_geometry(void)
SUCCEED_RETURN(errctx); 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) int main(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
@@ -1007,6 +1139,7 @@ int main(void)
CATCH(errctx, test_collision_cross_shape()); CATCH(errctx, test_collision_cross_shape());
CATCH(errctx, test_sprite_shapes()); CATCH(errctx, test_sprite_shapes());
CATCH(errctx, test_static_geometry()); CATCH(errctx, test_static_geometry());
CATCH(errctx, test_drawing_layer_persists());
} CLEANUP { } CLEANUP {
if ( font != NULL ) { if ( font != NULL ) {
TTF_CloseFont(font); TTF_CloseFont(font);