Let a caller take keystrokes without owning the event loop

akbasic's GET and GETKEY ask "is there a keystroke waiting, yes or no" and must
not require the interpreter to pump SDL events itself. akgl_controller_poll_key
drains one key per call from a fixed 32-entry ring that
akgl_controller_handle_event fills, so the host keeps pumping and the embedded
interpreter reads at its own pace. akgl_controller_flush_keys discards a
backlog.

The recording happens before the control-map scan, not after: that scan returns
as soon as a binding claims the event, so recording afterwards would have
dropped exactly the keys a game also acts on. A full buffer refuses the newest
keystroke rather than overwriting the oldest, so a caller reading a line gets
what was typed first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 06:57:50 -04:00
parent 17e6e04c79
commit 1ddc64010a
4 changed files with 287 additions and 0 deletions

13
TODO.md
View File

@@ -708,6 +708,19 @@ a consumer is the wanted outcome. Each entry says what the BASIC verb needs, wha
synthetic `SDL_EVENT_KEY_DOWN` events through the existing handler and drain them, plus the synthetic `SDL_EVENT_KEY_DOWN` events through the existing handler and drain them, plus the
empty-buffer and overflow cases. empty-buffer and overflow cases.
**Resolved.** `akgl_controller_poll_key(int *keycode, bool *available)` and
`akgl_controller_flush_keys(void)` are in `include/akgl/controller.h`, over a
fixed `AKGL_CONTROLLER_KEY_BUFFER` (32) ring in `src/controller.c`.
`akgl_controller_handle_event` records every `SDL_EVENT_KEY_DOWN` *before* it
scans the control maps, so a key bound to an actor still reaches a polling
caller — the scan returns as soon as a binding claims the event, and doing it
afterwards would have lost exactly the keys a game also acts on. An empty
buffer is success with `available` false, not an error. A full buffer drops
the newest key rather than overwriting the oldest, matching the Commodore
keyboard buffer and keeping what was typed first. `tests/controller.c` covers
drain order, the release-is-not-a-keystroke case, a key shared with a control
map, flush, both NULL arguments, and overflow plus reuse afterwards.
## Carried over ## Carried over
1. **Make character-to-sprite state bindings release their references symmetrically.** 1. **Make character-to-sprite state bindings release their references symmetrically.**

View File

