diff --git a/Source/Emulator/voyager.c b/Source/Emulator/voyager.c index 1c2bda6..f8d8c01 100644 --- a/Source/Emulator/voyager.c +++ b/Source/Emulator/voyager.c @@ -66,6 +66,66 @@ static int takeHeadless(int *argc, char *argv[]) { static Texture2D screenTexture; 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) { // 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 @@ -108,6 +168,8 @@ static void presentFrame(void) { DrawTexturePro(screenTexture, from, to, (Vector2){ 0, 0 }, 0.0f, WHITE); } 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 @@ -117,30 +179,27 @@ static int voyagerKey(int mayWait) { if (!windowOpen) { return CONSOLE_GONE; } - if (mayWait) { - if (WindowShouldClose()) { - windowOpen = 0; - return CONSOLE_GONE; - } - // 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. - presentFrame(); + // Whatever is already waiting, however long ago it was typed. This is the answer to + // both questions, and asking it first is what makes a program that polls rarely see + // every key rather than one in six. + const int waiting = keyTake(); + if (waiting != CONSOLE_NOTHING_YET) { + return waiting; } - // Asked without waiting, this takes whatever the last frame's event poll left behind - // and returns at once. A program polling the status port sixty times between frames - // must not be charged a frame for each look. - int character = GetCharPressed(); - if (character > 0 && character < 128) { - return character; + if (!mayWait) { + // A poll is a poll. Presenting here would charge a frame for every glance, and a + // program that looks in a loop would run at the frame rate. + return CONSOLE_NOTHING_YET; } - switch (GetKeyPressed()) { - // The keys a character queue does not carry, because they are not characters. - case KEY_ENTER: case KEY_KP_ENTER: return '\n'; - case KEY_BACKSPACE: return 0x08; - case KEY_TAB: return '\t'; - case KEY_ESCAPE: return 0x1B; - default: return CONSOLE_NOTHING_YET; + if (WindowShouldClose()) { + windowOpen = 0; + return CONSOLE_GONE; } + // 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[]) {