Give the memory controller to a bus rather than to the machine

The third and largest piece of the peripheral core, and like the two before it nothing
changes: 169 tests, and the two that would notice a misroute pass loudest of all.

A CONTROLLER IS THE MOST STATEFUL THING ON THIS MACHINE - a source bank and address, a
destination, a length, five guard registers and a table describing every bank it can reach.
Two processors sharing one would interleave into nonsense: one sets a source, the other sets
a destination, the first issues a blit and moves the wrong bytes somewhere else again. No
arbitration fixes that, because there is nothing to arbitrate - both writes were legal and
the result belongs to neither of them.

So it is a struct threaded through all seventeen functions that touch it, rather than a
pointer to a current one set on the way in. The smaller change was tempting and is the shape
that produced the stale reset flag and the editor's IsNew surviving a second run, both this
week: state that has to be set on the right path before anything reads it. A controller is
where that goes wrong quietly rather than loudly.

THE STATICS WERE DELETED RATHER THAN LEFT, which is what made this safe to do mechanically.
A missed reference is a compile error rather than a variable that still exists and serves the
wrong core, so "did I catch all 165?" stopped being a judgement and became a question the
compiler answered.

Two things the transformation nearly got wrong, both caught by reading rather than by
building. guardStart and guardEnd are fields of Bank as well as registers of the controller,
so banks[n].guardStart had to keep its name while a bare guardStart changed - the difference
between a fence and the register about to be written into one. And a definition and a call
look alike enough that the first attempt turned publishBank(number) into
publishBank(Controller *c, number); definitions start at column zero here and calls never do.

Tests/agree.sh is what says this is right, more than the count does. It builds the same disk
with SplitDisk and with CosmOS and compares byte for byte, and every filesystem operation on
the machine goes through the controller - so a blit that went to the wrong place would corrupt
a disk that is checked against one built by different code entirely. Tests/cycles.sh covers
the other half, since what a transfer costs depends on pendingCycles landing in the right one.

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 17:32:10 -04:00
co-authored by Claude Opus 5
parent 5be5bea994
commit 33afc20abc
5 changed files with 219 additions and 180 deletions
+23 -3
View File
@@ -626,6 +626,26 @@ static uint8_t consoleStatus(void) {
// entire reason these are a struct rather than an array sitting here.
static InterruptLines machineLines;
// And its own controller, for the same reason. Everything on this bus that moves memory means
// this one.
static Controller theMachinesController;
Controller *machineController(void) {
return &theMachinesController;
}
static uint8_t machineControllerWrite(uint8_t value, uint8_t port) {
return controllerWrite(&theMachinesController, value, port);
}
static uint8_t machineControllerRead(uint8_t port) {
return controllerRead(&theMachinesController, port);
}
static unsigned long machineControllerCycles(void) {
return controllerTakeCycles(&theMachinesController);
}
// Whether somebody has asked the machine to start over, and taking that request away.
static int resetWanted = 0;
@@ -659,7 +679,7 @@ int takeResetRequest(void) {
static const Bus theMachinesBus = {
OutputHandler,
InputHandler,
controllerTakeCycles,
machineControllerCycles,
takeIdleCycles,
nextPendingInterrupt,
clearInterrupt,
@@ -1001,7 +1021,7 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
refusedPort = Address;
// The controller answers on a block of ports, which is a range rather than a list.
if (Address >= CONTROLLER_PORT_BASE && Address <= CONTROLLER_PORT_TOP) {
return controllerWrite(DataByte, Address);
return machineControllerWrite(DataByte, Address);
}
// And so does the screen.
if (Address >= PORT_VIDEO && Address <= PORT_VIDEO_TOP) {
@@ -1096,7 +1116,7 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
uint8_t InputHandler(uint8_t Address) {
refusedPort = Address;
if (Address >= CONTROLLER_PORT_BASE && Address <= CONTROLLER_PORT_TOP) {
return controllerRead(Address);
return machineControllerRead(Address);
}
if (Address >= PORT_VIDEO && Address <= PORT_VIDEO_TOP) {
return videoRead(Address);