/** * @file sprite_akgl.c * @brief Wires the sprite backend record to libakgl's actors. * * Each of BASIC's eight sprites becomes a full libakgl object graph -- a * spritesheet holding one texture, a sprite cutting one frame from it, a * character mapping state 0 to that sprite, and an actor instantiating the * character -- so a host game sees BASIC's sprites in libakgl's actor registry * alongside its own and can do anything to them it can do to an actor. * * **None of that comes from JSON.** akgl_sprite_load_json() is a thin wrapper * over the same four initializers plus writes to public struct fields, and every * field it fills from a document -- frame list, animation speed, loop flags, * state-to-sprite map -- is something a Commodore sprite does not have. The one * exception is the image, and that is exactly where this file *does* call * libakgl: `SPRSAV "ship.png", 1` resolves through akgl_path_relative() and * loads through akgl_spritesheet_initialize(), the same two calls * akgl_sprite_load_json_spritesheet() makes. * * Every actor gets a renderfunc of its own rather than akgl_actor_render(). It * was two reasons, both filed upstream, and **libakgl 0.5.0 fixed one of them**: * the default used to compute its destination height from the sprite's *width*, * drawing a 24x21 Commodore sprite as a 24x24 square, and it now uses the * height. That was libakgl defect 26. * * The remaining reason is the one that still requires this file: an akgl_Actor * carries a single scalar `scale` applied to both axes, which cannot express * SPRITE's separate x- and y-expand bits. libakgl records that as an open item * and names this interpreter as the caller that needs it; the shape it takes -- * scale_x/scale_y beside scale, or replacing scale outright -- is a design * decision over there rather than a patch. Until it lands, installing a function * pointer is libakgl's own extension point for exactly this, so nothing is * forked. */ #include #include #include #include #include #include #include #include #include #include #include #include /** @brief The colour conversion, same as src/graphics_akgl.c's. */ static SDL_Color to_sdl(akbasic_Color color) { SDL_Color out; out.r = color.r; out.g = color.g; out.b = color.b; out.a = color.a; return out; } /** @brief Recover the backend's own state, or say that it has none. */ static akerr_ErrorContext *state_of(akbasic_SpriteBackend *self, akbasic_AkglSprites **dest) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in akgl sprite backend"); *dest = (akbasic_AkglSprites *)self->self; FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_NULLPOINTER, "akgl sprite backend has no state"); SUCCEED_RETURN(errctx); } /** @brief Recover the state and turn a BASIC sprite number into a slot index. */ static akerr_ErrorContext *slot_of(akbasic_SpriteBackend *self, int n, akbasic_AkglSprites **state, int *index) { PREPARE_ERROR(errctx); PASS(errctx, state_of(self, state)); FAIL_ZERO_RETURN(errctx, (n >= 1 && n <= AKBASIC_MAX_SPRITES), AKBASIC_ERR_BOUNDS, "Sprite %d is outside 1..%d", n, AKBASIC_MAX_SPRITES); *index = n - 1; SUCCEED_RETURN(errctx); } /** * @brief Draw one BASIC sprite, in place of akgl_actor_render(). * * Differs from the default in exactly one way now, noted at the top of this file: * the two expansion bits scale the axes independently, where an actor's single * `scale` cannot. Taking the destination height from the sprite's height used to * be the second difference and is the default's behaviour as of libakgl 0.5.0 -- * the line below is no longer a correction, only agreement. * * Like the default, it skips rather than reports: an actor with no image, one * that is hidden, one whose slot has been reused. A frame is not the place to * fail. */ static akerr_ErrorContext *spr_render_actor(akgl_Actor *obj) { PREPARE_ERROR(errctx); akbasic_AkglSprites *state = NULL; akgl_Sprite *sprite = NULL; SDL_FRect src; SDL_FRect dest; int i = 0; int found = -1; FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL actor in sprite render"); if ( !obj->visible || obj->basechar == NULL ) { SUCCEED_RETURN(errctx); } state = (akbasic_AkglSprites *)obj->actorData; FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "Sprite actor \"%s\" carries no backend state", obj->name); for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) { if ( state->actors[i] == obj ) { found = i; break; } } if ( found < 0 ) { SUCCEED_RETURN(errctx); } sprite = state->sprites[found]; if ( sprite == NULL || sprite->sheet == NULL || sprite->sheet->texture == NULL ) { SUCCEED_RETURN(errctx); } PASS(errctx, akgl_spritesheet_coords_for_frame(sprite, &src, 0)); dest.x = obj->x - (akgl_camera != NULL ? akgl_camera->x : 0.0f); dest.y = obj->y - (akgl_camera != NULL ? akgl_camera->y : 0.0f); dest.w = (float32_t)sprite->width * (state->xexpand[found] ? 2.0f : 1.0f); dest.h = (float32_t)sprite->height * (state->yexpand[found] ? 2.0f : 1.0f); PASS(errctx, state->renderer->draw_texture(state->renderer, sprite->sheet->texture, &src, &dest, 0, NULL, SDL_FLIP_NONE)); SUCCEED_RETURN(errctx); } /** @brief Tear down slot @p i, releasing its texture with it. */ static akerr_ErrorContext *release_slot(akbasic_AkglSprites *state, int i) { PREPARE_ERROR(errctx); /* * Actor first, then character, sprite and sheet. Each borrows the one below * it without taking a reference, so releasing in the other order leaves a * live object pointing at a zeroed pool slot. */ if ( state->actors[i] != NULL ) { PASS(errctx, akgl_heap_release_actor(state->actors[i])); state->actors[i] = NULL; } if ( state->characters[i] != NULL ) { PASS(errctx, akgl_heap_release_character(state->characters[i])); state->characters[i] = NULL; } if ( state->sprites[i] != NULL ) { PASS(errctx, akgl_heap_release_sprite(state->sprites[i])); state->sprites[i] = NULL; } if ( state->sheets[i] != NULL ) { /* This is what destroys the texture: see akgl_heap_release_spritesheet. */ PASS(errctx, akgl_heap_release_spritesheet(state->sheets[i])); state->sheets[i] = NULL; } SUCCEED_RETURN(errctx); } /** * @brief Build slot @p i's object graph around a sheet that is already loaded. * * The names are fixed 128-byte buffers, not string literals, because * akgl_sprite_initialize() and akgl_actor_initialize() memcpy a whole * AKGL_*_MAX_NAME_LENGTH from whatever they are handed. */ static akerr_ErrorContext *build_slot(akbasic_AkglSprites *state, int i, int width, int height) { PREPARE_ERROR(errctx); char spritename[AKGL_SPRITE_MAX_NAME_LENGTH]; char charname[AKGL_SPRITE_MAX_NAME_LENGTH]; char actorname[AKGL_ACTOR_MAX_NAME_LENGTH]; float32_t oldx = 0.0f; float32_t oldy = 0.0f; bool oldvisible = false; if ( state->actors[i] != NULL ) { oldx = state->actors[i]->x; oldy = state->actors[i]->y; oldvisible = state->actors[i]->visible; } memset(spritename, 0, sizeof(spritename)); memset(charname, 0, sizeof(charname)); memset(actorname, 0, sizeof(actorname)); SDL_snprintf(spritename, sizeof(spritename), "akbasic:sprite:%d", i + 1); SDL_snprintf(charname, sizeof(charname), "akbasic:character:%d", i + 1); SDL_snprintf(actorname, sizeof(actorname), "akbasic:actor:%d", i + 1); PASS(errctx, akgl_heap_next_sprite(&state->sprites[i])); PASS(errctx, akgl_sprite_initialize(state->sprites[i], spritename, state->sheets[i])); state->sprites[i]->frames = 1; state->sprites[i]->frameids[0] = 0; state->sprites[i]->width = (uint32_t)width; state->sprites[i]->height = (uint32_t)height; PASS(errctx, akgl_heap_next_character(&state->characters[i])); PASS(errctx, akgl_character_initialize(state->characters[i], charname)); PASS(errctx, akgl_character_sprite_add(state->characters[i], state->sprites[i], 0)); PASS(errctx, akgl_heap_next_actor(&state->actors[i])); PASS(errctx, akgl_actor_initialize(state->actors[i], actorname)); PASS(errctx, akgl_actor_set_character(state->actors[i], charname)); state->actors[i]->state = 0; state->actors[i]->actorData = state; state->actors[i]->renderfunc = spr_render_actor; /* * Position and visibility survive a redefinition. SPRSAV changes what a * sprite looks like, not where it is or whether it is on -- a program that * animates by swapping patterns in a loop would otherwise have to re-issue * SPRITE and MOVSPR after every frame. */ state->actors[i]->x = oldx; state->actors[i]->y = oldy; state->actors[i]->visible = oldvisible; SUCCEED_RETURN(errctx); } /** @brief Take ownership of @p surface as slot @p i's image, replacing whatever was there. */ static akerr_ErrorContext *install_surface(akbasic_AkglSprites *state, int i, SDL_Surface *surface) { PREPARE_ERROR(errctx); SDL_Texture *texture = NULL; char sheetname[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH]; int width = 0; int height = 0; FAIL_ZERO_RETURN(errctx, (surface != NULL), AKERR_NULLPOINTER, "NULL surface for a sprite"); width = surface->w; height = surface->h; texture = SDL_CreateTextureFromSurface(state->renderer->sdl_renderer, surface); FAIL_ZERO_RETURN(errctx, (texture != NULL), AKGL_ERR_SDL, "Could not upload a sprite pattern: %s", SDL_GetError()); /* * Nearest-neighbour. A sprite is a 24x21 bitmap and an expanded one is drawn * at twice the size; smoothing it would turn a pixel into a smudge, which is * not what anybody typing SPRSAV into a BASIC has in mind. */ SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST); PASS(errctx, release_slot(state, i)); /* * The sheet is assembled rather than initialized: akgl_spritesheet_initialize * loads an image from a path, and this pattern came from bytes or from a * saved region. Everything about it still matches what that function * produces, so akgl_heap_release_spritesheet() tears it down the same way -- * which is what makes the texture's ownership one rule rather than two. */ PASS(errctx, akgl_heap_next_spritesheet(&state->sheets[i])); memset(state->sheets[i], 0, sizeof(*state->sheets[i])); memset(sheetname, 0, sizeof(sheetname)); SDL_snprintf(sheetname, sizeof(sheetname), "akbasic:sheet:%d", i + 1); SDL_strlcpy(state->sheets[i]->name, sheetname, sizeof(state->sheets[i]->name)); state->sheets[i]->texture = texture; state->sheets[i]->refcount = 1; SDL_SetPointerProperty(AKGL_REGISTRY_SPRITESHEET, state->sheets[i]->name, state->sheets[i]); PASS(errctx, build_slot(state, i, width, height)); SUCCEED_RETURN(errctx); } /** @brief Write one unpacked pattern into @p surface, a set bit in @p fg and a clear one in @p bg. */ static akerr_ErrorContext *paint_pattern(SDL_Surface *surface, const uint8_t *pixels, akbasic_Color fg, akbasic_Color bg) { PREPARE_ERROR(errctx); int x = 0; int y = 0; for ( y = 0; y < AKBASIC_SPRITE_HEIGHT; y++ ) { for ( x = 0; x < AKBASIC_SPRITE_WIDTH; x++ ) { akbasic_Color c = (pixels[(y * AKBASIC_SPRITE_WIDTH) + x] != 0 ? fg : bg); FAIL_ZERO_RETURN(errctx, SDL_WriteSurfacePixel(surface, x, y, c.r, c.g, c.b, c.a), AKGL_ERR_SDL, "Could not write sprite pixel %d,%d: %s", x, y, SDL_GetError()); } } SUCCEED_RETURN(errctx); } static akerr_ErrorContext *spr_define(akbasic_SpriteBackend *self, int n, const uint8_t *pattern, int bytes, akbasic_Color fg, akbasic_Color bg) { PREPARE_ERROR(errctx); akbasic_AkglSprites *state = NULL; SDL_Surface *surface = NULL; uint8_t pixels[AKBASIC_SPRITE_WIDTH * AKBASIC_SPRITE_HEIGHT]; int index = 0; PASS(errctx, slot_of(self, n, &state, &index)); PASS(errctx, akbasic_sprite_unpack(pattern, bytes, pixels)); surface = SDL_CreateSurface(AKBASIC_SPRITE_WIDTH, AKBASIC_SPRITE_HEIGHT, SDL_PIXELFORMAT_RGBA32); FAIL_ZERO_RETURN(errctx, (surface != NULL), AKGL_ERR_SDL, "Could not create a sprite surface: %s", SDL_GetError()); ATTEMPT { /* * The pixel loop is its own function so that it can be reached with a * single CATCH. Written inline it would need a FAIL inside two nested * loops inside this ATTEMPT, and the _BREAK forms expand to a C break * that escapes only the inner loop. */ CATCH(errctx, paint_pattern(surface, pixels, fg, bg)); CATCH(errctx, install_surface(state, index, surface)); } CLEANUP { SDL_DestroySurface(surface); } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *spr_define_shape(akbasic_SpriteBackend *self, int n, int handle) { PREPARE_ERROR(errctx); akbasic_AkglSprites *state = NULL; int index = 0; PASS(errctx, slot_of(self, n, &state, &index)); FAIL_ZERO_RETURN(errctx, (state->graphics != NULL), AKBASIC_ERR_DEVICE, "SPRSAV from a saved shape needs a graphics backend, and this host supplied none"); FAIL_ZERO_RETURN(errctx, (handle >= 0 && handle < state->graphics->shapecount), AKBASIC_ERR_VALUE, "Shape handle %d names no saved region", handle); FAIL_ZERO_RETURN(errctx, (state->graphics->shapes[handle] != NULL), AKBASIC_ERR_VALUE, "Shape handle %d has been released", handle); PASS(errctx, install_surface(state, index, state->graphics->shapes[handle])); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *spr_define_file(akbasic_SpriteBackend *self, int n, const char *path, const char *root) { PREPARE_ERROR(errctx); akbasic_AkglSprites *state = NULL; akgl_String *resolved = NULL; int index = 0; bool missing = false; PASS(errctx, slot_of(self, n, &state, &index)); FAIL_ZERO_RETURN(errctx, (path != NULL && path[0] != '\0'), AKBASIC_ERR_VALUE, "SPRSAV was given an empty file name"); PASS(errctx, akgl_heap_next_string(&resolved)); ATTEMPT { CATCH(errctx, akgl_string_initialize(resolved, NULL)); /* * The same resolution a sprite document gets for the spritesheet it * names: the working directory first, then the directory the file doing * the naming lives in -- here, the directory the BASIC program was * loaded from. A program stored beside its art therefore works whether * it was launched from its own directory or from anywhere else. */ CATCH(errctx, akgl_path_relative((char *)(root != NULL ? root : "."), (char *)path, resolved)); CATCH(errctx, release_slot(state, index)); CATCH(errctx, akgl_heap_next_spritesheet(&state->sheets[index])); /* * And the same load. The two frame-size arguments are dead upstream -- * akgl_spritesheet_initialize does not write them -- and a Commodore * sprite has no frame grid anyway, so the image is one whole frame. */ CATCH(errctx, akgl_spritesheet_initialize(state->sheets[index], 0, 0, (char *)&resolved->data)); FAIL_ZERO_BREAK(errctx, (state->sheets[index]->texture != NULL), AKGL_ERR_SDL, "Loaded %s but it produced no texture", path); SDL_SetTextureScaleMode(state->sheets[index]->texture, SDL_SCALEMODE_NEAREST); /* * The image's own size, not 24x21. A Commodore sprite is 24x21 because * that is what the hardware could address; there is no reason to throw * away art that is not sprite-shaped on a machine with no such limit. */ CATCH(errctx, build_slot(state, index, state->sheets[index]->texture->w, state->sheets[index]->texture->h)); } CLEANUP { IGNORE(akgl_heap_release_string(resolved)); } PROCESS(errctx) { } HANDLE(errctx, ENOENT) { /* * Recorded rather than raised here. Returning from inside a HANDLE block * leaves FINISH's bookkeeping undone, so the flag is set and the new * error is raised below where an ordinary return is safe. */ missing = true; } FINISH(errctx, true); FAIL_NONZERO_RETURN(errctx, missing, AKBASIC_ERR_VALUE, "No such sprite image: %s", path); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *spr_show(akbasic_SpriteBackend *self, int n, bool visible) { PREPARE_ERROR(errctx); akbasic_AkglSprites *state = NULL; int index = 0; PASS(errctx, slot_of(self, n, &state, &index)); if ( state->actors[index] != NULL ) { state->actors[index]->visible = visible; } SUCCEED_RETURN(errctx); } static akerr_ErrorContext *spr_move(akbasic_SpriteBackend *self, int n, double x, double y) { PREPARE_ERROR(errctx); akbasic_AkglSprites *state = NULL; int index = 0; PASS(errctx, slot_of(self, n, &state, &index)); if ( state->actors[index] != NULL ) { state->actors[index]->x = (float32_t)x; state->actors[index]->y = (float32_t)y; } SUCCEED_RETURN(errctx); } static akerr_ErrorContext *spr_configure(akbasic_SpriteBackend *self, int n, akbasic_Color color, bool xexpand, bool yexpand, bool behind) { PREPARE_ERROR(errctx); akbasic_AkglSprites *state = NULL; SDL_Color sdl = to_sdl(color); int index = 0; PASS(errctx, slot_of(self, n, &state, &index)); state->xexpand[index] = xexpand; state->yexpand[index] = yexpand; /* * Colour is a texture modulation rather than a repaint: a sprite defined * from a pattern was drawn in the colour SPRSAV was given, and SPRITE's * colour argument tints it. For a sprite loaded from an image that means a * white image takes the colour and a coloured one is shaded by it, which is * what texture modulation does everywhere else. */ if ( state->sheets[index] != NULL && state->sheets[index]->texture != NULL ) { SDL_SetTextureColorMod(state->sheets[index]->texture, sdl.r, sdl.g, sdl.b); } /* * Priority is not honoured. A C128 draws a low-priority sprite behind the * bitmap screen; here the text layer and the drawing surface are the same * render target and sprites are composited on top of it every frame, so * there is nothing to go behind. Recorded in TODO.md section 5. */ (void)behind; SUCCEED_RETURN(errctx); } static akerr_ErrorContext *spr_shared_colors(akbasic_SpriteBackend *self, akbasic_Color c1, akbasic_Color c2) { PREPARE_ERROR(errctx); akbasic_AkglSprites *state = NULL; PASS(errctx, state_of(self, &state)); /* * Nothing to do yet, and saying so is better than pretending. Multicolour * mode packs two bitmap bits per pixel and selects between the sprite's own * colour and these two shared registers; SPRSAV here takes one bit per pixel, * so there is no second bit to select with. The state is kept by the * interpreter, RSPCOLOR reads it back, and the day a multicolour pattern * format exists this is where it lands. */ (void)c1; (void)c2; SUCCEED_RETURN(errctx); } /** * @brief Is slot @p i a thing that can be collided with at all? * * The three guards the pair loop used to repeat inline. A slot with no actor was * never defined; a slot with no sprite has no frame and therefore no size; and an * invisible sprite is out of the world entirely, which is the cheapest way a * program has of switching one off without forgetting where it was. */ static bool slot_collidable(akbasic_AkglSprites *state, int i) { return (state->actors[i] != NULL && state->sprites[i] != NULL && state->actors[i]->visible && state->shapekind[i] != AKBASIC_SHAPE_NONE); } /** * @brief Point slot @p i's proxy at where the sprite is and what size it is now. * * The shape is rebuilt every scan rather than cached against a dirty flag. It is * derived from the sprite's frame and the two expansion bits, both of which a * program may change at any statement, and building a box is a handful of * arithmetic -- cheaper than being wrong about what invalidates it. * * **The mask override is the load-bearing line.** akgl_collision_shape_box() * leaves a shape on the ACTOR layer responding to STATIC only, so that giving a * town full of NPCs hitboxes does not make them shove each other around. That is * the right default for a tile game and exactly wrong here: BASIC's collision is * sprite-against-sprite first, and taking the library's default would make * BUMP(1) report nothing at all, silently. */ /** * @brief Record the shape SPRHIT asked for. The next scan builds it. * * Nothing is built here, and the reason is worth stating: a frame-fitting shape * depends on the picture and the expansion bits, both of which may change after * this call and before the next scan, so deriving it now would be deriving it * from the wrong numbers. Zeroing the synced rectangle is what makes the next * scan rebuild rather than recognise the position as unchanged and skip. */ static akerr_ErrorContext *spr_shape(akbasic_SpriteBackend *self, int n, int kind, double x1, double y1, double x2, double y2) { PREPARE_ERROR(errctx); akbasic_AkglSprites *state = NULL; int i = 0; PASS(errctx, slot_of(self, n, &state, &i)); state->shapekind[i] = kind; state->shapex1[i] = (float32_t)x1; state->shapey1[i] = (float32_t)y1; state->shapex2[i] = (float32_t)x2; state->shapey2[i] = (float32_t)y2; state->shapeexplicit[i] = (x2 > x1 && y2 > y1); memset(&state->syncedbox[i], 0, sizeof(state->syncedbox[i])); SUCCEED_RETURN(errctx); } /** * @brief The rectangle slot @p i collides with, in device pixels. * * Either what SPRHIT was given, offset to where the sprite is, or the whole * drawn frame -- which is what a sprite nobody has shaped has always collided * with, and is the default this must keep reproducing exactly. */ static void collision_box(akbasic_AkglSprites *state, int i, SDL_FRect *dest) { dest->x = state->actors[i]->x; dest->y = state->actors[i]->y; if ( state->shapeexplicit[i] ) { dest->x += state->shapex1[i]; dest->y += state->shapey1[i]; dest->w = state->shapex2[i] - state->shapex1[i]; dest->h = state->shapey2[i] - state->shapey1[i]; return; } dest->w = (float32_t)state->sprites[i]->width * (state->xexpand[i] ? 2.0f : 1.0f); dest->h = (float32_t)state->sprites[i]->height * (state->yexpand[i] ? 2.0f : 1.0f); } static akerr_ErrorContext AKERR_NOIGNORE *sync_proxy(akbasic_AkglSprites *state, int i, SDL_FRect *box) { PREPARE_ERROR(errctx); SDL_FRect body; /* * **Nothing to do if nothing moved.** The scan runs at the top of every * interpreter step -- up to 256 times a rendered frame -- and a sprite moves * at most once in that time, so the overwhelming majority of syncs would * rewrite a proxy with what it already holds. Four float comparisons to find * that out are far cheaper than the shape build and the sync they skip. * * Compared against the last synced rectangle rather than tracked with a dirty * flag a verb sets. A flag would be smaller and would be wrong: this file's * own header says a host game sees BASIC's sprites in libakgl's actor * registry "and can do anything to them it can do to an actor", which * includes moving one behind the interpreter's back. Comparing the answer * cannot miss that; flagging the question can. */ if ( state->syncedbox[i].x == box->x && state->syncedbox[i].y == box->y && state->syncedbox[i].w == box->w && state->syncedbox[i].h == box->h ) { SUCCEED_RETURN(errctx); } body.x = 0.0f; body.y = 0.0f; body.w = box->w; body.h = box->h; /* * A zero-sized frame would be refused by the shape builder, and a sprite can * legitimately have one before its sheet is loaded. Treat it as not * collidable rather than raising: the program has not done anything wrong. */ if ( body.w <= 0.0f || body.h <= 0.0f ) { SUCCEED_RETURN(errctx); } switch ( state->shapekind[i] ) { case AKBASIC_SHAPE_CIRCLE: /* * Inscribed, and in the *smaller* of the two half-extents. A circle that * fitted the larger one would poke out of the rectangle the program * asked for, which is the opposite of what "give this sprite a circle" * means to somebody looking at a disc drawn inside a square frame. */ PASS(errctx, akgl_collision_shape_circle(&state->shapes[i], body.w / 2.0f, body.h / 2.0f, (body.w < body.h ? body.w : body.h) / 2.0f, 0.0f)); break; case AKBASIC_SHAPE_CAPSULE_X: case AKBASIC_SHAPE_CAPSULE_Y: PASS(errctx, akgl_collision_shape_capsule(&state->shapes[i], &body, (uint8_t)state->shapekind[i], 0.0f)); break; default: PASS(errctx, akgl_collision_shape_box(&state->shapes[i], &body, 0.0f)); break; } state->shapes[i].collidemask = AKGL_COLLISION_LAYER_ACTOR | AKGL_COLLISION_LAYER_STATIC; PASS(errctx, akgl_collision_proxy_sync(state->proxies[i], &state->shapes[i], box->x, box->y, 0.0f)); state->syncedbox[i] = *box; SUCCEED_RETURN(errctx); } /** * @brief Keep @p contact for slot @p i if it is deeper than what is there. * * A sprite may be in several overlaps at once -- a ball touching two bricks at a * corner -- and only one can be reported. The deepest is the one that has to be * undone, so it is the one worth keeping. * * The normal comes out of libakgl pointing out of the *other* shape and toward * this one, which is the direction a program wants: move along it by the depth * and you are clear. */ static void record_contact(akbasic_AkglSprites *state, int i, int what, int other, akgl_Contact *contact) { akbasic_Contact *dest = &state->contacts[i]; if ( state->hascontact[i] && dest->depth >= (double)contact->depth ) { return; } dest->what = what; dest->other = other; dest->nx = (double)contact->nx; dest->ny = (double)contact->ny; dest->depth = (double)contact->depth; dest->px = (double)contact->px; dest->py = (double)contact->py; /* * The minimum translation axis, from the normal rather than from an overlap * rectangle: whichever component is larger is the axis the shapes are least * far through, and therefore the one to reverse. Computed here because doing * it in BASIC means comparing two floats, which is exactly where this * dialect's left-operand rule bites. */ dest->axis = ((contact->nx < 0.0f ? -contact->nx : contact->nx) >= (contact->ny < 0.0f ? -contact->ny : contact->ny)) ? 1 : 2; state->hascontact[i] = true; } static akerr_ErrorContext *spr_contact(akbasic_SpriteBackend *self, int n, akbasic_Contact *dest, bool *found) { PREPARE_ERROR(errctx); akbasic_AkglSprites *state = NULL; int i = 0; PASS(errctx, slot_of(self, n, &state, &i)); FAIL_ZERO_RETURN(errctx, (dest != NULL && found != NULL), AKERR_NULLPOINTER, "NULL argument in contact"); *found = state->hascontact[i]; if ( *found ) { *dest = state->contacts[i]; } SUCCEED_RETURN(errctx); } /** * @brief Run the scan if anything has changed, and leave both masks on the state. * * One worker behind two entry points, because both masks come out of the same * pass and computing them separately would be computing the sprite boxes twice * for one answer. `spr_collisions()` reports the sprite-against-sprite mask and * `spr_solids()` the sprite-against-static one; either may be called first, and * the cache below is what makes that true rather than merely usually true. */ static akerr_ErrorContext AKERR_NOIGNORE *run_scan(akbasic_AkglSprites *state) { PREPARE_ERROR(errctx); akgl_Contact contact; SDL_FRect box[AKBASIC_MAX_SPRITES]; bool live[AKBASIC_MAX_SPRITES]; bool synced[AKBASIC_MAX_SPRITES]; bool hit = false; int i = 0; int j = 0; for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) { live[i] = slot_collidable(state, i); synced[i] = false; memset(&box[i], 0, sizeof(box[i])); if ( !live[i] ) { continue; } collision_box(state, i, &box[i]); } /* * **Nothing moved, so nothing can have changed.** * * The scan's inputs are exactly these boxes and which slots are collidable; * static geometry invalidates the cache where it is registered. So if all * eight match what the last scan saw, the answer is the answer it gave, and * eight rectangle comparisons are far cheaper than recomputing it. * * This is what makes static geometry affordable. The scan runs at the top of * every interpreter step -- up to 256 times a rendered frame -- and eight * sprites against sixty-four rectangles is five hundred and twelve tests. * That is fine once a frame and ruinous 256 times, and a sprite moves at * most once in that window. * * It changes nothing a program can observe: the same mask comes back, so * BUMP accumulates the same bits and the interrupt is raised on the same * steps. */ if ( state->lastvalid ) { bool same = true; for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) { if ( live[i] != state->lastlive[i] || box[i].x != state->lastbox[i].x || box[i].y != state->lastbox[i].y || box[i].w != state->lastbox[i].w || box[i].h != state->lastbox[i].h ) { same = false; break; } } if ( same ) { SUCCEED_RETURN(errctx); } } state->lastmask = 0; state->lastsolidmask = 0; memset(state->hascontact, 0, sizeof(state->hascontact)); /* * **Two phases, and the split is the whole performance story.** * * The obvious implementation -- sync all eight proxies, then run * akgl_collision_test() on all twenty-eight pairs -- was measured at 984 ns a * scan against 96 ns for the four-comparison loop it replaced. At 256 scans a * frame that is 21% of a frame, and it bought nothing: the mask came back * bit-identical and the contact was thrown away. * * So: reject on the bounding boxes first, in the four comparisons that were * always here, and pay for the narrowphase only on a pair that survives. * Nearly every scan of a real game rejects all twenty-eight, and a pair that * does overlap is worth an exact answer. This is not a workaround for a slow * library -- it is what a broad phase *is*, and akgl_collision_test() does the * same thing internally with the proxies' own bounds before it commits to a * solver. * * **The narrowphase still decides.** The box test only says "maybe"; the bit * is set by what the library answers. That matters for a shape that is not a * box -- a circle inscribed in a frame overlaps a smaller region than the * frame does -- so the prefilter can over-report and must never be the final * word. Today every shape is the whole frame and the two always agree, which * is exactly why this is the moment to get the ordering right rather than * the moment it starts to matter. * * AKGL_COLLISION_TEST_PLANAR because a Commodore sprite has no z. Without it * the extrusion the shape builder chose takes part in the test and the * minimum axis can come back as a depth along z, which no BASIC program can * act on. * * Still box against box, not pixel against pixel: a C128's VIC-II collides on * set pixels, so two sprites whose boxes overlap but whose art does not are * reported as colliding here and would not be there. TODO.md section 5. */ for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) { if ( !live[i] ) { continue; } for ( j = i + 1; j < AKBASIC_MAX_SPRITES; j++ ) { if ( !live[j] ) { continue; } if ( !(box[i].x < box[j].x + box[j].w && box[j].x < box[i].x + box[i].w && box[i].y < box[j].y + box[j].h && box[j].y < box[i].y + box[i].h) ) { continue; } /* * PASS rather than CATCH throughout: this is a loop, and CATCH * expands to a break, which would leave the remaining pairs untested * and the mask half built. */ if ( !synced[i] ) { PASS(errctx, sync_proxy(state, i, &box[i])); synced[i] = true; } if ( !synced[j] ) { PASS(errctx, sync_proxy(state, j, &box[j])); synced[j] = true; } hit = false; PASS(errctx, akgl_collision_test(state->proxies[i], state->proxies[j], AKGL_COLLISION_TEST_PLANAR, &contact, &hit)); if ( hit ) { state->lastmask |= (uint16_t)(1u << i); state->lastmask |= (uint16_t)(1u << j); record_contact(state, i, 1, j + 1, &contact); /* * And the mirror for the other sprite. The normal points out of * b toward a, so b's own copy has to be negated or both sprites * would be told to move the same way. */ contact.nx = -contact.nx; contact.ny = -contact.ny; record_contact(state, j, 1, i + 1, &contact); } } } /* * Every live sprite against every registered rectangle, same shape: reject * on the boxes, and only then ask the library. A wall is motionless, so its * proxy was synced when SOLID registered it and there is nothing to refresh * here. */ for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) { if ( !live[i] ) { continue; } for ( j = 0; j < AKBASIC_MAX_SOLIDS; j++ ) { if ( !state->solidactive[j] ) { continue; } if ( !(box[i].x < state->solidbox[j].x + state->solidbox[j].w && state->solidbox[j].x < box[i].x + box[i].w && box[i].y < state->solidbox[j].y + state->solidbox[j].h && state->solidbox[j].y < box[i].y + box[i].h) ) { continue; } if ( !synced[i] ) { PASS(errctx, sync_proxy(state, i, &box[i])); synced[i] = true; } hit = false; PASS(errctx, akgl_collision_test(state->proxies[i], state->solidproxies[j], AKGL_COLLISION_TEST_PLANAR, &contact, &hit)); if ( hit ) { state->lastsolidmask |= (uint16_t)(1u << i); record_contact(state, i, 2, j + 1, &contact); } } } for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) { state->lastbox[i] = box[i]; state->lastlive[i] = live[i]; } state->lastvalid = true; SUCCEED_RETURN(errctx); } /** * @brief Register, replace or retire static rectangle @p id. * * A proxy with a **NULL owner**: there is no actor behind a wall, and libakgl's * proxy carries the owner only so a resolver can push something. Nothing here * resolves anything, so the field stays empty and the shape is what matters. * * Layers are the other half. A wall sits on #AKGL_COLLISION_LAYER_STATIC and * responds to nothing, which is the asymmetry libakgl's masks exist for: the * sprite's own `collidemask` includes STATIC, so a sprite finds a wall and two * walls never test against each other. Sixty-four motionless rectangles * therefore cost nothing per scan beyond the boxes a sprite is compared to. */ static akerr_ErrorContext *spr_solid(akbasic_SpriteBackend *self, int id, bool define, double x1, double y1, double x2, double y2) { PREPARE_ERROR(errctx); akbasic_AkglSprites *state = NULL; SDL_FRect body; int i = 0; PASS(errctx, state_of(self, &state)); FAIL_ZERO_RETURN(errctx, (id >= 1 && id <= AKBASIC_MAX_SOLIDS), AKBASIC_ERR_BOUNDS, "Static shape %d is outside 1..%d", id, AKBASIC_MAX_SOLIDS); i = id - 1; /* * Anything that changes the geometry invalidates the cached answer. Cheapest * possible correctness: the scan's own change detection watches the sprites, * and this is the one input it cannot see moving. */ state->lastvalid = false; if ( !define ) { if ( state->solidproxies[i] != NULL ) { PASS(errctx, akgl_heap_release_collision_proxy(state->solidproxies[i])); state->solidproxies[i] = NULL; } state->solidactive[i] = false; SUCCEED_RETURN(errctx); } body.x = 0.0f; body.y = 0.0f; body.w = (float32_t)(x2 - x1); body.h = (float32_t)(y2 - y1); PASS(errctx, akgl_collision_shape_box(&state->solidshapes[i], &body, 0.0f)); state->solidshapes[i].flags |= AKGL_COLLISION_FLAG_STATIC; state->solidshapes[i].layermask = AKGL_COLLISION_LAYER_STATIC; state->solidshapes[i].collidemask = AKGL_COLLISION_LAYER_NONE; if ( state->solidproxies[i] == NULL ) { /* Adjacent, with nothing between: the slot is free until initialize takes it. */ PASS(errctx, akgl_heap_next_collision_proxy(&state->solidproxies[i])); PASS(errctx, akgl_collision_proxy_initialize(state->solidproxies[i], NULL, &state->solidshapes[i], (float32_t)x1, (float32_t)y1, 0.0f)); } else { PASS(errctx, akgl_collision_proxy_sync(state->solidproxies[i], &state->solidshapes[i], (float32_t)x1, (float32_t)y1, 0.0f)); } state->solidbox[i].x = (float32_t)x1; state->solidbox[i].y = (float32_t)y1; state->solidbox[i].w = body.w; state->solidbox[i].h = body.h; state->solidactive[i] = true; SUCCEED_RETURN(errctx); } static akerr_ErrorContext *spr_collisions(akbasic_SpriteBackend *self, uint16_t *mask) { PREPARE_ERROR(errctx); akbasic_AkglSprites *state = NULL; PASS(errctx, state_of(self, &state)); FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in collisions"); PASS(errctx, run_scan(state)); *mask = state->lastmask; SUCCEED_RETURN(errctx); } static akerr_ErrorContext *spr_solids(akbasic_SpriteBackend *self, uint16_t *mask) { PREPARE_ERROR(errctx); akbasic_AkglSprites *state = NULL; PASS(errctx, state_of(self, &state)); FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in solids"); PASS(errctx, run_scan(state)); *mask = state->lastsolidmask; SUCCEED_RETURN(errctx); } akerr_ErrorContext *akbasic_sprite_init_akgl(akbasic_SpriteBackend *obj, akbasic_AkglSprites *state, akgl_RenderBackend *renderer, akbasic_AkglGraphics *graphics) { PREPARE_ERROR(errctx); int i = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL && renderer != NULL), AKERR_NULLPOINTER, "NULL argument in sprite_init_akgl"); /* Before anything else in libakgl, so every AKGL_ERR_* has a name to print. */ PASS(errctx, akgl_error_init()); memset(state, 0, sizeof(*state)); state->renderer = renderer; state->graphics = graphics; /* * The eight collision proxies, claimed here and held for the life of the * backend. * * Up front rather than on demand because the pool is shared: a host game * embedding this interpreter draws its own shaped actors from the same * AKGL_MAX_HEAP_COLLISION_PROXY. Claiming late would mean a collision scan * discovering mid-game that a host had taken the last slot, and reporting it * against whichever BASIC line was unlucky. Claiming here makes it an * initialization failure that names the pool, before a program has run. * * A default box shape so a proxy is never initialized from an uninitialized * shape -- libakgl fixed exactly that defect in 3a6569e, where filing a proxy * under half-extents that had not been written yet was invisible to its * tests and visible only to a memory checker. Real geometry arrives on the * first sync_proxy(); this is only ever a placeholder. */ for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) { SDL_FRect placeholder = { 0.0f, 0.0f, 1.0f, 1.0f }; /* * A box fitting the frame, which is what every sprite collided with * before SPRHIT existed and is what one nobody has shaped must go on * collided with. */ state->shapekind[i] = AKBASIC_SHAPE_BOX; PASS(errctx, akgl_collision_shape_box(&state->shapes[i], &placeholder, 0.0f)); /* * Acquire and initialize adjacent, with nothing between them: the slot * is free until initialize takes the reference, and libakgl's heap says * so where next_collision_proxy is declared. */ PASS(errctx, akgl_heap_next_collision_proxy(&state->proxies[i])); PASS(errctx, akgl_collision_proxy_initialize(state->proxies[i], NULL, &state->shapes[i], 0.0f, 0.0f, 0.0f)); } memset(obj, 0, sizeof(*obj)); obj->self = state; obj->define = spr_define; obj->define_shape = spr_define_shape; obj->define_file = spr_define_file; obj->show = spr_show; obj->move = spr_move; obj->configure = spr_configure; obj->shared_colors = spr_shared_colors; obj->collisions = spr_collisions; obj->shape = spr_shape; obj->solid = spr_solid; obj->solids = spr_solids; obj->contact = spr_contact; SUCCEED_RETURN(errctx); } akerr_ErrorContext *akbasic_sprite_akgl_render(akbasic_SpriteBackend *obj) { PREPARE_ERROR(errctx); akbasic_AkglSprites *state = NULL; int i = 0; PASS(errctx, state_of(obj, &state)); for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) { if ( state->actors[i] == NULL ) { continue; } /* * PASS rather than CATCH: this is a loop, and CATCH expands to a break. * Through the actor's own renderfunc rather than calling spr_render_actor * directly, so a host that has replaced it gets what it asked for. */ PASS(errctx, state->actors[i]->renderfunc(state->actors[i])); } SUCCEED_RETURN(errctx); } void akbasic_sprite_akgl_shutdown(akbasic_SpriteBackend *obj) { akbasic_AkglSprites *state = NULL; int i = 0; if ( obj == NULL || obj->self == NULL ) { return; } state = (akbasic_AkglSprites *)obj->self; for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) { IGNORE(release_slot(state, i)); /* * The proxies go back too. They are not registered with any partitioner, * so there is nothing to unlink first -- which is the one ordering trap * here, and the reason worth writing down: releasing a proxy that *is* * registered leaves stale cell entries holding a pool index, and the * damage only shows when the slot is handed out again. */ if ( state->proxies[i] != NULL ) { IGNORE(akgl_heap_release_collision_proxy(state->proxies[i])); state->proxies[i] = NULL; } } for ( i = 0; i < AKBASIC_MAX_SOLIDS; i++ ) { if ( state->solidproxies[i] != NULL ) { IGNORE(akgl_heap_release_collision_proxy(state->solidproxies[i])); state->solidproxies[i] = NULL; } } }