Voyager installed its own input hook after machineStart, which had already installed the keyboard file's - so naming a keyboard file and running the window silently got the window, and the flag said nothing about being ignored. Both together is the combination a demo wants. Recording a flight needs the typing that STARTS it to be the same every time, because a human reaching the shell a moment later shifts every frame of the recording after it - while the flying itself has to come from whatever is actually in somebody's hands. So the window only takes the keyboard when no file was named. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
538 lines
26 KiB
C
538 lines
26 KiB
C
// 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 "pad.h"
|
|
#include "sound.h"
|
|
#include "io.h"
|
|
#include "utility.h"
|
|
#include "raylib.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <getopt.h>
|
|
|
|
// 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.
|
|
// ---- The pads, read as levels ----
|
|
//
|
|
// Once a frame, from whatever is actually there. THIS IS THE ONE THING THE CONSOLE CANNOT DO:
|
|
// a window knows which keys are down, a terminal only ever learns which one was pressed, and
|
|
// asking the console to report a key coming up would have been a promise it could keep behind
|
|
// a window and nowhere else.
|
|
//
|
|
// The keyboard drives pad nought as well as any real controller, so a game written for a pad
|
|
// is playable on a machine with none - and so is a game written for four, badly. What a game
|
|
// reads is the pad; it never learns which one of them somebody used.
|
|
static void readPads(void) {
|
|
for (int n = 0; n < PAD_COUNT; n++) {
|
|
uint8_t held = 0;
|
|
// ---- Pad nought is always there, because the keyboard is behind it ----
|
|
//
|
|
// Which is the useful answer rather than the literal one: a game that asks for a
|
|
// controller and finds none falls back to whatever the console can tell it, and
|
|
// behind a window the console is the worse of the two ways to read the same keys.
|
|
int there = (n == 0);
|
|
if (IsGamepadAvailable(n)) {
|
|
there = 1;
|
|
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_LEFT_FACE_RIGHT)) { held |= PAD_RIGHT; }
|
|
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_LEFT_FACE_LEFT)) { held |= PAD_LEFT; }
|
|
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_LEFT_FACE_DOWN)) { held |= PAD_DOWN; }
|
|
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_LEFT_FACE_UP)) { held |= PAD_UP; }
|
|
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_RIGHT_FACE_DOWN)) { held |= PAD_A; }
|
|
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT)){ held |= PAD_B; }
|
|
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_MIDDLE_RIGHT)) { held |= PAD_START; }
|
|
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_MIDDLE_LEFT)) { held |= PAD_SELECT; }
|
|
|
|
// ---- And the stick, which is what most people actually push ----
|
|
//
|
|
// The four buttons above are the D-PAD, and a lot of controllers made this
|
|
// century have one that nobody uses: the thumb goes on the stick, which reports
|
|
// as an axis and not as a button, so a pad that was plugged in and working did
|
|
// nothing at all.
|
|
//
|
|
// Past halfway counts as held. That is a blunt line and the right kind of blunt
|
|
// for a device that reports what is DOWN - a machine with eight bits a pad has
|
|
// nothing to say about three-fifths of a push, and picking the threshold here
|
|
// rather than in every program is the point of the pad being a device.
|
|
const float across = GetGamepadAxisMovement(n, GAMEPAD_AXIS_LEFT_X);
|
|
const float down = GetGamepadAxisMovement(n, GAMEPAD_AXIS_LEFT_Y);
|
|
if (across > 0.5f) { held |= PAD_RIGHT; }
|
|
if (across < -0.5f) { held |= PAD_LEFT; }
|
|
if (down > 0.5f) { held |= PAD_DOWN; }
|
|
if (down < -0.5f) { held |= PAD_UP; }
|
|
}
|
|
if (n == 0) {
|
|
// ---- And the keyboard, on top of it ----
|
|
//
|
|
// Arrows or WASD for the direction, Z and X for the buttons. OR-ed with whatever
|
|
// a real pad is doing rather than chosen between, so unplugging one mid-game does
|
|
// not leave somebody holding nothing.
|
|
if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) { held |= PAD_RIGHT; }
|
|
if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) { held |= PAD_LEFT; }
|
|
if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) { held |= PAD_DOWN; }
|
|
if (IsKeyDown(KEY_UP) || IsKeyDown(KEY_W)) { held |= PAD_UP; }
|
|
if (IsKeyDown(KEY_Z)) { held |= PAD_A; }
|
|
if (IsKeyDown(KEY_X)) { held |= PAD_B; }
|
|
if (IsKeyDown(KEY_ENTER)) { held |= PAD_START; }
|
|
if (IsKeyDown(KEY_TAB)) { held |= PAD_SELECT; }
|
|
}
|
|
padSet(n, there, held);
|
|
}
|
|
}
|
|
|
|
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;
|
|
// ---- And the keys that are not characters at all ----
|
|
//
|
|
// These used to fall through the default below and vanish, because there was no
|
|
// byte to turn them into. There is now, and it is the console's rather than
|
|
// this window's - a terminal reaches the same values by a different road, and a
|
|
// program is entitled not to know which of the two it is talking to.
|
|
case KEY_UP: keyPush(CONSOLE_KEY_UP); break;
|
|
case KEY_DOWN: keyPush(CONSOLE_KEY_DOWN); break;
|
|
case KEY_LEFT: keyPush(CONSOLE_KEY_LEFT); break;
|
|
case KEY_RIGHT: keyPush(CONSOLE_KEY_RIGHT); break;
|
|
case KEY_HOME: keyPush(CONSOLE_KEY_HOME); break;
|
|
case KEY_END: keyPush(CONSOLE_KEY_END); break;
|
|
case KEY_DELETE: keyPush(CONSOLE_KEY_DELETE); break;
|
|
default: break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- The reset button ----
|
|
//
|
|
// EMULATOR MAGIC, AND KNOWN TO BE. There is no reset line on this machine yet and no keyboard
|
|
// controller to assert one: the window reaches in and pokes the same flag a program pokes
|
|
// through the machine port. When those are designed, a keyboard controller will have to see
|
|
// this gesture and pull reset REGARDLESS OF WHAT THE CPU IS DOING - which is the property
|
|
// that matters and the one a port write cannot have, since a port write needs a program
|
|
// willing and able to make it.
|
|
//
|
|
// The shape of that is already visible here. A reset is normally noticed between
|
|
// instructions, and a halted machine runs none - so the window asks every frame rather than
|
|
// leaving it to the machine to notice, which is what real hardware would do with a line.
|
|
//
|
|
// ON REAL HARDWARE THIS IS NOT A KEY AT ALL. A Voyager has a button on the case, and what a
|
|
// window has instead of a case is a gesture. So the gesture wants two properties a single
|
|
// key does not have.
|
|
//
|
|
// It must not be a key SOFTWARE MIGHT WANT. A machine with a keyboard has function keys on
|
|
// it, and something will eventually have a use for F12 - which is where this was, and which
|
|
// would have meant taking it away again later.
|
|
//
|
|
// And it must not be reachable BY ACCIDENT. Restarting the machine throws away everything in
|
|
// memory, and a single key that does that sits one mistake away from losing work. Three keys
|
|
// together are not pressed by mistake.
|
|
//
|
|
// Control, Shift and R. It was Control, Alt and Delete, which has meant this since 1981 and
|
|
// is the one gesture nobody has to be told the meaning of - AND WHICH CANNOT BE USED.
|
|
//
|
|
// It is a secure attention key. Every serious operating system reserves it so that it always
|
|
// reaches the system and never an application, precisely so that a program cannot imitate a
|
|
// login screen; on Windows an application cannot see it at all without a kernel driver, and
|
|
// on Linux the desktop takes it. That is not an oversight to work around - it is the same
|
|
// guarantee a reset button wants, being enforced one layer further down, and there is no
|
|
// call this program can make that would win the argument.
|
|
//
|
|
// So the gesture has to be one the host has no opinion about. Control and Shift with a
|
|
// letter is about as free as a combination gets: it is not window management, not a virtual
|
|
// terminal switch, and not a shortcut any desktop claims by default.
|
|
//
|
|
// If a platform does send a character for it, nothing comes of that either - whatever
|
|
// arrives is in memory that is about to be thrown away.
|
|
//
|
|
// What it does is what writing MACHINE_RESET does: the machine starts the way it started, so
|
|
// the boot chain runs again and finds whatever the disk now says to run. Which is what makes
|
|
// a bare metal program escapable - Once puts a demo in front of the next start and deletes
|
|
// the request before jumping, so a demo that has taken the whole machine is one gesture from
|
|
// the system coming back, rather than closing the window and opening it again.
|
|
static void checkResetButton(void) {
|
|
const int control = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL);
|
|
const int shift = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
|
|
if (control && shift && IsKeyPressed(KEY_R)) {
|
|
requestReset();
|
|
}
|
|
}
|
|
|
|
// ---- The speaker ----
|
|
//
|
|
// The device makes its samples on emulated cycles and puts them in a ring; this takes them
|
|
// out and hands them to Raylib. Nothing here decides what a sound is, the same as nothing in
|
|
// presentFrame decides what the screen looks like - which is why the headless binary and this
|
|
// one make the same sound, and why Tests/sound.sh can check a device with no speaker in it.
|
|
//
|
|
// A sub-buffer at a time, because that is the unit Raylib hands back when it has finished
|
|
// playing one. At 48,000 a second and sixty frames, a frame of machine time is 800 samples,
|
|
// so 1,024 is a little over one and there are two of them.
|
|
#define SOUND_BUFFER 1024
|
|
static AudioStream soundStream;
|
|
static int speakerOn;
|
|
|
|
// ---- When the machine cannot keep up, and when it runs away ----
|
|
//
|
|
// Both directions happen and neither should be a crash. The machine runs a slice per frame
|
|
// against the wall clock, so a host that stalls leaves the ring short and a host running
|
|
// --fast fills it faster than anything can play it. The device drops when full, which is the
|
|
// runaway case. This is the other one: what is missing is filled by HOLDING THE LAST SAMPLE
|
|
// rather than by zeroes, because a jump to silence and back is a click and a held level is
|
|
// not. It is still a glitch; it is the quieter kind.
|
|
static void feedSpeaker(void) {
|
|
static int16_t buffer[SOUND_BUFFER];
|
|
static int16_t lastSample;
|
|
while (IsAudioStreamProcessed(soundStream)) {
|
|
const int taken = soundTake(buffer, SOUND_BUFFER);
|
|
if (taken > 0) {
|
|
lastSample = buffer[taken - 1];
|
|
}
|
|
for (int i = taken; i < SOUND_BUFFER; i++) {
|
|
buffer[i] = lastSample;
|
|
}
|
|
UpdateAudioStream(soundStream, buffer, SOUND_BUFFER);
|
|
}
|
|
}
|
|
|
|
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();
|
|
readPads();
|
|
checkResetButton();
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
// ---- The button has to reach a machine that is waiting ----
|
|
//
|
|
// A reset is acted on between instructions, and a machine blocked on a key is part way
|
|
// through one - so pressing the button while a program sits waiting would set the flag
|
|
// and nothing would ever come along to notice it. Which is precisely the moment a reset
|
|
// button earns its keep: a program that is stuck is the one you want to get out of.
|
|
//
|
|
// So the wait ends. The console treats that as the end of input, which it is for the
|
|
// machine that is about to stop existing, and the reset puts the console's input back.
|
|
//
|
|
// ASKED OF THE REQUEST ITSELF rather than remembered here. A flag of its own outlived
|
|
// the reset it belonged to: a program that never read the console - picture.bin, say,
|
|
// which draws and halts - left it set, and the NEXT machine's first read came back as
|
|
// the end of input. CosmOS booted and stopped immediately, having been told there was
|
|
// nobody there. There is one fact and it lives in one place.
|
|
if (resetIsPending()) {
|
|
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);
|
|
// ---- Escape is a byte, not a way out ----
|
|
//
|
|
// Raylib closes a window on Escape unless it is told not to, and this machine sends
|
|
// Escape to the console like any other key. So a program reading keys could be
|
|
// ended by one of them, taking whatever was in memory with it - which is a poor way
|
|
// to find out that a default was left as it was found.
|
|
SetExitKey(KEY_NULL);
|
|
|
|
// ---- What controllers the host can see, said out loud ----
|
|
//
|
|
// A pad that is plugged in and does nothing is indistinguishable from a pad the front
|
|
// end never noticed, and the difference is the whole of what to do about it: one is a
|
|
// mapping to fix and the other is a driver. Saying which at startup costs a line and
|
|
// answers it without anybody having to guess.
|
|
for (int n = 0; n < PAD_COUNT; n++) {
|
|
if (IsGamepadAvailable(n)) {
|
|
printf("Controller %d: %s\n", n, GetGamepadName(n));
|
|
}
|
|
}
|
|
|
|
// ---- And a speaker, if the host has one ----
|
|
//
|
|
// Asked for rather than assumed: a machine with no audio device is a perfectly good
|
|
// machine to look at, and a front end that refused to start without one would make
|
|
// the window depend on something the picture does not need.
|
|
InitAudioDevice();
|
|
if (IsAudioDeviceReady()) {
|
|
SetAudioStreamBufferSizeDefault(SOUND_BUFFER);
|
|
soundStream = LoadAudioStream(SOUND_SAMPLE_RATE, 16, 1);
|
|
PlayAudioStream(soundStream);
|
|
speakerOn = 1;
|
|
}
|
|
// 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. UNLESS A KEYBOARD FILE WAS
|
|
// NAMED, which used to be overridden here without a word: machineStart installs the
|
|
// file's hook and this replaced it, so --keyboard was a flag that did nothing behind
|
|
// a window and said nothing about it.
|
|
//
|
|
// Both together is the combination a demo wants. Recording a flight needs the typing
|
|
// that STARTS it to be the same every time - a human reaching the shell at a slightly
|
|
// different moment shifts every frame of the recording after it - while the flying
|
|
// itself comes from whatever is actually in somebody's hands.
|
|
if (options.keyboard == NULL) {
|
|
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.
|
|
while (windowOpen && !WindowShouldClose()) {
|
|
// Before the running check, not after it: a machine that has stopped is the one
|
|
// worth restarting, and it is the one that cannot notice a reset by itself.
|
|
machineTakeReset(&machine);
|
|
if (machineRunning(&machine)) {
|
|
machineRunSlice(&machine);
|
|
}
|
|
presentFrame();
|
|
// After the slice, so what the machine just made is what gets played.
|
|
if (speakerOn) {
|
|
feedSpeaker();
|
|
}
|
|
}
|
|
windowOpen = 0;
|
|
// Taken back before the machine stops, so nothing can ask a window that has gone.
|
|
consoleSetInputHook(NULL);
|
|
if (speakerOn) {
|
|
UnloadAudioStream(soundStream);
|
|
speakerOn = 0;
|
|
}
|
|
CloseAudioDevice();
|
|
UnloadTexture(screenTexture);
|
|
CloseWindow();
|
|
}
|
|
|
|
machineStop(&machine);
|
|
return machineReport(&machine);
|
|
}
|