The font comes from a chip, not from RAM that remembers

videoReset zeroed video memory and then wrote the font and the sixteen colour
schemes into it, and the comment above that said out loud what was wrong with
it: "everything here is ordinary video memory". RAM does not wake up with
anything in it. That was the last piece of magic in this device, and it looked
harmless until something wanted the font BACK - a program that redefines a glyph
had destroyed the only copy there was.

So the device has a character generator, the way the machines this one is
pretending to be really did, and the copy into RAM is a thing it DOES rather
than a state it mysteriously starts in. Command port 0x39: bit 0 for the font,
bit 1 for the schemes.

THE RAM IS STILL RAM. A program may overwrite every glyph and every colour and
should be able to, which is what makes this a tile engine rather than a text
display. What changed is that it is no longer a one way door.

NEITHER COMMAND CLEARS WHAT IT DOES NOT OWN. The font used to clear the whole of
tile memory before writing itself, which was harmless while it happened only at
reset and is wrong the moment a program can ask: a program that defined a tile
of its own and then wanted its text back would have paid for it with the tile.

The reason it is a chip rather than a file on the disk, which was the other
candidate: the boot chain prints before CosmOS exists. Stage one prints "?" when
there is nothing to boot, and if the font came off the disk then the message
about the disk having failed would be the one thing that could not be drawn. A
system that wants its own font still loads one over the top - the ROM is the
floor, not the policy.

Two things that had been worked around now simply work. The shell asks for both
whenever a program exits, so a program that redefined a letter no longer leaves
it unable to spell, and Grid no longer needs to have saved the screen to avoid
handing back green text on blue. And the fault screen asks for the glyphs first,
because a message spelled in somebody's tile graphics is no message at all.

