Make the cursor blink while the machine is waiting, and show how the palette works

THE CURSOR DID NOT BLINK, and the reason is worth stating: it blinks on the machine's clock,
and the machine's clock had stopped. A console waiting on a key stops the CPU, so no cycles
passed, so the phase never moved - and the one moment somebody is looking at a cursor is the
moment they are being asked to type.

Waiting is now charged as IDLE CYCLES, which is what they were built for: a machine stopped
on a device is not using memory, the same distinction WAIT makes, arrived at from the other
direction. And the devices are told as it happens rather than when the instruction finally
finishes, because a display controller does not stop blinking because the processor is
waiting on a keyboard, any more than a disk stops turning.

A keyboard file can now say NOTHING happened. A zero is a byte no keyboard sends, so it is
free to mean "a moment went by with nobody typing" - which is the commonest thing behind a
window and the only thing a file otherwise could not express. That unlocked the whole waiting
path: three checks that the cursor is lit, then dark half a second later, then lit again,
which is what blinking is.

And Programs/Examples/colours.asm, because the palette had nowhere a newcomer could read it.
It prints the sixteen pairs, prints each one again turned inside out, and then CHANGES ONE by
writing three bytes into the palette - so the difference between using the colours a machine
wakes up with and choosing your own is visible in one program. Its header explains what a
cell is, what the attribute nibble does, why palette entries are four bytes rather than
three, and why video memory has to be reached through the controller.

The manual now says where the palette lives and points at it.

SplitLint found a redundant RSTA in the example, which was worth acting on rather than
suppressing: the zero was already in A from the mode write two lines up, and saying so in a
comment teaches that SETD does not touch A, which is a thing worth knowing.

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 08:26:15 -04:00
co-authored by Claude Opus 5
parent d6feddd1b6
commit ff4b025058
11 changed files with 268 additions and 5 deletions
+4
View File
@@ -48,6 +48,10 @@ static inline uint8_t portIn(CPURegisters *cpu, uint8_t port) {
cpu->busCycles++;
uint8_t value = InputHandler(port);
cpu->busCycles += controllerTakeCycles();
// And whatever time went by while the device kept the machine waiting. Idle rather than
// bus, because a machine stopped on a port is not using memory - the same distinction
// WAIT makes, arrived at from the other direction.
cpu->idleCycles += takeIdleCycles();
return value;
}
+33 -2
View File
@@ -341,6 +341,34 @@ static void consoleDraw(uint8_t byte) {
// when one is typed, or -1 to say the window has gone.
static int (*inputHook)(int mayWait) = NULL;
// About one frame, which is how long a hook that presents takes to come back. It does not
// have to be exact - nothing is being measured, and the only thing downstream of it is a
// cursor blinking at somebody who is thinking about what to type.
#define CONSOLE_WAIT_CYCLES 16667
static unsigned long idleCycles = 0;
// The machine's clock as the devices last heard it. Kept here rather than passed about,
// because a device that has to know how long it has been waiting has to know what time it
// is now.
static unsigned long deviceNow = 0;
// A frame has gone by with nobody typing. TWO THINGS FOLLOW FROM THAT, and only doing the
// first is what left the cursor frozen: the CPU is told afterwards that it was stopped for a
// while, which is what idle cycles are - and the DEVICES are told now, because they are
// still running. A display controller blinking a cursor does not stop because the processor
// is waiting on a key, and neither does a disk finishing a read.
static void consoleWaited(void) {
idleCycles += CONSOLE_WAIT_CYCLES;
deviceTick(deviceNow + CONSOLE_WAIT_CYCLES);
}
unsigned long takeIdleCycles(void) {
const unsigned long taken = idleCycles;
idleCycles = 0;
return taken;
}
// ---- Line editing, which the terminal used to do ----
//
// A TERMINAL IN LINE MODE DOES NOT HAND A PROGRAM EVERY KEYSTROKE. It collects a line,
@@ -369,6 +397,8 @@ static int consoleGatherLine(void) {
return 0;
}
if (got < 0) {
// Nobody has typed yet, and the hook spent a frame keeping the window alive.
consoleWaited();
continue;
}
if (got == 0x08) {
@@ -436,7 +466,9 @@ uint8_t consoleReadByte(void) {
consoleEnded = 1;
return 0xFF;
}
// Nothing typed yet. The hook kept the window alive; ask it again.
// Nothing typed yet. The hook kept the window alive, which took a frame, and a
// frame of waiting is a frame of time passing.
consoleWaited();
}
}
unsigned char byte;
@@ -660,7 +692,6 @@ static uint8_t diskBuffer[DISK_BLOCK_BYTES];
// what a program that ignores the busy bit deserves to read.
// The machine's clock as devices see it, which the emulator advances as the CPU spends
// cycles. A device says when it will be finished in these, and is believed.
static unsigned long deviceNow = 0;
static void diskTransfer(uint8_t command);
static unsigned long diskLatency = 0;
+12
View File
@@ -250,6 +250,18 @@ void consoleSetInputHook(int (*hook)(int mayWait));
// display that refreshes, a port that waits on the host - wants exactly this shape.
void deviceTick(unsigned long now);
// ---- Time that passed while the machine was stopped ----
//
// A console waiting on a key it has not been given has stopped the machine, and time is
// still going by: the cursor still blinks, a disk still turns. That is exactly what idle
// cycles are for, and without them the machine's clock froze the moment somebody was asked
// a question - so the cursor stopped blinking precisely when there was a person looking at
// it and waiting to type.
//
// Returned and cleared, the way the controller's cycles are, and picked up in the same
// place: after a port access, by the CPU that was stopped.
unsigned long takeIdleCycles(void);
// How many cycles a block read or write takes. Zero means the answer is there before the
// next instruction is, which is what this machine has always done and what every recorded
// test assumes.
+10
View File
@@ -95,6 +95,16 @@ static int keyboardHook(int mayWait) {
if (byte == EOF) {
return CONSOLE_GONE;
}
// ---- A zero is a moment of nobody typing ----
//
// The commonest thing that happens behind a window is NOTHING: sixty times a second the
// console asks and is told to come back later, and everything that goes on while that is
// true - the clock advancing, a cursor blinking, a disk finishing - was unreachable from
// here, because a file always has another byte. A zero is a byte no keyboard sends, so it
// is free to mean the one thing a file otherwise cannot say.
if (byte == 0x00) {
return CONSOLE_NOTHING_YET;
}
return byte & 0xFF;
}