0.3.0 closed all ten API gaps this port had filed. The four workarounds go with them: the CMake block that declared libakgl's vendored dependencies by hand, the akgl/actor.h include in three files, and the six vtable pointers assigned by hand in two more, now akgl_render_bind2d(). Two gaps were capabilities rather than inconveniences, and both are now real: The line editor takes the composed UTF-8 text the ring carries in preference to the keycode, so shifted characters, keyboard layouts, compose keys and dead keys all work. A double quote can be typed, which means a BASIC string literal can be typed -- the sharp end of the old limitation. Letters are no longer folded to upper case. SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3 sweeps once rather than oscillating and TODO.md section 5 says so. A backend with no sweep still refuses the swept note and plays the held one. The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by temporarily demanding 0.4.0 and watching it fire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
274 lines
10 KiB
C
274 lines
10 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 `renderer` global and its `_akgl_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/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_renderer;
|
|
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());
|
|
window = obj->window;
|
|
/*
|
|
* Bind the 2D methods onto the renderer we just made. akgl_render_init2d()
|
|
* 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_bind2d(obj->renderer));
|
|
|
|
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));
|
|
|
|
/*
|
|
* 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));
|
|
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));
|
|
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;
|
|
}
|
|
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_DestroyWindow(obj->window);
|
|
obj->window = NULL;
|
|
window = NULL;
|
|
}
|
|
TTF_Quit();
|
|
if ( obj->ownssdl ) {
|
|
SDL_Quit();
|
|
obj->ownssdl = false;
|
|
}
|
|
obj->running = false;
|
|
}
|