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