Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
167
include/akbasic/akgl.h
Normal file
167
include/akbasic/akgl.h
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* @file akgl.h
|
||||
* @brief The libakgl-backed implementations of the sink and the three devices.
|
||||
*
|
||||
* Everything declared here lives in the separate `akbasic_akgl` target, which is
|
||||
* the only part of this project that links SDL. The core library builds and its
|
||||
* whole test suite runs on a machine with no SDL on it; that is why the records
|
||||
* these initializers populate are plain function-pointer structs and why this
|
||||
* header is the only one that includes a libakgl header.
|
||||
*
|
||||
* **The interpreter owns no window, no renderer and no event loop.** Every one
|
||||
* of these takes something the host already created and draws or plays through
|
||||
* it. None of them creates a device, and none of them pumps events.
|
||||
*
|
||||
* Each initializer calls akgl_error_init() first. It reserves libakgl's 256-260
|
||||
* status band and names every AKGL_ERR_* code; akgl_game_init() calls it as its
|
||||
* first statement, but a program driving subsystems directly -- which is exactly
|
||||
* what an embedded interpreter does -- never goes through akgl_game_init() and
|
||||
* has to call it itself. Skip it and every AKGL_ERR_* that reaches a stack trace
|
||||
* prints "Unknown Error". It is idempotent, so a host that already called it
|
||||
* loses nothing.
|
||||
*/
|
||||
|
||||
#ifndef _AKBASIC_AKGL_H_
|
||||
#define _AKBASIC_AKGL_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/renderer.h>
|
||||
|
||||
#include <akbasic/audio.h>
|
||||
#include <akbasic/graphics.h>
|
||||
#include <akbasic/input.h>
|
||||
#include <akbasic/sink.h>
|
||||
|
||||
/** @brief How many saved SSHAPE regions the graphics backend will hold at once. */
|
||||
#define AKBASIC_AKGL_MAX_SHAPES 16
|
||||
|
||||
/**
|
||||
* @brief State for the libakgl-backed text sink.
|
||||
*
|
||||
* The cursor, the wrap and the scroll live here rather than in the interpreter:
|
||||
* everything in the reference's basicruntime_graphics.go except Write and
|
||||
* Println, which are the sink interface itself.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
akgl_RenderBackend *renderer;
|
||||
TTF_Font *font;
|
||||
SDL_Color color;
|
||||
|
||||
int x; /* pixel origin of the text area */
|
||||
int y;
|
||||
int width; /* pixel size of the text area */
|
||||
int height;
|
||||
int cellw; /* one character cell, measured from the font */
|
||||
int cellh;
|
||||
int columns; /* the character grid the cell size works out to */
|
||||
int rows;
|
||||
|
||||
int cursorcol;
|
||||
int cursorrow;
|
||||
|
||||
/*
|
||||
* The scrollback the sink redraws every frame. A fixed grid rather than a
|
||||
* list of lines: the interpreter allocates nothing, and neither does this.
|
||||
*/
|
||||
char text[64][256];
|
||||
} akbasic_AkglSink;
|
||||
|
||||
/**
|
||||
* @brief State for the libakgl-backed graphics backend.
|
||||
*
|
||||
* The shape pool is why this exists at all. SSHAPE hands the BASIC program a
|
||||
* handle rather than the pixels -- see TODO.md section 5 -- and these are the
|
||||
* surfaces those handles refer to.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
akgl_RenderBackend *renderer;
|
||||
SDL_Surface *shapes[AKBASIC_AKGL_MAX_SHAPES];
|
||||
int shapecount;
|
||||
} akbasic_AkglGraphics;
|
||||
|
||||
/**
|
||||
* @brief Point a text sink at a renderer and a font the host already has.
|
||||
*
|
||||
* The character grid is derived by measuring one glyph with akgl_text_measure(),
|
||||
* which is the direct equivalent of the reference's font.SizeUTF8("A") -- and
|
||||
* which did not exist in libakgl until 42b60f7. A font that is not monospaced
|
||||
* still works; the grid is then sized by whatever "A" happens to measure, and
|
||||
* proportional glyphs simply do not line up in columns.
|
||||
*
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param state Storage for the sink's own state; must outlive the sink.
|
||||
* @param renderer The renderer the host already initialized; not created here.
|
||||
* @param font An open font; not opened or closed here.
|
||||
* @param w Width in pixels of the text area.
|
||||
* @param h Height in pixels of the text area.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When any pointer argument is NULL.
|
||||
* @throws AKBASIC_ERR_VALUE When the font measures a zero-width cell, which would divide by zero.
|
||||
* @throws AKBASIC_ERR_BOUNDS When the area is too small for even one character.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSink *state, akgl_RenderBackend *renderer, TTF_Font *font, int w, int h);
|
||||
|
||||
/**
|
||||
* @brief Draw whatever the sink currently holds.
|
||||
*
|
||||
* Separate from the sink's write path because the interpreter does not own the
|
||||
* frame: a host calls this when it is drawing, not when the script happens to
|
||||
* PRINT. A driver that only wants stdout never calls it at all.
|
||||
*
|
||||
* @param obj The sink to draw.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
||||
* @throws AKGL_ERR_SDL When the renderer refuses the text.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sink_akgl_render(akbasic_TextSink *obj);
|
||||
|
||||
/**
|
||||
* @brief Point a graphics backend at a renderer the host already has.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param state Storage for the shape pool; must outlive the backend.
|
||||
* @param renderer The renderer the host already initialized; not created here.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When any argument is NULL.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akbasic_AkglGraphics *state, akgl_RenderBackend *renderer);
|
||||
|
||||
/**
|
||||
* @brief Wire an audio backend to libakgl's tone generator.
|
||||
*
|
||||
* Calls akgl_audio_init(), which opens an SDL audio device. A host that owns its
|
||||
* own audio pipeline can call akgl_audio_init() itself beforehand -- it is
|
||||
* idempotent in the sense that mattered upstream: the voice table works whether
|
||||
* or not a device is open.
|
||||
*
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
||||
* @throws AKGL_ERR_SDL When no audio device can be opened.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_audio_init_akgl(akbasic_AudioBackend *obj);
|
||||
|
||||
/**
|
||||
* @brief Wire an input backend to libakgl's keystroke ring.
|
||||
*
|
||||
* Reads only. The ring is filled by akgl_controller_handle_event(), which the
|
||||
* *host* calls as it pumps SDL events -- so a script gets keystrokes without the
|
||||
* interpreter owning the event loop.
|
||||
*
|
||||
* Worth knowing before lending this to a script: that ring is process-global and
|
||||
* the host's own control maps read the same events. A script sitting in a GET
|
||||
* loop drains keystrokes the game will then never see. A host that cares either
|
||||
* withholds the input backend or supplies a filtered one of its own.
|
||||
*
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_input_init_akgl(akbasic_InputBackend *obj);
|
||||
|
||||
#endif // _AKBASIC_AKGL_H_
|
||||
Reference in New Issue
Block a user