// 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; 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(); } // 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(void) { if (!windowOpen || WindowShouldClose()) { windowOpen = 0; return CONSOLE_GONE; } presentFrame(); int character = GetCharPressed(); if (character > 0 && character < 128) { return character; } 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; } } 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. SetConfigFlags(FLAG_WINDOW_RESIZABLE); 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); }