Files
akbasic/src/frontend_akgl.c
Andrew Kesterson 3415c9aec6 Take libakgl 0.5.0 and rename every symbol it namespaced
0.5.0 gives every exported symbol the akgl_ prefix, which is the first libakgl
release to break this project's source rather than only its ABI. The soname goes
to libakgl.so.0.5 and include/akbasic/akgl.h asserts the floor.

What moved here: akgl_render_bind2d is akgl_render_2d_bind,
akgl_sprite_sheet_coords_for_frame is akgl_spritesheet_coords_for_frame, the
renderer, camera and window globals carry the prefix, and _akgl_renderer and
_akgl_camera are akgl_default_renderer and akgl_default_camera.

The renames were applied by site rather than by pattern, because renderer is also
a parameter name in src/sprite_akgl.c and a struct member throughout
src/frontend_akgl.c -- a substitution would have rewritten both without a word.
That is the same trap upstream describes hitting, and it is worth knowing that
the defect behind the rename was not cosmetic: an exported global called renderer
collided with a test's own variable, the executable's definition preempted the
library's, and every texture load in that suite failed while the suite passed.

0.5.0 also fixes libakgl defect 26, which was one of the two reasons
src/sprite_akgl.c installs its own renderfunc: akgl_actor_render took its
destination height from the sprite's width, drawing a 24x21 Commodore sprite as a
24x24 square. The renderfunc stays, because the other reason has not moved -- an
actor carries one scalar scale and SPRITE has separate x- and y-expand bits --
but the comments and TODO.md deviation 40 no longer claim a defect that is fixed.

Every documentation figure re-renders byte-identical under the new library, which
is what says the sprite and drawing paths did not move underneath them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 08:01:43 -04:00

344 lines
14 KiB
C

