Give the machine a frame to wait for

V3. The screen interrupts at each frame on hardware vector 0x30, and WAIT finally has
something worth sleeping on.

THERE WAS NO CLOCK. Every program that wanted to happen at a certain speed counted
instructions and hoped, which is why Snake's pause silently halved the day a cycle stopped
being an instruction and became a memory access - the program was right and the thing it was
counting changed underneath it. A screen finishing sixty times a second is a real beat, and
it is counted in the MACHINE'S cycles rather than the host's, so the same program sees the
same number of frames in the same number of cycles however fast anything really ran. That is
what makes a frame something a test can count and a recorded result can hold.

Status bit 0 goes up when a frame has gone by and reading the status port puts it down, so a
program with no handler can watch for it instead. Control bit 0 asks to be interrupted, and
is OFF when the machine starts: an interrupt with nothing installed to catch it is a fault,
so a screen that began interrupting the moment it was switched on would take down every
program written before frames existed.

More than one frame can pass between two looks, and the flag and the line are each one
thing, so several still mean one of each. A missed frame is missed.

Programs/Examples/frames.asm prints a dot a frame for a second: 1,000,324 cycles, and 996,460
of them spent asleep. That split is the thing worth seeing - a program that polled instead
would print the same sixty dots, take the same second, and spend every cycle of it on the
bus. Its header explains why waiting is not spinning and why a machine with a beat can stop
guessing at one.

Six checks in Tests/video.sh, and two of them are about the clock rather than the output,
because the output cannot tell the difference. That the machine slept through nearly all of
ten frames, and that polling three frames actually took three frames - a status flag that
stayed up once set would print exactly the same character and look perfectly correct.

Breaking the frame interrupt on purpose left a machine asleep for ever and hung the whole
suite, which is a worse way to be told than a failing check. Tests/video.sh bounds its runs
at ten seconds now, the way Tests/run.sh always has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-08-29 10:24:09 -04:00
co-authored by Claude Opus 5
parent 3da020898c
commit 1174bd9af5
8 changed files with 357 additions and 7 deletions
+52 -2
View File
@@ -4,6 +4,7 @@
#include "video.h"
#include "font.h"
#include "io.h"
#include <stdio.h>
#include <string.h>
@@ -71,6 +72,12 @@ static int cursorAtColumn = 0;
static int cursorVisible = 0;
static unsigned long videoNow = 0;
// When the last frame boundary went by, whether one has gone by unnoticed, and whether the
// screen is meant to say so out loud.
static unsigned long lastFrame = 0;
static int frameWaiting = 0;
static int frameInterrupts = 0;
void videoSetCursor(int row, int column, int visible) {
cursorAtRow = row;
cursorAtColumn = column;
@@ -79,6 +86,19 @@ void videoSetCursor(int row, int column, int visible) {
void videoTick(unsigned long now) {
videoNow = now;
// ---- Caught up rather than counted ----
//
// A loop, because more than one frame can go by between two looks: the machine runs in
// batches, and a slow host or a --fast run can cover several frames before anything asks.
// The flag and the line are each ONE THING, so several frames at once still mean one of
// each - a missed frame is missed, which is what missing one is.
while (now - lastFrame >= VIDEO_FRAME_CYCLES) {
lastFrame += VIDEO_FRAME_CYCLES;
frameWaiting = 1;
if (frameInterrupts) {
raiseInterrupt(PORT_VIDEO);
}
}
}
void videoLoadFont(void) {
@@ -133,6 +153,10 @@ void videoReset(void) {
scroll = 0;
renderedWidth = 0;
renderedHeight = 0;
lastFrame = videoNow;
frameWaiting = 0;
frameInterrupts = 0;
clearInterrupt(PORT_VIDEO);
// A machine wakes up able to show text. Everything here is ordinary video memory that a
// program may overwrite the moment it wants the screen for something else.
videoLoadFont();
@@ -153,6 +177,16 @@ uint8_t videoWrite(uint8_t value, uint8_t port) {
mode = value;
}
break;
case VIDEO_CONTROL:
frameInterrupts = (value & VIDEO_CONTROL_FRAME) != 0;
if (!frameInterrupts) {
// Asking to stop being interrupted takes down whatever was already asked
// for. A request that outlived the setting that made it would arrive at a
// program which had just said it did not want it - the same reasoning the
// console's interrupt bit is written under.
clearInterrupt(PORT_VIDEO);
}
break;
case VIDEO_SCROLL:
// Wrapped rather than clipped, because the map is a ring and every byte names a
// row that exists.
@@ -168,8 +202,24 @@ uint8_t videoWrite(uint8_t value, uint8_t port) {
uint8_t videoRead(uint8_t port) {
switch (port) {
// Reserved for the frame interrupt, which is the next rung. Zero until then.
case VIDEO_STATUS: return 0;
case VIDEO_STATUS: {
uint8_t status = 0;
if (frameWaiting) {
status |= VIDEO_STATUS_FRAME;
}
if (frameInterrupts) {
status |= VIDEO_STATUS_INTERRUPT;
}
// Looking is what answers it. A frame that has been noticed is not still
// waiting to be, and a program polling in a loop would otherwise see the first
// frame for ever.
frameWaiting = 0;
return status;
}
case VIDEO_CONTROL:
// Write only. Everything it sets is reported by the status port, and one fact
// wants one place to live.
return 0;
case VIDEO_MODE: return mode;
// Asked rather than assumed. A program that wants to know how wide the screen is
// should be able to find out, the same way it asks the console what mode it is in.
+26
View File
@@ -82,6 +82,32 @@
#define VIDEO_COLUMNS 0x32
#define VIDEO_ROWS 0x33
#define VIDEO_SCROLL 0x34
#define VIDEO_CONTROL 0x35
// ---- The frame ----
//
// A screen finishes drawing sixty times a second and then has a moment before it starts
// again, and that moment is the one safe time to change what it is drawing. It is also the
// only regular beat this machine has: there is no clock, and every program that wanted to
// happen at a certain speed has until now counted instructions and hoped.
//
// Sixty a second at a megahertz. On the MACHINE'S clock rather than the host's, so a program
// runs the same number of frames in the same number of cycles however fast anything really
// went - which is what makes a frame something a test can count.
#define VIDEO_FRAME_CYCLES 16667
// Set when a frame has gone by, and cleared by reading the status port. A program with no
// handler installed can wait on this instead, the way a program can poll the console rather
// than being interrupted by it.
#define VIDEO_STATUS_FRAME 0x01
// Whether the screen is set to interrupt, so that a program can ask what it asked for.
#define VIDEO_STATUS_INTERRUPT 0x02
// Asks to be interrupted at each frame, on hardware vector 0x30. OFF WHEN THE MACHINE
// STARTS, because an interrupt with nothing installed to catch it is a fault, and a machine
// that began interrupting the moment it was switched on would take any program that had not
// thought about frames down with it.
#define VIDEO_CONTROL_FRAME 0x01
void videoReset(void);