Controllers: four pads that say what is held

The console says WHICH KEY WENT DOWN, which is the right shape for typing
and the wrong one for playing. A game wants to know what is being held,
this frame, possibly several things at once, and a stream of presses
cannot say that: a key that is down and staying down sends nothing at all.
Lunar Porter's thrust is a burn per press for exactly that reason.

So a pad is its own device on ports 0x60 to 0x6F, reporting a LEVEL. One
read gives every button at once, holding is the natural thing to express,
two directions together cost nothing, and reading does not consume it - a
game may ask twice in a frame and be told the same thing both times.

Four of them, because a party is four. They cost a port each and nothing
at all when unused. The directions are the low nibble so "which way" is an
AND with 0x0F; the buttons are the high nibble for the same reason. 0x64
says which are really there, so a game can ask for a controller rather
than sitting silent while somebody presses things at it. They never
interrupt: a game polls once a frame because that is when it draws.

KEY-UP ON THE CONSOLE WAS THE OTHER WAY TO DO THIS AND WAS REJECTED. A
terminal hands over characters and can never report a key coming up
however it is asked, so it would have been a thing that worked behind a
window and silently did not down a wire. A separate device can honestly
say it is not there.

Voyager drives pad nought from the keyboard as well as from any real
controller, OR-ed rather than chosen between, so a game written for a pad
is playable on a machine with none and unplugging one mid-game does not
leave somebody holding nothing.

And a recorded path, which is what makes any of it testable: --pad names a
file of one byte a frame, and the manifest has an eighth column for it.
A BYTE A FRAME AND NOT A BYTE A READ - a level asked twice in one frame
has to answer the same both times, and a file that advanced per read would
depend on how the program happened to be written. Voyager's own tests run
headless with nobody holding anything, so without this the device would be
exercised only by somebody playing: the state the console's line editing
was in when it broke twice in two days.