Five video checks. Two runs each for the font and the schemes, since the map
holds a tile NUMBER and the glyph is looked up when the frame is drawn - so
restoring changes every cell using it, including ones drawn before, and what the
two runs differ by is the command. The third guards the decision not to clear:
tile 200 has to survive the font coming back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-01 16:21:24 -04:00
co-authored by Claude Opus 5
parent 925388c2f2
commit f97d15de08
7 changed files with 207 additions and 8 deletions
+7
View File
@@ -633,6 +633,13 @@ returned to the prompt, and the program is recorded as having stopped rather tha
A fault *below* where programs load is the system's own code, and there is nothing to go back A fault *below* where programs load is the system's own code, and there is nothing to go back
to. That one says so and stops. to. That one says so and stops.
**The glyphs and the colours come back too.** A program is as free to redefine a letter as
any other tile, so one that did has left the shell unable to spell, and one that wrote its
own palette used to hand back green text on blue. Both were permanent until the video device
grew a character generator to ask. The shell asks for both whenever a program exits, before
restoring a saved screen, so a program that saved one still gets back what was actually on
it and a program that saved nothing at least leaves something readable.
**The screen is put back into a mode text can be seen in first**, and that is the part that **The screen is put back into a mode text can be seen in first**, and that is the part that
matters most rather than the part that is prettiest. A program that faulted in bitmap mode matters most rather than the part that is prettiest. A program that faulted in bitmap mode
left the console with no text rows at all, so it draws nothing - the message about what went left the console with no text rows at all, so it draws nothing - the message about what went
+20
View File
@@ -1442,6 +1442,12 @@ faultScreen:
; have left every ink the same as every paper. Attribute one draws in palette entries 16 ; have left every ink the same as every paper. Attribute one draws in palette entries 16
; and 17, so those two are written and the rest of the program's colours are left alone - ; and 17, so those two are written and the rest of the program's colours are left alone -
; there is nothing to be gained here by taking away more than is needed. ; there is nothing to be gained here by taking away more than is needed.
; The glyphs first, since a program is as free to redefine a letter as any other tile and
; a message spelled in somebody's tile graphics is no message at all. It touches only the
; console's own tiles, so a program's are left where they are.
INIA 0x01
OUTA 0x39
CALL screenBank CALL screenBank
INIA 0d4 INIA 0d4
OUTA 0xE3 OUTA 0xE3
@@ -3177,6 +3183,20 @@ screenSane:
RSTA RSTA
OUTA 0x37 OUTA 0x37
OUTA 0x38 ; No fraction of a cell in either direction. OUTA 0x38 ; No fraction of a cell in either direction.
; ---- And glyphs and colours that can be read ----
;
; A program may redefine any tile and any colour, and the font lives in tiles like anything
; else - so a program that redefined a letter has left the shell unable to spell, and one
; that wrote its own palette handed back green text on blue. Both used to be permanent:
; there was nowhere to get the originals from, because the only copy was the one that had
; been drawn over. The device has a character generator now and this is what it is for.
;
; BEFORE screenGive, like everything else here, so that a program which SAVED the screen
; gets back what was actually on it rather than what a clean machine looks like. This is
; the floor for the programs that saved nothing.
INIA 0x03
OUTA 0x39
RET RET
; Video memory as bank 4, which is what makes it reachable at all. Bank 3 is the disk's; see ; Video memory as bank 4, which is what makes it reachable at all. Bank 3 is the disk's; see
+7 -1
View File
@@ -37,6 +37,10 @@
#Program #Program
#Include services.asm
#Base 0x4000
start: start:
; ---- The instrument ---- ; ---- The instrument ----
@@ -182,7 +186,7 @@ lastRing:
CIF CIF
RSTA RSTA
OUTA 0x35 OUTA 0x35
HALT SWI osExit
; Sixty times a second, and it has nothing to do. WAIT only needs something to have happened, ; Sixty times a second, and it has nothing to do. WAIT only needs something to have happened,
; and this is the something. A handler still has to exist: an interrupt with nothing installed ; and this is the something. A handler still has to exist: an interrupt with nothing installed
@@ -192,6 +196,8 @@ frame:
#Data #Data
#Base 0x2000
; ---- Notes and how long they last ---- ; ---- Notes and how long they last ----
; ;
; Pairs: a MIDI note, then a count of frames. 60 is middle C and every 12 is an octave. A zero ; Pairs: a MIDI note, then a count of frames. 60 is middle C and every 12 is an octave. A zero
+39 -7
View File
@@ -119,11 +119,15 @@ void videoTick(unsigned long now) {
} }
void videoLoadFont(void) { void videoLoadFont(void) {
// One bit a pixel becomes one byte a pixel: index 1 where the font has a dot and 0 // One bit a pixel becomes one byte a pixel: index 1 where the font has a dot and 0 where
// where it does not, which is what makes the two palette entries below mean ink and // it does not, which is what makes the two palette entries of a scheme mean ink and
// paper. Glyphs the font does not have are left blank rather than left as whatever was // paper.
// in tile memory. //
memset(videoRAM + VIDEO_TILE_BASE, 0, (size_t)VIDEO_TILE_COUNT * VIDEO_TILE_BYTES); // ONLY THE GLYPHS THE ROM HAS. Tile memory used to be cleared first, on the grounds that
// a glyph the font does not have should be blank rather than whatever was there - which
// was fine while this happened at reset and nothing else, and is wrong now that a program
// can ask for it. A program that defined a tile of its own above the font and then wanted
// its text back would have lost the tile to get it.
for (int glyph = 0; glyph < CONSOLE_FONT_GLYPHS && glyph < VIDEO_TILE_COUNT; glyph++) { for (int glyph = 0; glyph < CONSOLE_FONT_GLYPHS && glyph < VIDEO_TILE_COUNT; glyph++) {
uint8_t *tile = videoRAM + VIDEO_TILE_BASE + glyph * VIDEO_TILE_BYTES; uint8_t *tile = videoRAM + VIDEO_TILE_BASE + glyph * VIDEO_TILE_BYTES;
for (int y = 0; y < CONSOLE_FONT_BYTES; y++) { for (int y = 0; y < CONSOLE_FONT_BYTES; y++) {
@@ -133,6 +137,9 @@ void videoLoadFont(void) {
} }
} }
} }
}
void videoLoadPalette(void) {
uint8_t *palette = videoRAM + VIDEO_PALETTE_BASE; uint8_t *palette = videoRAM + VIDEO_PALETTE_BASE;
for (int bank = 0; bank < 8; bank++) { for (int bank = 0; bank < 8; bank++) {
// Colour on black, and then the same colour as paper with black ink, sixteen banks // Colour on black, and then the same colour as paper with black ink, sixteen banks
@@ -187,9 +194,11 @@ void videoReset(void) {
frameWaiting = 0; frameWaiting = 0;
frameInterrupts = 0; frameInterrupts = 0;
clearInterrupt(PORT_VIDEO); clearInterrupt(PORT_VIDEO);
// A machine wakes up able to show text. Everything here is ordinary video memory that a // A machine wakes up able to show text, and it does so by COPYING from the character
// program may overwrite the moment it wants the screen for something else. // generator into ordinary video memory - which a program may overwrite the moment it
// wants the screen for something else, and can ask back afterwards.
videoLoadFont(); videoLoadFont();
videoLoadPalette();
} }
uint8_t *videoMemory(uint32_t *capacity) { uint8_t *videoMemory(uint32_t *capacity) {
@@ -235,6 +244,25 @@ uint8_t videoWrite(uint8_t value, uint8_t port) {
case VIDEO_FINE_Y: case VIDEO_FINE_Y:
fineY = (uint8_t)(value & VIDEO_FINE_MASK); fineY = (uint8_t)(value & VIDEO_FINE_MASK);
break; break;
case VIDEO_COMMAND:
// ---- Asking the character generator for its contents ----
//
// Written, and it happens at once - the same shape as the console's Command port
// and the controller's, rather than a bit in a register that otherwise holds
// state. There is nothing to read back: what a copy did is visible in the memory
// it copied into.
//
// IN BITMAP MODE THE TILES ARE THE PICTURE, so asking for the font there draws
// glyphs across the top of it. That is not a special case being ignored; it is
// what the memory means in that mode, and a caller that wants text has to be in
// a mode that has some.
if (value & VIDEO_COMMAND_FONT) {
videoLoadFont();
}
if (value & VIDEO_COMMAND_PALETTE) {
videoLoadPalette();
}
break;
default: default:
// Everything else is read only or not there yet. Writing does nothing rather // Everything else is read only or not there yet. Writing does nothing rather
// than refusing: a port block reserved for later should be quiet, not fatal. // than refusing: a port block reserved for later should be quiet, not fatal.
@@ -272,6 +300,10 @@ uint8_t videoRead(uint8_t port) {
case VIDEO_SCROLL_COLUMN: return scrollColumn; case VIDEO_SCROLL_COLUMN: return scrollColumn;
case VIDEO_FINE_X: return fineX; case VIDEO_FINE_X: return fineX;
case VIDEO_FINE_Y: return fineY; case VIDEO_FINE_Y: return fineY;
case VIDEO_COMMAND:
// Write only, like the console's. A device that does something when told does
// not take instructions and hand out state through the same hole.
return 0;
// Asked rather than assumed. A program that wants to know how wide the screen is // 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. // should be able to find out, the same way it asks the console what mode it is in.
case VIDEO_COLUMNS: return (uint8_t)columnsFor(mode); case VIDEO_COLUMNS: return (uint8_t)columnsFor(mode);
+34
View File
@@ -124,6 +124,31 @@ int videoTextRows(void);
#define VIDEO_FINE_X 0x37 #define VIDEO_FINE_X 0x37
#define VIDEO_FINE_Y 0x38 #define VIDEO_FINE_Y 0x38
// ---- The character generator, which is a chip and not a memory that remembers ----
//
// The font and the sixteen colour schemes used to be WRITTEN INTO VIDEO RAM at reset, and
// that was the one piece of magic left in this device: RAM does not wake up with anything
// in it. It looked harmless until something needed the font BACK - a program that redefines
// a tile has overwritten a glyph, and there was nowhere to get it from, because the only
// copy was the one that had just been drawn over.
//
// So the device has a ROM, the way the machines this one is pretending to be really did,
// and the copy into RAM is a thing it DOES rather than a state it mysteriously starts in.
// Reset performs it, and a program can ask for it again.
//
// THE RAM IS STILL RAM. A program may overwrite every glyph and every colour, and should be
// able to: that is what makes a tile engine a tile engine rather than a text display. What
// has changed is that doing so is no longer a one way door.
//
// A SYSTEM THAT WANTS ITS OWN FONT still loads one over the top. This is the floor, not the
// policy - it is what makes a machine with no disk able to say so, and what lets a program
// with no system behind it put readable text on a screen.
#define VIDEO_COMMAND 0x39
// Copy the 135 glyphs back into the tiles they live in, leaving every other tile alone.
#define VIDEO_COMMAND_FONT 0x01
// Copy the sixteen ink and paper pairs back, leaving the rest of the palette alone.
#define VIDEO_COMMAND_PALETTE 0x02
// Eight pixels to a cell, so three bits say where inside one the view begins. // Eight pixels to a cell, so three bits say where inside one the view begins.
#define VIDEO_FINE_MASK 0x07 #define VIDEO_FINE_MASK 0x07
@@ -163,7 +188,16 @@ void videoReset(void);
// //
// The font is expanded into tile memory at reset rather than stored expanded: 1,088 bytes // The font is expanded into tile memory at reset rather than stored expanded: 1,088 bytes
// of one-bit rows against 16 kilobytes of tiles. // of one-bit rows against 16 kilobytes of tiles.
// Copies the character generator ROM into the tiles the console draws from, and the default
// schemes into the palette entries they live in. Reset does both; the Command port is how a
// program asks for either afterwards.
//
// NEITHER CLEARS WHAT IT DOES NOT OWN. The font writes glyphs 0 to 134 and stops, so a tile
// a program defined above them survives; the palette writes the two entries of each of the
// sixteen schemes and stops, so a program's own colours in between survive. Asking for the
// font back should not cost a program the tile it was drawing with.
void videoLoadFont(void); void videoLoadFont(void);
void videoLoadPalette(void);
// ---- The cursor ---- // ---- The cursor ----
// //
+18
View File
@@ -735,6 +735,24 @@ The alternative was to let a write of 8 step the column and set the fine part to
**None of the four does anything in bitmap mode**, which has no map to slide. **None of the four does anything in bitmap mode**, which has no map to slide.
### The Character Generator:
The font and the sixteen colour schemes come from a **ROM in the device**. Reset copies them into video memory, and Command port `0x39` copies them again on request:
| Port | Register |
| --- | --- |
| 0x39 | Command. Bit 0 asks for the font back, bit 1 for the sixteen schemes. Write only. |
**This used to be magic and now is not.** The glyphs were written into video RAM at reset and existed nowhere else, which looked harmless until something wanted the font *back*: RAM does not wake up with anything in it, and a program that redefined a glyph had destroyed the only copy there was. A machine with a character generator is what the machines this one is pretending to be actually had, and the copy into RAM is now a thing the device **does** rather than a state it mysteriously starts in.
The RAM is still RAM. A program may overwrite every glyph and every colour and should be able to - that is what makes this a tile engine rather than a text display. What changed is that doing so is no longer a one way door.
**Neither command clears what it does not own.** The font writes the 135 glyphs it has and stops, so a tile a program defined above them survives; the schemes write the two entries of each of the sixteen and stop, so a program's own colours in between survive. Asking for the font back must not cost a program the tile it was drawing with.
In bitmap mode the tiles *are* the picture, so asking for the font there draws glyphs across the top of it. That is not a case being ignored: it is what the memory means in that mode.
A system that wants a different font still loads one over the top. The ROM is the floor rather than the policy - it is what lets a machine with no disk say that it has no disk, and what lets a program with no system behind it put readable text on a screen.
### Writing On The Screen: ### Writing On The Screen:
A console on a machine with a screen sends every byte to both, because a machine with a screen and a serial line is an ordinary machine and there is one console driving both. A console on a machine with a screen sends every byte to both, because a machine with a screen and a serial line is an ordinary machine and there is one console driving both.
+82
View File
@@ -73,6 +73,16 @@ poke() {
$(( ($1 >> 8) & 0xFF )) $(( $1 & 0xFF )) $(( $2 & 0xFF )) $(( ($1 >> 8) & 0xFF )) $(( $1 & 0xFF )) $(( $2 & 0xFF ))
} }
# Many bytes from one address, using the Data port's own stepping rather than naming the
# address again for each. What a run of tile memory is for.
pokeRun() {
# pokeRun <address> <byte> <count>
printf ' INIA 0x%02X\n OUTA 0xE4\n INIA 0x%02X\n OUTA 0xE5\n INIA 0x%02X\n' \
$(( ($1 >> 8) & 0xFF )) $(( $1 & 0xFF )) $(( $2 & 0xFF ))
local i
for (( i = 0; i < $3; i++ )); do printf ' OUTA 0xE9\n'; done
}
port() { port() {
# port <port> <byte> # port <port> <byte>
printf ' INIA 0x%02X\n OUTA 0x%02X\n' $(( $2 & 0xFF )) $(( $1 & 0xFF )) printf ' INIA 0x%02X\n OUTA 0x%02X\n' $(( $2 & 0xFF )) $(( $1 & 0xFF ))
@@ -911,6 +921,78 @@ inked scrollback 2 1 \
&& result ok "what scrolled off is still there" "the origin went back and found it" \ && result ok "what scrolled off is still there" "the origin went back and found it" \
|| result no "what scrolled off is still there" "nothing at 2,1" || result no "what scrolled off is still there" "nothing at 2,1"
# ---- The character generator is a chip, not a memory that remembers ----
#
# The font and the sixteen schemes used to be written into video RAM at reset and existed
# nowhere else, so a program that overwrote a glyph had destroyed the only copy. They come
# from a ROM in the device now, and the Command port asks for either back.
#
# TWO RUNS RATHER THAN ONE PICTURE, because the map holds a tile NUMBER and the glyph is
# looked up when the frame is drawn - so restoring the font changes every cell using it,
# including the ones drawn before. What the two runs differ by is the command.
#
# The glyph for 'A' is filled with ink, which makes the cell a solid block, so the top left
# pixel of it is ink where a real 'A' has paper. That is a pixel no font disagrees about.
{ prologue
pokeRun 0x0840 0x01 64 # Tile 33, which is 'A', every pixel ink.
say "A"
epilogue
} | run fontwrecked || exit 1
[ "$(pixel fontwrecked 0 0)" = "216,216,216" ] \
&& result ok "a program can overwrite a glyph" "the wrecked A is a solid block" \
|| result no "a program can overwrite a glyph" "not ink at 0,0"
{ prologue
pokeRun 0x0840 0x01 64
port 0x39 0x01 # And ask the character generator for it back.
say "A"
epilogue
} | run fontback || exit 1
[ "$(pixel fontback 0 0)" = "0,0,0" ] \
&& result ok "and ask the device for it back" "the A has its own shape again" \
|| result no "and ask the device for it back" "still ink at 0,0"
# ---- And asking does not cost a program the tiles it defined ----
#
# The font used to clear the whole of tile memory before writing itself, which was harmless
# while it only happened at reset and is wrong the moment a program can ask for it: a program
# that defined a tile of its own and then wanted its text back would have paid for it with
# the tile. It writes the glyphs it has and stops.
{ prologue
pokeRun 0x3200 0x01 64 # Tile 200, well above anything the font occupies.
poke 0x4000 0xC8 # And that tile in the first cell of the map.
poke 0x4001 0x00
port 0x39 0x03 # Both the font and the palette back.
epilogue
} | run fontkeeps || exit 1
[ "$(pixel fontkeeps 0 0)" = "216,216,216" ] \
&& result ok "and leaves a program's own tiles alone" "tile 200 survived the font coming back" \
|| result no "and leaves a program's own tiles alone" "tile 200 was cleared"
# ---- The palette the same way ----
#
# Ink and paper made the same colour is a screen with writing on it that cannot be read,
# which is exactly what the fault screen has to survive. The device is asked for the sixteen
# schemes back and the writing returns.
{ prologue
poke 0xFC04 0x00; poke 0xFC05 0x00; poke 0xFC06 0x00 # Scheme 0's ink, made black.
say "A"
epilogue
} | run inkwrecked || exit 1
[ "$(pixel inkwrecked 3 1)" = "0,0,0" ] \
&& result ok "a program can overwrite a scheme" "ink and paper are the same colour" \
|| result no "a program can overwrite a scheme" "the A is still visible"
{ prologue
poke 0xFC04 0x00; poke 0xFC05 0x00; poke 0xFC06 0x00
port 0x39 0x02 # The schemes back, and only the schemes.
say "A"
epilogue
} | run inkback || exit 1
[ "$(pixel inkback 3 1)" = "216,216,216" ] \
&& result ok "and ask the device for those back too" "the A can be read again" \
|| result no "and ask the device for those back too" "still nothing at 3,1"
# ---- The fault screen, from a screen with nowhere to print on it ---- # ---- The fault screen, from a screen with nowhere to print on it ----
# #
# Every other test of the fault screen reads what came down the serial line, and the serial # Every other test of the fault screen reads what came down the serial line, and the serial