Files
AnachronautandClaude Opus 5 ee77d79780 A pad is sampled once a frame, and the recorder writes what was sampled
The live state is written by whatever watches real hardware, on ITS clock:
a window polls its keyboard once a HOST frame, which is not a machine
frame. Read straight through, that made a pad whose value could change in
the middle of a machine frame - breaking the one promise the device makes,
that asking twice in a frame gives the same answer both times. The manual
said it could not happen and the code allowed it.

It also made recordings that were not of the flight. The recorder sampled
on a frame boundary and the program read whenever it read, so the two saw
different bytes. A replay of that is a DIFFERENT FLIGHT, faithfully
reproduced: it flew a lander off the top of the screen that had never gone
there, and every check said the replay was deterministic and re-recorded
as itself, because it was. Both were true and neither was the point.

So the live state is latched once a frame. What the machine reads and what
the recorder writes are now the same thing by construction rather than by
two clocks happening to agree. Real hardware latches a controller once a
frame for the same reason.

WHAT IS STILL NOT COVERED: the latch itself. Every recorded pad already
changes only on a frame, so the tests cannot tell a latched live pad from
an unlatched one - the case that went wrong is the one with a real hand on
a real controller, which is the case a headless suite has none of. Said
here rather than left to look tested.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-03 14:43:58 -04:00

152 lines
5.9 KiB
C

// 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];
static int connected[PAD_COUNT];
// ---- What the machine is told, which changes only on a frame ----
//
// The live state is written by whatever is watching real hardware, on ITS clock - a window
// polls its keyboard once a host frame, and a host frame is not a machine frame. Read
// straight through, that made a pad whose value could change in the middle of a machine
// frame, which breaks the one promise the device makes: that asking twice in a frame gives
// the same answer both times.
//
// It also made recordings that were not of the flight. The recorder samples on a frame
// boundary and the program reads whenever it reads, so the two saw different bytes - and a
// replay of that is a DIFFERENT FLIGHT, faithfully reproduced. It flew a lander off the top
// of the screen that had never gone there.
//
// So the live state is latched here once a frame, and what the machine reads and what the
// recorder writes are the same thing by construction. Real hardware does this too, and for
// the same reason: a controller is sampled once a frame, not continuously.
static uint8_t reported[PAD_COUNT];
static FILE *recording;
// ---- 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;
reported[n] = 0;
connected[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;
}
// What the device would report for this pad, which is what a recording has to hold: a
// recording of a playback that wrote the LIVE state would be a file of noughts.
static uint8_t effective(int which) {
return (recorded[which] != NULL) ? held[which] : reported[which];
}
void padRecordTo(FILE *file) {
recording = file;
}
void padFromFile(int which, FILE *file) {
if (which < 0 || which >= PAD_COUNT) {
return;
}
recorded[which] = file;
}
void padSet(int which, int isConnected, uint8_t heldNow) {
if (which < 0 || which >= PAD_COUNT) {
return;
}
connected[which] = isConnected;
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++) {
// The live state is taken as it stands at the frame boundary and held there
// until the next one, so nothing the machine reads was never recorded.
reported[n] = live[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;
}
// ---- And a byte written for every frame that went by ----
//
// Inside the loop rather than after it, so a machine that jumped several frames at
// once still writes one byte for each of them. A recording is a TIMELINE, and one
// that skipped the frames nobody was looking at would play back faster than it was
// flown.
//
// Flushed as it goes, because a recording is usually stopped by whoever is playing
// rather than by the program ending, and a demo lost to a buffer would be a demo
// flown twice.
if (recording != NULL) {
uint8_t all = 0;
for (int n = 0; n < PAD_COUNT; n++) {
all |= effective(n);
}
fputc(all, recording);
fflush(recording);
}
}
}
uint8_t padRead(uint8_t port) {
if (port == PAD_PRESENT) {
uint8_t there = 0;
for (int n = 0; n < PAD_COUNT; n++) {
// A recording is a pad, and so is anything the front end says is plugged in.
// Counting only the recordings meant this said nought on the one machine that
// has real controllers, which is the only machine where the answer matters.
if (recorded[n] != NULL || connected[n]) {
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 effective(which);
}