// 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 is the largest screen the video device can produce, scaled up because a 320 by // 200 window is a postage stamp on a modern display. A smaller mode is drawn into the middle // of it rather than resizing the window out from under whoever is looking at it. #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(); // Not black. The border around a smaller mode should look like a machine with a screen // on, rather than like a window that failed to open. ClearBackground((Color){ 18, 22, 20, 255 }); if (width > 0 && height > 0) { // Centred, so changing mode moves the picture rather than the window. Rectangle from = { 0, 0, (float)width, (float)height }; Rectangle to = { (float)((VIDEO_MAX_WIDTH * SCREEN_SCALE - width * SCREEN_SCALE) / 2), (float)((VIDEO_MAX_HEIGHT * SCREEN_SCALE - height * SCREEN_SCALE) / 2), (float)(width * SCREEN_SCALE), (float)(height * SCREEN_SCALE) }; 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 { 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); }