0x50 is the timer, not free. The block this went in was chosen after
looking rather than before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-02 21:44:55 -04:00
co-authored by Claude Opus 5
parent 88ecb208f4
commit fed1453e6e
16 changed files with 413 additions and 5 deletions
+51
View File
@@ -0,0 +1,51 @@
; Asks the machine what its controllers are doing.
;
; A pad reports a LEVEL and not an event: one read gives every button at once, holding is the
; natural thing to say, and reading does not consume anything - so asking twice in a frame
; gives the same answer twice, which this checks by doing exactly that.
;
; The recording behind it is one byte a frame. It presses up, holds it a second frame, adds
; right, lets go of up, lets go of everything, then presses A and B together - which is the
; case a console's key-at-a-time stream cannot express at all.
;
; Written by Anachronaut
#Program
start:
INA 0x64
OUTA 0x00 ; Which pads are there: one bit each, so pad nought alone is 1.
RSTA
SETD.0 Count
STA.0
everyFrame:
INA 0x30
INIB 0x01
AND
BRQ everyFrame ; The frame the recording steps on.
INA 0x60
OUTA 0x00
; And again, without a frame in between. A level does not go away when it is looked at.
INA 0x60
OUTA 0x00
SETD.0 Count
LDA.0
INCA
STA.0
INIB 0d6
CCF
SUB
BNQ everyFrame
; A pad that is not there reads as nothing held, which is honest rather than an error.
INA 0x63
OUTA 0x00
HALT
#Data
Count:
0x00
+13
View File
@@ -7,6 +7,7 @@
#include "../Assembler/assembly.h" // For the fault vector numbers. #include "../Assembler/assembly.h" // For the fault vector numbers.
#include "controller.h" #include "controller.h"
#include "video.h" #include "video.h"
#include "pad.h"
#include "sound.h" #include "sound.h"
#include "font.h" #include "font.h"
#include <stdio.h> #include <stdio.h>
@@ -1168,6 +1169,9 @@ void deviceTick(unsigned long now) {
soundTick(now); soundTick(now);
// And the timer, which is the only beat a program can choose for itself. // And the timer, which is the only beat a program can choose for itself.
timerTick(now); timerTick(now);
// And the pads, whose recordings step on a frame so that a level read twice in one is
// the same level both times.
padTick(now);
if (diskPending && now >= diskReadyAt) { if (diskPending && now >= diskReadyAt) {
diskSettle(); diskSettle();
} }
@@ -1341,6 +1345,7 @@ static const DeviceRecord deviceTable[] = {
{ PORT_VIDEO, DEVICE_VIDEO, DEVICE_FLAG_HAS_MEMORY }, { PORT_VIDEO, DEVICE_VIDEO, DEVICE_FLAG_HAS_MEMORY },
{ PORT_SOUND, DEVICE_SOUND, 0 }, { PORT_SOUND, DEVICE_SOUND, 0 },
{ PORT_TIMER, DEVICE_TIMER, 0 }, { PORT_TIMER, DEVICE_TIMER, 0 },
{ PORT_PAD, DEVICE_PAD, 0 },
{ PORT_REGISTRY, DEVICE_REGISTRY, 0 }, { PORT_REGISTRY, DEVICE_REGISTRY, 0 },
}; };
static const int deviceCount = (int)(sizeof(deviceTable) / sizeof(deviceTable[0])); static const int deviceCount = (int)(sizeof(deviceTable) / sizeof(deviceTable[0]));
@@ -1393,6 +1398,11 @@ static const DeviceRecord *deviceOnPort(uint8_t port) {
if (port > PORT_TIMER && port <= PORT_TIMER_TOP) { if (port > PORT_TIMER && port <= PORT_TIMER_TOP) {
return deviceOnPort(PORT_TIMER); return deviceOnPort(PORT_TIMER);
} }
// Sixteen ports, one device: four pads, a port saying which are there, and eleven kept
// for the analogue axes that are not built.
if (port > PORT_PAD && port <= PORT_PAD_TOP) {
return deviceOnPort(PORT_PAD);
}
for (int i = 0; i < deviceCount; i++) { for (int i = 0; i < deviceCount; i++) {
if (deviceTable[i].port == port) { if (deviceTable[i].port == port) {
return &deviceTable[i]; return &deviceTable[i];
@@ -1570,6 +1580,9 @@ uint8_t InputHandler(uint8_t Address) {
if (Address >= PORT_SOUND && Address <= PORT_SOUND_TOP) { if (Address >= PORT_SOUND && Address <= PORT_SOUND_TOP) {
return soundRead(Address); return soundRead(Address);
} }
if (Address >= PORT_PAD && Address <= PORT_PAD_TOP) {
return padRead(Address);
}
if (Address >= PORT_TIMER && Address <= PORT_TIMER_TOP) { if (Address >= PORT_TIMER && Address <= PORT_TIMER_TOP) {
return timerRead(Address); return timerRead(Address);
} }
+3
View File
@@ -314,6 +314,9 @@ void consoleSetInputHook(int (*hook)(int mayWait));
#define DEVICE_VIDEO 0x14 #define DEVICE_VIDEO 0x14
#define DEVICE_SOUND 0x15 #define DEVICE_SOUND 0x15
#define DEVICE_TIMER 0x16 #define DEVICE_TIMER 0x16
// Game controllers. Polled, never interrupting: a game asks once a frame because that is when
// it draws, and an interrupt for every button would be the event model a pad exists to avoid.
#define DEVICE_PAD 0x17
// What a device brings besides itself. This means memory that somebody has to register // What a device brings besides itself. This means memory that somebody has to register
// with the controller, so the controller's own bank 2 does not count: it is already there. // with the controller, so the controller's own bank 2 does not count: it is already there.
+18
View File
@@ -8,6 +8,7 @@
#include "cpu.h" #include "cpu.h"
#include "controller.h" #include "controller.h"
#include "io.h" #include "io.h"
#include "pad.h"
#include "video.h" #include "video.h"
#include "sound.h" #include "sound.h"
#include "utility.h" #include "utility.h"
@@ -134,6 +135,7 @@ static int machineRestart(Machine *m) {
return 0; return 0;
} }
videoReset(); videoReset();
padReset();
soundReset(); soundReset();
timerReset(); timerReset();
consoleHome(); consoleHome();
@@ -207,6 +209,7 @@ uint8_t machineStart(Machine *m, const EmulatorOptions *options, const char *pro
// the device's, and a reset that left last program's screen up would be a reset that // the device's, and a reset that left last program's screen up would be a reset that
// did not happen. // did not happen.
videoReset(); videoReset();
padReset();
soundReset(); soundReset();
timerReset(); timerReset();
if (options->sound != NULL) { if (options->sound != NULL) {
@@ -220,6 +223,21 @@ uint8_t machineStart(Machine *m, const EmulatorOptions *options, const char *pro
if (m->options.debug) { if (m->options.debug) {
printRegisters(&m->cpu, Program, Data); printRegisters(&m->cpu, Program, Data);
} }
// ---- The pads, from files ----
//
// Opened here beside the keyboard because they are the same kind of thing: a recording
// standing in for a person, so that what a person would exercise is reachable from a
// suite. They are never closed, for the same reason the keyboard is not - the machine
// outlives the call and the host reclaims them when it stops.
for (int n = 0; n < options->padCount; n++) {
FILE *pad = fopen(options->pads[n], "rb");
if (pad == NULL) {
fprintf(stderr, "Error: Couldn't open pad file: %s\n", options->pads[n]);
return 0;
}
padFromFile(n, pad);
}
if (options->keyboard != NULL) { if (options->keyboard != NULL) {
keyboardFile = fopen(options->keyboard, "rb"); keyboardFile = fopen(options->keyboard, "rb");
if (keyboardFile == NULL) { if (keyboardFile == NULL) {
+94
View File
@@ -0,0 +1,94 @@
// pad.c
// Game controllers for the Voyager.
// Written by Anachronaut
#include "pad.h"
#include "video.h"
// What each pad is holding, and where each one gets it from.
static uint8_t held[PAD_COUNT];
static FILE *recorded[PAD_COUNT];
static uint8_t live[PAD_COUNT];
// ---- The frame the recordings advance on ----
//
// The screen's frame, and it is the same one on purpose: a game reads its pad once a frame
// because that is when it draws, so a byte a frame is a byte a poll for anything written the
// ordinary way - without making it a byte a READ, which would answer a game that asked twice
// differently from one that asked once.
//
// On the machine's clock, so a recording plays back the same over the same cycles however
// fast the host ran.
static unsigned long lastFrame;
static int started;
void padReset(void) {
for (int n = 0; n < PAD_COUNT; n++) {
held[n] = 0;
live[n] = 0;
// The files are NOT closed or forgotten. They were named on the command line and
// outlive a reset, the same as a disk image does: a machine that restarted itself
// and lost its controllers would be a strange thing to debug.
}
lastFrame = 0;
started = 0;
}
void padFromFile(int which, FILE *file) {
if (which < 0 || which >= PAD_COUNT) {
return;
}
recorded[which] = file;
}
void padSet(int which, uint8_t heldNow) {
if (which < 0 || which >= PAD_COUNT) {
return;
}
live[which] = heldNow;
}
void padTick(unsigned long now) {
// The first tick sets the clock rather than counting a frame from nought, or a machine
// that started late would take a run of bytes all at once.
if (!started) {
lastFrame = now;
started = 1;
}
while (now - lastFrame >= VIDEO_FRAME_CYCLES) {
lastFrame += VIDEO_FRAME_CYCLES;
for (int n = 0; n < PAD_COUNT; n++) {
if (recorded[n] == NULL) {
continue;
}
const int byte = fgetc(recorded[n]);
// ---- The end of a recording is nothing held ----
//
// Not a pad that vanishes and not the last frame repeating for ever. A recording
// that ran out and left a direction pressed would send whatever it was driving
// off the edge of the world long after the test meant to stop.
held[n] = (byte == EOF) ? 0 : (uint8_t)byte;
}
}
}
uint8_t padRead(uint8_t port) {
if (port == PAD_PRESENT) {
uint8_t there = 0;
for (int n = 0; n < PAD_COUNT; n++) {
if (recorded[n] != NULL) {
there |= (uint8_t)(1u << n);
}
}
return there;
}
const int which = port - PORT_PAD;
if (which < 0 || which >= PAD_COUNT) {
// Everything else in the block is reserved and reads as nothing, which is what a
// port block being kept for later should do.
return 0;
}
// A recording wins over a live pad, so a test is not at the mercy of whatever somebody
// is leaning on while it runs.
return (recorded[which] != NULL) ? held[which] : live[which];
}
+79
View File
@@ -0,0 +1,79 @@
// pad.h
// Game controllers for the Voyager.
// Written by Anachronaut
#ifndef PAD_H
#define PAD_H
#include <stdint.h>
#include <stdio.h>
// ---- What a pad is, and why it is not the console ----
//
// The console says WHICH KEY WENT DOWN. That is the right shape for typing and the wrong one
// for playing: a game wants to know what is being held, this frame, possibly several things
// at once, and a stream of presses cannot say that. A key that is down and staying down sends
// nothing at all.
//
// So a pad reports a LEVEL rather than an event. One read gives the state of every button at
// once, holding is the natural thing to express, and two directions at the same time costs
// nothing. Reading it does not consume it: a game may ask twice in a frame and get the same
// answer both times, which an event queue cannot promise.
//
// IT IS NOT AN EXTENSION OF THE CONSOLE, and that is deliberate. A terminal hands over
// characters and cannot report a key coming up however it is asked, so key-up on the console
// would have been a thing that worked behind a window and silently did not down a wire - and
// "the same program behaves the same everywhere" is worth more than the convenience. A
// separate device can honestly say it is not there.
#define PORT_PAD 0x60
#define PORT_PAD_TOP 0x6F
// Four, because a party is four. They cost a port each and nothing at all when unused.
#define PAD_COUNT 4
// ---- The buttons, in one byte ----
//
// The four directions in the low nibble, so "which way" is an AND with 0x0F and needs no
// shifting. The four buttons in the high nibble for the same reason.
#define PAD_RIGHT 0x01
#define PAD_LEFT 0x02
#define PAD_DOWN 0x04
#define PAD_UP 0x08
#define PAD_A 0x10
#define PAD_B 0x20
#define PAD_START 0x40
#define PAD_SELECT 0x80
// ---- Which of them are there ----
//
// One bit a pad, so a game can say "this wants a controller" rather than sitting silent while
// somebody presses things at it. A pad that is not there reads as nothing held, which is the
// same as a pad nobody is touching - the difference matters only to whoever wants to explain
// it, and that is exactly who this port is for.
#define PAD_PRESENT 0x64
// Ports 0x65 to 0x6F are reserved. Analogue axes are the thing they are being kept for: a
// paddle is two more bytes a pad and an argument about deadzones, and nothing wants one yet.
void padReset(void);
// ---- Recorded input, which is what makes this testable ----
//
// Voyager reads a real pad, and its tests run headless with no window and no hands. Without a
// recorded path this whole device would be exercised only by somebody playing, which is the
// state the console's line editing was in when it broke twice in two days.
//
// A file is one byte a FRAME, not one byte a read. A pad is a level: a game that asks twice
// in one frame has to be told the same thing both times, and a file that advanced per read
// would answer differently depending on how the game was written.
void padFromFile(int which, FILE *file);
// A live pad, set by whatever is watching real hardware. Ignored for a pad that has a file,
// so a recording always wins over whatever somebody happens to be holding.
void padSet(int which, uint8_t held);
void padTick(unsigned long now);
uint8_t padRead(uint8_t port);
#endif // PAD_H
+13 -1
View File
@@ -34,6 +34,10 @@ void printHelp(const char *programName) {
printf(" keyboard rather than a terminal. Which means the console does\n"); printf(" keyboard rather than a terminal. Which means the console does\n");
printf(" its own line editing, the way it must when a window is open\n"); printf(" its own line editing, the way it must when a window is open\n");
printf(" and there is no terminal behind it to do it.\n"); printf(" and there is no terminal behind it to do it.\n");
printf(" -P, --pad FILE Hold a controller from a file, one byte a frame. Given\n");
printf(" again for the next pad. A byte is the buttons held:\n");
printf(" 1 right, 2 left, 4 down, 8 up, 16 A, 32 B, 64 start,\n");
printf(" 128 select.\n");
printf(" -N, --sound FILE Save every sample the machine made, as raw signed 16 bit\n"); printf(" -N, --sound FILE Save every sample the machine made, as raw signed 16 bit\n");
printf(" at 48kHz. What --screen is for a picture: the only way to\n"); printf(" at 48kHz. What --screen is for a picture: the only way to\n");
printf(" check a sound on a machine with no speaker.\n"); printf(" check a sound on a machine with no speaker.\n");
@@ -52,6 +56,7 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
{"screen", required_argument, 0, 'S'}, {"screen", required_argument, 0, 'S'},
{"keyboard", required_argument, 0, 'K'}, {"keyboard", required_argument, 0, 'K'},
{"sound", required_argument, 0, 'N'}, {"sound", required_argument, 0, 'N'},
{"pad", required_argument, 0, 'P'},
{"help", no_argument, 0, 'h'}, {"help", no_argument, 0, 'h'},
{0, 0, 0, 0 } {0, 0, 0, 0 }
}; };
@@ -71,7 +76,7 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
*options = (EmulatorOptions){0}; *options = (EmulatorOptions){0};
// Parse options // Parse options
while ((opt = getopt_long(argc, argv, "dc:fhD:WL:S:K:N:R:", long_options, &option_index)) != -1) { while ((opt = getopt_long(argc, argv, "dc:fhD:WL:S:K:N:R:P:", long_options, &option_index)) != -1) {
switch (opt) { switch (opt) {
case 'd': case 'd':
options->debug = 1; options->debug = 1;
@@ -131,6 +136,13 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
case 'N': case 'N':
options->sound = optarg; options->sound = optarg;
break; break;
case 'P':
// Fills the pads in turn, the same way --disk fills the drives, so the first
// one named is pad nought and the machine has as many as were asked for.
if (options->padCount < PAD_DRIVE_COUNT) {
options->pads[options->padCount++] = optarg;
}
break;
case 'h': case 'h':
printHelp(argv[0]); printHelp(argv[0]);
return OPTIONS_HELP; return OPTIONS_HELP;
+11
View File
@@ -16,6 +16,9 @@
#define OPTIONS_HELP 1 // The user asked for help, so stop, but not because of an error. #define OPTIONS_HELP 1 // The user asked for help, so stop, but not because of an error.
#define OPTIONS_ERROR 2 // The command line was no good, stop and complain. #define OPTIONS_ERROR 2 // The command line was no good, stop and complain.
// As many as the machine has pads.
#define PAD_DRIVE_COUNT 4
typedef struct { typedef struct {
uint8_t debug; // Step one instruction at a time, printing the registers. uint8_t debug; // Step one instruction at a time, printing the registers.
uint8_t fast; // Ignore the cycle rate and run as fast as the host allows. uint8_t fast; // Ignore the cycle rate and run as fast as the host allows.
@@ -36,6 +39,14 @@ typedef struct {
const char *screen; // Where to save a picture of the screen when the machine stops. const char *screen; // Where to save a picture of the screen when the machine stops.
const char *keyboard; // Feed the console from this file as a keyboard, not a terminal. const char *keyboard; // Feed the console from this file as a keyboard, not a terminal.
const char *sound; // Where to save the samples the machine made, as raw 16 bit. const char *sound; // Where to save the samples the machine made, as raw 16 bit.
// ---- A controller made of a file ----
//
// One byte a frame, each byte the buttons held during it. Voyager reads a real pad, and
// its own tests run headless with nobody holding anything - so without this the device
// would be exercised only by somebody playing, which is exactly the state the console's
// line editing was in when it broke twice in two days.
const char *pads[PAD_DRIVE_COUNT];
int padCount; // --pad given more than once fills them in turn, like --disk.
} EmulatorOptions; } EmulatorOptions;
uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options); uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options);
+44
View File
@@ -22,6 +22,7 @@
#include "machine.h" #include "machine.h"
#include "video.h" #include "video.h"
#include "pad.h"
#include "sound.h" #include "sound.h"
#include "io.h" #include "io.h"
#include "utility.h" #include "utility.h"
@@ -106,6 +107,48 @@ static int keyTake(void) {
} }
// Everything Raylib has, taken before it can throw any of it away. // 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;
if (IsGamepadAvailable(n)) {
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; }
}
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, held);
}
}
static void drainKeyboard(void) { static void drainKeyboard(void) {
int character; int character;
while ((character = GetCharPressed()) > 0) { while ((character = GetCharPressed()) > 0) {
@@ -276,6 +319,7 @@ static void presentFrame(void) {
EndDrawing(); EndDrawing();
// EndDrawing has just polled, which is the one moment Raylib's queues hold anything. // EndDrawing has just polled, which is the one moment Raylib's queues hold anything.
drainKeyboard(); drainKeyboard();
readPads();
checkResetButton(); checkResetButton();
} }
+41
View File
@@ -602,6 +602,7 @@ If nothing is installed for the vector a device refused with, the machine stops
| 0x30 - 0x3F | The screen. See The Screen. It brings video memory, which is unreachable until it is registered as a bank. | 0x14 | | 0x30 - 0x3F | The screen. See The Screen. It brings video memory, which is unreachable until it is registered as a bank. | 0x14 |
| 0x40 - 0x4F | The sound device. See Making A Noise. Four channels, played by writing to ports; it brings no memory. | 0x15 | | 0x40 - 0x4F | The sound device. See Making A Noise. Four channels, played by writing to ports; it brings no memory. | 0x15 |
| 0x50 - 0x54 | The timer. See Keeping Time. Counts the machine's cycles and says when a period has gone by. | 0x16 | | 0x50 - 0x54 | The timer. See Keeping Time. Counts the machine's cycles and says when a period has gone by. | 0x16 |
| 0x60 - 0x6F | The game controllers. See Controllers. Four pads, polled, reporting what is held. | 0x17 |
| 0xE0 - 0xEF | The memory controller. See The Memory Controller. | 0x03 | | 0xE0 - 0xEF | The memory controller. See The Memory Controller. | 0x03 |
| 0xFF | The bus registry. See Asking What Is There. | 0x01 | | 0xFF | The bus registry. See Asking What Is There. | 0x01 |
@@ -1167,8 +1168,48 @@ One thing to be careful of: the registry remembers which port it was asked about
| 0x14 | Screen. | | 0x14 | Screen. |
| 0x15 | Sound. | | 0x15 | Sound. |
| 0x16 | Timer. | | 0x16 | Timer. |
| 0x17 | Game controllers. |
| 0x17 - 0xFF | Peripherals. | | 0x17 - 0xFF | Peripherals. |
## Controllers:
Four pads on ports 0x60 to 0x6F. Each one is **one byte, read, saying what is held right now**.
| Port | Holds |
| --- | --- |
| 0x60 - 0x63 | Pads 0 to 3. |
| 0x64 | Which pads are there, one bit each. |
| 0x65 - 0x6F | Reserved. |
| Bit | Button |
| --- | --- |
| 0x01 | Right |
| 0x02 | Left |
| 0x04 | Down |
| 0x08 | Up |
| 0x10 | A |
| 0x20 | B |
| 0x40 | Start |
| 0x80 | Select |
The four directions are the low nibble, so *which way* is an `AND` with `0x0F` and needs no shifting. The four buttons are the high nibble for the same reason.
### Why This Is Not The Console:
The console says **which key went down**. That is the right shape for typing and the wrong one for playing: a game wants to know what is being held, this frame, possibly several things at once, and a stream of presses cannot say that. A key that is down and staying down sends nothing at all.
A pad reports a **level** rather than an event. One read gives every button at once, holding is the natural thing to express, and two directions together cost nothing. **Reading does not consume it** - a game may ask twice in a frame and be told the same thing both times, which an event queue cannot promise.
Key-up on the console would have been the other way to do it, and it was rejected: a terminal hands over characters and can never report a key coming up however it is asked, so it would have been a thing that worked behind a window and silently did not down a wire. A separate device can honestly say it is not there.
**They never interrupt.** A game polls once a frame because that is when it draws, and an interrupt for every button would be exactly the event model a pad exists to avoid.
### When There Is No Pad:
A pad that is not there reads as nothing held, which is the same as a pad nobody is touching. The difference matters only to whoever wants to explain it, so 0x64 says which are really there and a game can ask for a controller rather than sitting silent while somebody presses things at it.
`Programs/testPrograms/padTest.asm` reads one twice a frame to show that looking does not take it away.
## Keeping Time: ## Keeping Time:
A period, in cycles, and a bit that says when one has gone by. A period, in cycles, and a bit that says when one has gone by.
+16 -1
View File
@@ -101,7 +101,7 @@ from `make`, not from here.
### 1. Recorded output ### 1. Recorded output
`Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares `Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares
everything it printed against a file in `Tests/expected`. 206 tests, of which 144 run, 35 everything it printed against a file in `Tests/expected`. 207 tests, of which 145 run, 35
only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image
given at all. given at all.
@@ -404,6 +404,21 @@ A keyboard file installs the same hook a window does, so the same path runs. It
test the window: Voyager's own key queue is still out of reach, and so is anything about test the window: Voyager's own key queue is still out of reach, and so is anything about
presenting frames. It tests the console, which is where the logic is. presenting frames. It tests the console, which is where the logic is.
**pad** names a file in `Tests/input` to be held on a controller, one byte a frame, each
byte the buttons held during it. It exists for the same reason as **keys** and matters more:
a pad reports what is *held*, and a suite has no hands.
**A byte a frame, not a byte a read.** A pad is a level, so a game that asks twice in one
frame has to be told the same thing both times, and a file that advanced per read would
answer differently depending on how the program happened to be written. The frame is the
machine's own, so a recording plays back the same over the same cycles however fast the host
ran.
It is also the only way this device is reachable at all. Voyager reads a real controller and
its own tests run `--headless`, with no window and nobody holding anything - so without a
recorded path a pad would be exercised only by somebody playing, which is exactly the state
the console's line editing was in when it broke twice in two days.
## Fixture Disks: ## Fixture Disks:
`Tests/makedisks.sh` builds 27 images with SplitDisk before anything runs, into `Tests/makedisks.sh` builds 27 images with SplitDisk before anything runs, into
Binary file not shown.
Binary file not shown.
+13 -1
View File
@@ -4,7 +4,7 @@
# One test per line, fields separated by '|'. Blank lines and lines starting # One test per line, fields separated by '|'. Blank lines and lines starting
# with '#' are ignored. # with '#' are ignored.
# #
# name | source | mode | stdin | limit | disk | keys # name | source | mode | stdin | limit | disk | keys | pad
# #
# source is relative to Programs/. Everything assembles from there with # source is relative to Programs/. Everything assembles from there with
# Libraries/ on the include path, and the binary is written into Tests/build. # Libraries/ on the include path, and the binary is written into Tests/build.
@@ -1000,6 +1000,18 @@ cosmosFlip | CosmOS/Source/cosmos.asm | run | cosmosFli
# the ball never moved at all - the whole 150 frames of it went past in the shell, reading a # the ball never moved at all - the whole 150 frames of it went past in the shell, reading a
# line made of nulls. Through a keyboard a null is silence, which is what makes it a wait. # line made of nulls. Through a keyboard a null is silence, which is what makes it a wait.
cosmosSprite | CosmOS/Source/cosmos.asm | run | - | 60000000 | disks/cosmos.img | cosmosSprite.keys cosmosSprite | CosmOS/Source/cosmos.asm | run | - | 60000000 | disks/cosmos.img | cosmosSprite.keys
# ---- A controller, which is a level and not an event ----
#
# The eighth column is a pad fixture: one byte a frame, each byte the buttons held during it.
# It exists for the same reason the keyboard fixture does - a device only a person can work is
# a device nothing checks - and it matters more here, because a pad is the ONE THING THE
# CONSOLE CANNOT DO. A terminal reports which key was pressed and can never report one coming
# up, so holding a direction, or holding two buttons at once, has no expression there at all.
#
# The recording presses up, holds it, adds right, lets go of up, lets go of everything, then
# presses A and B together. The program reads each frame twice to show that looking does not
# take it away.
padTest | testPrograms/padTest.asm | run | - | - | - | - | padTest.pad
# Which disk the registers mean. Several disks are one controller with a drive register # Which disk the registers mean. Several disks are one controller with a drive register
# rather than several devices, because a port is an immediate byte inside the instruction # rather than several devices, because a port is an immediate byte inside the instruction
# that names it and a program cannot compute one. Run with a single disk, so drive 1 is a # that names it and a program cannot compute one. Run with a single disk, so drive 1 is a
+16 -1
View File
@@ -171,7 +171,7 @@ uncolour() {
sed -i -E 's/\x1b\[[0-9;]*m//g' "$1" sed -i -E 's/\x1b\[[0-9;]*m//g' "$1"
} }
while IFS='|' read -r name src mode stdin limit disk keys; do while IFS='|' read -r name src mode stdin limit disk keys pad; do
name="$(trim "$name")" name="$(trim "$name")"
[ -z "$name" ] && continue [ -z "$name" ] && continue
case "$name" in \#*) continue ;; esac case "$name" in \#*) continue ;; esac
@@ -179,8 +179,10 @@ while IFS='|' read -r name src mode stdin limit disk keys; do
mode="$(trim "$mode")"; stdin="$(trim "$stdin")" mode="$(trim "$mode")"; stdin="$(trim "$stdin")"
limit="$(trim "$limit")"; disk="$(trim "$disk")" limit="$(trim "$limit")"; disk="$(trim "$disk")"
keys="$(trim "${keys:-}")" keys="$(trim "${keys:-}")"
pad="$(trim "${pad:-}")"
[ -z "$disk" ] && disk="-" [ -z "$disk" ] && disk="-"
[ -z "$keys" ] && keys="-" [ -z "$keys" ] && keys="-"
[ -z "$pad" ] && pad="-"
wanted "$name" || continue wanted "$name" || continue
@@ -247,6 +249,19 @@ while IFS='|' read -r name src mode stdin limit disk keys; do
fi fi
EMUARGS+=(--keyboard "$INPUT/$keys") EMUARGS+=(--keyboard "$INPUT/$keys")
fi fi
# ---- And a controller made of a file ----
#
# A pad reports what is HELD, and a suite has no hands. One byte a frame, which is
# what makes a device that only a person could otherwise exercise into one the
# recordings cover - the same argument as the keyboard fixture above.
if [ "$pad" != "-" ]; then
if [ ! -f "$INPUT/$pad" ]; then
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
report "FAIL" "$name" "missing pad fixture $pad"
continue
fi
EMUARGS+=(--pad "$INPUT/$pad")
fi
[ "$limit" != "-" ] && EMUARGS+=(--cycles "$limit") [ "$limit" != "-" ] && EMUARGS+=(--cycles "$limit")
# A disk starts fresh for every test, so a test cannot pass because of what a # A disk starts fresh for every test, so a test cannot pass because of what a
# previous one left lying on it. How that is arranged differs between a scratch # previous one left lying on it. How that is arranged differs between a scratch
+1 -1
View File
@@ -57,7 +57,7 @@ OBJ_DIR = Object
# SplitBit from Voyager is one file each: a terminal or a window. Anything that drifts out # SplitBit from Voyager is one file each: a terminal or a window. Anything that drifts out
# of the shared list and into one of those is behaviour the other does not have, which is # of the shared list and into one of those is behaviour the other does not have, which is
# the thing this split exists to prevent. # the thing this split exists to prevent.
MACHINE_SRCS = machine.c io.c controller.c video.c font.c sound.c synth.c utility.c cpu.c bootstrap.c assembly.c rom.c MACHINE_SRCS = machine.c io.c controller.c video.c pad.c font.c sound.c synth.c utility.c cpu.c bootstrap.c assembly.c rom.c
EMU_SRCS = emulator.c $(MACHINE_SRCS) EMU_SRCS = emulator.c $(MACHINE_SRCS)
VOY_SRCS = voyager.c $(MACHINE_SRCS) VOY_SRCS = voyager.c $(MACHINE_SRCS)
ASM_SRCS = Assembler.c assembly.c firstPass.c Assm-util.c secondPass.c ASM_SRCS = Assembler.c assembly.c firstPass.c Assm-util.c secondPass.c