From ee77d79780c2965a4914f414e7a158f5f44d762e Mon Sep 17 00:00:00 2001 From: Anachronaut Date: Thu, 3 Sep 2026 14:43:58 -0400 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW --- Source/Emulator/pad.c | 24 +++++++++++++++++++++++- SplitBit Programming Manual.md | 2 ++ demo.pad | Bin 1766 -> 1470 bytes 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Source/Emulator/pad.c b/Source/Emulator/pad.c index e709404..2baf08f 100644 --- a/Source/Emulator/pad.c +++ b/Source/Emulator/pad.c @@ -10,6 +10,24 @@ 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 ---- @@ -28,6 +46,7 @@ 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 @@ -40,7 +59,7 @@ void padReset(void) { // 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] : live[which]; + return (recorded[which] != NULL) ? held[which] : reported[which]; } void padRecordTo(FILE *file) { @@ -72,6 +91,9 @@ void padTick(unsigned long now) { 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; } diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index fce014d..2fcb78b 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -1219,6 +1219,8 @@ The console says **which key went down**. That is the right shape for typing and 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. +**A pad is sampled once a frame**, and what it reports does not change in between. That is not an implementation detail: what is watching a real controller runs on its own clock - a window polls its keyboard once a *host* frame, which is not a machine frame - and read straight through, a pad's value could change in the middle of a frame and a program asking twice would get two answers. Real hardware latches a controller once a frame for the same reason. + 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. diff --git a/demo.pad b/demo.pad index bba2c30e3c7995c431594c11e9eded6e579db44f..0142fcbad21c54d655f909ea39547b6a9fbd718e 100644 GIT binary patch literal 1470 zcmZP=PCx(xaC(UY0XPW(4op)RK>%GJNDNIWNC+Vrs;%o`VBR5Mdg^K+qJp0xX)b3c)qtW?