Reach hardware through backend records, and take the time from the host
Some checks failed
akbasic CI Build / cmake_build (push) Successful in 2m46s
akbasic CI Build / sanitizers (push) Successful in 3m2s
akbasic CI Build / coverage (push) Failing after 3m8s
akbasic CI Build / mutation_test (push) Successful in 7m21s

Groups G, I and E are unblocked but cannot be written yet: the core library is
free of SDL and builds with no libakgl present, so a graphics verb cannot call
akgl_draw_* and a sound verb cannot call akgl_audio_*. This adds what they call
instead.

Three records of function pointers -- akbasic_GraphicsBackend, _AudioBackend and
_InputBackend -- in the same shape as akbasic_TextSink, and the same shape
libakgl uses for akgl_RenderBackend. akbasic_runtime_set_devices() attaches any
subset; all three may be NULL and that is the standalone driver's normal state,
so a runtime with no backends still comes up and still prints. A verb that needs
one it was not given raises the new AKBASIC_ERR_DEVICE rather than dereferencing
a NULL vtable.

Two decisions worth stating. The graphics record has no circle entry point:
BASIC 7.0's CIRCLE takes two radii, an arc range, a rotation and a degree
increment, which makes it a polygon by definition, so it will be built from line
calls rather than from akgl_draw_circle. And coordinates are double rather than
an integer pixel address, because SCALE makes them fractional and rounding at
each verb rather than once at the backend accumulates drift along a polyline.

akbasic_runtime_settime() is how SOUND, PLAY and TEMPO get a clock without the
library reading one. Section 1.6 forbids blocking or owning a loop, so the caller
that owns the frame owns the time -- which is what libakgl already does, since
akgl_actor_logic_changeframe takes curtimems as an argument. Left unset it is
zero and every duration expires immediately: audible, but never a hang.

AKBASIC_ERR_LAST is a sentinel rather than a status, so tests/error_codes.c walks
every code looking for an unnamed one without anybody remembering to widen the
loop when a code is added.

tests/mockdevice.h records every backend call as a formatted line. The graphics
and audio verbs emit nothing a golden file can compare, so that log is where
their assertions have to live -- and since it needs no SDL, the whole of groups
G, I and E stays testable in the default build.

62/62 ctest, clean under -Wall -Wextra, doxygen clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 08:07:23 -04:00
parent 5077efc3a7
commit 9b9dd09c5a
12 changed files with 888 additions and 1 deletions

View File

@@ -34,5 +34,7 @@ akerr_ErrorContext *akbasic_error_register(void)
"Value Error"));
PASS(errctx, akerr_register_status_name(AKBASIC_OWNER, AKBASIC_ERR_STATE,
"State Error"));
PASS(errctx, akerr_register_status_name(AKBASIC_OWNER, AKBASIC_ERR_DEVICE,
"Device Error"));
SUCCEED_RETURN(errctx);
}

53
src/graphics_tables.c Normal file
View File

@@ -0,0 +1,53 @@
/**
* @file graphics_tables.c
* @brief Implements the BASIC 7.0 color palette lookup.
*
* This is a table, so it is laid out as one. The RGB values are the widely
* reproduced VIC-II palette (Pepto's measurement of a PAL 6569R3), which is what
* an emulator shows and therefore what somebody porting a listing expects to
* see. They are not a C128 ROM constant -- the real machine has no RGB anywhere
* in it, it has a chroma/luma encoder -- so this is a choice, not a transcription.
*/
#include <akerror.h>
#include <akbasic/error.h>
#include <akbasic/graphics.h>
/**
* @brief The sixteen BASIC 7.0 colors, indexed from 1 as BASIC indexes them.
*
* Slot 0 is unused and left black so an off-by-one reads as black rather than as
* whatever the last entry happened to be.
*/
static const akbasic_Color PALETTE[17] = {
{ 0x00, 0x00, 0x00, 0xff }, /* 0 -- unused; BASIC counts colors from 1 */
{ 0x00, 0x00, 0x00, 0xff }, /* 1 -- black */
{ 0xff, 0xff, 0xff, 0xff }, /* 2 -- white */
{ 0x88, 0x39, 0x32, 0xff }, /* 3 -- red */
{ 0x67, 0xb6, 0xbd, 0xff }, /* 4 -- cyan */
{ 0x8b, 0x3f, 0x96, 0xff }, /* 5 -- purple */
{ 0x55, 0xa0, 0x49, 0xff }, /* 6 -- green */
{ 0x40, 0x31, 0x8d, 0xff }, /* 7 -- blue */
{ 0xbf, 0xce, 0x72, 0xff }, /* 8 -- yellow */
{ 0x8b, 0x54, 0x29, 0xff }, /* 9 -- orange */
{ 0x57, 0x42, 0x00, 0xff }, /* 10 -- brown */
{ 0xb8, 0x69, 0x62, 0xff }, /* 11 -- light red */
{ 0x50, 0x50, 0x50, 0xff }, /* 12 -- dark grey */
{ 0x78, 0x78, 0x78, 0xff }, /* 13 -- medium grey */
{ 0x94, 0xe0, 0x89, 0xff }, /* 14 -- light green */
{ 0x78, 0x69, 0xc4, 0xff }, /* 15 -- light blue */
{ 0x9f, 0x9f, 0x9f, 0xff } /* 16 -- light grey */
};
akerr_ErrorContext *akbasic_graphics_palette(int index, akbasic_Color *dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER,
"NULL destination in palette");
FAIL_ZERO_RETURN(errctx, (index >= 1 && index <= 16), AKBASIC_ERR_BOUNDS,
"Color index %d out of range (1 to 16)", index);
*dest = PALETTE[index];
SUCCEED_RETURN(errctx);
}

View File

@@ -152,6 +152,41 @@ akerr_ErrorContext *akbasic_runtime_init(akbasic_Runtime *obj, akbasic_TextSink
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_runtime_set_devices(akbasic_Runtime *obj, akbasic_GraphicsBackend *graphics, akbasic_AudioBackend *audio, akbasic_InputBackend *input)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
"NULL runtime in set_devices");
/*
* No validation of the records themselves. A backend with a NULL entry point
* is caught at the call site by the verb that needs it, which can say which
* verb wanted what -- checking every pointer here would only be able to say
* that something, somewhere, was incomplete.
*/
obj->graphics = graphics;
obj->audio = audio;
obj->input = input;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_runtime_settime(akbasic_Runtime *obj, int64_t timems)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
"NULL runtime in settime");
/*
* Deliberately not rejecting a time that moves backwards. A host is free to
* drive this from a paused, scrubbed or replayed clock, and the only thing
* that happens is a note holding longer than it asked to.
*/
obj->timems = timems;
SUCCEED_RETURN(errctx);
}
/* ---------------------------------------------------------------- output -- */
akerr_ErrorContext *akbasic_runtime_write(akbasic_Runtime *obj, const char *text)