Files
akbasic/src/frontend_akgl.c

274 lines
10 KiB
C
Raw Normal View History

/**
* @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;
}