Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of those turned out to have been fixed or never ported and nobody had written it down; the audit records the evidence for each. Two of the seventeen were real. math_plus mutated its left operand when the operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT coverage because NEXT relied on the mutation, so tests/for_next.c came first and NEXT now writes the counter back itself. And the binary operators summed both numeric fields of their right operand, which no BASIC program can reach -- that one needed a test written against the value API. Writing the tests turned up eight defects nobody had listed. Seven are fixed: IF A = 2 THEN was a parse error; only == worked IF ... AND ... was a parse error, because a condition parsed as one relation IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN EXIT before any NEXT restarted the program and exhausted the variable pool READ never found a DATA line above it, and swallowed the lines between PRINT 2 + 2 at the prompt was filed as program text instead of answering a short read discarded its bytes, so COPY produced empty files every verb taking an argument list said "peek() returned nil token!" on none The eighth is not fixed and cannot be quietly: a FOR whose step overshoots runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two errors cancel for a step of 1, which is why neither was noticed. Correcting them changes the expected output of a checked-in acceptance file, and tests/reference/README.md forbids editing one to suit this interpreter. It is tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct contract, and TODO.md items 19 and 20. Sprites are real libakgl actors with a renderfunc of their own, because akgl_actor_render draws every sprite square and an actor has no per-axis scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle or a 63-element integer array -- a string here cannot hold a zero byte. Verbs that need hardware that does not exist are refused by name with the reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream. 94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen clean. The Go acceptance corpus stayed green throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
369 lines
16 KiB
C
369 lines
16 KiB
C
/**
|
|
* @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/actor.h>
|
|
#include <akgl/character.h>
|
|
#include <akgl/renderer.h>
|
|
#include <akgl/sprite.h>
|
|
#include <akgl/version.h>
|
|
|
|
/*
|
|
* The API floor is 0.3.0: akgl_render_bind2d(), akgl_audio_sweep(), and
|
|
* akgl_Keystroke with akgl_controller_poll_keystroke() all arrived there, and
|
|
* nothing in this target uses anything newer. The floor asserted below is
|
|
* 0.4.0 anyway, and the difference is ABI rather than API.
|
|
*
|
|
* The soname carries MAJOR.MINOR while the major is 0, so 0.3 and 0.4 are
|
|
* different ABIs *by declaration* -- libakgl's versioning policy says a 0.x
|
|
* minor bump may break, and the soname is built to match. 0.4.0 in fact changed
|
|
* no public struct: it is leak and overread fixes inside src/. The floor moves
|
|
* anyway, because the alternative is deciding for ourselves which of libakgl's
|
|
* minor releases were really compatible, and that is exactly the judgement the
|
|
* soname exists to take away from us.
|
|
*
|
|
* What it catches is the case the soname cannot: a build against 0.3 headers
|
|
* that happens to find a 0.4 library, or the reverse. Refused here rather than
|
|
* at link time, because a missing symbol names a function and this names the
|
|
* release.
|
|
*/
|
|
#if !AKGL_VERSION_AT_LEAST(0, 4, 0)
|
|
#error "akbasic's libakgl adaptors require libakgl 0.4.0 or later"
|
|
#endif
|
|
|
|
#include <akbasic/audio.h>
|
|
#include <akbasic/graphics.h>
|
|
#include <akbasic/input.h>
|
|
#include <akbasic/sink.h>
|
|
#include <akbasic/sprite.h>
|
|
|
|
/**
|
|
* @brief One full cursor blink in milliseconds, half on and half off.
|
|
*
|
|
* Half a second, which is about what a Commodore does and is slow enough to read
|
|
* under.
|
|
*/
|
|
#define AKBASIC_SINK_CURSOR_BLINK_MS 500
|
|
|
|
/** @brief How many saved SSHAPE regions the graphics backend will hold at once. */
|
|
#define AKBASIC_AKGL_MAX_SHAPES 16
|
|
|
|
/**
|
|
* @brief One frame of the host's own loop, borrowed by the sink's line editor.
|
|
*
|
|
* The sink has to be able to wait for a typed line without owning an event loop,
|
|
* and those two requirements only meet in one place: the host hands over a
|
|
* callback that does exactly one frame -- pump events, redraw, present -- and
|
|
* the editor calls it between keystrokes. The loop is still the host's; the
|
|
* editor just borrows it a frame at a time.
|
|
*
|
|
* Setting @p running false is how the host says the window closed. The editor
|
|
* reports end of input rather than raising, because that is what running out of
|
|
* input means everywhere else in sink.h.
|
|
*
|
|
* @param self Whatever the host passed to akbasic_sink_akgl_set_pump().
|
|
* @param running Set false to stop waiting; the editor then reports EOF.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
*/
|
|
typedef akerr_ErrorContext AKERR_NOIGNORE *(*akbasic_AkglPump)(void *self, bool *running);
|
|
|
|
/**
|
|
* @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;
|
|
/**
|
|
* What a text cell is erased to before its glyphs are drawn. Opaque black
|
|
* by default, which is a Commodore console and is what makes the text layer
|
|
* *authoritative* over the rows it occupies rather than transparent over
|
|
* them -- see akbasic_sink_akgl_render().
|
|
*/
|
|
SDL_Color background;
|
|
|
|
int x; /* pixel origin of the text area */
|
|
int y;
|
|
int width; /* pixel size of the text area */
|
|
int height;
|
|
/*
|
|
* The whole drawable area, kept so WINDOW can grow back out to it. Without
|
|
* it a window could only ever shrink, since `x`/`width` have by then been
|
|
* overwritten with the current window's.
|
|
*/
|
|
int fullx;
|
|
int fully;
|
|
int fullwidth;
|
|
int fullheight;
|
|
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];
|
|
|
|
/**
|
|
* Milliseconds for one full cursor blink, half on and half off. Zero holds
|
|
* the cursor solid, which is what a test wanting a deterministic frame sets.
|
|
*/
|
|
uint64_t cursorperiodms;
|
|
|
|
/*
|
|
* The line editor. Nothing here is live except while readline is running,
|
|
* and `editing` is what says so -- render() draws a cursor only then, and
|
|
* the scroll adjusts `editrow` only then.
|
|
*/
|
|
akbasic_AkglPump pump;
|
|
void *pumpself;
|
|
bool editing;
|
|
int editrow; /* where the line being typed starts */
|
|
int editcol;
|
|
int editlen; /* characters accepted so far */
|
|
int echolen; /* characters currently drawn, so a backspace */
|
|
/* knows how much to erase */
|
|
char editline[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 State for the libakgl-backed sprite backend.
|
|
*
|
|
* One libakgl actor per BASIC sprite, plus the sheet, sprite and character each
|
|
* actor needs -- built by hand rather than loaded from a sprite document, since
|
|
* a Commodore sprite has no animation, no frame list and no state map to
|
|
* describe. Everything is pooled by libakgl; what is held here are the pointers
|
|
* and the one texture per slot that this backend owns and must destroy.
|
|
*
|
|
* `graphics` is the graphics backend's state, borrowed so `SPRSAV A$, n` can
|
|
* resolve a handle SSHAPE minted. The two devices are separate records on
|
|
* purpose -- a host may supply one and withhold the other -- so this is a
|
|
* pointer that may be NULL rather than an assumption that both exist.
|
|
*/
|
|
typedef struct
|
|
{
|
|
akgl_RenderBackend *renderer;
|
|
akbasic_AkglGraphics *graphics;
|
|
|
|
akgl_SpriteSheet *sheets[AKBASIC_MAX_SPRITES];
|
|
akgl_Sprite *sprites[AKBASIC_MAX_SPRITES];
|
|
akgl_Character *characters[AKBASIC_MAX_SPRITES];
|
|
akgl_Actor *actors[AKBASIC_MAX_SPRITES];
|
|
|
|
/** Per-axis expansion, which an actor's single `scale` cannot carry. */
|
|
bool xexpand[AKBASIC_MAX_SPRITES];
|
|
bool yexpand[AKBASIC_MAX_SPRITES];
|
|
} akbasic_AkglSprites;
|
|
|
|
/**
|
|
* @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 Give the sink a line editor by lending it one frame of the host's loop.
|
|
*
|
|
* Without this, readline reports end of input immediately: a drawn text layer is
|
|
* not a source of lines, and a sink that blocked on the keyboard with no way to
|
|
* pump events would deadlock the process on its first INPUT. With it, readline
|
|
* collects keystrokes from libakgl's ring -- the one the host's own event pump
|
|
* fills -- echoes them into the grid, and returns the line on Return.
|
|
*
|
|
* **It is a real keyboard as of libakgl 0.3.0.** The ring carries the composed
|
|
* UTF-8 text SDL worked out from the keystroke, so shifted characters, a
|
|
* keyboard layout, a compose key and a dead key all do what the person typing
|
|
* expects -- including the double quote, without which a BASIC string literal
|
|
* cannot be typed at all. Until 0.3.0 the ring reported a keycode and nothing
|
|
* else, so none of that was reachable and letters were folded to upper case;
|
|
* that was libakgl API-gap item 10, filed from here.
|
|
*
|
|
* Composed text is taken over the keycode wherever there is any, which is what
|
|
* makes the layout work: on an AZERTY keyboard the key SDL calls `SDLK_Q`
|
|
* composes to "a", and "a" is what the program should see.
|
|
*
|
|
* Editing is Backspace to erase one character and Escape to abandon the line.
|
|
* There is no cursor movement within the line: the ring reports the arrow keys,
|
|
* but a script's own GET loop wants them.
|
|
*
|
|
* @param obj The sink to give an editor to.
|
|
* @param pump One frame of the host's loop, or NULL to take the editor away.
|
|
* @param self Passed back to @p pump untouched.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When `obj` is NULL or carries no sink state.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sink_akgl_set_pump(akbasic_TextSink *obj, akbasic_AkglPump pump, void *self);
|
|
|
|
/**
|
|
* @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 Point a sprite backend at a renderer the host already has.
|
|
*
|
|
* Requires akgl_heap_init() and akgl_registry_init() to have run -- every
|
|
* akgl_*_initialize ends in a registry write -- and requires the global `camera`
|
|
* to point somewhere, because akgl_actor_render() dereferences it without
|
|
* checking. akgl_game_init() does all three; a host that skips it, which is
|
|
* every host embedding this interpreter, does them itself.
|
|
* src/frontend_akgl.c is the worked example.
|
|
*
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @param state Storage for the sprite pool; must outlive the backend.
|
|
* @param renderer The renderer the host already initialized; not created here.
|
|
* @param graphics The graphics backend's state, so SPRSAV can resolve an SSHAPE handle; may be NULL.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When `obj`, `state` or `renderer` is NULL.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_init_akgl(akbasic_SpriteBackend *obj, akbasic_AkglSprites *state, akgl_RenderBackend *renderer, akbasic_AkglGraphics *graphics);
|
|
|
|
/**
|
|
* @brief Draw every defined, enabled sprite.
|
|
*
|
|
* Separate from the verbs for the same reason akbasic_sink_akgl_render() is: the
|
|
* interpreter does not own the frame. A host calls this when it is drawing.
|
|
*
|
|
* The actors are swept directly rather than through akgl_registry_iterate_actor(),
|
|
* which ends in FINISH_NORETURN -- an unhandled error inside it terminates the
|
|
* process, and goal 3 forbids anything in this library doing that. Sweeping our
|
|
* own eight also leaves a host's own actors alone.
|
|
*
|
|
* @param obj The backend to draw.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When `obj` is NULL or carries no state.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_akgl_render(akbasic_SpriteBackend *obj);
|
|
|
|
/**
|
|
* @brief Release every texture the sprite backend created.
|
|
*
|
|
* The actors, sprites, sheets and characters are libakgl's pooled objects and go
|
|
* back with akgl_heap_init(); the SDL textures behind the sheets are this
|
|
* backend's own and are not anybody else's to free.
|
|
*
|
|
* @param obj The backend to tear down. NULL is not an error.
|
|
*/
|
|
void akbasic_sprite_akgl_shutdown(akbasic_SpriteBackend *obj);
|
|
|
|
/**
|
|
* @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_
|