Keep a key until it is asked for
Input worked at the shell and not in Snake, and that split is the whole diagnosis: the shell blocks on a read, Snake polls the READY bit. Only the polling path was broken. RAYLIB CLEARS ITS CHARACTER QUEUE ON EVERY POLL, and a poll happens inside EndDrawing, so a key survives exactly one frame unless something takes it in that frame. The window presents sixty times a second and Snake looks about ten, so five keys in six were thrown away by the next present before the game ever glanced at them. A blocking read presented and then looked immediately, which is why the shell never noticed. The window now keeps its own queue, drained from Raylib at every present and emptied only when the console actually takes a byte. That is what this machine already promises about its console, and Snake's own comment is the specification: "the console keeps the next key until it is asked for, so a key pressed while the snake was moving is still there next frame". The hook was not honouring it. Asking the queue first also makes the two questions the same question. A poll takes whatever is waiting and returns at once, and a blocking read takes whatever is waiting, then presents a frame and looks again - so neither path can see a key the other would have missed. The queue drops its oldest when it fills, so somebody leaning on the keyboard while a program ignores it cannot push out what they typed most recently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
556a14b288
commit
bdb2d0d8e6
+80
-21
@@ -66,6 +66,66 @@ static int takeHeadless(int *argc, char *argv[]) {
|
|||||||
static Texture2D screenTexture;
|
static Texture2D screenTexture;
|
||||||
static int windowOpen = 0;
|
static int windowOpen = 0;
|
||||||
|
|
||||||
|
// ---- Keys are kept until they are asked for ----
|
||||||
|
//
|
||||||
|
// RAYLIB CLEARS ITS CHARACTER QUEUE ON EVERY POLL, and a poll happens inside EndDrawing, so
|
||||||
|
// a key survives exactly one frame unless something takes it. That is fine for a game that
|
||||||
|
// reads input every frame and wrong for everything else: Snake looks about ten times a
|
||||||
|
// second, so five keys in six were being thrown away by the next present before it ever
|
||||||
|
// glanced at them. The shell worked the whole time, because a blocking read presents and
|
||||||
|
// then looks immediately.
|
||||||
|
//
|
||||||
|
// So the window keeps its own queue, drained from Raylib at every present and emptied only
|
||||||
|
// when the console actually takes a byte. That is what the machine already promises - Snake's
|
||||||
|
// own comment says "the console keeps the next key until it is asked for" - and it makes the
|
||||||
|
// console's timing nobody else's business.
|
||||||
|
#define KEY_QUEUE 64
|
||||||
|
static unsigned char keyQueue[KEY_QUEUE];
|
||||||
|
static int keyHead = 0;
|
||||||
|
static int keyTail = 0;
|
||||||
|
|
||||||
|
static void keyPush(unsigned char byte) {
|
||||||
|
const int next = (keyTail + 1) % KEY_QUEUE;
|
||||||
|
if (next == keyHead) {
|
||||||
|
// Full, so the oldest goes. Somebody leaning on the keyboard while a program ignores
|
||||||
|
// it should not be able to push out what they typed most recently.
|
||||||
|
keyHead = (keyHead + 1) % KEY_QUEUE;
|
||||||
|
}
|
||||||
|
keyQueue[keyTail] = byte;
|
||||||
|
keyTail = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int keyTake(void) {
|
||||||
|
if (keyHead == keyTail) {
|
||||||
|
return CONSOLE_NOTHING_YET;
|
||||||
|
}
|
||||||
|
const int byte = keyQueue[keyHead];
|
||||||
|
keyHead = (keyHead + 1) % KEY_QUEUE;
|
||||||
|
return byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything Raylib has, taken before it can throw any of it away.
|
||||||
|
static void drainKeyboard(void) {
|
||||||
|
int character;
|
||||||
|
while ((character = GetCharPressed()) > 0) {
|
||||||
|
if (character < 128) {
|
||||||
|
keyPush((unsigned char)character);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int key;
|
||||||
|
while ((key = GetKeyPressed()) > 0) {
|
||||||
|
// Only the keys a character queue does not carry, because they are not characters.
|
||||||
|
// Everything else has already arrived above, and taking it again would double it.
|
||||||
|
switch (key) {
|
||||||
|
case KEY_ENTER: case KEY_KP_ENTER: keyPush('\n'); break;
|
||||||
|
case KEY_BACKSPACE: keyPush(0x08); break;
|
||||||
|
case KEY_TAB: keyPush('\t'); break;
|
||||||
|
case KEY_ESCAPE: keyPush(0x1B); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void presentFrame(void) {
|
static void presentFrame(void) {
|
||||||
// The device turns video memory into pixels; this puts them on the glass. Everything
|
// The device turns video memory into pixels; this puts them on the glass. Everything
|
||||||
// that decides what the screen looks like is in the machine, where the suite can
|
// that decides what the screen looks like is in the machine, where the suite can
|
||||||
@@ -108,6 +168,8 @@ static void presentFrame(void) {
|
|||||||
DrawTexturePro(screenTexture, from, to, (Vector2){ 0, 0 }, 0.0f, WHITE);
|
DrawTexturePro(screenTexture, from, to, (Vector2){ 0, 0 }, 0.0f, WHITE);
|
||||||
}
|
}
|
||||||
EndDrawing();
|
EndDrawing();
|
||||||
|
// EndDrawing has just polled, which is the one moment Raylib's queues hold anything.
|
||||||
|
drainKeyboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
// What the console asks while it is waiting. Presenting from in here is what keeps the
|
// What the console asks while it is waiting. Presenting from in here is what keeps the
|
||||||
@@ -117,30 +179,27 @@ static int voyagerKey(int mayWait) {
|
|||||||
if (!windowOpen) {
|
if (!windowOpen) {
|
||||||
return CONSOLE_GONE;
|
return CONSOLE_GONE;
|
||||||
}
|
}
|
||||||
if (mayWait) {
|
// Whatever is already waiting, however long ago it was typed. This is the answer to
|
||||||
if (WindowShouldClose()) {
|
// both questions, and asking it first is what makes a program that polls rarely see
|
||||||
windowOpen = 0;
|
// every key rather than one in six.
|
||||||
return CONSOLE_GONE;
|
const int waiting = keyTake();
|
||||||
}
|
if (waiting != CONSOLE_NOTHING_YET) {
|
||||||
// Presenting is what keeps the window answering while the machine waits, and
|
return waiting;
|
||||||
// EndDrawing paces it, so waiting for a key costs a frame rather than a spin.
|
|
||||||
presentFrame();
|
|
||||||
}
|
}
|
||||||
// Asked without waiting, this takes whatever the last frame's event poll left behind
|
if (!mayWait) {
|
||||||
// and returns at once. A program polling the status port sixty times between frames
|
// A poll is a poll. Presenting here would charge a frame for every glance, and a
|
||||||
// must not be charged a frame for each look.
|
// program that looks in a loop would run at the frame rate.
|
||||||
int character = GetCharPressed();
|
return CONSOLE_NOTHING_YET;
|
||||||
if (character > 0 && character < 128) {
|
|
||||||
return character;
|
|
||||||
}
|
}
|
||||||
switch (GetKeyPressed()) {
|
if (WindowShouldClose()) {
|
||||||
// The keys a character queue does not carry, because they are not characters.
|
windowOpen = 0;
|
||||||
case KEY_ENTER: case KEY_KP_ENTER: return '\n';
|
return CONSOLE_GONE;
|
||||||
case KEY_BACKSPACE: return 0x08;
|
|
||||||
case KEY_TAB: return '\t';
|
|
||||||
case KEY_ESCAPE: return 0x1B;
|
|
||||||
default: return CONSOLE_NOTHING_YET;
|
|
||||||
}
|
}
|
||||||
|
// Presenting is what keeps the window answering while the machine waits, and EndDrawing
|
||||||
|
// paces it, so waiting for a key costs a frame rather than a spin. It drains the
|
||||||
|
// keyboard on the way out, so anything just typed is here now.
|
||||||
|
presentFrame();
|
||||||
|
return keyTake();
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user