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:
@@ -207,6 +207,30 @@ typedef struct
|
||||
akgl_RenderBackend *renderer;
|
||||
SDL_Surface *shapes[AKBASIC_AKGL_MAX_SHAPES];
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user