akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
409 lines
17 KiB
C
409 lines
17 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 <akerror.h>
|
|
#include <akstdlib.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);
|
|
|
|
PASS(errctx, aksl_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));
|
|
/*
|
|
* And the UI, which loads the same font file again -- under a registry name
|
|
* of its own, at the same size. That is not a duplicate by accident: the
|
|
* sink holds a TTF_Font it opened itself and never registers it, and clay
|
|
* addresses a font by registry name, so the two cannot share one entry.
|
|
*/
|
|
PASS(errctx, akbasic_ui_init_akgl(&obj->ui, &obj->uistate, obj->renderer,
|
|
fontpath, fontsize, w, h));
|
|
|
|
/*
|
|
* 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));
|
|
PASS(errctx, akbasic_runtime_set_ui(rt, &obj->ui));
|
|
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;
|
|
bool consumed = false;
|
|
|
|
while ( SDL_PollEvent(&event) ) {
|
|
if ( event.type == SDL_EVENT_QUIT ||
|
|
event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED ) {
|
|
obj->running = false;
|
|
continue;
|
|
}
|
|
/*
|
|
* The UI gets first refusal. The mouse half never takes a keystroke, but
|
|
* a menu with entries takes Up, Down and Return -- which is why this has
|
|
* to come *before* the ring is filled, and why retiring a menu is how a
|
|
* program gives those keys back to the line editor.
|
|
*/
|
|
PASS(errctx, akbasic_ui_akgl_handle_event(&obj->ui, &event, &consumed));
|
|
if ( consumed ) {
|
|
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;
|
|
bool resume = false;
|
|
|
|
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);
|
|
}
|
|
|
|
/*
|
|
* **Take the drawing layer off the target before presenting.**
|
|
*
|
|
* SDL refuses to present while a render target is current, and this function
|
|
* is called from two places with different answers to "is one current". The
|
|
* frame loop calls it between steps, with the layer off. The sink's *line
|
|
* editor* calls it from inside a step -- borrowing a frame while it waits
|
|
* for a typed line, which is how a REPL redraws without owning a loop -- and
|
|
* there the layer is very much on.
|
|
*
|
|
* So end it here and put it back afterwards. `resume` remembers which case
|
|
* this was, because unconditionally beginning would leave the layer current
|
|
* after a frame-loop pump and every subsequent draw would land in it at the
|
|
* wrong time.
|
|
*/
|
|
resume = akbasic_graphics_akgl_layer_active(&obj->graphics);
|
|
PASS(errctx, akbasic_graphics_akgl_end(&obj->graphics));
|
|
|
|
/*
|
|
* No clear. The text layer is drawn over whatever is already there, which is
|
|
* also how a C128 stacks its text plane on its bitmap plane -- and the
|
|
* drawing layer beneath it is composited rather than accumulated, so there
|
|
* is nothing here that a clear would fix.
|
|
*/
|
|
/*
|
|
* The drawing layer first, under everything. A drawing verb rendered into a
|
|
* texture rather than into the back buffer -- see
|
|
* akbasic_graphics_akgl_begin() -- so this is where what the program drew
|
|
* reaches the frame, and it is why a picture no longer has to be redrawn
|
|
* every frame or captured into a sprite to be kept.
|
|
*/
|
|
PASS(errctx, akbasic_graphics_akgl_render(&obj->graphics));
|
|
PASS(errctx, akbasic_sink_akgl_render(&obj->akglsink));
|
|
/*
|
|
* Sprites last, so they are over the text as well as over the drawing. That
|
|
* 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));
|
|
/*
|
|
* The UI last of all, over the sprites as well as over the text. A menu the
|
|
* player is being asked to act on is not something a sprite may cover.
|
|
*/
|
|
PASS(errctx, akbasic_ui_akgl_render(&obj->ui));
|
|
FAIL_ZERO_RETURN(errctx, SDL_RenderPresent(obj->renderer->sdl_renderer),
|
|
AKGL_ERR_SDL, "Couldn't present the frame: %s", SDL_GetError());
|
|
if ( resume ) {
|
|
PASS(errctx, akbasic_graphics_akgl_begin(&obj->graphics));
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @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()));
|
|
/*
|
|
* The steps run with the drawing layer current, so a DRAW lands in a
|
|
* texture that outlives the frame. Bracketed here rather than inside
|
|
* each verb: one pair of SDL_SetRenderTarget calls a frame instead of
|
|
* one per statement, and it is also what makes SSHAPE read back what the
|
|
* program has just drawn.
|
|
*/
|
|
PASS(errctx, akbasic_graphics_akgl_begin(&obj->graphics));
|
|
PASS(errctx, akbasic_runtime_run(rt, AKBASIC_FRONTEND_STEPS_PER_FRAME));
|
|
PASS(errctx, akbasic_graphics_akgl_end(&obj->graphics));
|
|
PASS(errctx, akbasic_frontend_akgl_pump(obj, &running));
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
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_ui_akgl_shutdown(&obj->ui);
|
|
akbasic_sprite_akgl_shutdown(&obj->sprites);
|
|
akbasic_graphics_akgl_shutdown(&obj->graphics);
|
|
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;
|
|
}
|