Files
akbasic/tests/akgl_backends.c
Andrew Kesterson 5709dc160c 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
2026-08-02 10:46:10 -04:00

1163 lines
44 KiB
C

/**
* @file akgl_backends.c
* @brief Tests the libakgl-backed sink and device backends against real pixels.
*
* Everything here draws into a small software renderer under the dummy video
* driver and reads the target back, which is the pattern
* deps/libakgl/tests/draw.c established -- it needs no display, no offscreen
* harness and no audio hardware.
*
* These are the only tests in this repository that link SDL, and they only build
* under -DAKBASIC_WITH_AKGL=ON. Everything they exercise below the adaptor is
* libakgl's own and is tested over there; what is asserted here is the seam:
* that a BASIC verb reaches the right akgl call with the right geometry and the
* right colour, and that the text sink puts characters where its own cursor says
* they are.
*/
#include <string.h>
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <akerror.h>
#include <akgl/controller.h>
#include <akgl/error.h>
/*
* game.h purely for the `akgl_renderer` global and its `akgl_default_renderer` storage. The
* interpreter never calls akgl_game_init() -- it drives subsystems directly, and
* owning the game loop is exactly what goal 3 forbids -- but the draw calls read
* that global, so a test standing in for a host has to populate it the same way
* deps/libakgl/tests/draw.c does.
*/
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/renderer.h>
#include <akgl/text.h>
#include <akbasic/akgl.h>
#include <akbasic/error.h>
#include <akbasic/sprite.h>
#include <akbasic/runtime.h>
#include <akbasic/sink.h>
#include "testutil.h"
/** @brief Width and height of the offscreen target. Small enough to read back whole. */
#define TARGET_SIZE 128
/* The interpreter carries every pool it owns, so it does not fit on a stack. */
static akbasic_Runtime RUNTIME;
static akbasic_TextSink SINK;
static akbasic_StdioSink SINKSTATE;
static akbasic_AkglSink AKGLSINKSTATE;
static akbasic_TextSink AKGLSINK;
static akbasic_GraphicsBackend GRAPHICS;
static akbasic_AkglGraphics GRAPHICSSTATE;
static akbasic_InputBackend INPUT;
static akbasic_SpriteBackend SPRITES;
static akbasic_AkglSprites SPRITESSTATE;
static TTF_Font *font = NULL;
static char OUTPUT[8192];
static FILE *OUT = NULL;
/** @brief Report whether one pixel of @p shot carries @p color. Alpha is not compared. */
static bool pixel_is(SDL_Surface *shot, int x, int y, uint8_t r, uint8_t g, uint8_t b)
{
uint8_t gotr = 0;
uint8_t gotg = 0;
uint8_t gotb = 0;
uint8_t gota = 0;
if ( shot == NULL ) {
return false;
}
if ( !SDL_ReadSurfacePixel(shot, x, y, &gotr, &gotg, &gotb, &gota) ) {
return false;
}
return ((gotr == r) && (gotg == g) && (gotb == b));
}
/** @brief Clear the target to opaque black so a drawn pixel is unambiguous. */
static akerr_ErrorContext AKERR_NOIGNORE *clear_target(void)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0, 0, 0, 0xff),
AKGL_ERR_SDL, "%s", SDL_GetError());
FAIL_ZERO_RETURN(errctx, SDL_RenderClear(akgl_renderer->sdl_renderer),
AKGL_ERR_SDL, "%s", SDL_GetError());
SUCCEED_RETURN(errctx);
}
/** @brief Bring up a runtime with the akgl graphics backend attached. */
static akerr_ErrorContext AKERR_NOIGNORE *start_runtime(const char *source)
{
PREPARE_ERROR(errctx);
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_sprite_init_akgl(&SPRITES, &SPRITESSTATE, akgl_renderer, &GRAPHICSSTATE));
PASS(errctx, akbasic_runtime_set_devices(&RUNTIME, &GRAPHICS, NULL, NULL, &SPRITES));
PASS(errctx, akbasic_runtime_load(&RUNTIME, source));
PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN));
PASS(errctx, akbasic_runtime_run(&RUNTIME, 0));
SUCCEED_RETURN(errctx);
}
static void stop_runtime(void)
{
/*
* The sprite backend has to be torn down, not just dropped.
*
* This used to close the output file and nothing else, and got away with it
* because akbasic_sprite_init_akgl() claimed no pooled objects -- the actors
* and textures a program had created leaked, but at two or three per case
* and 64 actor slots nothing ever ran out. Claiming eight collision proxies
* per backend changed the arithmetic: twenty cases at eight apiece is a
* hundred and sixty against a pool of a hundred and twenty-eight, and the
* suite started failing in whichever case happened to be twelfth.
*
* 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;
}
}
/**
* @brief A DRAW reaches real pixels, in the colour COLOR bound to the source.
*
* This is the assertion the whole adaptor exists for: a BASIC line in, a lit
* pixel out, with nothing mocked in between.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_draw_reaches_pixels(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
PASS(errctx, clear_target());
PASS(errctx, start_runtime("10 COLOR 1, 3\n20 DRAW 1, 10, 20\n"));
TEST_REQUIRE_STR(OUTPUT, "");
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the render target back");
/* Palette index 3 is red: 0x88, 0x39, 0x32 in src/graphics_tables.c. */
TEST_REQUIRE(pixel_is(shot, 10, 20, 0x88, 0x39, 0x32),
"DRAW 1,10,20 after COLOR 1,3 should have lit (10,20) red");
TEST_REQUIRE(!pixel_is(shot, 11, 20, 0x88, 0x39, 0x32),
"DRAW should have lit one pixel, not two");
SDL_DestroySurface(shot);
stop_runtime();
SUCCEED_RETURN(errctx);
}
/**
* @brief The renderer's own size is the space BASIC draws into.
*
* The one test that puts a real SDL renderer behind item 9. The target here is
* 128x128 -- deliberately *smaller* than the 320x200 fallback -- so a `SCALE`
* that still divided by the old constants would put its far corner at 2.5 times
* the target's width and light nothing at all.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_size_is_the_renderer(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
PASS(errctx, clear_target());
PASS(errctx, start_runtime("10 PRINT RGR(1)\n20 PRINT RGR(2)\n"));
TEST_REQUIRE_STR(OUTPUT, "128\n128\n");
stop_runtime();
PASS(errctx, clear_target());
PASS(errctx, start_runtime("10 SCALE 1, 1000, 1000\n"
"20 DRAW 1, 1000, 1000\n"));
TEST_REQUIRE_STR(OUTPUT, "");
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the render target back");
/*
* The far corner of the user space lands on the far corner of the target.
* The old code divided by the 320x200 constants, which would have put this
* at (400, 400) -- off a 128x128 surface entirely, lighting nothing.
*/
TEST_REQUIRE(pixel_is(shot, TARGET_SIZE - 1, TARGET_SIZE - 1, 0xff, 0xff, 0xff),
"SCALE 1,1000,1000 then DRAW 1,1000,1000 should reach the bottom-right pixel");
SDL_DestroySurface(shot);
stop_runtime();
SUCCEED_RETURN(errctx);
}
/** @brief A BOX outlines: its corners are lit and its middle is not. */
static akerr_ErrorContext AKERR_NOIGNORE *test_box_outlines(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
PASS(errctx, clear_target());
PASS(errctx, start_runtime("10 BOX 1, 10, 10, 40, 40\n"));
TEST_REQUIRE_STR(OUTPUT, "");
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the render target back");
TEST_REQUIRE(pixel_is(shot, 10, 10, 0xff, 0xff, 0xff), "the box's corner should be lit");
TEST_REQUIRE(pixel_is(shot, 25, 10, 0xff, 0xff, 0xff), "the box's top edge should be lit");
TEST_REQUIRE(!pixel_is(shot, 25, 25, 0xff, 0xff, 0xff),
"an unrotated BOX outlines rather than fills");
SDL_DestroySurface(shot);
stop_runtime();
SUCCEED_RETURN(errctx);
}
/**
* @brief PAINT fills the region a BOX encloses and stops at its edge.
*
* Also the one place the akgl flood fill is exercised through a BASIC verb,
* which is where its partial-fill error would surface if it ever fired.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_paint_fills_region(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
PASS(errctx, clear_target());
PASS(errctx, start_runtime("10 BOX 1, 10, 10, 40, 40\n"
"20 COLOR 2, 6\n"
"30 PAINT 2, 25, 25\n"));
TEST_REQUIRE_STR(OUTPUT, "");
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the render target back");
/* Palette index 6 is green: 0x55, 0xa0, 0x49. */
TEST_REQUIRE(pixel_is(shot, 25, 25, 0x55, 0xa0, 0x49),
"the inside of the box should have been painted green");
TEST_REQUIRE(!pixel_is(shot, 50, 50, 0x55, 0xa0, 0x49),
"the paint should have stopped at the box's edge");
SDL_DestroySurface(shot);
stop_runtime();
SUCCEED_RETURN(errctx);
}
/** @brief SSHAPE and GSHAPE round-trip a region through the handle in a string. */
static akerr_ErrorContext AKERR_NOIGNORE *test_shape_roundtrip(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
PASS(errctx, clear_target());
PASS(errctx, start_runtime("10 BOX 1, 4, 4, 12, 12\n"
"20 SSHAPE A$, 0, 0, 16, 16\n"
"30 GSHAPE A$, 64, 64\n"));
TEST_REQUIRE_STR(OUTPUT, "");
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the render target back");
/* The saved corner was at (4,4); pasted at (64,64) it lands at (68,68). */
TEST_REQUIRE(pixel_is(shot, 68, 68, 0xff, 0xff, 0xff),
"the pasted shape should carry the box's corner");
SDL_DestroySurface(shot);
stop_runtime();
SUCCEED_RETURN(errctx);
}
/**
* @brief The sink measures a character grid and puts text where its cursor says.
*
* akgl_text_measure() is the call this whole file was blocked on until libakgl
* 42b60f7, so the grid it produces is worth asserting directly rather than only
* through what gets drawn.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_sink_grid_and_wrap(void)
{
PREPARE_ERROR(errctx);
int cellw = 0;
int cellh = 0;
PASS(errctx, akgl_text_measure(font, "A", &cellw, &cellh));
TEST_REQUIRE(cellw > 0 && cellh > 0, "the fixture font should measure a real cell");
PASS(errctx, akbasic_sink_init_akgl(&AKGLSINK, &AKGLSINKSTATE, akgl_renderer, font,
TARGET_SIZE, TARGET_SIZE));
TEST_REQUIRE_INT(AKGLSINKSTATE.cellw, cellw);
TEST_REQUIRE_INT(AKGLSINKSTATE.columns, TARGET_SIZE / cellw);
TEST_REQUIRE_INT(AKGLSINKSTATE.rows, TARGET_SIZE / cellh);
PASS(errctx, AKGLSINK.writeln(&AKGLSINK, "HELLO"));
PASS(errctx, AKGLSINK.write(&AKGLSINK, "WORLD"));
TEST_REQUIRE_STR(AKGLSINKSTATE.text[0], "HELLO");
TEST_REQUIRE_STR(AKGLSINKSTATE.text[1], "WORLD");
TEST_REQUIRE_INT(AKGLSINKSTATE.cursorrow, 1);
TEST_REQUIRE_INT(AKGLSINKSTATE.cursorcol, 5);
/* A clear empties the grid and takes the cursor home. */
PASS(errctx, AKGLSINK.clear(&AKGLSINK));
TEST_REQUIRE_STR(AKGLSINKSTATE.text[0], "");
TEST_REQUIRE_INT(AKGLSINKSTATE.cursorrow, 0);
TEST_REQUIRE_INT(AKGLSINKSTATE.cursorcol, 0);
/*
* A text area too small for one character is refused rather than silently
* producing a zero-column grid, which would divide by zero on the first
* wrap.
*/
TEST_REQUIRE_STATUS(akbasic_sink_init_akgl(&AKGLSINK, &AKGLSINKSTATE, akgl_renderer, font, 1, 1),
AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(akbasic_sink_init_akgl(&AKGLSINK, &AKGLSINKSTATE, NULL, font,
TARGET_SIZE, TARGET_SIZE),
AKERR_NULLPOINTER);
SUCCEED_RETURN(errctx);
}
/**
* @brief A character placed past a short row's end pads rather than vanishing.
*
* A row is a NUL-terminated string, so `CHAR 1, 6, 0, "#"` on an otherwise empty
* row used to store the `#` at column 6 with `text[0][0]` still `'\0'` -- and
* the render loop, which stops at the terminator, drew nothing at all. The write
* succeeded, the cursor moved, the stdout mirror showed the character, and the
* window stayed blank. That silent nothing is the trap; the documented
* truncation on the way back is fine and is asserted here too.
*
* The padding has to be spaces rather than whatever is in the buffer, because
* the buffer is not cleared between rows: the gap holds the tail of some longer
* row that used to be here, which is the second case below.
*
* TODO.md section 6 item 32.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_sink_writes_past_a_short_row(void)
{
PREPARE_ERROR(errctx);
PASS(errctx, akbasic_sink_init_akgl(&AKGLSINK, &AKGLSINKSTATE, akgl_renderer, font,
TARGET_SIZE, TARGET_SIZE));
PASS(errctx, AKGLSINK.moveto(&AKGLSINK, 6, 0));
PASS(errctx, AKGLSINK.write(&AKGLSINK, "#"));
TEST_REQUIRE_STR(AKGLSINKSTATE.text[0], " #");
/*
* Writing back over a long row still truncates it -- that half is the
* documented behaviour and the reason a program builds a row whole.
*/
PASS(errctx, AKGLSINK.clear(&AKGLSINK));
PASS(errctx, AKGLSINK.write(&AKGLSINK, "ABCDEFGHIJ"));
PASS(errctx, AKGLSINK.moveto(&AKGLSINK, 3, 0));
PASS(errctx, AKGLSINK.write(&AKGLSINK, "X"));
TEST_REQUIRE_STR(AKGLSINKSTATE.text[0], "ABCX");
/*
* And a write past *that* terminator pads with spaces rather than exposing
* the "EFGHIJ" still sitting in the buffer behind it. A pad that only
* replaced the terminator itself would leave those visible.
*/
PASS(errctx, AKGLSINK.moveto(&AKGLSINK, 7, 0));
PASS(errctx, AKGLSINK.write(&AKGLSINK, "Z"));
TEST_REQUIRE_STR(AKGLSINKSTATE.text[0], "ABCX Z");
SUCCEED_RETURN(errctx);
}
/** @brief The sink's text actually reaches the target when the host renders. */
static akerr_ErrorContext AKERR_NOIGNORE *test_sink_renders(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
int x = 0;
int y = 0;
bool lit = false;
PASS(errctx, clear_target());
PASS(errctx, akbasic_sink_init_akgl(&AKGLSINK, &AKGLSINKSTATE, akgl_renderer, font,
TARGET_SIZE, TARGET_SIZE));
PASS(errctx, AKGLSINK.write(&AKGLSINK, "X"));
PASS(errctx, akbasic_sink_akgl_render(&AKGLSINK));
/*
* Asserted as "something was drawn in the first cell" rather than against
* particular pixels: which ones a glyph lights is FreeType's business and it
* is free to hint them differently. The seam being tested is that the text
* reached the renderer at the cursor's cell at all.
*/
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the render target back");
for ( y = 0; y < AKGLSINKSTATE.cellh && !lit; y++ ) {
for ( x = 0; x < AKGLSINKSTATE.cellw && !lit; x++ ) {
lit = !pixel_is(shot, x, y, 0, 0, 0);
}
}
TEST_REQUIRE(lit, "the sink drew nothing into the first character cell");
SDL_DestroySurface(shot);
SUCCEED_RETURN(errctx);
}
/**
* @brief The input adaptor drains the ring the host's event pump fills.
*
* Synthetic key events through akgl_controller_handle_event(), which is exactly
* how a host feeds it, and then read back through the akbasic backend.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_input_backend(void)
{
PREPARE_ERROR(errctx);
SDL_Event event;
int keycode = 0;
bool available = false;
/*
* handle_event refuses a NULL appstate outright, so it needs something to
* point at even though nothing here reads it. deps/libakgl/tests/controller.c
* uses a placeholder for the same reason.
*/
int appstate_placeholder = 0;
PASS(errctx, akbasic_input_init_akgl(&INPUT));
PASS(errctx, INPUT.flush_keys(&INPUT));
/* An empty ring is success with nothing available, never an error. */
PASS(errctx, INPUT.poll_key(&INPUT, &keycode, &available));
TEST_REQUIRE(!available, "an empty ring should report nothing available");
memset(&event, 0, sizeof(event));
event.type = SDL_EVENT_KEY_DOWN;
event.key.key = SDLK_A;
PASS(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
PASS(errctx, INPUT.poll_key(&INPUT, &keycode, &available));
TEST_REQUIRE(available, "a key the host pumped should reach the interpreter");
TEST_REQUIRE_INT(keycode, SDLK_A);
PASS(errctx, INPUT.poll_key(&INPUT, &keycode, &available));
TEST_REQUIRE(!available, "the ring should drain to empty");
SUCCEED_RETURN(errctx);
}
/**
* @brief A sprite loaded from an image file lands on the target where MOVSPR put it.
*
* The one assertion in this repository that exercises the whole file path:
* a BASIC string that is not an SSHAPE handle, resolved by akgl_path_relative()
* against the working directory, decoded by SDL_image inside
* akgl_spritesheet_initialize(), and blitted through the actor's renderfunc.
*
* The fixture is 8x8 rather than 24x21 on purpose -- a sprite from a file takes
* the image's own size, and an assertion at (43, 33) rather than (40, 30) is
* what says the size came from the image rather than from a constant.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_sprite_from_file(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
PASS(errctx, clear_target());
PASS(errctx, start_runtime("10 SPRSAV \"assets/sprite8x8.png\", 1\n"
"20 SPRITE 1, 1\n"
"30 MOVSPR 1, 40, 30\n"));
TEST_REQUIRE_STR(OUTPUT, "");
PASS(errctx, akbasic_sprite_akgl_render(&SPRITES));
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the render target back");
/*
* Magenta, not white: SPRITE's colour argument modulates the texture and
* this program never gave one, so the image's own colour comes through.
*/
TEST_REQUIRE(pixel_is(shot, 43, 33, 0xff, 0x00, 0xff),
"the loaded sprite should be drawn at MOVSPR's coordinate");
TEST_REQUIRE(pixel_is(shot, 47, 37, 0xff, 0x00, 0xff),
"the whole 8x8 image should have been drawn, not one pixel of it");
TEST_REQUIRE(!pixel_is(shot, 49, 39, 0xff, 0x00, 0xff),
"an 8x8 sprite should stop after eight pixels");
SDL_DestroySurface(shot);
/* A path that names nothing is the program's mistake, and is reported. */
stop_runtime();
PASS(errctx, start_runtime("10 SPRSAV \"assets/nosuchfile.png\", 1\n"));
TEST_REQUIRE(strstr(OUTPUT, "No such sprite image") != NULL,
"a missing image should be reported by name, got \"%s\"", OUTPUT);
stop_runtime();
SUCCEED_RETURN(errctx);
}
/**
* @brief A sprite defined from 63 pattern bytes reaches real pixels, in its colour.
*
* The DATA-driven path a type-in listing uses, end to end: an integer array, the
* bit unpacking, a surface built a pixel at a time, and a texture. The pattern
* is one set bit in the top-left corner, so what is asserted is both that the
* pixel is lit *and* that its neighbour is not -- which is what catches a bit
* order that came through reversed.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_sprite_from_pattern(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
PASS(errctx, clear_target());
PASS(errctx, start_runtime("10 DIM P#(63)\n"
"20 P#(0) = 128\n"
"30 SPRSAV P#, 1\n"
"40 SPRITE 1, 1, 6\n"
"50 MOVSPR 1, 20, 20\n"));
TEST_REQUIRE_STR(OUTPUT, "");
PASS(errctx, akbasic_sprite_akgl_render(&SPRITES));
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the render target back");
/* Palette index 6 is green: 0x55, 0xa0, 0x49. */
TEST_REQUIRE(pixel_is(shot, 20, 20, 0x55, 0xa0, 0x49),
"the pattern's one set bit should be the sprite's top-left pixel");
TEST_REQUIRE(!pixel_is(shot, 21, 20, 0x55, 0xa0, 0x49),
"only one bit was set, so only one pixel should be lit");
TEST_REQUIRE(!pixel_is(shot, 43, 20, 0x55, 0xa0, 0x49),
"a reversed bit order would have lit the far end of the row");
SDL_DestroySurface(shot);
stop_runtime();
SUCCEED_RETURN(errctx);
}
/**
* @brief A region SSHAPE saved becomes a sprite, which is the shared-format claim.
*
* The C128 documents SPRSAV's string as the SSHAPE data format at a fixed 24x21,
* so reusing the shape pool here is faithful rather than a shortcut -- and this
* is what says the handle actually survives the trip through a BASIC string and
* into the sprite device.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_sprite_from_shape(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
PASS(errctx, clear_target());
PASS(errctx, start_runtime("10 COLOR 1, 3\n"
"20 BOX 1, 0, 0, 10, 10\n"
"30 SSHAPE A$, 0, 0, 10, 10\n"
"40 SPRSAV A$, 1\n"
"50 SPRITE 1, 1\n"
"60 MOVSPR 1, 60, 60\n"));
TEST_REQUIRE_STR(OUTPUT, "");
PASS(errctx, akbasic_sprite_akgl_render(&SPRITES));
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the render target back");
/*
* Palette index 3 is red, which is what the BOX was outlined in. The corner
* of the box is the (0,0) pixel of the saved region, so it lands exactly
* where MOVSPR put the sprite -- and its interior does not, which is what
* says a region was copied rather than a rectangle drawn.
*/
TEST_REQUIRE(pixel_is(shot, 60, 60, 0x88, 0x39, 0x32),
"the saved region's corner should have been drawn as a sprite");
TEST_REQUIRE(!pixel_is(shot, 65, 65, 0x88, 0x39, 0x32),
"the region was an outline, so its middle should not be red");
SDL_DestroySurface(shot);
stop_runtime();
SUCCEED_RETURN(errctx);
}
/**
* @brief Run one program and report the collision mask the device computes.
*
* Straight at the backend rather than through `BUMP(1)`, because what is under
* test is the overlap arithmetic and not the accumulate-and-clear the runtime
* wraps it in -- that half is already covered by tests/sprite_verbs.c against a
* mock. Going through BUMP here would assert both at once and blame the wrong
* one when either broke.
*/
static akerr_ErrorContext AKERR_NOIGNORE *mask_for(const char *source, uint16_t *dest)
{
PREPARE_ERROR(errctx);
PASS(errctx, start_runtime(source));
TEST_REQUIRE_STR(OUTPUT, "");
PASS(errctx, SPRITES.collisions(&SPRITES, dest));
SUCCEED_RETURN(errctx);
}
/**
* @brief What spr_collisions() answers, pinned before anything replaces it.
*
* **This code had no test at all.** tests/sprite_verbs.c drives the collision
* path end to end but through a mock backend, so the real overlap arithmetic
* could have changed what BUMP(1) reports for every program in existence and the
* suite would still have passed. That is the gap this closes, and it is closed
* *before* the arithmetic is touched so there is a before to compare an after
* against.
*
* Two of the cases below are the ones a replacement is most likely to get wrong,
* and each says so where it stands.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_collision_pairs(void)
{
PREPARE_ERROR(errctx);
uint16_t mask = 0;
/* Nothing defined: no actor, so nothing to compare and nothing set. */
PASS(errctx, mask_for("10 X# = 0\n", &mask));
TEST_REQUIRE_INT(mask, 0);
stop_runtime();
/*
* Two 24x21 patterns overlapping. Bit n-1 is sprite n, so sprites 1 and 2
* are 0x03.
*/
PASS(errctx, mask_for("10 DIM P#(63)\n"
"20 FOR I# = 0 TO 62\n"
"30 P#(I#) = 255\n"
"40 NEXT I#\n"
"50 SPRSAV P#, 1\n"
"60 SPRSAV P#, 2\n"
"70 SPRITE 1, 1\n"
"80 SPRITE 2, 1\n"
"90 MOVSPR 1, 10, 10\n"
"100 MOVSPR 2, 20, 20\n", &mask));
TEST_REQUIRE_INT(mask, 0x03);
stop_runtime();
/*
* **Edge to edge is not a collision.** Sprite 1 spans x 10..33 inclusive and
* sprite 2 starts at 34, so the test is `ax + aw < bx + 1` -- the strict `<`
* in spr_collisions(). A tile-aligned program puts sprites here constantly,
* and a replacement that answers "touching" instead of "overlapping" changes
* every one of them.
*/
PASS(errctx, mask_for("10 DIM P#(63)\n"
"20 FOR I# = 0 TO 62\n"
"30 P#(I#) = 255\n"
"40 NEXT I#\n"
"50 SPRSAV P#, 1\n"
"60 SPRSAV P#, 2\n"
"70 SPRITE 1, 1\n"
"80 SPRITE 2, 1\n"
"90 MOVSPR 1, 10, 10\n"
"100 MOVSPR 2, 34, 10\n", &mask));
TEST_REQUIRE_INT(mask, 0);
stop_runtime();
/* A hidden sprite is not in the world, whatever its coordinates say. */
PASS(errctx, mask_for("10 DIM P#(63)\n"
"20 FOR I# = 0 TO 62\n"
"30 P#(I#) = 255\n"
"40 NEXT I#\n"
"50 SPRSAV P#, 1\n"
"60 SPRSAV P#, 2\n"
"70 SPRITE 1, 1\n"
"80 SPRITE 2, 0\n"
"90 MOVSPR 1, 10, 10\n"
"100 MOVSPR 2, 10, 10\n", &mask));
TEST_REQUIRE_INT(mask, 0);
stop_runtime();
/*
* The x-expand bit doubles the box that collides, not just the one that
* draws. Sprite 1 unexpanded ends at x 33 and does not reach sprite 2 at 40;
* expanded it spans 48 and does.
*/
PASS(errctx, mask_for("10 DIM P#(63)\n"
"20 FOR I# = 0 TO 62\n"
"30 P#(I#) = 255\n"
"40 NEXT I#\n"
"50 SPRSAV P#, 1\n"
"60 SPRSAV P#, 2\n"
"70 SPRITE 1, 1, 1, 0, 1, 0\n"
"80 SPRITE 2, 1\n"
"90 MOVSPR 1, 10, 10\n"
"100 MOVSPR 2, 40, 10\n", &mask));
TEST_REQUIRE_INT(mask, 0x03);
stop_runtime();
SUCCEED_RETURN(errctx);
}
/**
* @brief A cross-shaped overlap is a collision, and it is the case to watch.
*
* A tall thin sprite crossing a short wide one overlaps without either
* rectangle containing a corner of the other. libakgl's akgl_collide_rectangles()
* is documented as answering "no" here -- a corner-containment test cannot see
* it -- which is why src/sprite_akgl.c does the four comparisons itself rather
* than calling it.
*
* So this assertion is the one that fails if collision is ever moved onto a
* corner-containment implementation, and it is worth having before rather than
* after such a move.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_collision_cross_shape(void)
{
PREPARE_ERROR(errctx);
uint16_t mask = 0;
/*
* SSHAPE captures a region at whatever size it is asked for, and a sprite
* made from one keeps that size -- which is the only way to get two sprites
* of different aspect, since a DATA pattern is always 24 by 21.
*
* Sprite 1 is about 6 wide and 40 tall at (30, 10), so it spans roughly
* x 30..36, y 10..50. Sprite 2 is about 40 wide and 6 tall at (10, 30), so
* roughly x 10..50, y 30..36. They cross in the middle, and **neither holds
* a corner of the other** -- which is the whole point.
*
* Nothing is drawn into either region first. Collision is by bounding box,
* so what a sprite's pixels contain does not enter into it; the only thing
* being borrowed from SSHAPE here is the size. That also keeps the margins
* wide enough that SSHAPE's inclusive-bound question cannot decide the
* answer either way.
*/
PASS(errctx, mask_for("10 GRAPHIC 1, 1\n"
"20 SSHAPE V$, 0, 0, 6, 40\n"
"30 SSHAPE H$, 0, 50, 40, 56\n"
"40 SPRSAV V$, 1\n"
"50 SPRSAV H$, 2\n"
"60 SPRITE 1, 1\n"
"70 SPRITE 2, 1\n"
"80 MOVSPR 1, 30, 10\n"
"90 MOVSPR 2, 10, 30\n", &mask));
TEST_REQUIRE_INT(mask, 0x03);
stop_runtime();
/*
* The two assertions that stop the one above from passing by accident.
*
* A cross is only a cross if the two sprites really are the long thin shapes
* this test believes they are, and nothing above proves that -- two 40x40
* blobs at the same coordinates would also report 0x03, for the ordinary
* reason, and the test would look like it was doing its job. So move each
* one clear along the axis it is *supposed* to be short on. A sprite that
* came out the wrong size stays overlapping and fails here.
*/
PASS(errctx, mask_for("10 GRAPHIC 1, 1\n"
"20 SSHAPE V$, 0, 0, 6, 40\n"
"30 SSHAPE H$, 0, 50, 40, 56\n"
"40 SPRSAV V$, 1\n"
"50 SPRSAV H$, 2\n"
"60 SPRITE 1, 1\n"
"70 SPRITE 2, 1\n"
"80 MOVSPR 1, 30, 10\n"
"90 MOVSPR 2, 10, 60\n", &mask));
TEST_REQUIRE_INT(mask, 0);
stop_runtime();
PASS(errctx, mask_for("10 GRAPHIC 1, 1\n"
"20 SSHAPE V$, 0, 0, 6, 40\n"
"30 SSHAPE H$, 0, 50, 40, 56\n"
"40 SPRSAV V$, 1\n"
"50 SPRSAV H$, 2\n"
"60 SPRITE 1, 1\n"
"70 SPRITE 2, 1\n"
"80 MOVSPR 1, 60, 10\n"
"90 MOVSPR 2, 10, 30\n", &mask));
TEST_REQUIRE_INT(mask, 0);
stop_runtime();
SUCCEED_RETURN(errctx);
}
/** @brief The 24x21 pattern program the shape cases share, with @p tail appended. */
static akerr_ErrorContext AKERR_NOIGNORE *two_sprites(const char *tail, uint16_t *dest)
{
PREPARE_ERROR(errctx);
char source[2048];
snprintf(source, sizeof(source),
"10 DIM P#(63)\n"
"20 FOR I# = 0 TO 62\n"
"30 P#(I#) = 255\n"
"40 NEXT I#\n"
"50 SPRSAV P#, 1\n"
"60 SPRSAV P#, 2\n"
"70 SPRITE 1, 1\n"
"80 SPRITE 2, 1\n"
"%s", tail);
PASS(errctx, mask_for(source, dest));
SUCCEED_RETURN(errctx);
}
/**
* @brief SPRHIT narrows what a sprite collides with, and the default is the frame.
*
* The first case is the one that matters most and the reason this whole change
* could be made at all: **a program that never says SPRHIT must behave exactly as
* it did.** Everything else here is new surface; that one is a promise.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_sprite_shapes(void)
{
PREPARE_ERROR(errctx);
uint16_t mask = 0;
/* Two 24x21 frames, ten pixels apart: overlapping, as they always were. */
PASS(errctx, two_sprites("90 MOVSPR 1, 10, 10\n"
"100 MOVSPR 2, 20, 20\n", &mask));
TEST_REQUIRE_INT(mask, 0x03);
stop_runtime();
/*
* A box in the top-left corner of sprite 1 only, four pixels square. Sprite
* 2's frame starts at 20 and the shape ends at 14, so they no longer meet --
* with both sprites in exactly the places the case above had them.
*/
PASS(errctx, two_sprites("85 SPRHIT 1, 1, 0, 0, 4, 4\n"
"90 MOVSPR 1, 10, 10\n"
"100 MOVSPR 2, 20, 20\n", &mask));
TEST_REQUIRE_INT(mask, 0);
stop_runtime();
/* And the shape is offset from the sprite, so moving the sprite moves it. */
PASS(errctx, two_sprites("85 SPRHIT 1, 1, 0, 0, 4, 4\n"
"90 MOVSPR 1, 18, 18\n"
"100 MOVSPR 2, 20, 20\n", &mask));
TEST_REQUIRE_INT(mask, 0x03);
stop_runtime();
/* Kind 0 takes a sprite out of collision while leaving it on the screen. */
PASS(errctx, two_sprites("85 SPRHIT 1, 0\n"
"90 MOVSPR 1, 10, 10\n"
"100 MOVSPR 2, 10, 10\n", &mask));
TEST_REQUIRE_INT(mask, 0);
stop_runtime();
/*
* A circle is inscribed in the frame, so two frames touching at their
* corners -- which is the false positive docs/08-sprites.md ships a figure
* of -- stop colliding when both become circles. This is the case that
* figure exists to complain about.
*/
PASS(errctx, two_sprites("90 MOVSPR 1, 0, 0\n"
"100 MOVSPR 2, 23, 20\n", &mask));
TEST_REQUIRE_INT(mask, 0x03);
stop_runtime();
PASS(errctx, two_sprites("85 SPRHIT 1, 2\n"
"86 SPRHIT 2, 2\n"
"90 MOVSPR 1, 0, 0\n"
"100 MOVSPR 2, 23, 20\n", &mask));
TEST_REQUIRE_INT(mask, 0);
stop_runtime();
SUCCEED_RETURN(errctx);
}
/** @brief Run a program and report the static-geometry mask. */
static akerr_ErrorContext AKERR_NOIGNORE *solidmask_for(const char *source, uint16_t *dest)
{
PREPARE_ERROR(errctx);
PASS(errctx, start_runtime(source));
TEST_REQUIRE_STR(OUTPUT, "");
PASS(errctx, SPRITES.solids(&SPRITES, dest));
SUCCEED_RETURN(errctx);
}
/**
* @brief SOLID puts a collidable rectangle on the field without spending a sprite.
*
* This is the point of the whole exercise. A brick is not a sprite -- there are
* eight and a wall wants sixty -- so until now a program could only collide with
* one by doing the arithmetic itself against its own array. Now it registers a
* rectangle and the interpreter answers.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_static_geometry(void)
{
PREPARE_ERROR(errctx);
uint16_t mask = 0;
/* A 24x21 sprite at (100, 100) squarely inside a rectangle. */
PASS(errctx, solidmask_for("10 DIM P#(63)\n"
"20 FOR I# = 0 TO 62\n"
"30 P#(I#) = 255\n"
"40 NEXT I#\n"
"50 SPRSAV P#, 1\n"
"60 SPRITE 1, 1\n"
"70 SOLID 7, 90, 90, 140, 140\n"
"80 MOVSPR 1, 100, 100\n", &mask));
TEST_REQUIRE_INT(mask, 0x01);
stop_runtime();
/* Moved clear of it, nothing. */
PASS(errctx, solidmask_for("10 DIM P#(63)\n"
"20 FOR I# = 0 TO 62\n"
"30 P#(I#) = 255\n"
"40 NEXT I#\n"
"50 SPRSAV P#, 1\n"
"60 SPRITE 1, 1\n"
"70 SOLID 7, 90, 90, 140, 140\n"
"80 MOVSPR 1, 200, 200\n", &mask));
TEST_REQUIRE_INT(mask, 0);
stop_runtime();
/*
* Retired, and the sprite has not moved. This is the assertion that catches
* a proxy released without its registration going with it -- the shape would
* still be found and the mask would still come back set.
*/
PASS(errctx, solidmask_for("10 DIM P#(63)\n"
"20 FOR I# = 0 TO 62\n"
"30 P#(I#) = 255\n"
"40 NEXT I#\n"
"50 SPRSAV P#, 1\n"
"60 SPRITE 1, 1\n"
"70 SOLID 7, 90, 90, 140, 140\n"
"80 MOVSPR 1, 100, 100\n"
"90 SOLID 7\n", &mask));
TEST_REQUIRE_INT(mask, 0);
stop_runtime();
/*
* A full wall registered, walked into, and cleared -- sixty-four of them, so
* the proxy budget is exercised at its ceiling and the pool has to come back
* intact afterwards or the next case fails.
*/
PASS(errctx, solidmask_for("10 DIM P#(63)\n"
"20 FOR I# = 0 TO 62\n"
"30 P#(I#) = 255\n"
"40 NEXT I#\n"
"50 SPRSAV P#, 1\n"
"60 SPRITE 1, 1\n"
"70 FOR I# = 1 TO 64\n"
"80 X# = I# * 30\n"
"90 SOLID I#, X#, 0, X# + 20, 20\n"
"100 NEXT I#\n"
"110 MOVSPR 1, 300, 5\n", &mask));
TEST_REQUIRE_INT(mask, 0x01);
stop_runtime();
/* And the sixty-fifth is refused by name rather than exhausting the pool. */
PASS(errctx, start_runtime("10 SOLID 65, 0, 0, 10, 10\n"));
TEST_REQUIRE(strstr(OUTPUT, "outside 1..64") != NULL,
"SOLID 65 should be refused, got \"%s\"", OUTPUT);
stop_runtime();
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);
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
ATTEMPT {
CATCH(errctx, akgl_error_init());
akgl_renderer = &akgl_default_renderer;
FAIL_ZERO_BREAK(errctx, SDL_Init(SDL_INIT_VIDEO), AKGL_ERR_SDL,
"Couldn't initialize SDL: %s", SDL_GetError());
FAIL_ZERO_BREAK(errctx, TTF_Init(), AKGL_ERR_SDL,
"Couldn't initialize SDL_ttf: %s", SDL_GetError());
FAIL_ZERO_BREAK(errctx,
SDL_CreateWindowAndRenderer("net/aklabs/akbasic/test_akgl_backends",
TARGET_SIZE, TARGET_SIZE, 0,
&akgl_window, &akgl_renderer->sdl_renderer),
AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
/*
* Bind the 2D methods. akgl_text_rendertextat() reaches through
* akgl_renderer->draw_texture and an SDL_Renderer alone does not fill that in;
* deps/libakgl/tests/draw.c gets away without it because the draw
* primitives take the SDL_Renderer directly. These six pointers were
* assigned by hand here until libakgl 0.3.0 split the bind half out of the
* init half -- API-gap item 7, filed from this file. Both were renamed in
* 0.5.0, from akgl_render_bind2d and akgl_render_init2d.
*/
CATCH(errctx, akgl_render_2d_bind(akgl_renderer));
/*
* The three things akgl_game_init() would have done and this file, being
* a host that deliberately never calls it, has to do itself: the object
* pools, the name registries, and a camera for akgl_actor_render() to
* dereference. src/frontend_akgl.c does the same, and says why at length.
*/
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
akgl_camera = &akgl_default_camera;
akgl_camera->x = 0.0f;
akgl_camera->y = 0.0f;
akgl_camera->w = (float)TARGET_SIZE;
akgl_camera->h = (float)TARGET_SIZE;
/*
* libakgl's own monospaced fixture, borrowed rather than copied. Being
* monospaced is what makes a character grid meaningful to assert on.
*/
font = TTF_OpenFont(AKBASIC_TEST_FONT, 12);
FAIL_ZERO_BREAK(errctx, font, AKGL_ERR_SDL,
"Couldn't open %s: %s", AKBASIC_TEST_FONT, SDL_GetError());
CATCH(errctx, test_draw_reaches_pixels());
CATCH(errctx, test_size_is_the_renderer());
CATCH(errctx, test_box_outlines());
CATCH(errctx, test_paint_fills_region());
CATCH(errctx, test_shape_roundtrip());
CATCH(errctx, test_sink_grid_and_wrap());
CATCH(errctx, test_sink_writes_past_a_short_row());
CATCH(errctx, test_sink_renders());
CATCH(errctx, test_input_backend());
CATCH(errctx, test_sprite_from_file());
CATCH(errctx, test_sprite_from_pattern());
CATCH(errctx, test_sprite_from_shape());
CATCH(errctx, test_collision_pairs());
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);
}
TTF_Quit();
SDL_Quit();
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {
LOG_ERROR_WITH_MESSAGE(errctx, "akgl backend test failed");
akbasic_test_failures += 1;
/*
* FINISH_NORETURN rather than FINISH, matching src/main.c: FINISH
* expands a `return __err_context` that this int-returning function
* cannot compile even when the branch is unreachable. HANDLE_DEFAULT
* above has already marked the context handled, so nothing aborts.
*/
} FINISH_NORETURN(errctx);
return akbasic_test_failures;
}