diff --git a/TODO.md b/TODO.md index d8b32b1..eb3d221 100644 --- a/TODO.md +++ b/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 diff --git a/docs/06-graphics.md b/docs/06-graphics.md index 11034bc..357dd91 100644 --- a/docs/06-graphics.md +++ b/docs/06-graphics.md @@ -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 diff --git a/docs/13-differences.md b/docs/13-differences.md index 23898e8..dfc1263 100644 --- a/docs/13-differences.md +++ b/docs/13-differences.md @@ -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 diff --git a/include/akbasic/akgl.h b/include/akbasic/akgl.h index 30c4be5..8da2343 100644 --- a/include/akbasic/akgl.h +++ b/include/akbasic/akgl.h @@ -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. * diff --git a/src/frontend_akgl.c b/src/frontend_akgl.c index 16a1ed7..05433ed 100644 --- a/src/frontend_akgl.c +++ b/src/frontend_akgl.c @@ -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; diff --git a/src/graphics_akgl.c b/src/graphics_akgl.c index f1f856c..652ef66 100644 --- a/src/graphics_akgl.c +++ b/src/graphics_akgl.c @@ -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; + } +} diff --git a/tests/akgl_backends.c b/tests/akgl_backends.c index 1fe0cca..7492efb 100644 --- a/tests/akgl_backends.c +++ b/tests/akgl_backends.c @@ -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);