/**
* @file frontend_akgl.c
* @brief The standalone program's SDL host, ported from the Go frontend.
*
* The reference's main.go opens an 800x600 window and a Commodore font and hands
* both to the runtime, which then owns the process until MODE_QUIT
* (basicruntime.go:682). Goal 3 forbids the second half of that outright, so
* what is ported here is the first half plus the loop the reference never had to
* write: pump events, run a bounded number of interpreter steps, draw, present,
* repeat.
*
* **This file is a host.** It is the only thing in the repository that creates a
* window, and it is in its own target so that fact is visible in the build graph
* rather than only in a comment. A game embedding the interpreter does not link
* it -- it already has a window, and it calls the four initializers in
* akbasic/akgl.h against its own.
*
* Everything the four adaptors need is created here and nowhere else:
*
* window + renderer ---> the akgl sink and the graphics backend
* SDL event pump ---> akgl_controller_handle_event, the input backend
* the frame ---> akbasic_sink_akgl_render, and the line editor
*/
#include <stddef.h>
#include <string.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.
* akgl_text_rendertextat() takes no renderer argument and reads that global, so
* a host that never calls akgl_game_init() -- which is every host embedding this
* interpreter, since owning the game loop is exactly what goal 3 forbids -- has
* to populate it itself. deps/libakgl/tests/draw.c does the same.
*/
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/renderer.h>
#include <akbasic/error.h>
#include <akbasic/frontend.h>
akerr_ErrorContext *akbasic_frontend_akgl_init(akbasic_AkglFrontend *obj, const char *title, int w, int h, const char *fontpath, int fontsize, FILE *mirror, FILE *in)
{
PREPARE_ERROR(errctx);
akbasic_TextSink *reader = NULL;
FAIL_ZERO_RETURN(errctx, (obj != NULL && title != NULL && fontpath != NULL),
AKERR_NULLPOINTER, "NULL argument in frontend_akgl_init");
FAIL_ZERO_RETURN(errctx, (w > 0 && h > 0 && fontsize > 0), AKBASIC_ERR_VALUE,
"A %dx%d window at %d points is not a window", w, h, fontsize);
memset(obj, 0, sizeof(*obj));
obj->width = w;
obj->height = h;
/* Before anything else in libakgl, so every AKGL_ERR_* has a name to print. */
PASS(errctx, akgl_error_init());
/*
* INIT_VIDEO only. The reference asks for INIT_EVERYTHING, which on a
* headless machine fails on a subsystem BASIC never touches; audio is opened
* separately below and is allowed to fail.
*/
FAIL_ZERO_RETURN(errctx, SDL_Init(SDL_INIT_VIDEO), AKGL_ERR_SDL,
"Couldn't initialize SDL: %s", SDL_GetError());
obj->ownssdl = true;
FAIL_ZERO_RETURN(errctx, TTF_Init(), AKGL_ERR_SDL,
"Couldn't initialize SDL_ttf: %s", SDL_GetError());
obj->renderer = &akgl_default_renderer;
akgl_renderer = obj->renderer;
FAIL_ZERO_RETURN(errctx,
SDL_CreateWindowAndRenderer(title, w, h, 0,
&obj->window, &obj->renderer->sdl_renderer),
AKGL_ERR_SDL, "Couldn't create the window: %s", SDL_GetError());
akgl_window = obj->window;
/*
* Without this SDL sends no SDL_EVENT_TEXT_INPUT at all, every keystroke
* reaches the ring with an empty `text`, and the line editor has nothing to
* type -- akgl/controller.h says so directly. It is off by default in SDL3
* and is per-window, so it is the host's job and this is the host.
*
* Not fatal if it fails: the editor falls back to the keycode, which is a
* worse keyboard rather than no keyboard. Failing to start a window over an
* input method that is not there would be the wrong trade.
*/
if ( !SDL_StartTextInput(obj->window) ) {
SDL_Log("Could not start text input (%s); typing falls back to keycodes",
SDL_GetError());
}
/*
* Bind the 2D methods onto the renderer we just made. akgl_render_2d_init()
* would do this too, but it creates its own window from the game properties
* first, which is the akgl_game_init() path a host with its own window is
* not on. libakgl 0.3.0 split the vtable half out for exactly this caller --
* it was API-gap item 7, and until it landed these six pointers were
* assigned by hand here and in tests/akgl_backends.c.
*/
PASS(errctx, akgl_render_2d_bind(obj->renderer));
/*
* The object pools and the name registries. Every akgl_*_initialize ends in
* a registry write and fails AKERR_KEY without these, so anything drawn as
* an actor -- which is what a BASIC sprite is -- needs them. akgl_game_init()
* is where they would normally be done, and it is the function goal 3 keeps
* this host out of.
*
* Unconditional, and it has to be. akgl_registry_init() overwrites the
* property-set ids without destroying the old sets, so repeating it would
* normally leak -- but this frontend owns SDL_Init and SDL_Quit, and
* SDL_Quit destroys every property set while leaving libakgl's ids pointing
* at them. So the previous registries are already gone, and skipping the
* call on the strength of a non-zero id is what actually breaks: the second
* frontend of a process gets ids that name nothing, and the first
* akgl_sprite_initialize fails AKERR_KEY.
*
* A game that embeds the interpreter does not link this file. It has already
* called akgl_game_init(), which does both of these itself.
*/
PASS(errctx, akgl_heap_init());
PASS(errctx, akgl_registry_init());
/*
* And the camera. akgl_actor_render() dereferences this global to translate
* world coordinates into screen ones and does not check it, so an unset
* camera is a crash the first time a sprite is drawn rather than an error
* anybody could act on. One screen's worth at the origin: BASIC has no verb
* that scrolls a view, so the world and the screen are the same thing here.
*/
akgl_camera = &akgl_default_camera;
akgl_camera->x = 0.0f;
akgl_camera->y = 0.0f;
akgl_camera->w = (float)w;
akgl_camera->h = (float)h;
obj->font = TTF_OpenFont(fontpath, (float)fontsize);
FAIL_ZERO_RETURN(errctx, (obj->font != NULL), AKGL_ERR_SDL,
"Couldn't open the font %s: %s", fontpath, SDL_GetError());
/*
* The two halves of the output, and then the tee that joins them. Which one
* answers readline is the whole difference between the two modes: a program
* read from a file comes in through the stdio half, and typed lines come in
* through the akgl half's line editor.
*/
PASS(errctx, akbasic_sink_init_akgl(&obj->akglsink, &obj->akglstate,
obj->renderer, obj->font, w, h));
PASS(errctx, akbasic_sink_init_stdio(&obj->stdiosink, &obj->stdiostate,
(mirror != NULL ? mirror : stdout), in));
reader = (in != NULL ? &obj->stdiosink : &obj->akglsink);
PASS(errctx, akbasic_sink_init_tee(&obj->sink, &obj->teestate,
&obj->akglsink, &obj->stdiosink, reader));
/*
* The editor waits for a typed line by borrowing frames from this very
* frontend -- which is why pump() has the akbasic_AkglPump signature and can
* be installed as-is.
*/
PASS(errctx, akbasic_sink_akgl_set_pump(&obj->akglsink,
akbasic_frontend_akgl_pump, obj));
PASS(errctx, akbasic_graphics_init_akgl(&obj->graphics, &obj->graphicsstate,
obj->renderer));
PASS(errctx, akbasic_input_init_akgl(&obj->input));
/*
* The graphics state goes with it so SPRSAV can turn an SSHAPE handle into a
* sprite. The two devices stay separate records -- a host may lend one and
* withhold the other -- and this is the one place they meet.
*/
PASS(errctx, akbasic_sprite_init_akgl(&obj->sprites, &obj->spritesstate,
obj->renderer, &obj->graphicsstate));
/*
* Audio last, in its own subsystem, and allowed to fail. The reference asks
* for INIT_EVERYTHING up front, which makes a machine with no sound card
* refuse to run BASIC at all; here the failure costs only SOUND and PLAY,
* which are then refused with AKBASIC_ERR_DEVICE -- exactly what a stdio
* build does. The subsystem is tried first because akgl_audio_init() cannot
* open a device without it, and reporting "no audio device" for what is
* really "audio was never initialized" would send somebody looking at their
* hardware.
*/
if ( !SDL_InitSubSystem(SDL_INIT_AUDIO) ) {
SDL_Log("No audio subsystem (%s); SOUND and PLAY will be refused", SDL_GetError());
obj->audioready = false;
} else {
ATTEMPT {
CATCH(errctx, akbasic_audio_init_akgl(&obj->audio));
obj->audioready = true;
} CLEANUP {
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {
LOG_ERROR_WITH_MESSAGE(errctx, "No audio device; SOUND and PLAY will be refused");
obj->audioready = false;
} FINISH(errctx, false);
}
obj->running = true;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_frontend_akgl_attach(akbasic_AkglFrontend *obj, akbasic_Runtime *rt)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL && rt != NULL), AKERR_NULLPOINTER,
"NULL argument in frontend_akgl_attach");
PASS(errctx, akbasic_runtime_init(rt, &obj->sink));
PASS(errctx, akbasic_runtime_set_devices(rt, &obj->graphics,
(obj->audioready ? &obj->audio : NULL),
&obj->input, &obj->sprites));
SUCCEED_RETURN(errctx);
}
/**
* @brief Drain the SDL event queue into libakgl's keystroke ring.
*
* Its own function because it is a loop, and CATCH inside one escapes only the
* loop -- PASS is the only thing that may appear in here.
*/
static akerr_ErrorContext AKERR_NOIGNORE *pump_events(akbasic_AkglFrontend *obj)
{
PREPARE_ERROR(errctx);
SDL_Event event;
while ( SDL_PollEvent(&event) ) {
if ( event.type == SDL_EVENT_QUIT ||
event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED ) {
obj->running = false;
continue;
}
/*
* Everything else goes to libakgl, which pushes key-downs into the ring
* the input backend reads before it consults its own control maps. The
* appstate is the frontend, which nothing downstream reads -- but a NULL
* one is refused outright.
*/
PASS(errctx, akgl_controller_handle_event(obj, &event));
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_frontend_akgl_pump(void *self, bool *running)
{
PREPARE_ERROR(errctx);
akbasic_AkglFrontend *obj = (akbasic_AkglFrontend *)self;
FAIL_ZERO_RETURN(errctx, (obj != NULL && running != NULL), AKERR_NULLPOINTER,
"NULL argument in frontend_akgl_pump");
PASS(errctx, pump_events(obj));
*running = obj->running;
if ( !obj->running ) {
SUCCEED_RETURN(errctx);
}
/*
* 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.
*/
PASS(errctx, akbasic_sink_akgl_render(&obj->akglsink));
/*
* Sprites last, so they are over the text as well as over the drawing. That
* is what a C128 does with a high-priority sprite, and the low-priority case
* -- behind the bitmap plane -- has nowhere to go here: see the note in
* spr_configure().
*/
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());
SUCCEED_RETURN(errctx);
}
/**
* @brief The frame loop, in its own function for the usual reason: it is a loop.
*/
static akerr_ErrorContext AKERR_NOIGNORE *drive_loop(akbasic_AkglFrontend *obj, akbasic_Runtime *rt)
{
PREPARE_ERROR(errctx);
bool running = true;
while ( running && rt->mode != AKBASIC_MODE_QUIT ) {
/*
* The host owns the clock -- the library reads none, because it owns no
* loop and must not block. SDL_GetTicks() is monotonic milliseconds,
* which is exactly what settime wants and saves this file a
* clock_gettime and a _POSIX_C_SOURCE.
*/
PASS(errctx, akbasic_runtime_settime(rt, (int64_t)SDL_GetTicks()));
PASS(errctx, akbasic_runtime_run(rt, AKBASIC_FRONTEND_STEPS_PER_FRAME));
PASS(errctx, akbasic_frontend_akgl_pump(obj, &running));
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_frontend_akgl_drive(akbasic_AkglFrontend *obj, akbasic_Runtime *rt)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL && rt != NULL), AKERR_NULLPOINTER,
"NULL argument in frontend_akgl_drive");
PASS(errctx, drive_loop(obj, rt));
SUCCEED_RETURN(errctx);
}
void akbasic_frontend_akgl_shutdown(akbasic_AkglFrontend *obj)
{
if ( obj == NULL ) {
return;
}
/* Before the renderer goes: these are textures it created. */
akbasic_sprite_akgl_shutdown(&obj->sprites);
if ( obj->font != NULL ) {
TTF_CloseFont(obj->font);
obj->font = NULL;
}
if ( obj->renderer != NULL && obj->renderer->sdl_renderer != NULL ) {
SDL_DestroyRenderer(obj->renderer->sdl_renderer);
obj->renderer->sdl_renderer = NULL;
}
if ( obj->window != NULL ) {
SDL_StopTextInput(obj->window);
SDL_DestroyWindow(obj->window);
obj->window = NULL;
akgl_window = NULL;
}
TTF_Quit();
if ( obj->ownssdl ) {
SDL_Quit();
obj->ownssdl = false;
}
obj->running = false;
}