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:
23
TODO.md
23
TODO.md
@@ -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
|
||||
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
|
||||
`examples/breakout/sprites/breakout.bas` spends two of its eight on the screen itself.**
|
||||
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.**~~
|
||||
**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
|
||||
the whole 800 by 540 region with `SSHAPE`, and installs the capture as sprite 2. It does the
|
||||
|
||||
@@ -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.
|
||||
|
||||
The graphics verbs draw straight to the renderer rather than into a display list, so
|
||||
anything drawn is overwritten by the text layer on the next frame. A program that wants
|
||||
its drawing to persist has to redraw it. This is recorded as a defect rather than a
|
||||
design; see Chapter 13.
|
||||
**A drawing stays.** The verbs render into a layer the frame composites underneath the
|
||||
text and the sprites, so a program draws its picture once and it is there on every frame
|
||||
after. It does not have to redraw it, and it does not have to capture it into a sprite.
|
||||
|
||||
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
|
||||
lines and then presents, and presenting throws the drawing buffer away — so a run of
|
||||
|
||||
@@ -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`
|
||||
and `SPRSAV`; you cannot save it or take its `LEN`.
|
||||
- **`WIDTH` is emulated** by drawing parallel passes.
|
||||
- **Drawing does not persist across frames.** The verbs draw straight to the renderer,
|
||||
so anything drawn is overwritten on the next frame unless the program redraws it.
|
||||
This is a defect, not a design.
|
||||
- **And a redraw has to *fit*.** The host runs a fixed number of source lines and then
|
||||
presents the frame, and presenting discards the drawing buffer — so a sequence of
|
||||
drawing verbs longer than one batch comes back **half drawn**, with an `SSHAPE` at the
|
||||
end capturing only what was issued since the present. "Redraw it every frame" is
|
||||
therefore not sufficient advice on its own. Measured against the standalone frontend's
|
||||
256 lines a batch: after synchronising to a jiffy edge, 220 lines of drawing survive a
|
||||
capture and 250 do not. The only way a program can see the boundary is to watch `TI#`,
|
||||
which is refreshed once per batch — so the step on which it changes is the first step
|
||||
of one. [Chapter 18](18-tutorial-breakout-artwork.md#step-5-find-the-frame-boundary)
|
||||
builds a pacing routine out of exactly that.
|
||||
- **Drawing persists, and the text layer covers it.** A drawing goes into a layer that
|
||||
survives the frame, so a program draws its picture once and it stays — no redrawing, no
|
||||
capturing it into a sprite. What is still true is that the text layer repaints every row
|
||||
it owns, opaque, every frame, and by default it owns the whole window. `WINDOW` shrinks
|
||||
it and hands the rest over. The two together are what makes a picture usable:
|
||||
`WINDOW 0, 0, 39, 1` keeps a one-row status line and gives the drawing verbs everything
|
||||
below it.
|
||||
- **A drawing still has to fit in one batch.** The host runs a fixed number of source lines
|
||||
and then presents, and an `SSHAPE` capture spanning that boundary comes back **half
|
||||
drawn** — the part issued since the present, over whatever was there before. Measured
|
||||
against the standalone frontend's 256 lines a batch: after synchronising to a jiffy edge,
|
||||
220 lines of drawing survive a capture and 250 do not. This bites a *capture*, not the
|
||||
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.
|
||||
|
||||
## Sound
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -251,6 +251,7 @@ akerr_ErrorContext *akbasic_frontend_akgl_pump(void *self, bool *running)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglFrontend *obj = (akbasic_AkglFrontend *)self;
|
||||
bool resume = false;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && running != NULL), AKERR_NULLPOINTER,
|
||||
"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
|
||||
* 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
|
||||
* already there, which is also how a C128 stacks its text plane on its
|
||||
* bitmap plane.
|
||||
* **Take the drawing layer off the target before presenting.**
|
||||
*
|
||||
* SDL refuses to present while a render target is current, and this function
|
||||
* is called from two places with different answers to "is one current". The
|
||||
* 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));
|
||||
/*
|
||||
* 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));
|
||||
FAIL_ZERO_RETURN(errctx, SDL_RenderPresent(obj->renderer->sdl_renderer),
|
||||
AKGL_ERR_SDL, "Couldn't present the frame: %s", SDL_GetError());
|
||||
if ( resume ) {
|
||||
PASS(errctx, akbasic_graphics_akgl_begin(&obj->graphics));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -297,7 +326,16 @@ static akerr_ErrorContext AKERR_NOIGNORE *drive_loop(akbasic_AkglFrontend *obj,
|
||||
* clock_gettime and a _POSIX_C_SOURCE.
|
||||
*/
|
||||
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_graphics_akgl_end(&obj->graphics));
|
||||
PASS(errctx, akbasic_frontend_akgl_pump(obj, &running));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
@@ -320,6 +358,7 @@ void akbasic_frontend_akgl_shutdown(akbasic_AkglFrontend *obj)
|
||||
}
|
||||
/* Before the renderer goes: these are textures it created. */
|
||||
akbasic_sprite_akgl_shutdown(&obj->sprites);
|
||||
akbasic_graphics_akgl_shutdown(&obj->graphics);
|
||||
if ( obj->font != NULL ) {
|
||||
TTF_CloseFont(obj->font);
|
||||
obj->font = NULL;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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