// voyager.c // The Segan Voyager // A SplitBit with a screen and a speaker attached // Written by Anachronaut // // ---- What this is ---- // // The same machine SplitBit runs, presented through a window instead of a terminal. Every // instruction, every device and every cycle is in machine.c and shared; this file opens a // window, gives the machine a slice of time per frame, and shows what came out. // // THAT ORDER MATTERS AND IS THE WHOLE DESIGN. The devices belong to the machine and advance // on emulated cycles, so the same program produces the same frames and the same samples // whether or not anybody is looking. Raylib presents; it does not decide. Which is what // lets a test suite with no display hold this binary to the same behaviour as the other // one. // // The window shows what the video device produced and decides nothing about it. Render is a // pure function of video memory, so the same program draws the same picture whether or not // anybody is watching - which is what lets a suite with no display check a screen. #include "machine.h" #include "video.h" #include "io.h" #include "utility.h" #include "raylib.h" #include #include #include // The window opens at the largest screen the device can produce, doubled, because a 640 by // 400 window is small on a modern display and a 320 by 200 one is a postage stamp. #define SCREEN_SCALE 2 // ---- Running without a window ---- // // Taken out of the arguments here rather than in the shared parser, because it is a fact // about this front end and the shared parser should not learn about a window that only one // binary has. Everything else on the command line means exactly what it means to SplitBit. // // It exists so the suite can run this binary at all: a test machine has no display, and a // front end that could only be exercised by a person looking at it would be a front end // nothing checks. Headless, Voyager must print byte for byte what SplitBit prints, and // Tests/voyager.sh holds it to that. static int takeHeadless(int *argc, char *argv[]) { int headless = 0; int out = 0; for (int i = 0; i < *argc; i++) { if (strcmp(argv[i], "--headless") == 0) { headless = 1; continue; } argv[out++] = argv[i]; } argv[out] = NULL; *argc = out; return headless; } // ---- The window, kept in one place ---- // // Both the frame loop and the input hook have to be able to present, because a machine // waiting for a key is still a machine somebody is looking at. A window that froze while a // program asked a question would look broken every time it asked one. 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 // reach it. videoRender(); int width, height; const uint8_t *frame = videoPixels(&width, &height); if (width > 0 && height > 0) { UpdateTextureRec(screenTexture, (Rectangle){ 0, 0, (float)width, (float)height }, frame); } BeginDrawing(); // Clearly not the screen. What is left over when the window's shape does not match the // picture's is a bezel, and it should look like one rather than like more screen. ClearBackground((Color){ 40, 40, 40, 255 }); if (width > 0 && height > 0) { // ---- Filling the window, in whole pixels ---- // // The largest whole-number scale that still fits. Whole numbers because a 320 by 200 // picture stretched by 2.7 is a picture with some rows twice as tall as their // neighbours, which on eight pixel glyphs is the difference between text and mush. // // The two modes are exactly a factor of two apart and the window opens at twice the // larger, so both fill it exactly: 320 by 200 at four, and 640 by 400 at two. // Changing mode therefore changes how sharp the screen is and not how big it is. const int windowWidth = GetScreenWidth(); const int windowHeight = GetScreenHeight(); int scale = windowWidth / width; const int fits = windowHeight / height; if (fits < scale) scale = fits; if (scale < 1) scale = 1; const int drawnWidth = width * scale; const int drawnHeight = height * scale; Rectangle from = { 0, 0, (float)width, (float)height }; Rectangle to = { (float)((windowWidth - drawnWidth) / 2), (float)((windowHeight - drawnHeight) / 2), (float)drawnWidth, (float)drawnHeight }; 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 // window answering, and EndDrawing paces it, so waiting for a key costs a frame rather // than a spin. static int voyagerKey(int mayWait) { if (!windowOpen) { return CONSOLE_GONE; } // 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; } 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; } 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[]) { int headless = takeHeadless(&argc, argv); EmulatorOptions options; uint8_t result = parseOptions(argc, argv, &options); if (result == OPTIONS_HELP) { printf(" --headless Run with no window, which is how the tests run it.\n"); return 0; } else if (result == OPTIONS_ERROR) { return 1; } char *programFile = NULL; if (optind < argc) { programFile = argv[optind]; optind++; } if (optind < argc) { fprintf(stderr, "Error: Unexpected argument: %s\n", argv[optind]); return 1; } Machine machine; uint8_t started = machineStart(&machine, &options, programFile); if (started == MACHINE_NOTHING_TO_RUN) { fprintf(stderr, "Error: No boot image and no disk, so there is nothing to run.\n"); printHelp(argv[0]); return 1; } else if (started != MACHINE_OK) { return 1; } if (headless) { // The same three lines SplitBit runs, and deliberately so: a headless Voyager is // not a reduced machine, it is the machine with nobody watching. while (machineRunning(&machine)) { machineRunSlice(&machine); } } else { // Resizable, because how big somebody wants a screen is not the machine's business. // The picture is rescaled to whatever the window becomes, in whole pixels. // // And presented in step with the display. Without the hint the frame limiter sleeps // towards sixty a second on its own clock, which beats against a screen refreshing on // its own - some frames shown twice, some skipped, and the machine handed an uneven // number of cycles each time because it takes them from the wall clock. The target // stays as well, for a driver that ignores the hint. SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_VSYNC_HINT); InitWindow(VIDEO_MAX_WIDTH * SCREEN_SCALE, VIDEO_MAX_HEIGHT * SCREEN_SCALE, "Segan Voyager"); SetTargetFPS(60); // One texture, updated in place. Making a new one every frame would be a new // allocation sixty times a second for a picture that is the same size every time. Image blank = GenImageColor(VIDEO_MAX_WIDTH, VIDEO_MAX_HEIGHT, BLACK); ImageFormat(&blank, PIXELFORMAT_UNCOMPRESSED_R8G8B8); screenTexture = LoadTextureFromImage(blank); UnloadImage(blank); windowOpen = 1; // The keyboard becomes the console's input, in place of a standard input the window // does not have. consoleSetInputHook(voyagerKey); // ---- A slice a frame ---- // // The machine gets its turn, then the window gets its turn. Closing the window // stops the machine, and the machine halting leaves the window up so that whatever // it drew is still there to look at - a program that ends should not take its // output off the screen with it. // A slice, then a frame. The machine halting leaves the window up so that whatever // it drew is still there to look at - a program that ends should not take its output // off the screen with it. while (windowOpen && !WindowShouldClose()) { if (machineRunning(&machine)) { machineRunSlice(&machine); } presentFrame(); } windowOpen = 0; // Taken back before the machine stops, so nothing can ask a window that has gone. consoleSetInputHook(NULL); UnloadTexture(screenTexture); CloseWindow(); } machineStop(&machine); return machineReport(&machine); }