Reach hardware through backend records, and take the time from the host
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:
@@ -101,6 +101,7 @@ set(AKBASIC_SOURCES
|
|||||||
src/environment.c
|
src/environment.c
|
||||||
src/error.c
|
src/error.c
|
||||||
src/grammar.c
|
src/grammar.c
|
||||||
|
src/graphics_tables.c
|
||||||
src/parser.c
|
src/parser.c
|
||||||
src/parser_commands.c
|
src/parser_commands.c
|
||||||
src/runtime.c
|
src/runtime.c
|
||||||
@@ -186,6 +187,7 @@ endif()
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
set(AKBASIC_TESTS
|
set(AKBASIC_TESTS
|
||||||
convert
|
convert
|
||||||
|
devices
|
||||||
environment_scope
|
environment_scope
|
||||||
error_codes
|
error_codes
|
||||||
grammar_leaves
|
grammar_leaves
|
||||||
|
|||||||
71
include/akbasic/audio.h
Normal file
71
include/akbasic/audio.h
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* @file audio.h
|
||||||
|
* @brief Declares the audio backend: where SOUND, PLAY, ENVELOPE and VOL land.
|
||||||
|
*
|
||||||
|
* Same reasoning as graphics.h -- the core library is free of SDL, so the sound
|
||||||
|
* verbs call through a record of function pointers and akbasic_audio_init_akgl()
|
||||||
|
* in the akbasic_akgl target wires that record to akgl_audio_*.
|
||||||
|
*
|
||||||
|
* The libakgl API this is modelled on is a tone generator, not a sample player:
|
||||||
|
* three voices, a waveform and an ADSR envelope each, mixed to one stream. That
|
||||||
|
* is the right shape, because BASIC 7.0's sound verbs describe notes rather than
|
||||||
|
* recordings. What it does not have is a filter stage, which is why FILTER is
|
||||||
|
* refused -- see TODO.md section 7.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AKBASIC_AUDIO_H_
|
||||||
|
#define _AKBASIC_AUDIO_H_
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akbasic/types.h>
|
||||||
|
|
||||||
|
/** @brief Number of independent voices, matching the SID and matching libakgl. */
|
||||||
|
#define AKBASIC_AUDIO_VOICES 3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Waveform selection, numbered as BASIC 7.0's SOUND numbers it.
|
||||||
|
*
|
||||||
|
* Declared here rather than reused from akgl/audio.h so the core library does
|
||||||
|
* not include a libakgl header. The akgl backend maps these across; the values
|
||||||
|
* happen to agree today and the mapping is still written out, because two
|
||||||
|
* enumerations agreeing by accident is not a contract.
|
||||||
|
*/
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
AKBASIC_WAVE_TRIANGLE = 0, /* SOUND waveform 0 */
|
||||||
|
AKBASIC_WAVE_SAWTOOTH = 1, /* SOUND waveform 1 */
|
||||||
|
AKBASIC_WAVE_SQUARE = 2, /* SOUND waveform 2, and the power-on default */
|
||||||
|
AKBASIC_WAVE_NOISE = 3 /* SOUND waveform 3 */
|
||||||
|
} akbasic_Waveform;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Where the sound verbs play.
|
||||||
|
*
|
||||||
|
* Voices here are numbered from zero. BASIC numbers them 1 through 3 and the
|
||||||
|
* conversion happens once, in the verb handler, so a backend never sees a BASIC
|
||||||
|
* voice number.
|
||||||
|
*
|
||||||
|
* Durations are milliseconds. BASIC counts in jiffies (1/60 s) and note lengths
|
||||||
|
* derived from TEMPO; both are converted in src/audio_tables.c before they get
|
||||||
|
* here, so a backend never sees a jiffy either.
|
||||||
|
*/
|
||||||
|
typedef struct akbasic_AudioBackend
|
||||||
|
{
|
||||||
|
void *self;
|
||||||
|
|
||||||
|
/** Start a note on a voice, for a bounded duration. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*tone)(struct akbasic_AudioBackend *self, int voice, double hz, int ms);
|
||||||
|
/** Silence a voice immediately. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*stop)(struct akbasic_AudioBackend *self, int voice);
|
||||||
|
/** Select the waveform a voice synthesises. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*waveform)(struct akbasic_AudioBackend *self, int voice, akbasic_Waveform waveform);
|
||||||
|
/** Set a voice's ADSR envelope. Sustain is a level from 0.0 to 1.0, not a time. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*envelope)(struct akbasic_AudioBackend *self, int voice, int attack, int decay, double sustain, int release);
|
||||||
|
/** Set the master output level, 0.0 to 1.0. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*volume)(struct akbasic_AudioBackend *self, double level);
|
||||||
|
/** Report whether a voice is still sounding. PLAY's queue uses this to pace itself. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*voice_active)(struct akbasic_AudioBackend *self, int voice, bool *active);
|
||||||
|
} akbasic_AudioBackend;
|
||||||
|
|
||||||
|
#endif // _AKBASIC_AUDIO_H_
|
||||||
@@ -50,6 +50,8 @@ enum {
|
|||||||
AKBASIC_ERR_ENVIRONMENT, /** Environment pool exhausted or orphaned environment */
|
AKBASIC_ERR_ENVIRONMENT, /** Environment pool exhausted or orphaned environment */
|
||||||
AKBASIC_ERR_VALUE, /** A value was malformed, truncated or unconvertible */
|
AKBASIC_ERR_VALUE, /** A value was malformed, truncated or unconvertible */
|
||||||
AKBASIC_ERR_STATE, /** A verb ran outside the block structure it requires */
|
AKBASIC_ERR_STATE, /** A verb ran outside the block structure it requires */
|
||||||
|
AKBASIC_ERR_DEVICE, /** A verb needs a graphics, audio or input backend the runtime has not been given, or one that cannot do what was asked */
|
||||||
|
AKBASIC_ERR_LAST, /** One past the last real code. Not a status; tests/error_codes.c walks up to it so a new code is checked for a name without anybody remembering to widen the loop. */
|
||||||
AKBASIC_ERR_LIMIT = AKBASIC_ERR_BASE + 256
|
AKBASIC_ERR_LIMIT = AKBASIC_ERR_BASE + 256
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
105
include/akbasic/graphics.h
Normal file
105
include/akbasic/graphics.h
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* @file graphics.h
|
||||||
|
* @brief Declares the graphics backend: where DRAW, BOX, CIRCLE and PAINT land.
|
||||||
|
*
|
||||||
|
* The core library is free of SDL and builds with no libakgl present, so the
|
||||||
|
* graphics verbs cannot call akgl_draw_* directly. They call through a record of
|
||||||
|
* function pointers instead -- the same shape as akbasic_TextSink, and the same
|
||||||
|
* shape libakgl itself uses for akgl_RenderBackend and akgl_PhysicsBackend.
|
||||||
|
*
|
||||||
|
* akbasic_graphics_init_akgl() lives in the separate akbasic_akgl target and
|
||||||
|
* draws through whatever renderer the host game already initialized. A host that
|
||||||
|
* renders some other way supplies its own record and never links libakgl at all.
|
||||||
|
*
|
||||||
|
* A runtime with no graphics backend is the normal case -- the standalone driver
|
||||||
|
* has none -- so every graphics verb refuses with AKBASIC_ERR_DEVICE rather than
|
||||||
|
* dereferencing a NULL vtable.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AKBASIC_GRAPHICS_H_
|
||||||
|
#define _AKBASIC_GRAPHICS_H_
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akbasic/types.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief An RGBA color, in the shape SDL_Color has without requiring SDL.
|
||||||
|
*
|
||||||
|
* BASIC never names a color this way -- COLOR takes a source and a 1-16 palette
|
||||||
|
* index -- so this is what the palette table in src/graphics_tables.c converts
|
||||||
|
* to, and what the backend receives.
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint8_t r;
|
||||||
|
uint8_t g;
|
||||||
|
uint8_t b;
|
||||||
|
uint8_t a;
|
||||||
|
} akbasic_Color;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Where the graphics verbs draw.
|
||||||
|
*
|
||||||
|
* Coordinates are `double` rather than an integer pixel address because SCALE
|
||||||
|
* makes them fractional: a program running at a 1023x1023 logical scale on a
|
||||||
|
* 320x200 screen produces non-integer pixel positions, and rounding at each verb
|
||||||
|
* rather than once at the backend accumulates visible drift along a polyline.
|
||||||
|
*
|
||||||
|
* There is deliberately no circle entry point. BASIC 7.0's CIRCLE takes two
|
||||||
|
* radii, a start and end angle, a rotation and a degree increment -- it is a
|
||||||
|
* polygon by definition -- so it is built from `line` calls in the verb handler.
|
||||||
|
* See TODO.md section 5.
|
||||||
|
*/
|
||||||
|
typedef struct akbasic_GraphicsBackend
|
||||||
|
{
|
||||||
|
void *self;
|
||||||
|
|
||||||
|
/** Plot one pixel. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*point)(struct akbasic_GraphicsBackend *self, double x, double y, akbasic_Color color);
|
||||||
|
/** Draw a line between two points. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*line)(struct akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color);
|
||||||
|
/** Outline an axis-aligned rectangle. A rotated BOX becomes four `line` calls. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*rect)(struct akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color);
|
||||||
|
/** Fill an axis-aligned rectangle. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*filled_rect)(struct akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color);
|
||||||
|
/**
|
||||||
|
* Flood-fill the region containing (x, y).
|
||||||
|
*
|
||||||
|
* May report AKERR_OUTOFBOUNDS having filled only part of the region: the
|
||||||
|
* akgl implementation walks spans from a fixed stack and gives up rather
|
||||||
|
* than growing it. PAINT reports that to the program instead of leaving a
|
||||||
|
* half-painted screen unexplained.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*paint)(struct akbasic_GraphicsBackend *self, int x, int y, akbasic_Color color);
|
||||||
|
/** Clear the whole drawing surface to one color. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*clear)(struct akbasic_GraphicsBackend *self, akbasic_Color color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a rectangular region and yield an opaque handle to it.
|
||||||
|
*
|
||||||
|
* SSHAPE stores the handle in a BASIC string, not the pixels -- see TODO.md
|
||||||
|
* section 5 for what that deviation costs.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*save_shape)(struct akbasic_GraphicsBackend *self, int x1, int y1, int x2, int y2, int *handle);
|
||||||
|
/** Blit a saved region back at (x, y). */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*paste_shape)(struct akbasic_GraphicsBackend *self, int handle, double x, double y);
|
||||||
|
/** Release every saved region. NEW and CLR call this; nothing else reclaims a slot. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*free_shapes)(struct akbasic_GraphicsBackend *self);
|
||||||
|
} akbasic_GraphicsBackend;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Convert a BASIC color index to the RGBA the backend draws with.
|
||||||
|
*
|
||||||
|
* BASIC 7.0 numbers its sixteen colors from 1, not 0; index 17 is the "current"
|
||||||
|
* color on a real C128 and has no meaning here.
|
||||||
|
*
|
||||||
|
* @param index Palette index, 1 through 16.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When `dest` is NULL.
|
||||||
|
* @throws AKBASIC_ERR_BOUNDS When `index` is outside 1..16.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_palette(int index, akbasic_Color *dest);
|
||||||
|
|
||||||
|
#endif // _AKBASIC_GRAPHICS_H_
|
||||||
42
include/akbasic/input.h
Normal file
42
include/akbasic/input.h
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* @file input.h
|
||||||
|
* @brief Declares the input backend: where GET and GETKEY read from.
|
||||||
|
*
|
||||||
|
* Same reasoning as graphics.h and audio.h. The libakgl implementation is
|
||||||
|
* akgl_controller_poll_key(), which drains a keystroke ring the library fills
|
||||||
|
* from SDL events the *host* pumps -- so the interpreter answers "is there a key
|
||||||
|
* waiting" without owning the event loop, which is what goal 3 requires.
|
||||||
|
*
|
||||||
|
* One thing an embedding host should know: that ring is process-global and the
|
||||||
|
* host's own control maps read the same events. An interpreter 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AKBASIC_INPUT_H_
|
||||||
|
#define _AKBASIC_INPUT_H_
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akbasic/types.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Where the console input verbs read.
|
||||||
|
*/
|
||||||
|
typedef struct akbasic_InputBackend
|
||||||
|
{
|
||||||
|
void *self;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take the next buffered keystroke without waiting for one.
|
||||||
|
*
|
||||||
|
* An empty buffer is success with `*available` false, never an error: GET on
|
||||||
|
* an empty buffer yields the empty string, which is ordinary BASIC and
|
||||||
|
* happens on most iterations of any GET loop.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*poll_key)(struct akbasic_InputBackend *self, int *keycode, bool *available);
|
||||||
|
/** Discard everything buffered. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *(*flush_keys)(struct akbasic_InputBackend *self);
|
||||||
|
} akbasic_InputBackend;
|
||||||
|
|
||||||
|
#endif // _AKBASIC_INPUT_H_
|
||||||
@@ -18,8 +18,11 @@
|
|||||||
|
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akbasic/audio.h>
|
||||||
#include <akbasic/environment.h>
|
#include <akbasic/environment.h>
|
||||||
#include <akbasic/grammar.h>
|
#include <akbasic/grammar.h>
|
||||||
|
#include <akbasic/graphics.h>
|
||||||
|
#include <akbasic/input.h>
|
||||||
#include <akbasic/sink.h>
|
#include <akbasic/sink.h>
|
||||||
#include <akbasic/types.h>
|
#include <akbasic/types.h>
|
||||||
#include <akbasic/value.h>
|
#include <akbasic/value.h>
|
||||||
@@ -84,6 +87,27 @@ typedef struct akbasic_Runtime
|
|||||||
akbasic_Environment *environment;
|
akbasic_Environment *environment;
|
||||||
akbasic_TextSink *sink;
|
akbasic_TextSink *sink;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The device backends, any of which may be NULL. The standalone driver
|
||||||
|
* supplies none of them, so a PRINT-only program must keep working; every
|
||||||
|
* verb that needs one refuses with AKBASIC_ERR_DEVICE instead.
|
||||||
|
*/
|
||||||
|
akbasic_GraphicsBackend *graphics;
|
||||||
|
akbasic_AudioBackend *audio;
|
||||||
|
akbasic_InputBackend *input;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The host's clock, in milliseconds, as of its last akbasic_runtime_settime()
|
||||||
|
* call. The interpreter does not read a clock: it owns no loop and must not
|
||||||
|
* block, so the caller that owns the frame owns the time. libakgl does the
|
||||||
|
* same thing -- akgl_actor_logic_changeframe() takes curtimems as an
|
||||||
|
* argument rather than asking the system for it.
|
||||||
|
*
|
||||||
|
* Left at zero, every duration expires on the step after it starts. That is
|
||||||
|
* wrong but never a hang, which is the right way for it to fail.
|
||||||
|
*/
|
||||||
|
int64_t timems;
|
||||||
|
|
||||||
/* REPL line assembly */
|
/* REPL line assembly */
|
||||||
char userline[AKBASIC_MAX_LINE_LENGTH];
|
char userline[AKBASIC_MAX_LINE_LENGTH];
|
||||||
|
|
||||||
@@ -104,6 +128,43 @@ typedef struct akbasic_Runtime
|
|||||||
*/
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_init(akbasic_Runtime *obj, akbasic_TextSink *sink);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_init(akbasic_Runtime *obj, akbasic_TextSink *sink);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Attach the device backends a host is willing to lend the interpreter.
|
||||||
|
*
|
||||||
|
* Any argument may be NULL, which is how a host withholds a capability: a script
|
||||||
|
* given no audio backend gets an error from SOUND rather than silence. Call it
|
||||||
|
* after akbasic_runtime_init() and before akbasic_runtime_start(); calling it
|
||||||
|
* again mid-run is allowed and takes effect on the next verb.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param graphics Where DRAW, BOX, CIRCLE and PAINT land; may be NULL.
|
||||||
|
* @param audio Where SOUND, PLAY and VOL land; may be NULL.
|
||||||
|
* @param input Where GET and GETKEY read; may be NULL.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_devices(akbasic_Runtime *obj, akbasic_GraphicsBackend *graphics, akbasic_AudioBackend *audio, akbasic_InputBackend *input);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Tell the interpreter what time the host thinks it is.
|
||||||
|
*
|
||||||
|
* SOUND durations, PLAY note lengths and TEMPO are all time-based, and section
|
||||||
|
* 1.6 forbids the library blocking or reading a clock of its own. A host calls
|
||||||
|
* this once a frame before akbasic_runtime_run(); the standalone driver calls it
|
||||||
|
* from a monotonic clock each step.
|
||||||
|
*
|
||||||
|
* Time is only ever compared, never differenced against a wall clock, so any
|
||||||
|
* monotonic millisecond source will do. It is not required to advance, and a
|
||||||
|
* host that never calls this leaves it at zero -- durations then expire
|
||||||
|
* immediately, which is audible but never a hang.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param timems The host's current time in milliseconds.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_settime(akbasic_Runtime *obj, int64_t timems);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Reset the per-line state without disturbing the program or variables.
|
* @brief Reset the per-line state without disturbing the program or variables.
|
||||||
* @param obj Object to initialize, inspect, or modify.
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
|||||||
@@ -34,5 +34,7 @@ akerr_ErrorContext *akbasic_error_register(void)
|
|||||||
"Value Error"));
|
"Value Error"));
|
||||||
PASS(errctx, akerr_register_status_name(AKBASIC_OWNER, AKBASIC_ERR_STATE,
|
PASS(errctx, akerr_register_status_name(AKBASIC_OWNER, AKBASIC_ERR_STATE,
|
||||||
"State Error"));
|
"State Error"));
|
||||||
|
PASS(errctx, akerr_register_status_name(AKBASIC_OWNER, AKBASIC_ERR_DEVICE,
|
||||||
|
"Device Error"));
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|||||||
53
src/graphics_tables.c
Normal file
53
src/graphics_tables.c
Normal 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);
|
||||||
|
}
|
||||||
@@ -152,6 +152,41 @@ akerr_ErrorContext *akbasic_runtime_init(akbasic_Runtime *obj, akbasic_TextSink
|
|||||||
SUCCEED_RETURN(errctx);
|
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 -- */
|
/* ---------------------------------------------------------------- output -- */
|
||||||
|
|
||||||
akerr_ErrorContext *akbasic_runtime_write(akbasic_Runtime *obj, const char *text)
|
akerr_ErrorContext *akbasic_runtime_write(akbasic_Runtime *obj, const char *text)
|
||||||
|
|||||||
171
tests/devices.c
Normal file
171
tests/devices.c
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
/**
|
||||||
|
* @file devices.c
|
||||||
|
* @brief Tests the device backend records, the palette table and the host clock.
|
||||||
|
*
|
||||||
|
* These are the pieces groups G, I and E are built on. Nothing here runs a BASIC
|
||||||
|
* verb -- that is what the per-group suites do -- it establishes that a runtime
|
||||||
|
* can be given backends, can be given none, and reports the time it was handed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <akbasic/error.h>
|
||||||
|
#include <akbasic/graphics.h>
|
||||||
|
#include <akbasic/runtime.h>
|
||||||
|
|
||||||
|
#include "harness.h"
|
||||||
|
#include "mockdevice.h"
|
||||||
|
#include "testutil.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A runtime with no backends attached must still come up and still print.
|
||||||
|
*
|
||||||
|
* This is the standalone driver's situation and the default for an embedding
|
||||||
|
* host that has not opted in, so it is the case that has to keep working.
|
||||||
|
*/
|
||||||
|
static void test_no_devices_is_normal(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||||||
|
TEST_REQUIRE(HARNESS_RUNTIME.graphics == NULL, "graphics backend should start NULL");
|
||||||
|
TEST_REQUIRE(HARNESS_RUNTIME.audio == NULL, "audio backend should start NULL");
|
||||||
|
TEST_REQUIRE(HARNESS_RUNTIME.input == NULL, "input backend should start NULL");
|
||||||
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.timems, 0);
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT \"HI\"\n"));
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "HI\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Attaching backends stores them; attaching NULLs takes them away again. */
|
||||||
|
static void test_set_devices(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||||||
|
mock_devices_init();
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
|
||||||
|
&MOCK_AUDIO, &MOCK_INPUT));
|
||||||
|
TEST_REQUIRE(HARNESS_RUNTIME.graphics == &MOCK_GRAPHICS, "graphics backend was not stored");
|
||||||
|
TEST_REQUIRE(HARNESS_RUNTIME.audio == &MOCK_AUDIO, "audio backend was not stored");
|
||||||
|
TEST_REQUIRE(HARNESS_RUNTIME.input == &MOCK_INPUT, "input backend was not stored");
|
||||||
|
|
||||||
|
/* Withholding a capability is spelled NULL, and must actually detach. */
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, NULL, NULL));
|
||||||
|
TEST_REQUIRE(HARNESS_RUNTIME.graphics == &MOCK_GRAPHICS, "graphics backend should have stayed");
|
||||||
|
TEST_REQUIRE(HARNESS_RUNTIME.audio == NULL, "audio backend should have been detached");
|
||||||
|
TEST_REQUIRE(HARNESS_RUNTIME.input == NULL, "input backend should have been detached");
|
||||||
|
|
||||||
|
TEST_REQUIRE_STATUS(akbasic_runtime_set_devices(NULL, NULL, NULL, NULL), AKERR_NULLPOINTER);
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief The clock is stored verbatim, including a value that moves backwards. */
|
||||||
|
static void test_settime(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 1000));
|
||||||
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.timems, 1000);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A host driving a paused or scrubbed clock is allowed. Rejecting it would
|
||||||
|
* make the interpreter an authority on the host's own time, which it is not.
|
||||||
|
*/
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 400));
|
||||||
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.timems, 400);
|
||||||
|
|
||||||
|
TEST_REQUIRE_STATUS(akbasic_runtime_settime(NULL, 0), AKERR_NULLPOINTER);
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief The palette covers 1 to 16 and refuses everything else. */
|
||||||
|
static void test_palette(void)
|
||||||
|
{
|
||||||
|
akbasic_Color color;
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
memset(&color, 0, sizeof(color));
|
||||||
|
|
||||||
|
for ( index = 1; index <= 16; index++ ) {
|
||||||
|
TEST_REQUIRE_OK(akbasic_graphics_palette(index, &color));
|
||||||
|
TEST_REQUIRE_INT(color.a, 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Two entries pinned by value, so a shifted table is a failure and not a shrug. */
|
||||||
|
TEST_REQUIRE_OK(akbasic_graphics_palette(1, &color));
|
||||||
|
TEST_REQUIRE(color.r == 0x00 && color.g == 0x00 && color.b == 0x00,
|
||||||
|
"color 1 should be black, got %02x%02x%02x", color.r, color.g, color.b);
|
||||||
|
TEST_REQUIRE_OK(akbasic_graphics_palette(2, &color));
|
||||||
|
TEST_REQUIRE(color.r == 0xff && color.g == 0xff && color.b == 0xff,
|
||||||
|
"color 2 should be white, got %02x%02x%02x", color.r, color.g, color.b);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* BASIC counts from 1. Zero is the off-by-one somebody writes when they
|
||||||
|
* forget that, and it has to be refused rather than quietly return slot 0.
|
||||||
|
*/
|
||||||
|
TEST_REQUIRE_STATUS(akbasic_graphics_palette(0, &color), AKBASIC_ERR_BOUNDS);
|
||||||
|
TEST_REQUIRE_STATUS(akbasic_graphics_palette(17, &color), AKBASIC_ERR_BOUNDS);
|
||||||
|
TEST_REQUIRE_STATUS(akbasic_graphics_palette(-1, &color), AKBASIC_ERR_BOUNDS);
|
||||||
|
TEST_REQUIRE_STATUS(akbasic_graphics_palette(1, NULL), AKERR_NULLPOINTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief The mock backends themselves record what they were told. */
|
||||||
|
static void test_mock_records(void)
|
||||||
|
{
|
||||||
|
akbasic_Color red = { 0xff, 0x00, 0x00, 0xff };
|
||||||
|
bool active = false;
|
||||||
|
int handle = -1;
|
||||||
|
int keycode = 0;
|
||||||
|
bool available = false;
|
||||||
|
|
||||||
|
mock_devices_init();
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(MOCK_GRAPHICS.point(&MOCK_GRAPHICS, 10.0, 20.0, red));
|
||||||
|
TEST_REQUIRE_OK(MOCK_GRAPHICS.line(&MOCK_GRAPHICS, 0.0, 0.0, 5.0, 5.0, red));
|
||||||
|
TEST_REQUIRE_STR(MOCK.log, "point 10.0,20.0 #ff0000\nline 0.0,0.0-5.0,5.0 #ff0000\n");
|
||||||
|
|
||||||
|
/* A saved shape yields a handle, and only a handle that exists can be pasted. */
|
||||||
|
TEST_REQUIRE_OK(MOCK_GRAPHICS.save_shape(&MOCK_GRAPHICS, 0, 0, 8, 8, &handle));
|
||||||
|
TEST_REQUIRE_INT(handle, 0);
|
||||||
|
TEST_REQUIRE_OK(MOCK_GRAPHICS.paste_shape(&MOCK_GRAPHICS, handle, 16.0, 16.0));
|
||||||
|
TEST_REQUIRE_STATUS(MOCK_GRAPHICS.paste_shape(&MOCK_GRAPHICS, 99, 0.0, 0.0), AKBASIC_ERR_BOUNDS);
|
||||||
|
|
||||||
|
/* The exhaustion switch is what lets the PAINT test reach the partial-fill path. */
|
||||||
|
MOCK.paint_exhausts = true;
|
||||||
|
TEST_REQUIRE_STATUS(MOCK_GRAPHICS.paint(&MOCK_GRAPHICS, 1, 1, red), AKERR_OUTOFBOUNDS);
|
||||||
|
MOCK.paint_exhausts = false;
|
||||||
|
TEST_REQUIRE_OK(MOCK_GRAPHICS.paint(&MOCK_GRAPHICS, 1, 1, red));
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(MOCK_AUDIO.tone(&MOCK_AUDIO, 0, 440.0, 250));
|
||||||
|
TEST_REQUIRE_OK(MOCK_AUDIO.voice_active(&MOCK_AUDIO, 0, &active));
|
||||||
|
TEST_REQUIRE(active, "voice 0 should be sounding after a tone");
|
||||||
|
TEST_REQUIRE_OK(MOCK_AUDIO.stop(&MOCK_AUDIO, 0));
|
||||||
|
TEST_REQUIRE_OK(MOCK_AUDIO.voice_active(&MOCK_AUDIO, 0, &active));
|
||||||
|
TEST_REQUIRE(!active, "voice 0 should be silent after a stop");
|
||||||
|
TEST_REQUIRE_STATUS(MOCK_AUDIO.tone(&MOCK_AUDIO, AKBASIC_AUDIO_VOICES, 440.0, 1),
|
||||||
|
AKBASIC_ERR_BOUNDS);
|
||||||
|
|
||||||
|
/* An empty key buffer is success with available false, never an error. */
|
||||||
|
TEST_REQUIRE_OK(MOCK_INPUT.poll_key(&MOCK_INPUT, &keycode, &available));
|
||||||
|
TEST_REQUIRE(!available, "an empty mock buffer should report nothing available");
|
||||||
|
|
||||||
|
mock_push_keys("AB");
|
||||||
|
TEST_REQUIRE_OK(MOCK_INPUT.poll_key(&MOCK_INPUT, &keycode, &available));
|
||||||
|
TEST_REQUIRE(available, "a primed mock buffer should report a key");
|
||||||
|
TEST_REQUIRE_INT(keycode, 'A');
|
||||||
|
TEST_REQUIRE_OK(MOCK_INPUT.poll_key(&MOCK_INPUT, &keycode, &available));
|
||||||
|
TEST_REQUIRE_INT(keycode, 'B');
|
||||||
|
TEST_REQUIRE_OK(MOCK_INPUT.poll_key(&MOCK_INPUT, &keycode, &available));
|
||||||
|
TEST_REQUIRE(!available, "the mock buffer should drain to empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
test_no_devices_is_normal();
|
||||||
|
test_set_devices();
|
||||||
|
test_settime();
|
||||||
|
test_palette();
|
||||||
|
test_mock_records();
|
||||||
|
return akbasic_test_failures;
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ int main(void)
|
|||||||
* "Unknown Error" in every stack trace that carries it, which nothing else
|
* "Unknown Error" in every stack trace that carries it, which nothing else
|
||||||
* in the suite would notice.
|
* in the suite would notice.
|
||||||
*/
|
*/
|
||||||
for ( code = AKBASIC_ERR_SYNTAX; code <= AKBASIC_ERR_STATE; code++ ) {
|
for ( code = AKBASIC_ERR_SYNTAX; code < AKBASIC_ERR_LAST; code++ ) {
|
||||||
const char *name = akerr_name_for_status(code, NULL);
|
const char *name = akerr_name_for_status(code, NULL);
|
||||||
TEST_REQUIRE(name != NULL, "status %d has a NULL name", code);
|
TEST_REQUIRE(name != NULL, "status %d has a NULL name", code);
|
||||||
TEST_REQUIRE(strcmp(name, "Unknown Error") != 0,
|
TEST_REQUIRE(strcmp(name, "Unknown Error") != 0,
|
||||||
@@ -31,6 +31,16 @@ int main(void)
|
|||||||
|
|
||||||
TEST_REQUIRE_STR(akerr_name_for_status(AKBASIC_ERR_SYNTAX, NULL), "Syntax Error");
|
TEST_REQUIRE_STR(akerr_name_for_status(AKBASIC_ERR_SYNTAX, NULL), "Syntax Error");
|
||||||
TEST_REQUIRE_STR(akerr_name_for_status(AKBASIC_ERR_STATE, NULL), "State Error");
|
TEST_REQUIRE_STR(akerr_name_for_status(AKBASIC_ERR_STATE, NULL), "State Error");
|
||||||
|
TEST_REQUIRE_STR(akerr_name_for_status(AKBASIC_ERR_DEVICE, NULL), "Device Error");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The sentinel must stay inside the band it terminates. If somebody ever adds
|
||||||
|
* 250 codes, the loop above walks straight out of our reservation and starts
|
||||||
|
* asserting on names libakgl or nobody owns.
|
||||||
|
*/
|
||||||
|
TEST_REQUIRE(AKBASIC_ERR_LAST < AKBASIC_ERR_LIMIT,
|
||||||
|
"AKBASIC_ERR_LAST %d has run past the reserved band ending at %d",
|
||||||
|
AKBASIC_ERR_LAST, AKBASIC_ERR_LIMIT);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The assertion that proves the range is actually ours rather than merely
|
* The assertion that proves the range is actually ours rather than merely
|
||||||
|
|||||||
333
tests/mockdevice.h
Normal file
333
tests/mockdevice.h
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
/**
|
||||||
|
* @file mockdevice.h
|
||||||
|
* @brief Recording graphics, audio and input backends for the device tests.
|
||||||
|
*
|
||||||
|
* The graphics and audio verbs produce no output a golden file can compare, so
|
||||||
|
* the assertions have to be about what reached the backend. These record every
|
||||||
|
* call as a formatted line, which a test compares against the sequence it
|
||||||
|
* expected -- what was drawn, in what order, in what color.
|
||||||
|
*
|
||||||
|
* Nothing here needs SDL, which is the point of the backend records in the first
|
||||||
|
* place: the whole of groups G, I and E is testable in the default build.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AKBASIC_TEST_MOCKDEVICE_H_
|
||||||
|
#define _AKBASIC_TEST_MOCKDEVICE_H_
|
||||||
|
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akbasic/audio.h>
|
||||||
|
#include <akbasic/error.h>
|
||||||
|
#include <akbasic/graphics.h>
|
||||||
|
#include <akbasic/input.h>
|
||||||
|
|
||||||
|
/** @brief Room for the call log. Longer than any test needs; overflow truncates loudly. */
|
||||||
|
#define MOCK_LOG_SIZE 8192
|
||||||
|
/** @brief How many keystrokes mock_input can be primed with. */
|
||||||
|
#define MOCK_MAX_KEYS 32
|
||||||
|
/** @brief How many regions mock_graphics will hand out handles for. */
|
||||||
|
#define MOCK_MAX_SHAPES 8
|
||||||
|
|
||||||
|
/** @brief State shared by all three mock backends, so one log records the lot. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
char log[MOCK_LOG_SIZE];
|
||||||
|
size_t used;
|
||||||
|
bool overflowed;
|
||||||
|
|
||||||
|
/* Graphics */
|
||||||
|
int shapes; /* handles handed out so far */
|
||||||
|
bool paint_exhausts; /* when true, paint reports a partial fill */
|
||||||
|
|
||||||
|
/* Audio */
|
||||||
|
bool voice_active[AKBASIC_AUDIO_VOICES];
|
||||||
|
|
||||||
|
/* Input */
|
||||||
|
int keys[MOCK_MAX_KEYS];
|
||||||
|
int keycount;
|
||||||
|
int keynext;
|
||||||
|
} akbasic_MockDevice;
|
||||||
|
|
||||||
|
static akbasic_MockDevice MOCK;
|
||||||
|
static akbasic_GraphicsBackend MOCK_GRAPHICS;
|
||||||
|
static akbasic_AudioBackend MOCK_AUDIO;
|
||||||
|
static akbasic_InputBackend MOCK_INPUT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Append one formatted call to the log.
|
||||||
|
*
|
||||||
|
* Deliberately not an akerr_ErrorContext * function. It is called from inside
|
||||||
|
* the vtable entry points, and a test whose log overflowed should say so through
|
||||||
|
* the assertion on the log contents rather than by raising an error that the
|
||||||
|
* verb under test would then report as a device failure.
|
||||||
|
*/
|
||||||
|
__attribute__((format(printf, 1, 2)))
|
||||||
|
static void mock_log(const char *format, ...)
|
||||||
|
{
|
||||||
|
va_list args;
|
||||||
|
int written = 0;
|
||||||
|
|
||||||
|
if ( MOCK.used >= sizeof(MOCK.log) - 1 ) {
|
||||||
|
MOCK.overflowed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
va_start(args, format);
|
||||||
|
written = vsnprintf(MOCK.log + MOCK.used, sizeof(MOCK.log) - MOCK.used, format, args);
|
||||||
|
va_end(args);
|
||||||
|
if ( written < 0 || (size_t)written >= sizeof(MOCK.log) - MOCK.used ) {
|
||||||
|
MOCK.overflowed = true;
|
||||||
|
MOCK.used = sizeof(MOCK.log) - 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MOCK.used += (size_t)written;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------------------- graphics -- */
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_point(akbasic_GraphicsBackend *self, double x, double y, akbasic_Color color)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
mock_log("point %.1f,%.1f #%02x%02x%02x\n", x, y, color.r, color.g, color.b);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_line(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
mock_log("line %.1f,%.1f-%.1f,%.1f #%02x%02x%02x\n", x1, y1, x2, y2, color.r, color.g, color.b);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
mock_log("rect %.1f,%.1f-%.1f,%.1f #%02x%02x%02x\n", x1, y1, x2, y2, color.r, color.g, color.b);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_filled_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
mock_log("fillrect %.1f,%.1f-%.1f,%.1f #%02x%02x%02x\n", x1, y1, x2, y2, color.r, color.g, color.b);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_paint(akbasic_GraphicsBackend *self, int x, int y, akbasic_Color color)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
mock_log("paint %d,%d #%02x%02x%02x\n", x, y, color.r, color.g, color.b);
|
||||||
|
/*
|
||||||
|
* Stands in for the akgl flood fill running out of its fixed span stack,
|
||||||
|
* which reports AKERR_OUTOFBOUNDS having filled part of the region. PAINT has
|
||||||
|
* to surface that rather than treat it as success.
|
||||||
|
*/
|
||||||
|
FAIL_ZERO_RETURN(errctx, (!MOCK.paint_exhausts), AKERR_OUTOFBOUNDS,
|
||||||
|
"mock span stack exhausted");
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_clear(akbasic_GraphicsBackend *self, akbasic_Color color)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
mock_log("clear #%02x%02x%02x\n", color.r, color.g, color.b);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_save_shape(akbasic_GraphicsBackend *self, int x1, int y1, int x2, int y2, int *handle)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (handle != NULL), AKERR_NULLPOINTER, "NULL handle in save_shape");
|
||||||
|
FAIL_ZERO_RETURN(errctx, (MOCK.shapes < MOCK_MAX_SHAPES), AKBASIC_ERR_DEVICE,
|
||||||
|
"mock shape pool exhausted");
|
||||||
|
*handle = MOCK.shapes;
|
||||||
|
MOCK.shapes += 1;
|
||||||
|
mock_log("save %d,%d-%d,%d -> %d\n", x1, y1, x2, y2, *handle);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_paste_shape(akbasic_GraphicsBackend *self, int handle, double x, double y)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (handle >= 0 && handle < MOCK.shapes), AKBASIC_ERR_BOUNDS,
|
||||||
|
"no such mock shape %d", handle);
|
||||||
|
mock_log("paste %d at %.1f,%.1f\n", handle, x, y);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_free_shapes(akbasic_GraphicsBackend *self)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
MOCK.shapes = 0;
|
||||||
|
mock_log("freeshapes\n");
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ audio -- */
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_tone(akbasic_AudioBackend *self, int voice, double hz, int ms)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
|
||||||
|
"mock voice %d out of range", voice);
|
||||||
|
MOCK.voice_active[voice] = true;
|
||||||
|
mock_log("tone v%d %.1fhz %dms\n", voice, hz, ms);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_audio_stop(akbasic_AudioBackend *self, int voice)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
|
||||||
|
"mock voice %d out of range", voice);
|
||||||
|
MOCK.voice_active[voice] = false;
|
||||||
|
mock_log("stop v%d\n", voice);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_waveform(akbasic_AudioBackend *self, int voice, akbasic_Waveform waveform)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
|
||||||
|
"mock voice %d out of range", voice);
|
||||||
|
mock_log("wave v%d %d\n", voice, (int)waveform);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_envelope(akbasic_AudioBackend *self, int voice, int attack, int decay, double sustain, int release)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
|
||||||
|
"mock voice %d out of range", voice);
|
||||||
|
mock_log("env v%d %d/%d/%.2f/%d\n", voice, attack, decay, sustain, release);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_volume(akbasic_AudioBackend *self, double level)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
mock_log("vol %.2f\n", level);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_voice_active(akbasic_AudioBackend *self, int voice, bool *active)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (active != NULL), AKERR_NULLPOINTER, "NULL active in voice_active");
|
||||||
|
FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
|
||||||
|
"mock voice %d out of range", voice);
|
||||||
|
*active = MOCK.voice_active[voice];
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ input -- */
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_poll_key(akbasic_InputBackend *self, int *keycode, bool *available)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (keycode != NULL && available != NULL), AKERR_NULLPOINTER,
|
||||||
|
"NULL argument in poll_key");
|
||||||
|
if ( MOCK.keynext >= MOCK.keycount ) {
|
||||||
|
/* An empty buffer is success, not an error -- see input.h. */
|
||||||
|
*available = false;
|
||||||
|
*keycode = 0;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
*keycode = MOCK.keys[MOCK.keynext];
|
||||||
|
*available = true;
|
||||||
|
MOCK.keynext += 1;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *mock_flush_keys(akbasic_InputBackend *self)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)self;
|
||||||
|
MOCK.keynext = MOCK.keycount;
|
||||||
|
mock_log("flushkeys\n");
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- fixture -- */
|
||||||
|
|
||||||
|
/** @brief Reset the recorder and populate all three vtables. */
|
||||||
|
__attribute__((unused))
|
||||||
|
static void mock_devices_init(void)
|
||||||
|
{
|
||||||
|
memset(&MOCK, 0, sizeof(MOCK));
|
||||||
|
|
||||||
|
MOCK_GRAPHICS.self = &MOCK;
|
||||||
|
MOCK_GRAPHICS.point = mock_point;
|
||||||
|
MOCK_GRAPHICS.line = mock_line;
|
||||||
|
MOCK_GRAPHICS.rect = mock_rect;
|
||||||
|
MOCK_GRAPHICS.filled_rect = mock_filled_rect;
|
||||||
|
MOCK_GRAPHICS.paint = mock_paint;
|
||||||
|
MOCK_GRAPHICS.clear = mock_clear;
|
||||||
|
MOCK_GRAPHICS.save_shape = mock_save_shape;
|
||||||
|
MOCK_GRAPHICS.paste_shape = mock_paste_shape;
|
||||||
|
MOCK_GRAPHICS.free_shapes = mock_free_shapes;
|
||||||
|
|
||||||
|
MOCK_AUDIO.self = &MOCK;
|
||||||
|
MOCK_AUDIO.tone = mock_tone;
|
||||||
|
MOCK_AUDIO.stop = mock_audio_stop;
|
||||||
|
MOCK_AUDIO.waveform = mock_waveform;
|
||||||
|
MOCK_AUDIO.envelope = mock_envelope;
|
||||||
|
MOCK_AUDIO.volume = mock_volume;
|
||||||
|
MOCK_AUDIO.voice_active = mock_voice_active;
|
||||||
|
|
||||||
|
MOCK_INPUT.self = &MOCK;
|
||||||
|
MOCK_INPUT.poll_key = mock_poll_key;
|
||||||
|
MOCK_INPUT.flush_keys = mock_flush_keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Prime the input backend with keystrokes GET will drain in order. */
|
||||||
|
__attribute__((unused))
|
||||||
|
static void mock_push_keys(const char *keys)
|
||||||
|
{
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
MOCK.keycount = 0;
|
||||||
|
MOCK.keynext = 0;
|
||||||
|
for ( i = 0; keys != NULL && keys[i] != '\0' && MOCK.keycount < MOCK_MAX_KEYS; i++ ) {
|
||||||
|
MOCK.keys[MOCK.keycount] = (int)(unsigned char)keys[i];
|
||||||
|
MOCK.keycount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Forget everything logged so far, keeping the primed state. */
|
||||||
|
__attribute__((unused))
|
||||||
|
static void mock_log_reset(void)
|
||||||
|
{
|
||||||
|
memset(MOCK.log, 0, sizeof(MOCK.log));
|
||||||
|
MOCK.used = 0;
|
||||||
|
MOCK.overflowed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // _AKBASIC_TEST_MOCKDEVICE_H_
|
||||||
Reference in New Issue
Block a user