Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
0.3.0 closed all ten API gaps this port had filed. The four workarounds go with them: the CMake block that declared libakgl's vendored dependencies by hand, the akgl/actor.h include in three files, and the six vtable pointers assigned by hand in two more, now akgl_render_bind2d(). Two gaps were capabilities rather than inconveniences, and both are now real: The line editor takes the composed UTF-8 text the ring carries in preference to the keycode, so shifted characters, keyboard layouts, compose keys and dead keys all work. A double quote can be typed, which means a BASIC string literal can be typed -- the sharp end of the old limitation. Letters are no longer folded to upper case. SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3 sweeps once rather than oscillating and TODO.md section 5 says so. A backend with no sweep still refuses the swept note and plays the held one. The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by temporarily demanding 0.4.0 and watching it fire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,6 @@
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/controller.h>
|
||||
#include <akgl/error.h>
|
||||
/*
|
||||
@@ -358,26 +357,14 @@ int main(void)
|
||||
&window, &renderer->sdl_renderer),
|
||||
AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
||||
/*
|
||||
* Populate the backend's vtable by hand, which is not where anybody would
|
||||
* expect to have to do it.
|
||||
*
|
||||
* akgl_text_rendertextat() reaches through renderer->draw_texture, and an
|
||||
* SDL_Renderer alone does not fill that in --
|
||||
* Bind the 2D methods. akgl_text_rendertextat() reaches through
|
||||
* renderer->draw_texture and an SDL_Renderer alone does not fill that in;
|
||||
* deps/libakgl/tests/draw.c gets away without it because the draw
|
||||
* primitives take the SDL_Renderer directly. The obvious call,
|
||||
* akgl_render_init2d(), does install these six pointers, but it also
|
||||
* creates its own window from the game properties and writes to the
|
||||
* `camera` global, so it is part of the akgl_game_init() path rather than
|
||||
* something a caller who already has a renderer can use. A host embedding
|
||||
* this interpreter is in exactly that position. Filed upstream: what is
|
||||
* wanted is the vtable half of init2d on its own.
|
||||
* primitives take the SDL_Renderer directly. These six pointers were
|
||||
* assigned by hand here until libakgl 0.3.0 split akgl_render_bind2d out
|
||||
* of akgl_render_init2d -- API-gap item 7, filed from this file.
|
||||
*/
|
||||
renderer->shutdown = &akgl_render_2d_shutdown;
|
||||
renderer->frame_start = &akgl_render_2d_frame_start;
|
||||
renderer->frame_end = &akgl_render_2d_frame_end;
|
||||
renderer->draw_texture = &akgl_render_2d_draw_texture;
|
||||
renderer->draw_mesh = &akgl_render_2d_draw_mesh;
|
||||
renderer->draw_world = &akgl_render_2d_draw_world;
|
||||
CATCH(errctx, akgl_render_bind2d(renderer));
|
||||
|
||||
/*
|
||||
* libakgl's own monospaced fixture, borrowed rather than copied. Being
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/controller.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
@@ -82,6 +81,9 @@ static bool anything_drawn(SDL_Surface *shot, int x0, int y0, int w, int h)
|
||||
* Straight into SDL's own queue rather than into libakgl's ring, so the
|
||||
* frontend's event pump is the thing under test: nothing reaches the
|
||||
* interpreter unless pump_events() drained this and handed it over.
|
||||
*
|
||||
* Key-down only, with no composed text -- which is what a cursor key, a function
|
||||
* key or a bare modifier produces.
|
||||
*/
|
||||
static void push_key(int keycode)
|
||||
{
|
||||
@@ -93,13 +95,62 @@ static void push_key(int keycode)
|
||||
SDL_PushEvent(&event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Queue a printable keystroke: the key-down *and* the text it composed to.
|
||||
*
|
||||
* Two events, because that is what a real keyboard produces and what libakgl
|
||||
* assembles a keystroke from -- SDL_EVENT_KEY_DOWN records the press and
|
||||
* SDL_EVENT_TEXT_INPUT delivers the character the layout, the shift state, a
|
||||
* compose key and a dead key between them worked out. A keycode alone cannot
|
||||
* express any of that, which is why the ring carries both.
|
||||
*
|
||||
* `text` is deliberately allowed to differ from `keycode`: that is how a shifted
|
||||
* character and a non-US layout are tested at all.
|
||||
*/
|
||||
static void push_text_key(int keycode, const char *text)
|
||||
{
|
||||
SDL_Event event;
|
||||
|
||||
push_key(keycode);
|
||||
memset(&event, 0, sizeof(event));
|
||||
event.type = SDL_EVENT_TEXT_INPUT;
|
||||
event.text.text = text;
|
||||
SDL_PushEvent(&event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A one-character string with static storage, for one printable byte.
|
||||
*
|
||||
* SDL_PushEvent copies the SDL_Event but *not* the string an
|
||||
* SDL_EVENT_TEXT_INPUT points at, so the text has to outlive the call that
|
||||
* queued it and must not be reused before the queue is drained. A local buffer
|
||||
* is neither: the first attempt at push_line() used one and every event in the
|
||||
* line ended up pointing at the same overwritten two bytes, which showed up as
|
||||
* a REPL that never received a line and a test that hung rather than failed.
|
||||
*/
|
||||
static const char *one_char(unsigned char c)
|
||||
{
|
||||
static char table[128][2];
|
||||
static bool built = false;
|
||||
int i = 0;
|
||||
|
||||
if ( !built ) {
|
||||
for ( i = 0; i < 128; i++ ) {
|
||||
table[i][0] = (char)i;
|
||||
table[i][1] = '\0';
|
||||
}
|
||||
built = true;
|
||||
}
|
||||
return table[c & 0x7f];
|
||||
}
|
||||
|
||||
/** @brief Queue a whole typed line, terminator included. */
|
||||
static void push_line(const char *text)
|
||||
{
|
||||
size_t i = 0;
|
||||
|
||||
for ( i = 0; text[i] != '\0'; i++ ) {
|
||||
push_key((int)(unsigned char)text[i]);
|
||||
push_text_key((int)(unsigned char)text[i], one_char((unsigned char)text[i]));
|
||||
}
|
||||
push_key('\r');
|
||||
}
|
||||
@@ -269,9 +320,9 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_window_close_stops(void)
|
||||
* @brief The line editor turns keystrokes into a line, echoed as it is typed.
|
||||
*
|
||||
* Read through the sink directly rather than through the REPL, because what is
|
||||
* being asserted here is the editing itself: the fold to upper case that the
|
||||
* ring's missing modifier state forces, the backspace, and the echo landing in
|
||||
* the grid where the cursor says it is.
|
||||
* being asserted here is the editing itself: that the *composed text* is what
|
||||
* gets typed rather than the keycode, the backspace, the escape, and the echo
|
||||
* landing in the grid where the cursor says it is.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_line_editor(void)
|
||||
{
|
||||
@@ -282,40 +333,65 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_line_editor(void)
|
||||
PASS(errctx, start_frontend(NULL));
|
||||
PASS(errctx, FRONTEND.input.flush_keys(&FRONTEND.input));
|
||||
|
||||
/* "prXnt" with the X backspaced away, typed in lower case. */
|
||||
push_key('p');
|
||||
push_key('r');
|
||||
push_key('x');
|
||||
push_key('\b');
|
||||
push_key('i');
|
||||
push_key('n');
|
||||
push_key('t');
|
||||
/* "prXnt" with the X backspaced away. Lower case survives, which it did not
|
||||
* before libakgl 0.3.0 -- the editor folded everything to upper case because
|
||||
* a keycode was all it had. */
|
||||
push_text_key('p', "p");
|
||||
push_text_key('r', "r");
|
||||
push_text_key('x', "x");
|
||||
push_key(SDLK_BACKSPACE);
|
||||
push_text_key('i', "i");
|
||||
push_text_key('n', "n");
|
||||
push_text_key('t', "t");
|
||||
push_key('\r');
|
||||
|
||||
PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
|
||||
TEST_REQUIRE(!eof, "a submitted line is not end of input");
|
||||
/*
|
||||
* Upper case because akgl_controller_poll_key() reports a keycode and no
|
||||
* modifier state, so Shift is invisible and lower case is unreachable. A
|
||||
* C128 is upper case too, which is why this is the right resolution rather
|
||||
* than merely the available one. Filed upstream.
|
||||
*/
|
||||
TEST_REQUIRE_STR(line, "PRINT");
|
||||
TEST_REQUIRE_STR(line, "print");
|
||||
/* The echo is in the grid, and the backspace really removed a character. */
|
||||
TEST_REQUIRE_STR(FRONTEND.akglstate.text[0], "PRINT");
|
||||
TEST_REQUIRE_STR(FRONTEND.akglstate.text[0], "print");
|
||||
TEST_REQUIRE_INT(FRONTEND.akglstate.cursorrow, 1);
|
||||
TEST_REQUIRE_INT(FRONTEND.akglstate.cursorcol, 0);
|
||||
TEST_REQUIRE(!FRONTEND.akglstate.editing, "the editor should be idle once the line is in");
|
||||
|
||||
/* Escape abandons a line rather than submitting it. */
|
||||
push_key('z');
|
||||
push_key(0x1b);
|
||||
push_key('o');
|
||||
push_key('k');
|
||||
/*
|
||||
* A shifted character, which is the whole point of the 0.3.0 keystroke API:
|
||||
* the key is still SDLK_2 and the character is '"'. Without the composed
|
||||
* text there is no way to tell those apart, and a BASIC string literal could
|
||||
* not be typed at the window at all.
|
||||
*/
|
||||
push_text_key(SDLK_2, "\"");
|
||||
push_text_key('h', "H");
|
||||
push_text_key(SDLK_2, "\"");
|
||||
push_key('\r');
|
||||
PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
|
||||
TEST_REQUIRE_STR(line, "OK");
|
||||
TEST_REQUIRE_STR(FRONTEND.akglstate.text[1], "OK");
|
||||
TEST_REQUIRE_STR(line, "\"H\"");
|
||||
|
||||
/*
|
||||
* And a key whose composed character has nothing to do with its keycode,
|
||||
* which is what a non-US layout looks like: AZERTY's SDLK_Q types "a".
|
||||
*/
|
||||
push_text_key(SDLK_Q, "a");
|
||||
push_key('\r');
|
||||
PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
|
||||
TEST_REQUIRE_STR(line, "a");
|
||||
|
||||
/* A key that composes to nothing is not typed: an arrow is not a character. */
|
||||
push_key(SDLK_UP);
|
||||
push_key(SDLK_LEFT);
|
||||
push_text_key('z', "z");
|
||||
push_key('\r');
|
||||
PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
|
||||
TEST_REQUIRE_STR(line, "z");
|
||||
|
||||
/* Escape abandons a line rather than submitting it. */
|
||||
push_text_key('z', "z");
|
||||
push_key(SDLK_ESCAPE);
|
||||
push_text_key('o', "o");
|
||||
push_text_key('k', "k");
|
||||
push_key('\r');
|
||||
PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
|
||||
TEST_REQUIRE_STR(line, "ok");
|
||||
|
||||
/* A closed window ends the wait, reported as end of input rather than raised. */
|
||||
push_quit();
|
||||
|
||||
@@ -131,20 +131,58 @@ static void test_sound(void)
|
||||
harness_stop();
|
||||
|
||||
/*
|
||||
* The frequency sweep is refused rather than faked. Faking it would mean
|
||||
* re-issuing tones from step(), which would tie audible pitch to how often
|
||||
* the host calls us -- a tune that changes key with the frame rate.
|
||||
* The frequency sweep reaches the device, which it could not until libakgl
|
||||
* 0.3.0 grew akgl_audio_sweep -- it was refused outright before, because
|
||||
* faking it would have meant re-issuing tones from step() and tying audible
|
||||
* pitch to how often the host calls us.
|
||||
*
|
||||
* Sweeping *down*: register 1000 is below register 16777, so the endpoints
|
||||
* decide the direction and both the SID and akgl_audio_sweep read them that
|
||||
* way.
|
||||
*/
|
||||
TEST_REQUIRE_OK(run_program("10 SOUND 1, 1000, 60, 1, 500, 10\n"));
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "frequency sweep") != NULL,
|
||||
"a sweep should be refused, got \"%s\"", HARNESS_OUTPUT);
|
||||
TEST_REQUIRE_OK(run_program("10 SOUND 1, 16777, 60, 2, 1000, 10\n"));
|
||||
TEST_REQUIRE(strstr(MOCK.log, "sweep v0 ") != NULL,
|
||||
"a sweep should reach the device, got \"%s\"", MOCK.log);
|
||||
TEST_REQUIRE(strstr(MOCK.log, "1000ms") != NULL,
|
||||
"60 jiffies should still be 1000 ms on a sweep, got \"%s\"", MOCK.log);
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "") != NULL && HARNESS_OUTPUT[0] == '\0',
|
||||
"a sweep should not be refused, got \"%s\"", HARNESS_OUTPUT);
|
||||
harness_stop();
|
||||
|
||||
/* A sweep direction of zero is "no sweep" and must still play. */
|
||||
/* A direction outside 0..3 is a typo worth catching. */
|
||||
TEST_REQUIRE_OK(run_program("10 SOUND 1, 1000, 60, 9, 500, 10\n"));
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "SOUND direction 9") != NULL,
|
||||
"direction 9 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
||||
harness_stop();
|
||||
|
||||
/* A sweep needs all three of its arguments. */
|
||||
TEST_REQUIRE_OK(run_program("10 SOUND 1, 1000, 60, 1\n"));
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "direction, a minimum and a step") != NULL,
|
||||
"a partial sweep should be refused, got \"%s\"", HARNESS_OUTPUT);
|
||||
harness_stop();
|
||||
|
||||
/* A sweep direction of zero is "no sweep" and must still play a held note. */
|
||||
TEST_REQUIRE_OK(run_program("10 SOUND 1, 1000, 60, 0\n"));
|
||||
TEST_REQUIRE(strstr(MOCK.log, "tone v0 ") != NULL,
|
||||
"direction 0 is not a sweep and should play, got \"%s\"", MOCK.log);
|
||||
harness_stop();
|
||||
|
||||
/*
|
||||
* A backend with no sweep is still a valid backend -- that is what a host
|
||||
* written against libakgl 0.2.0 looks like -- and SOUND refuses the swept
|
||||
* note rather than dereferencing a NULL entry point.
|
||||
*/
|
||||
TEST_REQUIRE_OK(harness_start(NULL));
|
||||
mock_devices_init();
|
||||
MOCK_AUDIO.sweep = NULL;
|
||||
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
|
||||
&MOCK_AUDIO, &MOCK_INPUT));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SOUND 1, 1000, 60, 1, 500, 10\n"));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "audio device that can sweep") != NULL,
|
||||
"a backend without sweep should refuse, got \"%s\"", HARNESS_OUTPUT);
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/** @brief ENVELOPE maps the SID rate numbers, and sustain is a level not a time. */
|
||||
|
||||
@@ -191,6 +191,19 @@ static akerr_ErrorContext *mock_tone(akbasic_AudioBackend *self, int voice, doub
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *mock_sweep(akbasic_AudioBackend *self, int voice, double from_hz, double to_hz, double step_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("sweep v%d %.1fhz->%.1fhz step %.1fhz %dms\n",
|
||||
voice, from_hz, to_hz, step_hz, ms);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *mock_audio_stop(akbasic_AudioBackend *self, int voice)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -296,6 +309,13 @@ static void mock_devices_init(void)
|
||||
|
||||
MOCK_AUDIO.self = &MOCK;
|
||||
MOCK_AUDIO.tone = mock_tone;
|
||||
/*
|
||||
* Set here, and deliberately cleared again by the one test that needs a
|
||||
* backend without it: `sweep` may be NULL, which is what a host written
|
||||
* against libakgl 0.2.0 looks like, and SOUND has to refuse rather than
|
||||
* crash on one.
|
||||
*/
|
||||
MOCK_AUDIO.sweep = mock_sweep;
|
||||
MOCK_AUDIO.stop = mock_audio_stop;
|
||||
MOCK_AUDIO.waveform = mock_waveform;
|
||||
MOCK_AUDIO.envelope = mock_envelope;
|
||||
|
||||
Reference in New Issue
Block a user