@@ -13,6 +13,16 @@
#define AKGL_MAX_CONTROL_MAPS 8 #define AKGL_MAX_CONTROL_MAPS 8
#define AKGL_MAX_CONTROLS 32 #define AKGL_MAX_CONTROLS 32
/**
* @brief Keystrokes akgl_controller_handle_event() will hold for a poller.
*
* The Commodore keyboard buffer this serves held ten. Thirty-two is enough that
* a program which polls once per frame never loses a key to a fast typist, and
* small enough that the buffer stays a fixed-size object in the library's data
* segment.
*/
#define AKGL_CONTROLLER_KEY_BUFFER 32
/** @brief Maps one SDL input to pressed and released callbacks. */ /** @brief Maps one SDL input to pressed and released callbacks. */
typedef struct { typedef struct {
uint32_t event_on; uint32_t event_on;
@@ -117,4 +127,39 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void); akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void);
/**
* @brief Take the oldest waiting keystroke, if there is one.
*
* The rest of this header is built around the host pumping SDL events into
* akgl_controller_handle_event(), which suits a game loop and does not suit an
* embedded interpreter asking "is there a key waiting, yes or no" without
* owning the event loop itself. Every key press that reaches
* akgl_controller_handle_event() is recorded in a fixed ring buffer first,
* whether or not a control map claims it, and this drains that buffer one
* keystroke per call.
*
* When no key is waiting the call still succeeds: @p available is set to
* `false` and @p keycode to 0. The caller polls, it does not block.
*
* A full buffer drops the *newest* keystroke rather than the oldest, so what
* was typed first is what is read first. This runs on whichever thread pumps
* events; it is not synchronized.
*
* @param keycode Output destination populated with the SDL keycode, or 0.
* @param available Output destination set to `true` when a keystroke was taken.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available);
/**
* @brief Discard every keystroke waiting in the buffer.
*
* For a caller that has been ignoring input and does not want a backlog acted
* on the moment it starts polling again.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void);
#endif // _CONTROLLER_H_ #endif // _CONTROLLER_H_

View File

@@ -12,6 +12,39 @@
akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS]; akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
/*
* Keystrokes waiting for akgl_controller_poll_key(). Filled by
* akgl_controller_handle_event() before it consults the control maps, so a key
* that also drives an actor is still delivered to a polling caller.
*
* head is the next slot to read, count is how many are waiting. Both index a
* fixed array rather than a queue object, which is the whole point: a host that
* never polls cannot make this grow.
*/
static SDL_Keycode keybuffer[AKGL_CONTROLLER_KEY_BUFFER];
static int keybuffer_head = 0;
static int keybuffer_count = 0;
/**
* @brief Record one keystroke, discarding it if the buffer is already full.
*
* Dropping the newest rather than overwriting the oldest is deliberate. A
* caller reading a line of input wants the characters that were typed first;
* overwriting would hand it the tail of what the user typed and silently lose
* the head.
*/
static void keybuffer_push(SDL_Keycode key)
{
int tail = 0;
if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) {
return;
}
tail = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER;
keybuffer[tail] = key;
keybuffer_count += 1;
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void) akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void)
{ {
int count; int count;
@@ -65,6 +98,13 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate"); FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
// Before the control maps, not after: a key bound to an actor is still a
// key an interpreter polling with akgl_controller_poll_key() wants to see,
// and the scan below returns as soon as a binding claims the event.
if ( event->type == SDL_EVENT_KEY_DOWN ) {
keybuffer_push(event->key.key);
}
ATTEMPT { ATTEMPT {
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) { for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
curmap = &GAME_ControlMaps[i]; curmap = &GAME_ControlMaps[i];
@@ -386,3 +426,32 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, keycode, AKERR_NULLPOINTER, "NULL keycode destination");
FAIL_ZERO_RETURN(errctx, available, AKERR_NULLPOINTER, "NULL availability destination");
if ( keybuffer_count == 0 ) {
// An empty buffer is the ordinary case, not a failure: the caller is
// asking whether a key is waiting, and the answer is no.
*keycode = 0;
*available = false;
SUCCEED_RETURN(errctx);
}
*keycode = (int)keybuffer[keybuffer_head];
*available = true;
keybuffer_head = (keybuffer_head + 1) % AKGL_CONTROLLER_KEY_BUFFER;
keybuffer_count -= 1;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void)
{
PREPARE_ERROR(errctx);
keybuffer_head = 0;
keybuffer_count = 0;
SUCCEED_RETURN(errctx);
}

View File

@@ -524,6 +524,164 @@ akerr_ErrorContext *test_controller_device_enumeration(void)
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext *test_controller_poll_key(void)
{
PREPARE_ERROR(e);
SDL_Event event;
int keycode = -1;
bool available = true;
ATTEMPT {
reset_control_maps();
CATCH(e, make_player());
CATCH(e, akgl_controller_flush_keys());
// An empty buffer answers "no key waiting" and succeeds.
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
"polling an empty key buffer");
TEST_ASSERT(e, available == false,
"polling an empty buffer reported a key was available");
TEST_ASSERT(e, keycode == 0,
"polling an empty buffer left keycode at %d, expected 0", keycode);
// A key press pumped through the handler is drained by the poller,
// exactly once.
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_A);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching a key press with no control maps installed");
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling for that key");
TEST_ASSERT(e, available == true, "the pressed key was not available to the poller");
TEST_ASSERT(e, keycode == SDLK_A,
"the poller returned keycode %d, expected %d", keycode, (int)SDLK_A);
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling again");
TEST_ASSERT(e, available == false, "the same keystroke was delivered twice");
// Key releases are not keystrokes.
make_key_event(&event, SDL_EVENT_KEY_UP, TEST_KBID, SDLK_A);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching a key release");
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling after a release");
TEST_ASSERT(e, available == false, "a key release was buffered as a keystroke");
// Keys come back in the order they were pressed.
CATCH(e, akgl_controller_flush_keys());
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_1);
CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_2);
CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_3);
CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the first key");
TEST_ASSERT(e, keycode == SDLK_1, "the first key out was %d, expected %d",
keycode, (int)SDLK_1);
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the second key");
TEST_ASSERT(e, keycode == SDLK_2, "the second key out was %d, expected %d",
keycode, (int)SDLK_2);
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the third key");
TEST_ASSERT(e, keycode == SDLK_3, "the third key out was %d, expected %d",
keycode, (int)SDLK_3);
// A key that a control map also acts on still reaches the poller, so a
// game and an embedded interpreter can share one keyboard.
CATCH(e, akgl_controller_flush_keys());
CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));
player->state = 0;
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching a bound key press");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
"the bound key stopped driving the actor (state %d)", player->state);
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
"polling for a key a control map claimed");
TEST_ASSERT(e, available == true, "a bound key never reached the key buffer");
TEST_ASSERT(e, keycode == SDLK_LEFT, "the poller returned keycode %d, expected %d",
keycode, (int)SDLK_LEFT);
// Flushing discards the backlog.
CATCH(e, akgl_controller_flush_keys());
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_B);
CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
TEST_EXPECT_OK(e, akgl_controller_flush_keys(), "flushing a buffer with a key in it");
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling after a flush");
TEST_ASSERT(e, available == false, "a flush left a keystroke behind");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_poll_key(NULL, &available),
"polling into a NULL keycode");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_poll_key(&keycode, NULL),
"polling into a NULL availability flag");
} CLEANUP {
reset_control_maps();
IGNORE(akgl_controller_flush_keys());
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_controller_poll_key_overflow(void)
{
PREPARE_ERROR(e);
SDL_Event event;
int keycode = -1;
bool available = true;
bool ordered = true;
bool pumped = true;
int i = 0;
ATTEMPT {
reset_control_maps();
CATCH(e, akgl_controller_flush_keys());
// Fill the buffer exactly, then press one more. The overflowing key is
// the one that is dropped -- what was typed first survives.
for ( i = 0; i < AKGL_CONTROLLER_KEY_BUFFER + 1; i++ ) {
akerr_ErrorContext *pumpresult = NULL;
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_A + i);
pumpresult = akgl_controller_handle_event(&appstate_placeholder, &event);
if ( pumpresult != NULL ) {
pumpresult->handled = true;
pumpresult = akerr_release_error(pumpresult);
pumped = false;
}
}
TEST_ASSERT(e, pumped == true, "dispatching the overflow key presses failed");
for ( i = 0; i < AKGL_CONTROLLER_KEY_BUFFER; i++ ) {
akerr_ErrorContext *pollresult = akgl_controller_poll_key(&keycode, &available);
if ( pollresult != NULL ) {
pollresult->handled = true;
pollresult = akerr_release_error(pollresult);
ordered = false;
}
TEST_ASSERT_FLAG(ordered, available == true);
TEST_ASSERT_FLAG(ordered, keycode == (int)(SDLK_A + i));
}
TEST_ASSERT(e, ordered == true,
"a full buffer did not return the first %d keys in order",
AKGL_CONTROLLER_KEY_BUFFER);
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
"polling after draining a full buffer");
TEST_ASSERT(e, available == false,
"the key pressed past capacity was buffered anyway (keycode %d)", keycode);
// The buffer is reusable after an overflow rather than wedged.
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_Z);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching a key after an overflow");
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
"polling after an overflow");
TEST_ASSERT(e, available == true, "the buffer stayed full after being drained");
TEST_ASSERT(e, keycode == SDLK_Z, "the poller returned keycode %d, expected %d",
keycode, (int)SDLK_Z);
} CLEANUP {
reset_control_maps();
IGNORE(akgl_controller_flush_keys());
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
int main(void) int main(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
@@ -551,6 +709,8 @@ int main(void)
CATCH(errctx, test_controller_gamepad_button_handlers()); CATCH(errctx, test_controller_gamepad_button_handlers());
CATCH(errctx, test_controller_device_events()); CATCH(errctx, test_controller_device_events());
CATCH(errctx, test_controller_device_enumeration()); CATCH(errctx, test_controller_device_enumeration());
CATCH(errctx, test_controller_poll_key());
CATCH(errctx, test_controller_poll_key_overflow());
} CLEANUP { } CLEANUP {
SDL_Quit(); SDL_Quit();
} PROCESS(errctx) { } PROCESS(errctx) {