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:
13
TODO.md
13
TODO.md
@@ -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
|
||||
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
|
||||
|
||||
1. **Make character-to-sprite state bindings release their references symmetrically.**
|
||||
|
||||
@@ -13,6 +13,16 @@
|
||||
#define AKGL_MAX_CONTROL_MAPS 8
|
||||
#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. */
|
||||
typedef struct {
|
||||
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.
|
||||
*/
|
||||
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_
|
||||
|
||||
@@ -12,6 +12,39 @@
|
||||
|
||||
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)
|
||||
{
|
||||
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, 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 {
|
||||
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
|
||||
curmap = &GAME_ControlMaps[i];
|
||||
@@ -386,3 +426,32 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
|
||||
} PROCESS(errctx) {
|
||||
} 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);
|
||||
}
|
||||
|
||||
@@ -524,6 +524,164 @@ akerr_ErrorContext *test_controller_device_enumeration(void)
|
||||
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)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -551,6 +709,8 @@ int main(void)
|
||||
CATCH(errctx, test_controller_gamepad_button_handlers());
|
||||
CATCH(errctx, test_controller_device_events());
|
||||
CATCH(errctx, test_controller_device_enumeration());
|
||||
CATCH(errctx, test_controller_poll_key());
|
||||
CATCH(errctx, test_controller_poll_key_overflow());
|
||||
} CLEANUP {
|
||||
SDL_Quit();
|
||||
} PROCESS(errctx) {
|
||||
|
||||
Reference in New Issue
Block a user