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 Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
@@ -262,3 +262,143 @@ akerr_ErrorContext *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akb
|
||||
obj->free_shapes = gfx_free_shapes;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user