Compare commits
62
Commits
+12
@@ -8,6 +8,7 @@
|
|||||||
/SplitBit
|
/SplitBit
|
||||||
/SplitDisk
|
/SplitDisk
|
||||||
/SplitLint
|
/SplitLint
|
||||||
|
/Voyager
|
||||||
/CLAUDE.md
|
/CLAUDE.md
|
||||||
/claudeResume.sh
|
/claudeResume.sh
|
||||||
/codexResume.sh
|
/codexResume.sh
|
||||||
@@ -28,3 +29,14 @@ __pycache__/
|
|||||||
# Kate leaves these beside a file it has open.
|
# Kate leaves these beside a file it has open.
|
||||||
.*.kate-swp
|
.*.kate-swp
|
||||||
Source/Emulator/rom.c
|
Source/Emulator/rom.c
|
||||||
|
|
||||||
|
# Other people's repositories, kept here to be read rather than to be built. soundThing is
|
||||||
|
# where the synthesizer comes from and Hatchet-GPU is where the font and the scroll register
|
||||||
|
# idea come from, and neither belongs to this one: they carry their own .git directories,
|
||||||
|
# their own build outputs and their own compiled binaries. What this repository takes from
|
||||||
|
# them gets vendored deliberately, with a note saying where it came from.
|
||||||
|
/External/
|
||||||
|
|
||||||
|
# The personal disk 'make run-voyager' puts in drive 1. Made on demand, never rebuilt, and
|
||||||
|
# not cleaned - it is the one place in here where something made ON the machine can live.
|
||||||
|
/Disks/
|
||||||
|
|||||||
@@ -54,8 +54,15 @@
|
|||||||
#Base 0x4000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
|
; ---- Room for a PATH, not a name ----
|
||||||
|
;
|
||||||
|
; Sixty-three, which was twenty-three. That was the right size when everything lived in
|
||||||
|
; the root and a name was a name. The disk now carries the whole source tree, so
|
||||||
|
; "/Source/CosmOS/Assembler/classify.asm" is an ordinary thing to type - thirty-seven
|
||||||
|
; characters, and it was being cut down to a name that meant something else, or nothing.
|
||||||
|
; The shell reads sixty-three characters of command line, so nothing longer can arrive.
|
||||||
SETD.0 FileName
|
SETD.0 FileName
|
||||||
INIB 0d23
|
INIB 0d63
|
||||||
SWI osArgument
|
SWI osArgument
|
||||||
SETD.0 FileName
|
SETD.0 FileName
|
||||||
LDA.0
|
LDA.0
|
||||||
@@ -78,6 +85,14 @@ start:
|
|||||||
RSTA
|
RSTA
|
||||||
SETD.0 TooLong
|
SETD.0 TooLong
|
||||||
STA.0
|
STA.0
|
||||||
|
; ---- Cleared here rather than in the Data Segment ----
|
||||||
|
;
|
||||||
|
; A program is loaded once and may be run many times: "load Edit.sbx" then "run" twice is
|
||||||
|
; two sessions over one copy of the data, so anything a session changes has to be put back
|
||||||
|
; by the session and not by the loader. A zero written in the Data Segment is the state
|
||||||
|
; this program starts in the FIRST time and never again.
|
||||||
|
SETD.0 IsNew
|
||||||
|
STA.0
|
||||||
CALL loadFile
|
CALL loadFile
|
||||||
|
|
||||||
SETD.0 TooLong
|
SETD.0 TooLong
|
||||||
@@ -88,6 +103,17 @@ start:
|
|||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
SETD.0 CommaText
|
SETD.0 CommaText
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
|
|
||||||
|
; A file that was not there is a new document, which this is the way to start - but say so,
|
||||||
|
; rather than saying nothing and letting it look like an empty one that already existed.
|
||||||
|
SETD.0 IsNew
|
||||||
|
LDA.0
|
||||||
|
BRA sayCount
|
||||||
|
SETD.0 NewText
|
||||||
|
SWI osPrintString
|
||||||
|
CALL newLine
|
||||||
|
BRI announced
|
||||||
|
sayCount:
|
||||||
CALL countLines
|
CALL countLines
|
||||||
MVQA
|
MVQA
|
||||||
CALL printByte
|
CALL printByte
|
||||||
@@ -102,6 +128,7 @@ oneLine:
|
|||||||
sayLines:
|
sayLines:
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
CALL newLine
|
CALL newLine
|
||||||
|
announced:
|
||||||
|
|
||||||
commandLoop:
|
commandLoop:
|
||||||
SETD.0 PromptText
|
SETD.0 PromptText
|
||||||
@@ -578,7 +605,7 @@ loadFile:
|
|||||||
SETD.0 FileName
|
SETD.0 FileName
|
||||||
SETD.1 0x40 0x00
|
SETD.1 0x40 0x00
|
||||||
SWI osFileRead
|
SWI osFileRead
|
||||||
BNQ loadNothing
|
BNQ loadNew
|
||||||
|
|
||||||
; How many bytes came back. The service says so in DP3, which is one of the two things a
|
; How many bytes came back. The service says so in DP3, which is one of the two things a
|
||||||
; service is allowed to answer in, and a file that fits in memory has a length that fits
|
; service is allowed to answer in, and a file that fits in memory has a length that fits
|
||||||
@@ -677,13 +704,29 @@ splitLast:
|
|||||||
; A file that does not end in a newline still has a last line in it.
|
; A file that does not end in a newline still has a last line in it.
|
||||||
SETD.2 EntryLength
|
SETD.2 EntryLength
|
||||||
LDA.2
|
LDA.2
|
||||||
BRA loadNothing
|
BRA loadDone
|
||||||
RSTA
|
RSTA
|
||||||
STA.1
|
STA.1
|
||||||
SETD.0 Entry
|
SETD.0 Entry
|
||||||
CALL makeNode
|
CALL makeNode
|
||||||
CALL appendNode
|
CALL appendNode
|
||||||
loadNothing:
|
; ---- Where a load finishes ----
|
||||||
|
;
|
||||||
|
; NOT WHERE ONE FAILS, which the old name for this said and which is a different thing: every
|
||||||
|
; successful load arrives here too, once its last line has been dealt with. That is worth
|
||||||
|
; being plain about, because a flag set here on the strength of the name was set on every
|
||||||
|
; file that opened perfectly well.
|
||||||
|
loadDone:
|
||||||
|
RET
|
||||||
|
|
||||||
|
; A name that is not on the disk is a new document rather than a mistake, which is what makes
|
||||||
|
; this the way to start one. Remembered, because "0 lines" is what an empty file that IS on
|
||||||
|
; the disk also says, and a name typed slightly wrong looks exactly like the document you
|
||||||
|
; meant to open - right up until you save it somewhere new.
|
||||||
|
loadNew:
|
||||||
|
SETD.0 IsNew
|
||||||
|
INIA 0x01
|
||||||
|
STA.0
|
||||||
RET
|
RET
|
||||||
|
|
||||||
; DP3 is a node. Puts it on the end of the list.
|
; DP3 is a node. Puts it on the end of the list.
|
||||||
@@ -838,6 +881,8 @@ ColonText:
|
|||||||
": "
|
": "
|
||||||
CommaText:
|
CommaText:
|
||||||
", "
|
", "
|
||||||
|
NewText:
|
||||||
|
"new file"
|
||||||
LinesText:
|
LinesText:
|
||||||
" lines"
|
" lines"
|
||||||
LineText:
|
LineText:
|
||||||
@@ -861,8 +906,12 @@ NoWriteText:
|
|||||||
TooLongText:
|
TooLongText:
|
||||||
"a line in it is longer than this can edit, so it has not been opened"
|
"a line in it is longer than this can edit, so it has not been opened"
|
||||||
|
|
||||||
|
; Whether the name was not on the disk, which makes this a new document rather than one
|
||||||
|
; being opened. Zero until loadFile finds out otherwise.
|
||||||
|
IsNew:
|
||||||
|
0x00
|
||||||
FileName:
|
FileName:
|
||||||
#Reserve 0d24
|
#Reserve 0d64
|
||||||
Command:
|
Command:
|
||||||
#Reserve 0d41
|
#Reserve 0d41
|
||||||
; A hundred and twenty eight and the zero that ends it, which is what a line is everywhere
|
; A hundred and twenty eight and the zero that ends it, which is what a line is everywhere
|
||||||
|
|||||||
@@ -0,0 +1,455 @@
|
|||||||
|
; Grid, the first program to use the screen as a screen.
|
||||||
|
;
|
||||||
|
; Everything drawn on this machine so far has been text or a bitmap. The tile engine has
|
||||||
|
; been there since the screen was built and nothing had touched it: the console uses it,
|
||||||
|
; but only ever to put a letter in a cell, which is the one thing it can do that a plain
|
||||||
|
; character display could do too.
|
||||||
|
;
|
||||||
|
; This redefines a tile, fills a map bigger than the screen with it, and then scrolls that
|
||||||
|
; map by writing ONE BYTE A FRAME. No memory moves. The rows above and below the screen are
|
||||||
|
; already there, so what a scroll costs is not the 2,000 bytes of a screenful but the one
|
||||||
|
; byte that says which row is on top.
|
||||||
|
;
|
||||||
|
; ---- Where its tiles live ----
|
||||||
|
;
|
||||||
|
; At 200, and the font is why. The machine wakes with the font in tile memory - glyph n at
|
||||||
|
; tile n, for 135 of the 256 - so a program that starts writing tiles at zero paints over
|
||||||
|
; the alphabet and the shell it is going to hand the machine back to. Above 135 is empty and
|
||||||
|
; nobody else's, so nothing here has to be put back afterwards except the map.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
#Base 0x4000
|
||||||
|
|
||||||
|
start:
|
||||||
|
; ---- Reaching video memory ----
|
||||||
|
;
|
||||||
|
; The CPU cannot touch it. It belongs to the device, and the only way in is to give it a
|
||||||
|
; bank number and go through the memory controller - the same as the disk's buffer.
|
||||||
|
;
|
||||||
|
; FOUR, BECAUSE THREE IS THE DISK'S. Bank numbers are one namespace for the whole machine
|
||||||
|
; and nothing hands them out: 0 is Program Memory, 1 is Data, 2 is the bank table, and
|
||||||
|
; CosmOS gives 3 to the disk's buffer when it mounts. This asked for 3, which does not
|
||||||
|
; fail - it succeeds, and the disk's buffer quietly becomes the screen. Every read the
|
||||||
|
; filesystem made after that came out of video memory, so the shell found an empty disk
|
||||||
|
; and could not start anything by name. Nothing said a word.
|
||||||
|
INIA 0d4
|
||||||
|
OUTA 0xE3 ; DestBank: the number it will answer to.
|
||||||
|
INIA 0x30
|
||||||
|
OUTA 0xE2 ; SourceLow: the port of the device that owns it.
|
||||||
|
INIA 0x03
|
||||||
|
OUTA 0xE8 ; RegisterBank.
|
||||||
|
|
||||||
|
; ---- Asking for the screen back afterwards ----
|
||||||
|
;
|
||||||
|
; Everything below overwrites a tile, all sixteen colour schemes and every cell of the map,
|
||||||
|
; and none of that is this program's to keep. The system puts it somewhere and gives it back
|
||||||
|
; at exit - and if it says it cannot, this carries on anyway, because it did before there
|
||||||
|
; was anywhere to put it.
|
||||||
|
SWI osTakeScreen
|
||||||
|
MVQA
|
||||||
|
SETD.0 ScreenKept
|
||||||
|
STA.0 ; Nought means the system will put it back.
|
||||||
|
|
||||||
|
CALL putTile
|
||||||
|
CALL putPalette
|
||||||
|
CALL putMap
|
||||||
|
|
||||||
|
; Key mode, so that a key arrives when it is pressed rather than when Return is. Put back
|
||||||
|
; before this returns, and CosmOS puts it back too if a program forgets.
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x02
|
||||||
|
|
||||||
|
; ---- The loop ----
|
||||||
|
;
|
||||||
|
; A frame, then one pixel down and one across. This used to move a whole cell every fourth
|
||||||
|
; frame, because a cell was as fine as the screen could be moved - eight pixels at a time,
|
||||||
|
; which reads as the picture jumping rather than travelling.
|
||||||
|
everyFrame:
|
||||||
|
CALL waitFrame
|
||||||
|
|
||||||
|
; Anything typed ends it. Asked for, never waited for: the console holds the key until
|
||||||
|
; somebody wants it, so nothing pressed between frames is lost.
|
||||||
|
INA 0x01
|
||||||
|
INIB 0x01 ; READY
|
||||||
|
AND
|
||||||
|
BNQ finished
|
||||||
|
|
||||||
|
; ---- A pixel down, and the cell it belongs to ----
|
||||||
|
;
|
||||||
|
; Fine is the low three bits of the register and does not carry, so this does: eight steps
|
||||||
|
; inside the cell and then one step of the origin. The AND is both the wrap and the test -
|
||||||
|
; Q coming out as nought is exactly the moment the cell boundary was crossed.
|
||||||
|
SETD.0 FineDown
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
INIB 0x07
|
||||||
|
AND
|
||||||
|
STQ.0
|
||||||
|
OUTQ 0x38
|
||||||
|
BNQ stepAcross
|
||||||
|
|
||||||
|
; The map is 128 rows against a screen of 25 or 50, so the origin walks a ring: what leaves
|
||||||
|
; the top has not gone anywhere and comes back round.
|
||||||
|
SETD.0 OriginDown
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
INIB 0x7F
|
||||||
|
AND
|
||||||
|
STQ.0
|
||||||
|
OUTQ 0x34
|
||||||
|
|
||||||
|
stepAcross:
|
||||||
|
; And the same sideways, which is the axis that did not exist at all until now. There are
|
||||||
|
; 128 columns against the 80 shown, so this ring is shallower but it is the same ring.
|
||||||
|
SETD.0 FineAcross
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
INIB 0x07
|
||||||
|
AND
|
||||||
|
STQ.0
|
||||||
|
OUTQ 0x37
|
||||||
|
BNQ everyFrame
|
||||||
|
|
||||||
|
SETD.0 OriginAcross
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
INIB 0x7F
|
||||||
|
AND
|
||||||
|
STQ.0
|
||||||
|
OUTQ 0x36
|
||||||
|
BRI everyFrame
|
||||||
|
|
||||||
|
finished:
|
||||||
|
; The key that stopped it, taken so the shell does not find it waiting.
|
||||||
|
INA 0x00
|
||||||
|
|
||||||
|
; ---- Putting the screen back ----
|
||||||
|
;
|
||||||
|
; ---- What used to be here ----
|
||||||
|
;
|
||||||
|
; Four scroll registers put back, the whole map filled with spaces, the cursor sent home,
|
||||||
|
; and palette bank 0 written out by hand - and it was STILL wrong, because the other fifteen
|
||||||
|
; banks kept this program's colours and there was nowhere to have put the real ones.
|
||||||
|
;
|
||||||
|
; All of it is osTakeScreen's now, and it gives back what was actually there rather than
|
||||||
|
; what a clean machine looks like.
|
||||||
|
;
|
||||||
|
; ---- Unless it said no ----
|
||||||
|
;
|
||||||
|
; A machine with no volatile drive has nowhere to keep a screen, and answers so. Being told
|
||||||
|
; no is not a fault and not a reason to stop: it means doing what this program did before
|
||||||
|
; there was anywhere to put one, which is leaving a blank screen rather than a grid with
|
||||||
|
; somebody's prompt printed into it.
|
||||||
|
SETD.0 ScreenKept
|
||||||
|
LDA.0
|
||||||
|
BRA gridScreenKept
|
||||||
|
|
||||||
|
RSTA
|
||||||
|
OUTA 0x34
|
||||||
|
OUTA 0x36 ; The origins, or the shell looks at a corner of the map.
|
||||||
|
|
||||||
|
INIA 0d4
|
||||||
|
OUTA 0xE3
|
||||||
|
INIA 0x40
|
||||||
|
OUTA 0xE4
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE5
|
||||||
|
OUTA 0xE2 ; Tile nought is the space and attribute nought is plain.
|
||||||
|
INIA 0x80
|
||||||
|
OUTA 0xE6
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE7
|
||||||
|
INIA 0x02
|
||||||
|
OUTA 0xE8 ; Fill the whole map with it.
|
||||||
|
|
||||||
|
; And the colours the machine wakes up in, which is as near as this can get to the ones it
|
||||||
|
; took: grey on black is what plain text has always been here.
|
||||||
|
INIA 0d4
|
||||||
|
OUTA 0xE3
|
||||||
|
INIA 0xFC
|
||||||
|
OUTA 0xE4
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE5
|
||||||
|
OUTA 0xE9
|
||||||
|
OUTA 0xE9
|
||||||
|
OUTA 0xE9
|
||||||
|
OUTA 0xE9
|
||||||
|
INIA 0xD8
|
||||||
|
OUTA 0xE9
|
||||||
|
OUTA 0xE9
|
||||||
|
OUTA 0xE9
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE9
|
||||||
|
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x05 ; Cleared, which also puts the cursor home.
|
||||||
|
|
||||||
|
gridScreenKept:
|
||||||
|
RSTA
|
||||||
|
OUTA 0x02 ; Line mode, the way it was found.
|
||||||
|
RSTA ; splitlint[redundant-assignment]: an exit status, not a mode
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
; ---- A frame ----
|
||||||
|
;
|
||||||
|
; Asked for rather than waited on with an interrupt. Polling costs a program nothing it
|
||||||
|
; needs here and saves installing a vector, and the status bit is honest: reading it is what
|
||||||
|
; answers it, so this cannot see the same frame twice.
|
||||||
|
waitFrame:
|
||||||
|
INA 0x30
|
||||||
|
INIB 0x01
|
||||||
|
AND
|
||||||
|
BRQ waitFrame
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- The tile ----
|
||||||
|
;
|
||||||
|
; Blitted rather than written a byte at a time, because it is already sixty four bytes of
|
||||||
|
; Data Segment and the controller will move it in one command. Tile n starts at n times 64,
|
||||||
|
; so tile 200 starts at 12,800, which is 0x3200.
|
||||||
|
putTile:
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0xE0 ; SourceBank: Data Memory.
|
||||||
|
; The pointer BEFORE storing through it. Written the other way round the first time, which
|
||||||
|
; assembles perfectly and stores the address into wherever DP1 was last left - so the blit
|
||||||
|
; read its sixty four bytes from nowhere in particular and tile 200 came out as noise.
|
||||||
|
SETD.1 TileArtAt
|
||||||
|
SETD.0 TileArt
|
||||||
|
STD.0.1
|
||||||
|
LDA.1
|
||||||
|
OUTA 0xE1
|
||||||
|
INCD.1
|
||||||
|
LDA.1
|
||||||
|
OUTA 0xE2
|
||||||
|
INIA 0d4
|
||||||
|
OUTA 0xE3
|
||||||
|
INIA 0x32
|
||||||
|
OUTA 0xE4
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE5
|
||||||
|
OUTA 0xE6
|
||||||
|
INIA 0d64
|
||||||
|
OUTA 0xE7
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0xE8 ; Blit.
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- The colours ----
|
||||||
|
;
|
||||||
|
; The tile is drawn in indices 0 and 1, and the low nibble of a cell's attribute is ADDED to
|
||||||
|
; every index in it, sixteen at a time. So the same sixty four bytes appear in sixteen colour
|
||||||
|
; schemes, and what this writes is those schemes: entry 16n is the ground and 16n+1 the line.
|
||||||
|
; Nothing is duplicated to get them.
|
||||||
|
;
|
||||||
|
; ---- Nothing here works out an address ----
|
||||||
|
;
|
||||||
|
; The first try computed where entry 16n lives - 0xFC00 plus 64n - and was wrong twice for
|
||||||
|
; the same reason, which is that this machine cannot multiply and pretending otherwise is
|
||||||
|
; where the bugs go. 64n reaches 960, so the address spans four pages and the high byte moves
|
||||||
|
; too; and doubling A by adding B to it needs B to hold A first, which RSTB is the opposite
|
||||||
|
; of.
|
||||||
|
;
|
||||||
|
; So it writes all 256 entries in order and never computes anything. The controller's Data
|
||||||
|
; port steps the address on after every byte, so the whole palette is one sweep of 1,024
|
||||||
|
; writes with no arithmetic in it at all. The colours that change do so by adding sixteen to
|
||||||
|
; a running value, which is the same reason.
|
||||||
|
putPalette:
|
||||||
|
INIA 0d4
|
||||||
|
OUTA 0xE3
|
||||||
|
INIA 0xFC
|
||||||
|
OUTA 0xE4
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE5
|
||||||
|
|
||||||
|
SETD.0 Scheme
|
||||||
|
STA.0
|
||||||
|
SETD.0 LineRed
|
||||||
|
STA.0
|
||||||
|
INIA 0xFF
|
||||||
|
SETD.0 LineGreen
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
everyScheme:
|
||||||
|
; Entry 16n, the ground: the same dark under every scheme.
|
||||||
|
INIA 0d16
|
||||||
|
OUTA 0xE9
|
||||||
|
OUTA 0xE9
|
||||||
|
INIA 0d24
|
||||||
|
OUTA 0xE9
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE9
|
||||||
|
|
||||||
|
; Entry 16n+1, the line: more red and less green the further down the map it is, so that
|
||||||
|
; scrolling is visibly going somewhere rather than showing the same row again.
|
||||||
|
SETD.0 LineRed
|
||||||
|
LDA.0
|
||||||
|
OUTA 0xE9
|
||||||
|
SETD.0 LineGreen
|
||||||
|
LDA.0
|
||||||
|
OUTA 0xE9
|
||||||
|
INIA 0d96
|
||||||
|
OUTA 0xE9
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE9
|
||||||
|
|
||||||
|
; The fourteen this tile never asks for. Written anyway, because the sweep is what keeps
|
||||||
|
; the address right and skipping them would mean working one out.
|
||||||
|
INIA 0d14
|
||||||
|
SETD.0 Spare
|
||||||
|
STA.0
|
||||||
|
everySpare:
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE9
|
||||||
|
OUTA 0xE9
|
||||||
|
OUTA 0xE9
|
||||||
|
OUTA 0xE9
|
||||||
|
SETD.0 Spare
|
||||||
|
LDA.0
|
||||||
|
DECA
|
||||||
|
STA.0
|
||||||
|
BNA everySpare
|
||||||
|
|
||||||
|
SETD.0 LineRed
|
||||||
|
LDA.0
|
||||||
|
INIB 0d16
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
STQ.0
|
||||||
|
SETD.0 LineGreen
|
||||||
|
LDA.0
|
||||||
|
CCF
|
||||||
|
SUB ; B is still sixteen, from the red just above.
|
||||||
|
STQ.0
|
||||||
|
|
||||||
|
SETD.0 Scheme
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
CCF
|
||||||
|
SUB ; And still sixteen here, which is also how many schemes there are.
|
||||||
|
BNQ everyScheme
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- The map ----
|
||||||
|
;
|
||||||
|
; All 128 rows, not the 25 the screen shows. That is the whole point of a map bigger than
|
||||||
|
; the screen: the rows above and below are already drawn, so scrolling is a change of origin
|
||||||
|
; rather than a change of anything.
|
||||||
|
;
|
||||||
|
; A ROW IS A PAGE, which is why the row number is written straight into DestHigh and the
|
||||||
|
; column arithmetic disappears. The controller's Data port steps the address on after every
|
||||||
|
; byte, so a row is a loop over two writes with no address handling in it at all.
|
||||||
|
putMap:
|
||||||
|
RSTA
|
||||||
|
SETD.0 MapRow
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
everyRow:
|
||||||
|
INIA 0d4
|
||||||
|
OUTA 0xE3
|
||||||
|
SETD.0 MapRow
|
||||||
|
LDA.0
|
||||||
|
INIB 0x40
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
OUTQ 0xE4 ; The map starts at 0x4000 and a row is a page.
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE5
|
||||||
|
|
||||||
|
; The attribute is the row number's low nibble, so the schemes band down the map and
|
||||||
|
; repeat every sixteen rows. DP0 is still MapRow, from the row's address above.
|
||||||
|
LDA.0
|
||||||
|
INIB 0x0F
|
||||||
|
AND
|
||||||
|
SETD.0 RowAttribute
|
||||||
|
STQ.0
|
||||||
|
|
||||||
|
; ---- The map's width, which is not the screen's ----
|
||||||
|
;
|
||||||
|
; A hundred and twenty-eight, because that is how many cells a map row holds: 256 bytes at
|
||||||
|
; two bytes a cell, whatever mode the screen is in. It is a property of video memory rather
|
||||||
|
; than of the display, so there is no register to ask and nothing to ask it of.
|
||||||
|
;
|
||||||
|
; This said forty first, and filled half of an eighty column screen. Then it asked the
|
||||||
|
; screen how wide it was, which fixed what could be seen and was still wrong: scrolling
|
||||||
|
; sideways walked off the 80 filled columns into the 48 that were not, and the grid went
|
||||||
|
; blank for six seconds before coming round again.
|
||||||
|
;
|
||||||
|
; ASKING THE SCREEN IS RIGHT FOR FILLING A SCREEN AND WRONG FOR FILLING A MAP. A program
|
||||||
|
; that writes one screenful wants the window; a program that scrolls wants everything the
|
||||||
|
; window can be moved over.
|
||||||
|
INIA 0d128
|
||||||
|
SETD.0 RowCells
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
everyCell:
|
||||||
|
INIA 0d200
|
||||||
|
OUTA 0xE9
|
||||||
|
SETD.0 RowAttribute
|
||||||
|
LDA.0
|
||||||
|
OUTA 0xE9
|
||||||
|
SETD.0 RowCells
|
||||||
|
LDA.0
|
||||||
|
DECA
|
||||||
|
STA.0
|
||||||
|
BNA everyCell
|
||||||
|
|
||||||
|
SETD.0 MapRow
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
INIB 0x80
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ everyRow
|
||||||
|
RET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
#Base 0x2000
|
||||||
|
|
||||||
|
ScreenKept:
|
||||||
|
0x00
|
||||||
|
FineDown:
|
||||||
|
0x00
|
||||||
|
FineAcross:
|
||||||
|
0x00
|
||||||
|
OriginDown:
|
||||||
|
0x00
|
||||||
|
OriginAcross:
|
||||||
|
0x00
|
||||||
|
Scheme:
|
||||||
|
0x00
|
||||||
|
Spare:
|
||||||
|
0x00
|
||||||
|
LineRed:
|
||||||
|
0x00
|
||||||
|
LineGreen:
|
||||||
|
0x00
|
||||||
|
MapRow:
|
||||||
|
0x00
|
||||||
|
RowAttribute:
|
||||||
|
0x00
|
||||||
|
RowCells:
|
||||||
|
0x00
|
||||||
|
TileArtAt:
|
||||||
|
0x00 0x00
|
||||||
|
|
||||||
|
; ---- Eight by eight, a byte a pixel ----
|
||||||
|
;
|
||||||
|
; A line along the top and one down the left. Tiled edge to edge they meet, so a screenful
|
||||||
|
; of this one tile is a continuous grid rather than 1,000 separate boxes.
|
||||||
|
TileArt:
|
||||||
|
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
|
||||||
|
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
|
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||||
@@ -36,8 +36,8 @@
|
|||||||
|
|
||||||
start:
|
start:
|
||||||
CALL seedGlider
|
CALL seedGlider
|
||||||
SETD.0 ClearScreen
|
INIA 0x01
|
||||||
CALL printString
|
OUTA 0x05 ; Console command: clear the screen
|
||||||
SETD.3 GenerationsLeft
|
SETD.3 GenerationsLeft
|
||||||
INIA 0xFF
|
INIA 0xFF
|
||||||
STA.3
|
STA.3
|
||||||
@@ -126,8 +126,9 @@ seedGlider:
|
|||||||
RET
|
RET
|
||||||
|
|
||||||
renderBoard:
|
renderBoard:
|
||||||
SETD.0 CursorHome
|
RSTA
|
||||||
CALL printString
|
OUTA 0x03
|
||||||
|
OUTA 0x04 ; Cursor to row 0, column 0
|
||||||
SETD.1 RowCount
|
SETD.1 RowCount
|
||||||
SETD.2 ColCount
|
SETD.2 ColCount
|
||||||
INIA 0d16
|
INIA 0d16
|
||||||
@@ -371,12 +372,6 @@ RanOutText:
|
|||||||
StoppedText:
|
StoppedText:
|
||||||
"stopped"
|
"stopped"
|
||||||
|
|
||||||
ClearScreen:
|
|
||||||
0x1B
|
|
||||||
"[2J"
|
|
||||||
CursorHome:
|
|
||||||
0x1B
|
|
||||||
"[H"
|
|
||||||
|
|
||||||
; 18 by 18 cells with the current and next states interleaved, so 648 bytes. The
|
; 18 by 18 cells with the current and next states interleaved, so 648 bytes. The
|
||||||
; original leaves this implicit and leans on Data Memory being zero, which works but
|
; original leaves this implicit and leans on Data Memory being zero, which works but
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
start:
|
start:
|
||||||
SETD.0 Name
|
SETD.0 Name
|
||||||
INIB 0d29
|
INIB 0d63
|
||||||
SWI osArgument
|
SWI osArgument
|
||||||
SETD.0 Name
|
SETD.0 Name
|
||||||
LDA.0
|
LDA.0
|
||||||
@@ -111,19 +111,23 @@ noName:
|
|||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
INIA 0d2
|
INIA 0d2
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
; ---- What went wrong, in words ----
|
||||||
|
;
|
||||||
|
; It used to print the number the filesystem answered with, as "error 2". THERE IS NO SUCH
|
||||||
|
; VOCABULARY: the library documents its answer as zero or not zero and never as a code, so
|
||||||
|
; the number named nothing and could not be looked up - it just looked like it could.
|
||||||
|
;
|
||||||
|
; A name that is not on the disk is the only way opening fails that a person can do anything
|
||||||
|
; about, and it is nearly always a name typed slightly wrong. Saying so is more use than any
|
||||||
|
; number would have been.
|
||||||
openFailed:
|
openFailed:
|
||||||
SETD.0 OpenError
|
SETD.0 OpenError
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
BRI printError
|
BRI failed
|
||||||
readFailed:
|
readFailed:
|
||||||
SETD.0 ReadError
|
SETD.0 ReadError
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
printError:
|
failed:
|
||||||
RSTA
|
|
||||||
MVQB
|
|
||||||
SWI osPrintNumber
|
|
||||||
SETD.0 NewLine
|
|
||||||
SWI osPrintString
|
|
||||||
; ITS OWN EXIT, and it did not have one. This fell through into finished and reported
|
; ITS OWN EXIT, and it did not have one. This fell through into finished and reported
|
||||||
; that everything was fine, having just printed the reason it was not - which nobody
|
; that everything was fine, having just printed the reason it was not - which nobody
|
||||||
; noticed while the only reader was a person, who could see both.
|
; noticed while the only reader was a person, who could see both.
|
||||||
@@ -140,9 +144,11 @@ Usage:
|
|||||||
"more: give me a file name
|
"more: give me a file name
|
||||||
"
|
"
|
||||||
OpenError:
|
OpenError:
|
||||||
"more: cannot find the file, error "
|
"more: there is no file by that name
|
||||||
|
"
|
||||||
ReadError:
|
ReadError:
|
||||||
"more: cannot read the file, error "
|
"more: the disk would not give me that file
|
||||||
|
"
|
||||||
NewLine:
|
NewLine:
|
||||||
0x0A 0x00
|
0x0A 0x00
|
||||||
MorePrompt:
|
MorePrompt:
|
||||||
@@ -150,7 +156,7 @@ MorePrompt:
|
|||||||
ClearPrompt:
|
ClearPrompt:
|
||||||
0x0A 0x00
|
0x0A 0x00
|
||||||
Name:
|
Name:
|
||||||
#Reserve 0d29
|
#Reserve 0d64
|
||||||
Remaining:
|
Remaining:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
LinesLeft:
|
LinesLeft:
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
; What the console just handed over, in hexadecimal and by name.
|
||||||
|
;
|
||||||
|
; The keys that are not characters - the arrows, Home, End and forward Delete - arrive as
|
||||||
|
; the console's own values above ASCII rather than as the escape sequences a terminal sends
|
||||||
|
; or as nothing at all, which is what a window used to make of them. This is what shows
|
||||||
|
; that, and it shows both halves of the rule in one run:
|
||||||
|
;
|
||||||
|
; A LINE FIRST, read the way everything reads one. Line mode delivers characters, so the
|
||||||
|
; keys are dropped before they reach the buffer and what comes back is what a person could
|
||||||
|
; have typed. Pressing Up while something else is collecting a line does nothing, which is
|
||||||
|
; an improvement on putting an escape and a bracket in the middle of it.
|
||||||
|
;
|
||||||
|
; THEN THE KEYS, in key mode, where a program has asked for every keystroke as it happens
|
||||||
|
; and these are keystrokes like any other.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
#Base 0x4000
|
||||||
|
|
||||||
|
start:
|
||||||
|
SETD.0 LineText
|
||||||
|
CALL printString
|
||||||
|
CALL newLine
|
||||||
|
|
||||||
|
SETD.0 Buffer
|
||||||
|
INIB 0d63
|
||||||
|
CALL readLine
|
||||||
|
|
||||||
|
SETD.0 Buffer
|
||||||
|
CALL showBytes
|
||||||
|
|
||||||
|
SETD.0 KeyText
|
||||||
|
CALL printString
|
||||||
|
CALL newLine
|
||||||
|
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x02 ; Key mode. Nothing echoes, so everything below says what it saw.
|
||||||
|
keyLoop:
|
||||||
|
INA 0x00
|
||||||
|
INIB 0xFF
|
||||||
|
XOR
|
||||||
|
BRQ keyDone ; Nothing more is coming.
|
||||||
|
INIB 0x71 ; q, which is how this is stopped.
|
||||||
|
XOR
|
||||||
|
BRQ keyDone
|
||||||
|
CALL showKey
|
||||||
|
BRI keyLoop
|
||||||
|
|
||||||
|
keyDone:
|
||||||
|
RSTA
|
||||||
|
OUTA 0x02 ; Line mode, the way it was found.
|
||||||
|
SETD.0 DoneText
|
||||||
|
CALL printString
|
||||||
|
CALL newLine
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
; DP0 names a string of bytes ending in a zero. Prints each as two hexadecimal digits, so
|
||||||
|
; that what is in the buffer can be read rather than guessed at.
|
||||||
|
showBytes:
|
||||||
|
LDA.0
|
||||||
|
BRA showBytesDone
|
||||||
|
CALL printByteHex
|
||||||
|
INIA 0x20
|
||||||
|
OUTA 0x00
|
||||||
|
INCD.0
|
||||||
|
BRI showBytes
|
||||||
|
showBytesDone:
|
||||||
|
CALL newLine
|
||||||
|
RET
|
||||||
|
|
||||||
|
; A holds a key. Prints its value and then what it is.
|
||||||
|
;
|
||||||
|
; A survives a CALL, so the byte is still here after printing it - but only until something
|
||||||
|
; else is put in A, which the space below does. So it is kept where the naming can find it.
|
||||||
|
showKey:
|
||||||
|
SETD.1 KeyByte
|
||||||
|
STA.1
|
||||||
|
CALL printByteHex
|
||||||
|
INIA 0x20
|
||||||
|
OUTA 0x00
|
||||||
|
SETD.1 KeyByte
|
||||||
|
LDA.1
|
||||||
|
|
||||||
|
; XOR leaves the answer in Q and A alone, so one load stands for the whole ladder.
|
||||||
|
INIB 0x80
|
||||||
|
XOR
|
||||||
|
BRQ keyUp
|
||||||
|
INIB 0x81
|
||||||
|
XOR
|
||||||
|
BRQ keyDown
|
||||||
|
INIB 0x82
|
||||||
|
XOR
|
||||||
|
BRQ keyLeft
|
||||||
|
INIB 0x83
|
||||||
|
XOR
|
||||||
|
BRQ keyRight
|
||||||
|
INIB 0x84
|
||||||
|
XOR
|
||||||
|
BRQ keyHome
|
||||||
|
INIB 0x85
|
||||||
|
XOR
|
||||||
|
BRQ keyEnd
|
||||||
|
INIB 0x86
|
||||||
|
XOR
|
||||||
|
BRQ keyDelete
|
||||||
|
|
||||||
|
; An ordinary character, which is its own best name.
|
||||||
|
OUTA 0x00
|
||||||
|
CALL newLine
|
||||||
|
RET
|
||||||
|
|
||||||
|
keyUp:
|
||||||
|
SETD.0 UpText
|
||||||
|
BRI keySay
|
||||||
|
keyDown:
|
||||||
|
SETD.0 DownText
|
||||||
|
BRI keySay
|
||||||
|
keyLeft:
|
||||||
|
SETD.0 LeftText
|
||||||
|
BRI keySay
|
||||||
|
keyRight:
|
||||||
|
SETD.0 RightText
|
||||||
|
BRI keySay
|
||||||
|
keyHome:
|
||||||
|
SETD.0 HomeText
|
||||||
|
BRI keySay
|
||||||
|
keyEnd:
|
||||||
|
SETD.0 EndText
|
||||||
|
BRI keySay
|
||||||
|
keyDelete:
|
||||||
|
SETD.0 DeleteText
|
||||||
|
keySay:
|
||||||
|
CALL printString
|
||||||
|
CALL newLine
|
||||||
|
RET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
#Base 0x2000
|
||||||
|
|
||||||
|
LineText:
|
||||||
|
"a line, then keys. q stops."
|
||||||
|
KeyText:
|
||||||
|
"keys:"
|
||||||
|
DoneText:
|
||||||
|
"done"
|
||||||
|
|
||||||
|
UpText:
|
||||||
|
"up"
|
||||||
|
DownText:
|
||||||
|
"down"
|
||||||
|
LeftText:
|
||||||
|
"left"
|
||||||
|
RightText:
|
||||||
|
"right"
|
||||||
|
HomeText:
|
||||||
|
"home"
|
||||||
|
EndText:
|
||||||
|
"end"
|
||||||
|
DeleteText:
|
||||||
|
"delete"
|
||||||
|
|
||||||
|
KeyByte:
|
||||||
|
0x00
|
||||||
|
|
||||||
|
Buffer:
|
||||||
|
#Reserve 0d64
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Boot start
|
||||||
|
|
||||||
|
#Include console.asm
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
; The 16-bit segmented sieve rewritten for SplitBit's four-Data-Pointer ISA.
|
; The 16-bit segmented sieve rewritten for SplitBit's four-Data-Pointer ISA.
|
||||||
;
|
;
|
||||||
; This deliberately implements the same algorithm and emits the same text as
|
; This deliberately implements the same algorithm and emits the same text as
|
||||||
; 16bitSegmentedSieve.asm, making the two versions useful as a direct comparison.
|
; 16bitSieve.asm, making the two versions useful as a direct comparison.
|
||||||
; DP0 walks PrimeStates, DP1 holds Page, DP2 walks Segment, and volatile DP3
|
; DP0 walks PrimeStates, DP1 holds Page, DP2 walks Segment, and volatile DP3
|
||||||
; marks multiples. CALL preserves the first three pointers automatically.
|
; marks multiples. CALL preserves the first three pointers automatically.
|
||||||
|
|
||||||
|
|||||||
@@ -60,13 +60,17 @@ start:
|
|||||||
CALL placeSnake
|
CALL placeSnake
|
||||||
CALL placeFood
|
CALL placeFood
|
||||||
|
|
||||||
SETD.0 ClearScreen
|
INIA 0x01
|
||||||
CALL printString
|
OUTA 0x05 ; Console command: clear the screen
|
||||||
|
|
||||||
; Key mode, so that one key is one byte and arrives when it is pressed. It is put back
|
; Key mode, so that one key is one byte and arrives when it is pressed. It is put back
|
||||||
; before this returns, and CosmOS puts it back too in case a program stops without
|
; before this returns, and CosmOS puts it back too in case a program stops without
|
||||||
; doing so.
|
; doing so.
|
||||||
INIA 0x01
|
;
|
||||||
|
; A is already 1 from the clear above, and leaving this out would save a byte by tying a
|
||||||
|
; console COMMAND to a console MODE that happens to share a number. That is a coincidence
|
||||||
|
; rather than a saving, and it would break silently if either ever moved.
|
||||||
|
INIA 0x01 ; splitlint[redundant-assignment]: see above
|
||||||
OUTA 0x02
|
OUTA 0x02
|
||||||
|
|
||||||
gameLoop:
|
gameLoop:
|
||||||
@@ -529,8 +533,9 @@ removeTail:
|
|||||||
; The whole board, every frame, from the top left corner. Sixteen by sixteen is small
|
; The whole board, every frame, from the top left corner. Sixteen by sixteen is small
|
||||||
; enough that working out what changed would cost more than sending it all again.
|
; enough that working out what changed would cost more than sending it all again.
|
||||||
draw:
|
draw:
|
||||||
SETD.0 CursorHome
|
RSTA
|
||||||
CALL printString
|
OUTA 0x03
|
||||||
|
OUTA 0x04 ; Cursor to row 0, column 0
|
||||||
SETD.0 BorderText
|
SETD.0 BorderText
|
||||||
CALL printString
|
CALL printString
|
||||||
CALL newLine
|
CALL newLine
|
||||||
@@ -586,12 +591,19 @@ drawPut:
|
|||||||
|
|
||||||
; ---- Waiting ----
|
; ---- Waiting ----
|
||||||
;
|
;
|
||||||
; There is no clock on this machine, so time is counted in instructions. At the emulated
|
; There is no clock on this machine, so time is counted in cycles. At the emulated rate this
|
||||||
; rate this is about an eighth of a second, which is a speed a person can play at. Running
|
; is about an eighth of a second, which is a speed a person can play at. Running the emulator
|
||||||
; the emulator faster or slower moves it, and that is the honest answer: the machine has
|
; faster or slower moves it, and that is the honest answer: the machine has no way to know
|
||||||
; no way to know how long a second is and this program is not going to pretend it does.
|
; how long a second is and this program is not going to pretend it does.
|
||||||
|
;
|
||||||
|
; THE COUNT USED TO BE 256 AND THE COMMENT USED TO SAY INSTRUCTIONS. When a cycle stopped
|
||||||
|
; being an instruction and became a memory access, every loop in the machine got dearer and
|
||||||
|
; this one silently doubled - the game has been running at half the speed it says ever since,
|
||||||
|
; in a terminal as much as in a window. The inner loop is a DECA and a BNA, one byte and
|
||||||
|
; three, so four cycles a turn: 122 times 256 times 4 is about 125,000, which is an eighth of
|
||||||
|
; a second at a megahertz.
|
||||||
pause:
|
pause:
|
||||||
RSTB
|
INIB 0d122
|
||||||
pauseOuter:
|
pauseOuter:
|
||||||
RSTA
|
RSTA
|
||||||
pauseInner:
|
pauseInner:
|
||||||
@@ -642,12 +654,6 @@ RandomSeed:
|
|||||||
RandomState:
|
RandomState:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
|
|
||||||
ClearScreen:
|
|
||||||
0x1B
|
|
||||||
"[2J"
|
|
||||||
CursorHome:
|
|
||||||
0x1B
|
|
||||||
"[H"
|
|
||||||
|
|
||||||
BorderText:
|
BorderText:
|
||||||
"+----------------+"
|
"+----------------+"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
start:
|
start:
|
||||||
SETD.0 Name
|
SETD.0 Name
|
||||||
INIB 0d29
|
INIB 0d63
|
||||||
SWI osArgument
|
SWI osArgument
|
||||||
SETD.0 Name
|
SETD.0 Name
|
||||||
LDA.0
|
LDA.0
|
||||||
@@ -81,7 +81,7 @@ ReadError:
|
|||||||
NewLine:
|
NewLine:
|
||||||
0x0A 0x00
|
0x0A 0x00
|
||||||
Name:
|
Name:
|
||||||
#Reserve 0d29
|
#Reserve 0d64
|
||||||
Remaining:
|
Remaining:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ start:
|
|||||||
; Where to go is the argument. Nothing else about this program says a directory name, so
|
; Where to go is the argument. Nothing else about this program says a directory name, so
|
||||||
; running it anywhere else moves it anywhere else.
|
; running it anywhere else moves it anywhere else.
|
||||||
SETD.0 Where
|
SETD.0 Where
|
||||||
INIB 0d40
|
INIB 0d63
|
||||||
SWI osArgument
|
SWI osArgument
|
||||||
SETD.0 Where
|
SETD.0 Where
|
||||||
LDA.0
|
LDA.0
|
||||||
@@ -88,7 +88,7 @@ noFile:
|
|||||||
#Base 0x2000
|
#Base 0x2000
|
||||||
|
|
||||||
Where:
|
Where:
|
||||||
#Reserve 0d40
|
#Reserve 0d64
|
||||||
Left:
|
Left:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
Went:
|
Went:
|
||||||
|
|||||||
@@ -244,6 +244,26 @@ srcIncludeSkip:
|
|||||||
RET
|
RET
|
||||||
|
|
||||||
srcIncludeGone:
|
srcIncludeGone:
|
||||||
|
; ---- Say which one ----
|
||||||
|
;
|
||||||
|
; It said nothing at all, and the assembler's catch-all then said "nothing was written" -
|
||||||
|
; which is true and useless. Three programs on the disk could be read and not assembled,
|
||||||
|
; and finding out why meant reading the assembler rather than the message.
|
||||||
|
;
|
||||||
|
; SrcName holds the library path by now, because that was the second place tried, so this
|
||||||
|
; says where it looked as well as what for. Both are worth having: a name that is not in
|
||||||
|
; /Lib and not beside you is usually one or the other spelt wrong.
|
||||||
|
SETD.0 GoneText
|
||||||
|
SWI osPrintString
|
||||||
|
SETD.0 IncWanted
|
||||||
|
SWI osPrintString
|
||||||
|
SETD.0 GoneWhereText
|
||||||
|
SWI osPrintString
|
||||||
|
SETD.0 SrcName
|
||||||
|
SWI osPrintString
|
||||||
|
SETD.0 GoneEndText
|
||||||
|
SWI osPrintString
|
||||||
|
|
||||||
; The file is not there. The stack is left as it is: the caller stops the assembly, and
|
; The file is not there. The stack is left as it is: the caller stops the assembly, and
|
||||||
; unwinding for the sake of tidiness would only hide where it happened.
|
; unwinding for the sake of tidiness would only hide where it happened.
|
||||||
RSTA
|
RSTA
|
||||||
@@ -596,6 +616,13 @@ IncSlot:
|
|||||||
IncSlotLeft:
|
IncSlotLeft:
|
||||||
0x00
|
0x00
|
||||||
|
|
||||||
|
GoneText:
|
||||||
|
"cannot find "
|
||||||
|
GoneWhereText:
|
||||||
|
", not beside the file that asked and not as "
|
||||||
|
GoneEndText:
|
||||||
|
"
|
||||||
|
"
|
||||||
TooDeepText:
|
TooDeepText:
|
||||||
"included files are nested deeper than this assembler will follow
|
"included files are nested deeper than this assembler will follow
|
||||||
"
|
"
|
||||||
|
|||||||
+416
-11
@@ -56,17 +56,12 @@ emulator, and disk-image tool. From the repository root, build those tools first
|
|||||||
make
|
make
|
||||||
```
|
```
|
||||||
|
|
||||||
Then build CosmOS and all of its applications:
|
That builds CosmOS, all of its applications, and a disk to boot them from - one makefile
|
||||||
|
covers the machine and the system. To rebuild only part of it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd Programs
|
|
||||||
make cosmos
|
make cosmos
|
||||||
```
|
make disk
|
||||||
|
|
||||||
Build a fresh SBFS application disk as well:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
make cosmos-disk
|
|
||||||
```
|
```
|
||||||
|
|
||||||
To boot CosmOS with that disk attached:
|
To boot CosmOS with that disk attached:
|
||||||
@@ -75,15 +70,158 @@ To boot CosmOS with that disk attached:
|
|||||||
make run-cosmos
|
make run-cosmos
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or on the Voyager, which is the same machine with a screen and a speaker instead of a
|
||||||
|
terminal:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make run-voyager
|
||||||
|
```
|
||||||
|
|
||||||
|
Both depend on the disk rather than merely using it, which is worth knowing: **what is on a
|
||||||
|
disk is whatever was built when the disk was made.** A machine whose console has changed
|
||||||
|
will start an old image quite happily and its programs will draw whatever the old way now
|
||||||
|
means, which is a confusing thing to debug and an easy thing to avoid.
|
||||||
|
|
||||||
The generated files are kept under `Programs/build/`:
|
The generated files are kept under `Programs/build/`:
|
||||||
|
|
||||||
- `CosmOS/Source/cosmos.bin` is the bootable CosmOS image.
|
- `CosmOS/Source/cosmos.bin` is the bootable CosmOS image.
|
||||||
- `CosmOS/Apps/*.sbx` are loadable application images.
|
- `CosmOS/Apps/*.sbx` are loadable application images.
|
||||||
- `cosmos.img` is the SBFS disk containing those applications.
|
- `cosmos.img` is the SBFS disk containing those applications.
|
||||||
|
|
||||||
|
**The disk carries every source in `Programs/`, mirrored.** Not a list kept in the makefile -
|
||||||
|
a list goes stale the moment somebody adds a program and forgets to name it, and what they
|
||||||
|
forgot is invisible until they go looking for it on the machine. Putting a file where the
|
||||||
|
others live is the whole of putting it on the disk.
|
||||||
|
|
||||||
|
That matters most for the things nobody thought worth shipping a binary of. A demo that is
|
||||||
|
not interesting enough to build by default is still worth having the source of, because the
|
||||||
|
machine can build it:
|
||||||
|
|
||||||
|
```
|
||||||
|
> cd /Source/Examples
|
||||||
|
/Source/Examples> Asm colours.asm
|
||||||
|
wrote colours.bin: program 114, data 86, labels 8
|
||||||
|
```
|
||||||
|
|
||||||
|
Two things are left behind. `build`, because what a project builds is not what it wrote. And
|
||||||
|
anything whose name is longer than a directory entry holds, which is **refused rather than
|
||||||
|
skipped**: a disk quietly missing a file is exactly the failure a mirror exists to prevent,
|
||||||
|
so the build stops and says which name to shorten.
|
||||||
|
|
||||||
|
`/Lib` still holds the library sources separately, because that is where an `#Include` looks
|
||||||
|
after looking beside the file that asked. The same files therefore appear twice - once as
|
||||||
|
what a program includes, once as part of the source tree - and that is the difference between
|
||||||
|
an installed library and a copy of the source.
|
||||||
|
|
||||||
The disk is rebuilt from scratch when its applications change, so its contents describe
|
The disk is rebuilt from scratch when its applications change, so its contents describe
|
||||||
the current source tree rather than accumulating files left by older builds.
|
the current source tree rather than accumulating files left by older builds.
|
||||||
|
|
||||||
|
## Scripts:
|
||||||
|
|
||||||
|
`do <file>` runs the lines in a file as though somebody had typed them. Every command works
|
||||||
|
the same way it does at the prompt, because the only thing a script changes is where the next
|
||||||
|
line comes from - the shell splits it, matches it and runs it without knowing the difference.
|
||||||
|
|
||||||
|
```
|
||||||
|
#! script
|
||||||
|
; Build the system and put it where the machine will find it.
|
||||||
|
echo building CosmOS
|
||||||
|
cd /Source/CosmOS
|
||||||
|
Asm cosmos.asm
|
||||||
|
echo done
|
||||||
|
```
|
||||||
|
|
||||||
|
`echo` is a command rather than a program on purpose. `Say.sbx` has printed words since before
|
||||||
|
there were scripts and is the wrong shape for one: being a program, it has to be found on the
|
||||||
|
disk and loaded and started, it prefixes what it was told with `it says:`, and the system
|
||||||
|
prints `finished` after it - three lines of noise around one line of narration.
|
||||||
|
|
||||||
|
**The first two bytes must be `#!`**, or the shell refuses the file and says so. That is what
|
||||||
|
tells a script from anything else, and it is deliberately not the name and not a flag in the
|
||||||
|
directory entry: the rule this filesystem keeps is that an entry holds only what the content
|
||||||
|
cannot say about itself, and a script can say what it is. The loader already refuses anything
|
||||||
|
that does not begin `SBEX`, so the two kinds of runnable file turn each other away without
|
||||||
|
either of them having been told about the other.
|
||||||
|
|
||||||
|
What follows the `#!` is ignored. It is where the name of an interpreter goes if there is ever
|
||||||
|
a second one; today there is one and it is this shell.
|
||||||
|
|
||||||
|
**`#` is a directive and `;` is a comment, exactly as in SplitBit assembly.** One rule across
|
||||||
|
the machine rather than two dialects: `#` means this line is about the file, `;` means ignore
|
||||||
|
this line. Comments and blank lines never reach the shell at all - they are dropped by the
|
||||||
|
reader, so they are not echoed and the dispatch never sees a line it would have to know to
|
||||||
|
ignore. This is not Unix's convention and is not trying to be; there `#!` genuinely is a
|
||||||
|
comment that only the kernel looks at, while here the shell requires it.
|
||||||
|
|
||||||
|
**Each line is echoed as it runs**, after the prompt, so that a script reads exactly like
|
||||||
|
somebody typing it and a script that stops says where.
|
||||||
|
|
||||||
|
**`#quiet` turns that off and `#loud` turns it back on.** The prompt and the echo go together,
|
||||||
|
because together they are what makes a script look like typing - so a quiet script gets
|
||||||
|
neither, and what it prints is all that appears. That is for the scripts whose own output is
|
||||||
|
the point, where prompts interleaved with the message are just in the way:
|
||||||
|
|
||||||
|
```
|
||||||
|
#! script
|
||||||
|
#quiet
|
||||||
|
clear
|
||||||
|
echo Segan Voyager
|
||||||
|
echo CosmOS ready.
|
||||||
|
```
|
||||||
|
|
||||||
|
A nested script inherits quiet from the one that started it, on the grounds that a build which
|
||||||
|
asked for quiet meant its helpers too, and gets its own setting back when the helper returns.
|
||||||
|
A script started from the prompt always begins loud.
|
||||||
|
|
||||||
|
**Anything else beginning with `#` is handed to the shell**, which does not know it, says so,
|
||||||
|
and stops the script. A script that asked for something this shell cannot do should not carry
|
||||||
|
on as though it had been given it.
|
||||||
|
|
||||||
|
## Starting Itself:
|
||||||
|
|
||||||
|
If `/System/Boot/startup.sh` is there, it runs before anybody can type - every way of reaching
|
||||||
|
the prompt for the first time goes through it, including the one where there is no disk, in
|
||||||
|
which case there is simply nothing to find.
|
||||||
|
|
||||||
|
**A missing one says nothing**, because a clean install has none and a machine that complained
|
||||||
|
every boot about a file nobody wrote would be teaching its owner to ignore it. A file that *is*
|
||||||
|
there and does not begin with `#!` is the other case entirely - somebody meant that to run - so
|
||||||
|
it says so and carries on to the prompt.
|
||||||
|
|
||||||
|
Between them, `startup.sh` and `#quiet` are how a machine gets a face:
|
||||||
|
|
||||||
|
```
|
||||||
|
#! script
|
||||||
|
#quiet
|
||||||
|
clear
|
||||||
|
echo Segan Voyager
|
||||||
|
echo CosmOS ready.
|
||||||
|
```
|
||||||
|
|
||||||
|
**A script stops at the first line that does not work.** A build whose first step failed and
|
||||||
|
whose second step ran anyway produces something wrong and reports success, which is the whole
|
||||||
|
reason the shell now remembers whether a line worked. What counts as not working is a command
|
||||||
|
that failed, a name the shell does not know, or a program that exited with a status. Nothing
|
||||||
|
is printed but `stopped: that line did not work` - whatever failed has already said what was
|
||||||
|
wrong in words.
|
||||||
|
|
||||||
|
**A script running out is not the same as typing running out.** The console ending means
|
||||||
|
there is nobody there and the shell stops; a script ending means go back to whoever asked for
|
||||||
|
it, so the next line comes from the console again.
|
||||||
|
|
||||||
|
The interactive assembler reads its lines the same way, so a script can contain a block of
|
||||||
|
assembly and end it with a `.` just as you would by hand.
|
||||||
|
|
||||||
|
**A script can run another script, four deep.** What is remembered when one script starts
|
||||||
|
another is a position and not a buffer - the name, which block comes next, how many are left,
|
||||||
|
and where in the block it had got to. The block itself is read again on the way back, which
|
||||||
|
costs one disk read per return and saves a 257-byte buffer per level. Four is deep enough for
|
||||||
|
a script calling a script that calls a helper, and shallow enough that a script which runs
|
||||||
|
itself says `do: scripts are only four deep` rather than filling memory.
|
||||||
|
|
||||||
|
**A line that fails stops every level**, not just the innermost. A build whose helper script
|
||||||
|
failed should not carry on in the script that called the helper.
|
||||||
|
|
||||||
## Shell Commands:
|
## Shell Commands:
|
||||||
|
|
||||||
CosmOS currently provides these built-in commands:
|
CosmOS currently provides these built-in commands:
|
||||||
@@ -93,6 +231,10 @@ CosmOS currently provides these built-in commands:
|
|||||||
| `dir` | List the files on the mounted disk and their sizes. |
|
| `dir` | List the files on the mounted disk and their sizes. |
|
||||||
| `load <path>` | Read and validate an SBEX application, then place its code and data where its header requests. |
|
| `load <path>` | Read and validate an SBEX application, then place its code and data where its header requests. |
|
||||||
| `run [words]` | Start the loaded application and make the rest of the line available to it as an argument. |
|
| `run [words]` | Start the loaded application and make the rest of the line available to it as an argument. |
|
||||||
|
| `do <script>` | Run the lines in a file as though they had been typed. See Scripts. |
|
||||||
|
| `echo [words]` | Say the rest of the line, or a blank line with nothing after it. |
|
||||||
|
| `clear` | Empty the screen. |
|
||||||
|
| `drive [n]` | Say which disk the shell is on, or go to another. See Several Disks. |
|
||||||
| `cd [path]` | Go to a directory, or to the root with nothing after it. |
|
| `cd [path]` | Go to a directory, or to the root with nothing after it. |
|
||||||
| `mkdir <path>` | Make a directory. |
|
| `mkdir <path>` | Make a directory. |
|
||||||
| `rmdir <path>` | Remove one, if it is empty. |
|
| `rmdir <path>` | Remove one, if it is empty. |
|
||||||
@@ -141,6 +283,37 @@ facility and as a test that CosmOS correctly restores its Stack and vector table
|
|||||||
every run; and `load` is how the monitor puts an arbitrary file in front of itself, which
|
every run; and `load` is how the monitor puts an arbitrary file in front of itself, which
|
||||||
is a thing typing a name deliberately cannot do.
|
is a thing typing a name deliberately cannot do.
|
||||||
|
|
||||||
|
### Typing A Line:
|
||||||
|
|
||||||
|
The shell reads what you type a key at a time and edits the line itself, which is why the
|
||||||
|
line can be moved about in at all.
|
||||||
|
|
||||||
|
| Key | What it does |
|
||||||
|
| -- | -- |
|
||||||
|
| Left, Right | Move a character. |
|
||||||
|
| Home, End | Go to the start of the line or the end of it. |
|
||||||
|
| Backspace | Take out the character before the cursor. |
|
||||||
|
| Delete | Take out the one under it. |
|
||||||
|
| Return | Finish the line, wherever the cursor happens to be sitting. |
|
||||||
|
|
||||||
|
Anything typed goes in where the cursor is, so a word left out of the middle of a line is
|
||||||
|
put back by moving there and typing it, and the rest of the line moves along.
|
||||||
|
|
||||||
|
**This used to be three different things depending on where the machine was running.** On a
|
||||||
|
terminal the host held the line and did the echoing and the backspacing; behind a window the
|
||||||
|
console's own gatherer did it; from a file nothing did it at all. One job, three
|
||||||
|
implementations, and none of them here - which is why there was no way to move about in a
|
||||||
|
line, and nowhere for a history to live. Now the console delivers keys and says nothing
|
||||||
|
about what they mean, the same way it reports what a drive is and says nothing about what
|
||||||
|
should be on it, and the shell decides.
|
||||||
|
|
||||||
|
The line holds 127 characters. It held 63 until the shell could edit one, which is when the
|
||||||
|
limit started to be felt: a copy between two disks with a directory on each is most of the
|
||||||
|
way there before anything has been said.
|
||||||
|
|
||||||
|
Everything else that reads a line - an application calling `osReadLine`, the editor - is
|
||||||
|
unchanged and still gets a plain line with no editing in it.
|
||||||
|
|
||||||
### Paths:
|
### Paths:
|
||||||
|
|
||||||
Everywhere CosmOS takes a filename it will take a path: names with `/` between them,
|
Everywhere CosmOS takes a filename it will take a path: names with `/` between them,
|
||||||
@@ -163,6 +336,29 @@ Each *name* along a path is still the 22 characters a directory entry holds, and
|
|||||||
one is refused rather than cut short, because a name cut to 22 characters is a different
|
one is refused rather than cut short, because a name cut to 22 characters is a different
|
||||||
name that might well be some other file's.
|
name that might well be some other file's.
|
||||||
|
|
||||||
|
### What It Costs:
|
||||||
|
|
||||||
|
**The assembler is superlinear in what it reads**, and that is worth knowing before reaching
|
||||||
|
for it on something large. Cycles per byte of source climb with the size of the source:
|
||||||
|
|
||||||
|
| Source | Bytes | Cycles a byte |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `colours.asm` | 4,299 | 1,383 |
|
||||||
|
| `Edit.asm` | 16,778 | about 3,000 |
|
||||||
|
| `cosmos.asm` | about 104,000 | 6,290 |
|
||||||
|
|
||||||
|
So assembling the operating system is 654 million cycles, which is eleven minutes at a
|
||||||
|
megahertz. **It is not the disk**: the same build costs 654 million on a disk carrying the
|
||||||
|
whole source tree and 653 million on a flat one with a sixth as many files. The suspected
|
||||||
|
cause is looking a label up by walking the whole table, of which there are about nine
|
||||||
|
hundred, once for every reference - suspected rather than measured.
|
||||||
|
|
||||||
|
Nothing is being done about it, deliberately. Development happens with the host assembler,
|
||||||
|
which is where the tooling is, and the machine assembling itself is a demonstration that it
|
||||||
|
can rather than the way anybody works. But faster hardware buys a constant factor and does
|
||||||
|
not change the shape of the curve, so the program that eventually forces this is not CosmOS
|
||||||
|
- it is the first one twice its size.
|
||||||
|
|
||||||
### The Working Directory:
|
### The Working Directory:
|
||||||
|
|
||||||
`cd` moves the machine. A path beginning with `/` is measured from the root and anything
|
`cd` moves the machine. A path beginning with `/` is measured from the root and anything
|
||||||
@@ -323,7 +519,7 @@ without stopping the machine.
|
|||||||
|
|
||||||
## What Is On The Disk:
|
## What Is On The Disk:
|
||||||
|
|
||||||
`make -C Programs cosmos-disk` builds the disk this system is meant to be met on, and it is
|
`make disk` builds the disk this system is meant to be met on, and it is
|
||||||
laid out in three directories:
|
laid out in three directories:
|
||||||
|
|
||||||
| Where | What |
|
| Where | What |
|
||||||
@@ -367,6 +563,7 @@ from every assembly file in it. Several are old programs written for the bare ma
|
|||||||
| Settle | Says how the last start went and tells the machine to stop falling back, in 353 bytes. A program rather than a shell word, because the shell is for what cannot be done without it. |
|
| Settle | Says how the last start went and tells the machine to stop falling back, in 353 bytes. A program rather than a shell word, because the shell is for what cannot be done without it. |
|
||||||
| Files | Writes a file, reads it back, renames it and deletes it, in 675 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. |
|
| Files | Writes a file, reads it back, renames it and deletes it, in 675 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. |
|
||||||
| Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. |
|
| Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. |
|
||||||
|
| Grid | The first program to use the screen as a screen. Redefines a tile above the font, fills all 128 map rows, and scrolls it diagonally a pixel at a time. |
|
||||||
| Edit | A line editor. |
|
| Edit | A line editor. |
|
||||||
| Stream | Reads an 84,000 byte file through a buffer of 256, which is what says a file bigger than Data Memory can be read at all. |
|
| Stream | Reads an 84,000 byte file through a buffer of 256, which is what says a file bigger than Data Memory can be read at all. |
|
||||||
| Type | Prints a named text file a block at a time, including one too large to fit in Data Memory. |
|
| Type | Prints a named text file a block at a time, including one too large to fit in Data Memory. |
|
||||||
@@ -466,7 +663,7 @@ Typed in as bytes, checked by disassembling it back, and run. It ends with `SWI
|
|||||||
|
|
||||||
`Edit` is the first program on this machine that makes a file a person typed - every byte on every disk before it was put there by the host tool. It is line oriented in the manner of `ed`: `l` lists, `a` adds at the end, `i` and `c` and `d` take a line number, `w` writes and `q` stops.
|
`Edit` is the first program on this machine that makes a file a person typed - every byte on every disk before it was put there by the host tool. It is line oriented in the manner of `ed`: `l` lists, `a` adds at the end, `i` and `c` and `d` take a line number, `w` writes and `q` stops.
|
||||||
|
|
||||||
It includes nothing but `services.asm` and `text.asm`: the filesystem and the console are the system's, asked for rather than carried. That is what brought `Edit` down from 4,941 bytes to 2,157 bytes without a line of its own logic changing - and the way that was checked is worth knowing, because the recorded output of the `cosmosEdit` test did not move by a single byte across the rewrite.
|
It includes nothing but `services.asm` and `text.asm`: the filesystem and the console are the system's, asked for rather than carried. That is what brought `Edit` down from 4,941 bytes to 2,243 bytes without a line of its own logic changing - and the way that was checked is worth knowing, because the recorded output of the `cosmosEdit` test did not move by a single byte across the rewrite.
|
||||||
|
|
||||||
**A line it reads in is at most 128 characters**, the same length a line is everywhere else
|
**A line it reads in is at most 128 characters**, the same length a line is everywhere else
|
||||||
on this machine, and a file with a longer one is refused rather than opened. Refused rather
|
on this machine, and a file with a longer one is refused rather than opened. Refused rather
|
||||||
@@ -491,7 +688,7 @@ which assembles its own now.
|
|||||||
|
|
||||||
### A Clean Install:
|
### A Clean Install:
|
||||||
|
|
||||||
`make cosmos-disk` in `Programs/` lays down a disk the machine can start itself from, and
|
`make disk` lays down a disk the machine can start itself from, and
|
||||||
`make run-cosmos` starts it - with no boot image named, so the emulator shadows its ROM and
|
`make run-cosmos` starts it - with no boot image named, so the emulator shadows its ROM and
|
||||||
reads the disk for everything else.
|
reads the disk for everything else.
|
||||||
|
|
||||||
@@ -515,6 +712,56 @@ none, and a clean install having nothing to configure is the right default.
|
|||||||
outside with nothing on the disk consulted. That is what a debugger does, and it is what to
|
outside with nothing on the disk consulted. That is what a debugger does, and it is what to
|
||||||
use when the thing being debugged is the boot chain, since it skips the boot chain.
|
use when the thing being debugged is the boot chain, since it skips the boot chain.
|
||||||
|
|
||||||
|
### Grid, and what a tile engine costs:
|
||||||
|
|
||||||
|
Everything else drawn on this machine has been text or a bitmap. `Grid` is the first program
|
||||||
|
to use the tile engine as an engine, and it is worth reading for the size of the numbers.
|
||||||
|
|
||||||
|
It scrolls **a pixel a frame, diagonally**, and the whole of that is four port writes and two
|
||||||
|
carries. The map is 128 rows and 128 columns against a screen of 50 and 80, so the cells
|
||||||
|
around the edge are already drawn - scrolling moves the origin rather than 2,000 bytes of
|
||||||
|
screen, and what leaves the top has not gone anywhere.
|
||||||
|
|
||||||
|
It fills **all 128 columns of every map row**, not the eighty the screen shows. That is the
|
||||||
|
distinction a scrolling program has to make: asking the screen how wide it is - which there is
|
||||||
|
a register for - gives you the window, and a program that scrolls wants everything the window
|
||||||
|
can be moved over. Filling only the window leaves 48 empty columns, and scrolling sideways
|
||||||
|
walks into them.
|
||||||
|
|
||||||
|
The coarse registers move a whole cell and the fine ones move the remainder, and **they do not
|
||||||
|
carry into each other**, so the program does:
|
||||||
|
|
||||||
|
```asm
|
||||||
|
SETD.0 FineDown
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
INIB 0x07
|
||||||
|
AND
|
||||||
|
STQ.0
|
||||||
|
OUTQ 0x38
|
||||||
|
BNQ stepAcross ; Still inside the cell.
|
||||||
|
; ... and here, one step of the row origin.
|
||||||
|
```
|
||||||
|
|
||||||
|
The `AND` is both the wrap and the test: `Q` coming out as nought is exactly the moment the
|
||||||
|
cell boundary was crossed. It moved eight pixels every fourth frame before the fine registers
|
||||||
|
existed, which reads as the picture jumping rather than travelling.
|
||||||
|
|
||||||
|
It puts its tile at **200**, because the machine wakes with the font in tile memory - glyph n
|
||||||
|
at tile n, for 135 of the 256 - so a program that starts at zero paints over the alphabet and
|
||||||
|
the shell it is about to hand the machine back to. Above 135 is empty and nobody else's.
|
||||||
|
|
||||||
|
Its sixteen colour schemes are **one tile**, not sixteen. A cell's attribute nibble is added
|
||||||
|
to every palette index in it, sixteen at a time, so the same 64 bytes come out in sixteen
|
||||||
|
colourings and the map bands down the screen as it scrolls.
|
||||||
|
|
||||||
|
**What it cannot give back is the palette.** The console's colours are sixteen banks at
|
||||||
|
exactly the entries the attribute nibble lands on, so any program using the nibble overwrites
|
||||||
|
them and there is nowhere else for it to write. `Grid` restores bank 0 - grey on black, what
|
||||||
|
plain text has always been - and leaves the other fifteen as it made them. The proper answer
|
||||||
|
is a command to the screen meaning "give me back what you woke up with", the way the console
|
||||||
|
has one for clearing. There is not one yet, and this is the first program that ever wanted it.
|
||||||
|
|
||||||
## The Application Model:
|
## The Application Model:
|
||||||
|
|
||||||
CosmOS divides the two SplitBit address spaces by convention:
|
CosmOS divides the two SplitBit address spaces by convention:
|
||||||
@@ -582,6 +829,163 @@ An application may also include its own libraries or access hardware ports direc
|
|||||||
The services are an interface offered by the system, not the only way software is allowed
|
The services are an interface offered by the system, not the only way software is allowed
|
||||||
to use the computer.
|
to use the computer.
|
||||||
|
|
||||||
|
### Several Disks:
|
||||||
|
|
||||||
|
The machine has four drives behind one controller, and `drive` says which one the shell is
|
||||||
|
standing on. `drive 1` goes to another, and **the working directory goes with it** - where you
|
||||||
|
are on a disk is part of which disk you are on, not something the shell keeps on the side. Go
|
||||||
|
back and you are where you were.
|
||||||
|
|
||||||
|
Every drive is mounted at boot: the controller says how many are plugged in and each is tried
|
||||||
|
in turn. A drive with nothing in it, or a disk this cannot read, is left unmounted rather than
|
||||||
|
stopping the others, so a machine with a good disk in drive 0 and a blank in drive 1 starts
|
||||||
|
normally. `drive 1` then says there is nothing readable there, which is a different answer
|
||||||
|
from there being no such drive.
|
||||||
|
|
||||||
|
**What a mounted disk is, is eight bytes**: where its directory starts, how many blocks it is,
|
||||||
|
how big the disk is, and where you are on it. They sit together in the data segment on purpose,
|
||||||
|
because changing drives is one copy out and one copy in - and the other three thousand lines
|
||||||
|
of filesystem go on reading the same four names they always have and never learn that more
|
||||||
|
than one disk exists. That is the whole reason this was affordable.
|
||||||
|
|
||||||
|
The version is not among them. It is checked at mount and thrown away, because a version one
|
||||||
|
disk's zero parent already reads as "in the root", which is where all of its files are.
|
||||||
|
|
||||||
|
**A path may name a drive**, as a digit and a colon on the front: `1:/notes`, or `1:` on its
|
||||||
|
own for wherever that drive already was. It is handled where every path in the system arrives,
|
||||||
|
so it works for anything that takes one rather than for whichever commands somebody remembered.
|
||||||
|
|
||||||
|
**Naming a drive goes there and stays there.** Switching for the length of one command and
|
||||||
|
switching back reads better in a listing and cannot work: what a path resolves to is a start
|
||||||
|
block and a length, and those mean nothing without the drive they were read from. A `load`
|
||||||
|
that resolved on drive 1 and then read its blocks on drive 0 would read the right blocks of
|
||||||
|
the wrong disk.
|
||||||
|
|
||||||
|
**A name that begins with a digit is still a name**, because the colon is the whole of what
|
||||||
|
tells the two apart. `2things` is a directory; `2:` is a drive.
|
||||||
|
|
||||||
|
A drive the machine cannot read makes the whole path unfindable, and says so as `no such
|
||||||
|
file` - which it is, since there is nowhere for the rest of it to be.
|
||||||
|
|
||||||
|
**Copying between two disks is one command**: `Copy 1:/notes.txt 0:/keep.txt`. Every
|
||||||
|
`osFileBlock` names its path again and so goes back to the source drive; the write stream
|
||||||
|
remembers the drive it was opened on and returns there for each block. Between them the copy
|
||||||
|
walks back and forth without `Copy` itself knowing there is more than one disk.
|
||||||
|
|
||||||
|
### A Disk Of Your Own:
|
||||||
|
|
||||||
|
`make run-voyager` puts a second disk in **drive 1**, at `Disks/personal.img`, and a scratch
|
||||||
|
drive made of memory in **drive 2**. The scratch drive is what `osTakeScreen` writes to; drive
|
||||||
|
1 is yours and comes after nothing, so adding the scratch drive later did not renumber it. It is made the
|
||||||
|
first time it is needed and then left alone: never rebuilt, never cleaned, never committed.
|
||||||
|
|
||||||
|
That last part is the point. Everything else in this repository is made from source and can be
|
||||||
|
thrown away without losing anything - but a disk is where something *made on the machine*
|
||||||
|
lives, and a disk that `make clean` deletes is not a disk of your own. It sits outside
|
||||||
|
`build/` for exactly that reason, and `Disks/` is in `.gitignore`.
|
||||||
|
|
||||||
|
Delete it by hand if you ever want a fresh one.
|
||||||
|
|
||||||
|
### Disks Made Of Memory:
|
||||||
|
|
||||||
|
A drive the machine calls **volatile** loses everything when the machine stops. CosmOS formats
|
||||||
|
one it cannot read, because a drive whose contents do not survive never had anything to lose,
|
||||||
|
and mounts it like any other - so `--ram-disk 2048` gives you a working disk with nothing on
|
||||||
|
it, brought up before you reach a prompt.
|
||||||
|
|
||||||
|
**It leaves every other unreadable drive alone.** An unformatted floppy is not an invitation.
|
||||||
|
That distinction is the machine's to state and the system's to act on: the hardware says what
|
||||||
|
a drive *is*, and says nothing about filesystems, which is what leaves room for a system that
|
||||||
|
would rather have its own.
|
||||||
|
|
||||||
|
The size to format comes from the drive, not from a superblock - a superblock states a size
|
||||||
|
too, and that is no use on a disk which has not got one. The directory is sixteen blocks, 128
|
||||||
|
names, chosen rather than worked out: a scratch disk runs out of names long before it runs out
|
||||||
|
of room, and this machine cannot divide.
|
||||||
|
|
||||||
|
### Giving The Screen Back:
|
||||||
|
|
||||||
|
A program that takes the whole screen leaves the shell a blank one, and whatever was on it -
|
||||||
|
the listing you were reading, the error you were about to act on - is gone. There is nowhere
|
||||||
|
to put 48K of video memory on a machine with 64K of Data Memory that CosmOS is already living
|
||||||
|
in.
|
||||||
|
|
||||||
|
**A drive made of memory is somewhere.** `SWI osTakeScreen` says *"I am about to use the whole
|
||||||
|
screen, and would like what is on it now put back when I exit."* The system writes video
|
||||||
|
memory to a file on the scratch drive and restores it from `handleExit`, alongside the vectors
|
||||||
|
and console mode it already puts back. Q is zero if that was arranged; a machine with no
|
||||||
|
volatile drive says no, and **a program told no should carry on regardless**, because it was
|
||||||
|
going to before this existed.
|
||||||
|
|
||||||
|
**A program told no must cope.** There is no volatile drive on every machine, and a refusal is
|
||||||
|
not a fault - it means doing what the program would have done before there was anywhere to
|
||||||
|
save a screen. `Grid` clears up after itself when refused, which is the difference between a
|
||||||
|
clean prompt and a prompt printed into somebody's grid.
|
||||||
|
|
||||||
|
**The system always leaves the screen usable, refusal or not.** The fine scroll registers go
|
||||||
|
back to zero at every program exit, because the console draws in whole cells and a view three
|
||||||
|
pixels into one puts every character three pixels out for ever. That is true whether or not
|
||||||
|
the picture could be saved, so it is not part of the saving.
|
||||||
|
|
||||||
|
**It is not automatic, and that is the point.** Saving on every program start would be cheap
|
||||||
|
enough, but restoring on every exit would be wrong: `dir`, `Files` and `Say` print and stop,
|
||||||
|
and their output is the reason you ran them. A program that says nothing behaves exactly as
|
||||||
|
every program did before this existed.
|
||||||
|
|
||||||
|
**Tiles, map and palette all go** - 196 pages, and a block on the front holding the cursor,
|
||||||
|
the four scroll registers and the mode. The map's off-screen rows are the console's
|
||||||
|
scrollback; the tiles are the font, which a program that redefines one has overwritten; and
|
||||||
|
the palette is where the console's own colours live. `Grid` used to give back the map and not
|
||||||
|
the colours, and handed the shell green text on a blue ground.
|
||||||
|
|
||||||
|
It also replaced thirty lines of `Grid`: four scroll registers put back by hand, the map
|
||||||
|
filled with spaces, the cursor sent home, and palette bank 0 written out - all of which was
|
||||||
|
*still* wrong, because the other fifteen banks kept `Grid`'s colours and there was nowhere to
|
||||||
|
have kept the real ones.
|
||||||
|
|
||||||
|
### Where A Program Is Looked For:
|
||||||
|
|
||||||
|
Three places, tried in order:
|
||||||
|
|
||||||
|
1. Where you are.
|
||||||
|
2. `/Apps` on the disk you are on.
|
||||||
|
3. `/Apps` on drive 0.
|
||||||
|
|
||||||
|
The first makes a program you are working on the one that runs. The second makes `Snake` work
|
||||||
|
from any directory. **The third makes the system's programs work from a disk of your own** -
|
||||||
|
one with your files on it and no system - which is most of the point of having a second disk.
|
||||||
|
|
||||||
|
**Fetching a program does not move you, and neither does running one.** The drive is put back
|
||||||
|
after the load, because by then the program is in memory and the block numbers it came from
|
||||||
|
mean nothing; and put back again when the program exits, because a program that copies between
|
||||||
|
two disks moves the drive as its own paths need it to and being left wherever it finished is
|
||||||
|
not what anybody asked for. So `Copy 1:/a 0:/b` leaves you exactly where you were.
|
||||||
|
|
||||||
|
### Bank Numbers Are One Namespace:
|
||||||
|
|
||||||
|
A program that wants a device's memory registers it as a bank, and **bank numbers belong to
|
||||||
|
the whole machine**. Nothing hands them out and nothing refuses a number that is already
|
||||||
|
spoken for - registering one that is taken does not fail, it succeeds, and whatever held it
|
||||||
|
before quietly answers to nothing.
|
||||||
|
|
||||||
|
| Bank | Whose |
|
||||||
|
| --- | --- |
|
||||||
|
| 0 | Program Memory. The machine's. |
|
||||||
|
| 1 | Data Memory. The machine's. |
|
||||||
|
| 2 | The bank table. The machine's. |
|
||||||
|
| 3 | The disk's buffer, given at mount by `sbfsMount`. CosmOS needs it for as long as it is running. |
|
||||||
|
| 4 and up | Free for a program to use. |
|
||||||
|
|
||||||
|
`Grid` learned this the hard way and is the reason the table is here. It asked for 3, took the
|
||||||
|
disk's buffer, and every read the filesystem made afterwards came out of video memory - so the
|
||||||
|
shell found an empty disk and could not start anything by name, several commands after the
|
||||||
|
program that did it had exited. Nothing said a word, because from the controller's point of
|
||||||
|
view nothing went wrong.
|
||||||
|
|
||||||
|
**A program returns a bank by giving it back**, which today means knowing what was there
|
||||||
|
before. There is no service that hands out a free number, and if this becomes a common thing
|
||||||
|
for programs to want then that is what should exist rather than a longer table.
|
||||||
|
|
||||||
## What A Program May Ask The System For:
|
## What A Program May Ask The System For:
|
||||||
|
|
||||||
A loaded program is on its own hardware and can do anything the machine can do - it is a fence, not a wall. But the things it usually wants are things the system is already doing, and asking is both shorter and the only way to reach code that was assembled separately. `CALL` needs a label, and a label has to be in the same assembly; `SWI` needs only a number both sides agree on.
|
A loaded program is on its own hardware and can do anything the machine can do - it is a fence, not a wall. But the things it usually wants are things the system is already doing, and asking is both shorter and the only way to reach code that was assembled separately. `CALL` needs a label, and a label has to be in the same assembly; `SWI` needs only a number both sides agree on.
|
||||||
@@ -608,6 +1012,7 @@ Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, w
|
|||||||
| osPrintNumber | A and B together are a number. Prints it in decimal, without leading zeroes. |
|
| osPrintNumber | A and B together are a number. Prints it in decimal, without leading zeroes. |
|
||||||
| osBreak | Stops the program, shows every register as it had them, waits for a key, and carries on. |
|
| osBreak | Stops the program, shows every register as it had them, waits for a key, and carries on. |
|
||||||
| osLastStatus | Q answers what the last program exited with: 0 it did what it was asked, 1 it did not, 2 it was asked wrongly. A program may give its own meanings if it says so. |
|
| osLastStatus | Q answers what the last program exited with: 0 it did what it was asked, 1 it did not, 2 it was asked wrongly. A program may give its own meanings if it says so. |
|
||||||
|
| osTakeScreen | Says this program is about to use the whole screen and would like what is on it put back when it exits. Q is zero if that was arranged; anything else means it was not, which is the ordinary answer on a machine with no volatile drive. See Giving The Screen Back. |
|
||||||
| osBootState | Q answers how the last start went: 0 settled, 1 trying, 2 fell back. A machine with no disk answers settled, because there is nothing there to be unsettled about. |
|
| osBootState | Q answers how the last start went: 0 settled, 1 trying, 2 fell back. A machine with no disk answers settled, because there is nothing there to be unsettled about. |
|
||||||
| osBootSettle | Puts it back to settled, which is how a machine that fell back is told the situation has changed. Q is zero if the disk took it. **Settling is the only write a program gets** - marking a start as trying or fallen back is the loader's business, and a service that let a program claim either would let it lie about something the loader cannot check. |
|
| osBootSettle | Puts it back to settled, which is how a machine that fell back is told the situation has changed. Q is zero if the disk took it. **Settling is the only write a program gets** - marking a start as trying or fallen back is the loader's business, and a service that let a program claim either would let it lie about something the loader cannot check. |
|
||||||
|
|
||||||
|
|||||||
+1461
-59
File diff suppressed because it is too large
Load Diff
@@ -136,6 +136,409 @@ sbfsMountTooBig:
|
|||||||
ADD ; Q is not zero: not a disk this will mount.
|
ADD ; Q is not zero: not a disk this will mount.
|
||||||
RET
|
RET
|
||||||
|
|
||||||
|
; ---- Making a disk into a filesystem ----
|
||||||
|
;
|
||||||
|
; The machine hands out blocks and says how many; what they mean is the system's business.
|
||||||
|
; That is the whole reason this exists here rather than only in the host tool: a machine with
|
||||||
|
; a drive made of memory comes up with a drive full of zeroes, and zeroes are not a
|
||||||
|
; filesystem. Somebody has to write the first one, and it should be whoever is going to read
|
||||||
|
; it - which is also what leaves room for a system that would rather have its own.
|
||||||
|
;
|
||||||
|
; Q is zero if it worked. Everything on the drive is lost, which is why nothing calls this
|
||||||
|
; except on a drive the machine has said is volatile.
|
||||||
|
sbfsFormat:
|
||||||
|
; How big it is, which only the drive can say. A superblock would say too, and a disk with
|
||||||
|
; no superblock is exactly the case this is for.
|
||||||
|
INA 0x27
|
||||||
|
SETD.1 SbfsScratch
|
||||||
|
STA.1
|
||||||
|
INA 0x28
|
||||||
|
INCD.1
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
; The buffer, cleared, because everything not written below has to be nought and the
|
||||||
|
; controller's buffer holds whatever was last read.
|
||||||
|
SETD.0 SbfsBuffer
|
||||||
|
RSTB
|
||||||
|
sbfsFormatClear:
|
||||||
|
RSTA
|
||||||
|
STA.0
|
||||||
|
INCD.0
|
||||||
|
DECB
|
||||||
|
BNB sbfsFormatClear ; 256 of them: B wraps from nought to 255 and back to nought.
|
||||||
|
|
||||||
|
; "SBFS", and the version. Two, because a disk made now has directories.
|
||||||
|
SETD.0 SbfsMagic
|
||||||
|
SETD.1 SbfsBuffer
|
||||||
|
INIB 0d4
|
||||||
|
sbfsFormatMagic:
|
||||||
|
LDA.0
|
||||||
|
STA.1
|
||||||
|
INCD.0
|
||||||
|
INCD.1
|
||||||
|
DECB
|
||||||
|
BNB sbfsFormatMagic
|
||||||
|
INIA 0d2
|
||||||
|
STA.1 ; Offset 4: the version.
|
||||||
|
|
||||||
|
; Offset 6, how many blocks the disk has, as the drive reported it.
|
||||||
|
SETD.1 SbfsBuffer
|
||||||
|
DPUP.1 0d06
|
||||||
|
SETD.0 SbfsScratch
|
||||||
|
LDA.0
|
||||||
|
STA.1
|
||||||
|
INCD.0
|
||||||
|
INCD.1
|
||||||
|
LDA.0
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
; Offset 8, where the directory starts: block one, straight after this one.
|
||||||
|
SETD.1 SbfsBuffer
|
||||||
|
DPUP.1 0d08
|
||||||
|
RSTA
|
||||||
|
STA.1
|
||||||
|
INCD.1
|
||||||
|
INIA 0d1
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
; Offset 10, how many blocks of directory. SIXTEEN, WHICH IS 128 NAMES, and chosen rather
|
||||||
|
; than worked out: a scratch disk runs out of names long before it runs out of room, and
|
||||||
|
; this machine cannot divide, so a number that fits every size this is used for is worth
|
||||||
|
; more than arithmetic to find a better one.
|
||||||
|
SETD.1 SbfsBuffer
|
||||||
|
DPUP.1 0d10
|
||||||
|
RSTA
|
||||||
|
STA.1
|
||||||
|
INCD.1
|
||||||
|
INIA 0d16
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
; Block nought, written.
|
||||||
|
SETD.0 SbfsBlock
|
||||||
|
RSTA
|
||||||
|
STA.0
|
||||||
|
INCD.0
|
||||||
|
STA.0
|
||||||
|
SETD.1 SbfsBuffer
|
||||||
|
CALL sbfsBufferIn
|
||||||
|
CALL sbfsWriteBlock
|
||||||
|
BNQ sbfsFormatFailed
|
||||||
|
|
||||||
|
; And the directory cleared, so that no entry is in use. The buffer is already nought
|
||||||
|
; everywhere the superblock did not reach, so it is cleared once more and written sixteen
|
||||||
|
; times rather than built again each time.
|
||||||
|
SETD.0 SbfsBuffer
|
||||||
|
RSTB
|
||||||
|
sbfsFormatBlank:
|
||||||
|
RSTA
|
||||||
|
STA.0
|
||||||
|
INCD.0
|
||||||
|
DECB
|
||||||
|
BNB sbfsFormatBlank
|
||||||
|
|
||||||
|
INIA 0d16
|
||||||
|
SETD.1 SbfsCount
|
||||||
|
STA.1
|
||||||
|
sbfsFormatDirectory:
|
||||||
|
SETD.0 SbfsBlock
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0 ; Blocks one to sixteen. A directory never crosses 255 here.
|
||||||
|
SETD.1 SbfsBuffer
|
||||||
|
CALL sbfsBufferIn
|
||||||
|
CALL sbfsWriteBlock
|
||||||
|
BNQ sbfsFormatFailed
|
||||||
|
SETD.1 SbfsCount
|
||||||
|
LDA.1
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
BNA sbfsFormatDirectory
|
||||||
|
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
sbfsFormatFailed:
|
||||||
|
RSTA
|
||||||
|
INIB 0d1
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- Every drive the machine has ----
|
||||||
|
;
|
||||||
|
; Asked for rather than assumed: the controller says how many are plugged in, and each is
|
||||||
|
; selected and mounted in turn. A drive with nothing in it, or a disk this cannot read, is
|
||||||
|
; left unmounted rather than stopping the others - a machine with a good disk in drive 0 and
|
||||||
|
; a blank in drive 1 should start.
|
||||||
|
;
|
||||||
|
; Q is zero if drive 0 mounted, because that is the one the system came off and the one the
|
||||||
|
; shell will be standing in when it gets a prompt.
|
||||||
|
sbfsMountAll:
|
||||||
|
RSTA
|
||||||
|
SETD.1 SbfsMounted
|
||||||
|
STA.1
|
||||||
|
SETD.1 SbfsDrive
|
||||||
|
STA.1
|
||||||
|
OUTA 0x24 ; Drive 0, whatever the controller was left on.
|
||||||
|
INIA 0xFF
|
||||||
|
SETD.1 SbfsScratch1
|
||||||
|
STA.1 ; No scratch drive until one is found.
|
||||||
|
|
||||||
|
; The live record belongs to nobody yet, so every slot starts empty and a drive that fails
|
||||||
|
; to mount keeps an empty one.
|
||||||
|
SETD.0 SbfsMountTable
|
||||||
|
INIB 0d32
|
||||||
|
sbfsMountClear:
|
||||||
|
RSTA
|
||||||
|
STA.0
|
||||||
|
INCD.0
|
||||||
|
DECB
|
||||||
|
BNB sbfsMountClear
|
||||||
|
|
||||||
|
INA 0x25
|
||||||
|
SETD.1 SbfsDriveCount
|
||||||
|
STA.1
|
||||||
|
RSTA
|
||||||
|
SETD.1 SbfsDriveAt
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
sbfsMountEach:
|
||||||
|
SETD.1 SbfsDriveAt
|
||||||
|
LDA.1
|
||||||
|
SETD.1 SbfsDriveCount
|
||||||
|
LDB.1
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ sbfsMountAllDone ; Past the last one.
|
||||||
|
|
||||||
|
; Selected directly rather than through sbfsUse: there is nothing in the live record worth
|
||||||
|
; putting back yet, and sbfsUse would copy eight bytes of nothing into a slot.
|
||||||
|
SETD.1 SbfsDriveAt
|
||||||
|
LDA.1
|
||||||
|
OUTA 0x24
|
||||||
|
SETD.1 SbfsDrive
|
||||||
|
STA.1
|
||||||
|
RSTA
|
||||||
|
SETD.1 SbfsBufferKnown
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
CALL sbfsMount
|
||||||
|
BRQ sbfsMountGot
|
||||||
|
|
||||||
|
; ---- Nothing readable, and whether that is an invitation ----
|
||||||
|
;
|
||||||
|
; A drive the machine calls VOLATILE loses everything when the machine stops, so a volatile
|
||||||
|
; drive with no filesystem on it never had one to lose and bringing it up is the system's
|
||||||
|
; job. Anything else is somebody's disk: an unformatted floppy is not an invitation, it is
|
||||||
|
; a blank floppy, and a system that formatted it on sight would be a system you could not
|
||||||
|
; safely put a disk into.
|
||||||
|
;
|
||||||
|
; THE MACHINE SAYS WHAT THE DRIVE IS AND NOTHING ABOUT FILESYSTEMS. A system that would
|
||||||
|
; rather have its own reads the same bit and writes whatever it likes.
|
||||||
|
INA 0x26
|
||||||
|
INIB 0x01 ; VOLATILE
|
||||||
|
AND
|
||||||
|
BRQ sbfsMountNext ; Not ours to touch.
|
||||||
|
|
||||||
|
CALL sbfsFormat
|
||||||
|
BNQ sbfsMountNext
|
||||||
|
CALL sbfsMount
|
||||||
|
BNQ sbfsMountNext
|
||||||
|
|
||||||
|
sbfsMountGot:
|
||||||
|
; ---- And whether this is the one to keep scratch on ----
|
||||||
|
;
|
||||||
|
; A volatile drive is somewhere the system may write without asking, because nothing on it
|
||||||
|
; outlives the machine. The FIRST one found is the scratch drive; a machine with two has
|
||||||
|
; made a decision nobody expressed, and taking the lower number is at least predictable.
|
||||||
|
INA 0x26
|
||||||
|
INIB 0x01
|
||||||
|
AND
|
||||||
|
BRQ sbfsMountNotScratch
|
||||||
|
SETD.1 SbfsScratch1
|
||||||
|
LDA.1
|
||||||
|
INIB 0xFF
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ sbfsMountNotScratch ; There is one already.
|
||||||
|
SETD.1 SbfsDriveAt
|
||||||
|
LDA.1
|
||||||
|
SETD.1 SbfsScratch1
|
||||||
|
STA.1
|
||||||
|
sbfsMountNotScratch:
|
||||||
|
|
||||||
|
; Mounted: remember which, and put the live record where it belongs.
|
||||||
|
SETD.1 SbfsDriveAt
|
||||||
|
LDA.1
|
||||||
|
CALL sbfsDriveBit
|
||||||
|
MVQA ; The bit is in Q, and RET put the old A back. OR reads A and B.
|
||||||
|
SETD.1 SbfsMounted
|
||||||
|
LDB.1
|
||||||
|
OR
|
||||||
|
STQ.1
|
||||||
|
|
||||||
|
SETD.1 SbfsDriveAt
|
||||||
|
LDA.1
|
||||||
|
CALL sbfsSlotAt
|
||||||
|
PSHD.3
|
||||||
|
POPD.1
|
||||||
|
SETD.0 SbfsMountLive
|
||||||
|
CALL sbfsCopyMount
|
||||||
|
|
||||||
|
sbfsMountNext:
|
||||||
|
SETD.1 SbfsDriveAt
|
||||||
|
LDA.1
|
||||||
|
INCA
|
||||||
|
STA.1
|
||||||
|
BRI sbfsMountEach
|
||||||
|
|
||||||
|
sbfsMountAllDone:
|
||||||
|
; Back to drive 0 and its record, which is where a shell starts. A stays nought throughout,
|
||||||
|
; which is the drive, the note on the buffer, and the slot to fetch.
|
||||||
|
RSTA
|
||||||
|
OUTA 0x24
|
||||||
|
SETD.1 SbfsDrive
|
||||||
|
STA.1
|
||||||
|
SETD.1 SbfsBufferKnown
|
||||||
|
STA.1
|
||||||
|
CALL sbfsSlotAt
|
||||||
|
PSHD.3
|
||||||
|
POPD.0
|
||||||
|
SETD.1 SbfsMountLive
|
||||||
|
CALL sbfsCopyMount
|
||||||
|
|
||||||
|
; Is drive 0 one of the ones that mounted?
|
||||||
|
INIA 0x01
|
||||||
|
SETD.1 SbfsMounted
|
||||||
|
LDB.1
|
||||||
|
AND
|
||||||
|
BRQ sbfsMountAllNone
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
sbfsMountAllNone:
|
||||||
|
RSTA
|
||||||
|
INIB 0d1
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; A = a drive number. Q is the bit that stands for it in SbfsMounted.
|
||||||
|
sbfsDriveBit:
|
||||||
|
INIB 0x01
|
||||||
|
sbfsDriveBitStep:
|
||||||
|
BRA sbfsDriveBitDone
|
||||||
|
PSHA
|
||||||
|
RSTA
|
||||||
|
SHL ; A and B are one register to SHL, so with A nought this is B
|
||||||
|
POPA ; doubled - which for four drives never reaches the top.
|
||||||
|
DECA
|
||||||
|
BRI sbfsDriveBitStep
|
||||||
|
sbfsDriveBitDone:
|
||||||
|
RSTA
|
||||||
|
CCF
|
||||||
|
ADD ; Q = B, which is the bit.
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- Changing which disk is the disk ----
|
||||||
|
;
|
||||||
|
; A holds the drive. Q is zero if it is now the one in use.
|
||||||
|
;
|
||||||
|
; The eight live bytes go back to the drive they belong to and the wanted drive's come in.
|
||||||
|
; Everything below this line in the file goes on reading the same four names it always has and
|
||||||
|
; never learns that more than one disk exists - which is the whole of why this is affordable.
|
||||||
|
;
|
||||||
|
; The buffer is FORGOTTEN, and that is not tidiness. The controller has one buffer shared by
|
||||||
|
; every drive, so the note of which block is in it is wrong the moment the drive changes.
|
||||||
|
; Leaving it would mean the next read of that block number quietly skipping the disk and
|
||||||
|
; handing back the other drive's data.
|
||||||
|
sbfsUse:
|
||||||
|
SETD.1 SbfsDrive
|
||||||
|
LDB.1
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ sbfsUseAlready ; Already there, and swapping would be a long way round to nothing.
|
||||||
|
|
||||||
|
PSHA ; The drive that was asked for, kept across the copying.
|
||||||
|
|
||||||
|
; The live record back to the drive it belongs to. DP1 is still SbfsDrive, from the
|
||||||
|
; comparison above.
|
||||||
|
LDA.1
|
||||||
|
CALL sbfsSlotAt
|
||||||
|
PSHD.3
|
||||||
|
POPD.1
|
||||||
|
SETD.0 SbfsMountLive
|
||||||
|
CALL sbfsCopyMount
|
||||||
|
|
||||||
|
; And the wanted drive's record into the live eight.
|
||||||
|
POPA
|
||||||
|
PSHA
|
||||||
|
CALL sbfsSlotAt
|
||||||
|
PSHD.3
|
||||||
|
POPD.0
|
||||||
|
SETD.1 SbfsMountLive
|
||||||
|
CALL sbfsCopyMount
|
||||||
|
|
||||||
|
POPA
|
||||||
|
SETD.1 SbfsDrive
|
||||||
|
STA.1
|
||||||
|
OUTA 0x24
|
||||||
|
|
||||||
|
; ---- The note on the buffer belongs to the drive that is leaving ----
|
||||||
|
;
|
||||||
|
; One buffer serves every drive, so "block 31 is in the buffer" stops being true the moment
|
||||||
|
; the drive changes, and a read of block 31 that trusted it would hand back the other disk.
|
||||||
|
;
|
||||||
|
; IT CANNOT CURRENTLY BE REACHED, and that is worth writing down rather than leaving as an
|
||||||
|
; implied claim. Only the file read-ahead consults the note - a directory scan deliberately
|
||||||
|
; does not - and finding a file requires a scan, which overwrites the note on the way past.
|
||||||
|
; Two disks were built with the same file at the same block to try to catch it and the
|
||||||
|
; answer was right either way.
|
||||||
|
;
|
||||||
|
; Kept because it is three instructions and it holds an invariant rather than patching a
|
||||||
|
; symptom: the note describes the selected drive. The day something reads two files without
|
||||||
|
; a directory between them, this is already true instead of being a bug with a story.
|
||||||
|
RSTA
|
||||||
|
SETD.1 SbfsBufferKnown
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
sbfsUseAlready:
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; DP3 = the eight bytes belonging to the drive in A. Stepped rather than multiplied, because
|
||||||
|
; this machine cannot multiply and there are at most three steps.
|
||||||
|
sbfsSlotAt:
|
||||||
|
SETD.3 SbfsMountTable
|
||||||
|
BRA sbfsSlotThere
|
||||||
|
sbfsSlotStep:
|
||||||
|
DPUP.3 0d08
|
||||||
|
DECA
|
||||||
|
BNA sbfsSlotStep
|
||||||
|
sbfsSlotThere:
|
||||||
|
RET
|
||||||
|
|
||||||
|
; Eight bytes, DP0 to DP1.
|
||||||
|
sbfsCopyMount:
|
||||||
|
INIB 0d8
|
||||||
|
sbfsCopyMountByte:
|
||||||
|
LDA.0
|
||||||
|
STA.1
|
||||||
|
INCD.0
|
||||||
|
INCD.1
|
||||||
|
DECB
|
||||||
|
BNB sbfsCopyMountByte
|
||||||
|
RET
|
||||||
|
|
||||||
; ---- Finding something by path ----
|
; ---- Finding something by path ----
|
||||||
;
|
;
|
||||||
; DP0 points at a path ending in a zero byte: names with '/' between them. A path that
|
; DP0 points at a path ending in a zero byte: names with '/' between them. A path that
|
||||||
@@ -188,12 +591,118 @@ sbfsFindRoot:
|
|||||||
ADD
|
ADD
|
||||||
RET
|
RET
|
||||||
|
|
||||||
|
; ---- Is there a drive on the front of this path, and can we go there? ----
|
||||||
|
;
|
||||||
|
; A digit and a colon. Q is zero if the path is usable, whether or not one was there; Q is one
|
||||||
|
; if a drive was named and it is not one this machine can read, which makes the whole path
|
||||||
|
; unfindable - because it is.
|
||||||
|
;
|
||||||
|
; NAMING A DRIVE GOES THERE AND STAYS THERE. The alternative was to switch for the operation
|
||||||
|
; and switch back, which reads better in a listing and cannot work: what a path resolves to is
|
||||||
|
; a start block and a length, and those mean nothing without the drive they were read from. A
|
||||||
|
; load that resolved on drive 1 and then read on drive 0 would read the right blocks of the
|
||||||
|
; wrong disk.
|
||||||
|
sbfsPathDrive:
|
||||||
|
SETD.1 SbfsPathAt
|
||||||
|
LDD.0.1
|
||||||
|
LDA.0
|
||||||
|
INIB 0d48 ; '0'
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRC sbfsPathNoDrive ; Borrowed, so it is below '0' and not a digit.
|
||||||
|
MVQA
|
||||||
|
INIB 0d10
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNC sbfsPathNoDrive ; Ten or more, so not a digit either.
|
||||||
|
|
||||||
|
; The character after it has to be a colon, or this is a name that begins with a digit.
|
||||||
|
; DP1 is still SbfsPathAt, from the top of this routine.
|
||||||
|
LDD.0.1
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
INIB 0d58 ; ':'
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ sbfsPathNoDrive
|
||||||
|
|
||||||
|
; It is a drive. Is it one this machine has, with something readable in it?
|
||||||
|
LDD.0.1
|
||||||
|
LDA.0
|
||||||
|
INIB 0d48
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
MVQA
|
||||||
|
SETD.1 SbfsPathWanted
|
||||||
|
STA.1
|
||||||
|
CALL sbfsDriveBit
|
||||||
|
MVQA
|
||||||
|
SETD.1 SbfsMounted
|
||||||
|
LDB.1
|
||||||
|
AND
|
||||||
|
BRQ sbfsPathBadDrive
|
||||||
|
|
||||||
|
SETD.1 SbfsPathWanted
|
||||||
|
LDA.1
|
||||||
|
CALL sbfsUse
|
||||||
|
|
||||||
|
; Past the digit and the colon. What follows is an ordinary path, and a bare "1:" is an
|
||||||
|
; empty one - which walks to where that drive already was.
|
||||||
|
SETD.1 SbfsPathAt
|
||||||
|
LDD.0.1
|
||||||
|
INCD.0
|
||||||
|
INCD.0
|
||||||
|
STD.0.1
|
||||||
|
|
||||||
|
sbfsPathNoDrive:
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
sbfsPathBadDrive:
|
||||||
|
RSTA
|
||||||
|
INIB 0d1
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
; The walk itself. Q is zero if the whole path was walked, and SbfsAt says where it ended -
|
; The walk itself. Q is zero if the whole path was walked, and SbfsAt says where it ended -
|
||||||
; which may be the root, and that is an answer rather than a failure.
|
; which may be the root, and that is an answer rather than a failure.
|
||||||
sbfsWalk:
|
sbfsWalk:
|
||||||
SETD.1 SbfsPathAt
|
SETD.1 SbfsPathAt
|
||||||
STD.0.1
|
STD.0.1
|
||||||
|
|
||||||
|
; ---- A drive in front of the path ----
|
||||||
|
;
|
||||||
|
; Done here because this is where every path in the system arrives - eight callers between
|
||||||
|
; the shell, the config reader and this file - so naming a drive works everywhere at once
|
||||||
|
; rather than in whichever commands somebody remembered.
|
||||||
|
CALL sbfsPathDrive
|
||||||
|
BNQ sbfsWalkNoDrive
|
||||||
|
|
||||||
|
; ---- And DP0 has to be told ----
|
||||||
|
;
|
||||||
|
; sbfsPathDrive moved SbfsPathAt past the digit and the colon, but RET put DP0 back the way
|
||||||
|
; it found it - so the test below for a leading separator was reading the DIGIT and calling
|
||||||
|
; every prefixed path relative. It only showed when the drive being named was standing
|
||||||
|
; somewhere other than its root, because a relative walk from the root is an absolute one.
|
||||||
|
SETD.1 SbfsPathAt
|
||||||
|
LDD.0.1
|
||||||
|
BRI sbfsWalkPath
|
||||||
|
|
||||||
|
; A drive that this machine cannot read makes the path unfindable, because it is: there is
|
||||||
|
; nowhere for the rest of it to be.
|
||||||
|
sbfsWalkNoDrive:
|
||||||
|
RSTA
|
||||||
|
INIB 0d1
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
sbfsWalkPath:
|
||||||
|
|
||||||
; Where it starts. A path beginning with a separator is measured from the root, which is
|
; Where it starts. A path beginning with a separator is measured from the root, which is
|
||||||
; zero because a parent is an entry index PLUS ONE and the root is not an entry.
|
; zero because a parent is an entry index PLUS ONE and the root is not an entry.
|
||||||
; Anything else is measured from wherever the machine already is.
|
; Anything else is measured from wherever the machine already is.
|
||||||
@@ -1413,10 +1922,37 @@ sbfsBoundsDone:
|
|||||||
; First fit, walking the directory, because with files laid down contiguously the
|
; First fit, walking the directory, because with files laid down contiguously the
|
||||||
; directory already says which blocks are spoken for. There is no allocation table to
|
; directory already says which blocks are spoken for. There is no allocation table to
|
||||||
; consult and none to keep right.
|
; consult and none to keep right.
|
||||||
|
;
|
||||||
|
; ---- Moving the candidate along without starting again ----
|
||||||
|
;
|
||||||
|
; This used to give up the moment it found something in the way: it moved the candidate past
|
||||||
|
; that one entry and STARTED THE DIRECTORY AGAIN FROM THE FIRST BLOCK. With files laid down
|
||||||
|
; one after another that is a restart per file, and every restart reads directory blocks off
|
||||||
|
; the disk until it reaches the next one in the way - which is further in each time. A disk
|
||||||
|
; of 183 files cost thousands of block reads to place one file, and assembling onto a disk
|
||||||
|
; with the whole source tree on it took eleven minutes with nearly all of it spent here.
|
||||||
|
;
|
||||||
|
; The candidate moves along DURING the pass now, and the pass carries on from where it is.
|
||||||
|
; Entries later in the directory are then tested against where the candidate has got to, so
|
||||||
|
; on a disk that has been appended to - which is what a disk mostly is - one pass walks it
|
||||||
|
; past everything and the next confirms there is nothing left in the way. Two passes rather
|
||||||
|
; than one per file.
|
||||||
|
;
|
||||||
|
; IT IS STILL FIRST FIT. The candidate only ever moves past something that genuinely
|
||||||
|
; overlaps it, and when it does there is nowhere below that could have held the run: the
|
||||||
|
; entry in the way covers everything up to its end, and it begins before the candidate ends.
|
||||||
|
; So nothing is skipped that first fit would have found - and because entries earlier in the
|
||||||
|
; directory were tested against an earlier candidate, the pass repeats until one goes by with
|
||||||
|
; the candidate standing still.
|
||||||
sbfsAllocate:
|
sbfsAllocate:
|
||||||
CALL sbfsFirstData
|
CALL sbfsFirstData
|
||||||
|
|
||||||
sbfsAllocTry:
|
sbfsAllocTry:
|
||||||
|
; The candidate has not moved yet this time round.
|
||||||
|
SETD.0 SbfsAllocMoved
|
||||||
|
RSTA
|
||||||
|
STA.0
|
||||||
|
|
||||||
SETD.0 SbfsCandEnd
|
SETD.0 SbfsCandEnd
|
||||||
SETD.2 SbfsCandidate
|
SETD.2 SbfsCandidate
|
||||||
CALL sbfsSetWord
|
CALL sbfsSetWord
|
||||||
@@ -1491,12 +2027,26 @@ sbfsAllocEntry:
|
|||||||
CALL sbfsCompareWord
|
CALL sbfsCompareWord
|
||||||
BNC sbfsAllocClear
|
BNC sbfsAllocClear
|
||||||
|
|
||||||
; They do overlap, so try again from the far end of whatever is in the way.
|
; They do overlap, so the candidate moves to the far end of what is in the way - and the
|
||||||
POPD.2
|
; scan carries on from here rather than beginning again, so whatever comes next is measured
|
||||||
|
; against where the candidate has got to.
|
||||||
|
;
|
||||||
|
; WHERE THIS ENTRY IS STAYS ON THE STACK THROUGHOUT. The work below wants DP2 for its own
|
||||||
|
; purposes and the scan needs it back on the entry to step to the next one, which is what
|
||||||
|
; sbfsAllocClear is for.
|
||||||
SETD.0 SbfsCandidate
|
SETD.0 SbfsCandidate
|
||||||
SETD.2 SbfsEntryEnd
|
SETD.2 SbfsEntryEnd
|
||||||
CALL sbfsSetWord
|
CALL sbfsSetWord
|
||||||
BRI sbfsAllocTry
|
SETD.0 SbfsCandEnd
|
||||||
|
SETD.2 SbfsCandidate
|
||||||
|
CALL sbfsSetWord
|
||||||
|
SETD.0 SbfsCandEnd
|
||||||
|
SETD.2 SbfsWantBlocks
|
||||||
|
CALL sbfsAddWord
|
||||||
|
INIA 0x01
|
||||||
|
SETD.0 SbfsAllocMoved
|
||||||
|
STA.0
|
||||||
|
; And on into sbfsAllocClear, which puts the entry back in DP2 and steps to the next one.
|
||||||
|
|
||||||
sbfsAllocClear:
|
sbfsAllocClear:
|
||||||
POPD.2
|
POPD.2
|
||||||
@@ -1517,6 +2067,16 @@ sbfsAllocNext:
|
|||||||
STA.1
|
STA.1
|
||||||
BNA sbfsAllocBlock
|
BNA sbfsAllocBlock
|
||||||
|
|
||||||
|
; ---- The pass is over ----
|
||||||
|
;
|
||||||
|
; Something was in the way, so start again from the far end of the furthest of them. That
|
||||||
|
; is one jump for however many files the candidate ran into, rather than one jump each.
|
||||||
|
SETD.0 SbfsAllocMoved
|
||||||
|
LDA.0
|
||||||
|
BRA sbfsAllocRoom ; It never moved, so nothing is in the way of where it is.
|
||||||
|
BRI sbfsAllocTry
|
||||||
|
|
||||||
|
sbfsAllocRoom:
|
||||||
; Nothing was in the way, so this is where it goes.
|
; Nothing was in the way, so this is where it goes.
|
||||||
SETD.0 SbfsFileStart
|
SETD.0 SbfsFileStart
|
||||||
SETD.2 SbfsCandidate
|
SETD.2 SbfsCandidate
|
||||||
@@ -2549,6 +3109,16 @@ sbfsStreamNoTemp:
|
|||||||
SETD.2 SbfsFileStart
|
SETD.2 SbfsFileStart
|
||||||
CALL sbfsSetWord
|
CALL sbfsSetWord
|
||||||
|
|
||||||
|
; ---- And which disk all of that is on ----
|
||||||
|
;
|
||||||
|
; A write stream is the one thing here that lives across service calls, so it is the one
|
||||||
|
; thing that can have the drive changed underneath it. Copying between two disks is exactly
|
||||||
|
; that: every osFileBlock re-resolves the SOURCE path and goes to its drive, and then
|
||||||
|
; osFileWrite has to come back here. The path was walked above, so the drive is right now.
|
||||||
|
INA 0x24
|
||||||
|
SETD.0 SbfsStreamDrive
|
||||||
|
STA.0
|
||||||
|
|
||||||
INIA 0x01
|
INIA 0x01
|
||||||
SETD.0 SbfsStreamOpen
|
SETD.0 SbfsStreamOpen
|
||||||
STA.0
|
STA.0
|
||||||
@@ -2558,6 +3128,13 @@ sbfsStreamNoTemp:
|
|||||||
ADD
|
ADD
|
||||||
RET
|
RET
|
||||||
|
|
||||||
|
; Back to the disk the open stream belongs to, whatever has been read in between.
|
||||||
|
sbfsStreamHere:
|
||||||
|
SETD.0 SbfsStreamDrive
|
||||||
|
LDA.0
|
||||||
|
CALL sbfsUse
|
||||||
|
RET
|
||||||
|
|
||||||
sbfsStreamNo:
|
sbfsStreamNo:
|
||||||
RSTA
|
RSTA
|
||||||
INIB 0d1
|
INIB 0d1
|
||||||
@@ -2571,6 +3148,7 @@ sbfsStreamWrite:
|
|||||||
SETD.0 SbfsStreamOpen
|
SETD.0 SbfsStreamOpen
|
||||||
LDA.0
|
LDA.0
|
||||||
BRA sbfsStreamNo
|
BRA sbfsStreamNo
|
||||||
|
CALL sbfsStreamHere
|
||||||
|
|
||||||
; Where the file is and how big it is, said again rather than looked up: all three were
|
; Where the file is and how big it is, said again rather than looked up: all three were
|
||||||
; settled when the temporary was made, and everything since has been describing whatever
|
; settled when the temporary was made, and everything since has been describing whatever
|
||||||
@@ -2597,6 +3175,7 @@ sbfsStreamFetch:
|
|||||||
SETD.0 SbfsStreamOpen
|
SETD.0 SbfsStreamOpen
|
||||||
LDA.0
|
LDA.0
|
||||||
BRA sbfsStreamNo
|
BRA sbfsStreamNo
|
||||||
|
CALL sbfsStreamHere
|
||||||
|
|
||||||
SETD.0 SbfsStreamTo
|
SETD.0 SbfsStreamTo
|
||||||
STD.1.0
|
STD.1.0
|
||||||
@@ -2633,6 +3212,7 @@ sbfsStreamDone:
|
|||||||
SETD.0 SbfsStreamOpen
|
SETD.0 SbfsStreamOpen
|
||||||
LDA.0
|
LDA.0
|
||||||
BRA sbfsStreamNo
|
BRA sbfsStreamNo
|
||||||
|
CALL sbfsStreamHere
|
||||||
|
|
||||||
; What it really came to, put aside before anything walks the disk.
|
; What it really came to, put aside before anything walks the disk.
|
||||||
SETD.0 SbfsStreamNewBlocks
|
SETD.0 SbfsStreamNewBlocks
|
||||||
@@ -3067,10 +3647,50 @@ SbfsStateWants:
|
|||||||
SbfsMagic:
|
SbfsMagic:
|
||||||
"SBFS"
|
"SBFS"
|
||||||
|
|
||||||
|
; ---- Which disk this is, in eight bytes ----
|
||||||
|
;
|
||||||
|
; THE ORDER AND THE ADJACENCY ARE LOAD BEARING. These four are everything that distinguishes
|
||||||
|
; one mounted disk from another, and they are together so that changing drives is one copy
|
||||||
|
; out and one copy in. Nothing else may be put between them.
|
||||||
|
;
|
||||||
|
; Everything else in this file is either a constant or scratch for the operation being done
|
||||||
|
; now, and only one operation is ever being done - which is why a filesystem of 3,300 lines
|
||||||
|
; needs an eight byte record to know more than one disk. The rest never learns there is more
|
||||||
|
; than one.
|
||||||
|
;
|
||||||
|
; The version is not here. It is checked at mount and thrown away, because a version one
|
||||||
|
; disk's zero parent already reads as "in the root", which is where all of its files are.
|
||||||
|
SbfsMountLive:
|
||||||
SbfsDirStart:
|
SbfsDirStart:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
SbfsDirBlocks:
|
SbfsDirBlocks:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
|
SbfsDiskBlocks:
|
||||||
|
0x00 0x00
|
||||||
|
SbfsCwd:
|
||||||
|
0x00 0x00
|
||||||
|
|
||||||
|
; One record a drive, and the live eight above are whichever is selected. Four, because the
|
||||||
|
; controller has four.
|
||||||
|
SbfsMountTable:
|
||||||
|
#Reserve 0d32
|
||||||
|
; Which drive the live record belongs to, and which drives were found to have a disk on them
|
||||||
|
; that this can read. A bit a drive, so drive n is bit n.
|
||||||
|
SbfsDrive:
|
||||||
|
0x00
|
||||||
|
SbfsMounted:
|
||||||
|
0x00
|
||||||
|
SbfsDriveCount:
|
||||||
|
0x00
|
||||||
|
SbfsDriveAt:
|
||||||
|
0x00
|
||||||
|
; Which drive the system may write scratch to, or 0xFF for a machine with none. It is a drive
|
||||||
|
; the machine called volatile, so nothing written there was ever going to survive anyway.
|
||||||
|
SbfsScratch1:
|
||||||
|
0xFF
|
||||||
|
SbfsPathWanted:
|
||||||
|
0x00
|
||||||
|
|
||||||
SbfsFileStart:
|
SbfsFileStart:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
SbfsFileBlocks:
|
SbfsFileBlocks:
|
||||||
@@ -3079,14 +3699,15 @@ SbfsFileTail:
|
|||||||
0x00
|
0x00
|
||||||
SbfsBlock:
|
SbfsBlock:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
SbfsDiskBlocks:
|
|
||||||
0x00 0x00
|
|
||||||
SbfsWantBlocks:
|
SbfsWantBlocks:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
SbfsCandidate:
|
SbfsCandidate:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
SbfsCandEnd:
|
SbfsCandEnd:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
|
; Whether the candidate had to move at all during the pass just finished. See sbfsAllocate.
|
||||||
|
SbfsAllocMoved:
|
||||||
|
0x00
|
||||||
SbfsEntryStart:
|
SbfsEntryStart:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
SbfsEntryEnd:
|
SbfsEntryEnd:
|
||||||
@@ -3111,8 +3732,6 @@ SbfsLeft:
|
|||||||
; thing that resolves a path is here. Keeping it in the shell would mean either handing it
|
; thing that resolves a path is here. Keeping it in the shell would mean either handing it
|
||||||
; down on every call or having the shell paste it onto the front of every name, and the
|
; down on every call or having the shell paste it onto the front of every name, and the
|
||||||
; second of those is how a name that is already absolute gets ruined.
|
; second of those is how a name that is already absolute gets ruined.
|
||||||
SbfsCwd:
|
|
||||||
0x00 0x00
|
|
||||||
|
|
||||||
; ---- What walking a path keeps ----
|
; ---- What walking a path keeps ----
|
||||||
;
|
;
|
||||||
@@ -3221,6 +3840,8 @@ SbfsTempName:
|
|||||||
"sbfs.part"
|
"sbfs.part"
|
||||||
|
|
||||||
; ---- What a file being written a block at a time keeps ----
|
; ---- What a file being written a block at a time keeps ----
|
||||||
|
SbfsStreamDrive:
|
||||||
|
0x00
|
||||||
SbfsStreamOpen:
|
SbfsStreamOpen:
|
||||||
0x00
|
0x00
|
||||||
SbfsStreamPath:
|
SbfsStreamPath:
|
||||||
|
|||||||
@@ -0,0 +1,562 @@
|
|||||||
|
; Reading shell lines out of a file.
|
||||||
|
;
|
||||||
|
; DP0 = the file's name
|
||||||
|
; CALL scriptOpen Q = 0 and a script is running, or Q says what was wrong:
|
||||||
|
; 1 there is no such file
|
||||||
|
; 2 it is not a script - no #! on the front
|
||||||
|
; 3 too many scripts inside each other
|
||||||
|
; DP0 = a buffer, B = how much room
|
||||||
|
; CALL scriptLine Q = 0 and there is a line in the buffer, or nonzero at the end
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
;
|
||||||
|
; ---- Why the shell reads files and not the other way round ----
|
||||||
|
;
|
||||||
|
; fileStream.asm does almost this and is deliberately not used. It is application machinery:
|
||||||
|
; More, Type and Wander each include it and each get their own copy in their own Data
|
||||||
|
; Segment. The shell reading a script through it would be a fourth copy, and the shell is the
|
||||||
|
; one place where that copy has to survive running a program - which is exactly the thing
|
||||||
|
; those programs are. Here the state belongs to the system and nothing a program does can
|
||||||
|
; reach it.
|
||||||
|
;
|
||||||
|
; ---- A block, and a nought on the end of it ----
|
||||||
|
;
|
||||||
|
; The reader walks a Data Pointer along the block and stops at a nought. A block is 256 bytes
|
||||||
|
; and a count of them does not fit in a byte, so every other way of knowing where the block
|
||||||
|
; ends costs sixteen bit arithmetic on every character. Writing a nought after the last byte
|
||||||
|
; costs one store per block and turns the whole question into "is this byte zero".
|
||||||
|
;
|
||||||
|
; The buffer is 257 bytes for that reason: a full block leaves the nought at the end of it.
|
||||||
|
; A script cannot contain a nought, which is not a restriction anybody will notice - it is
|
||||||
|
; text, and the #! on the front is what stops a program being read as one.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
; ---- One script inside another ----
|
||||||
|
;
|
||||||
|
; A build script calling a setup script is the first thing anybody tries, so what is saved
|
||||||
|
; when one script starts another is a POSITION AND NOT A BUFFER. The whole state of a
|
||||||
|
; running script is its name, which block comes next, how many are left, and where in the
|
||||||
|
; block it is - seventy bytes, laid out next to each other below so that saving it is one
|
||||||
|
; copy. The block itself is read again on the way back, which costs one disk read per return
|
||||||
|
; and saves 257 bytes a level.
|
||||||
|
;
|
||||||
|
; Four levels. Deep enough for a script calling a script that calls a helper, and shallow
|
||||||
|
; enough that a script which runs itself says so instead of filling memory.
|
||||||
|
scriptPush:
|
||||||
|
CALL scriptSlotAt
|
||||||
|
SETD.0 ScriptName
|
||||||
|
PSHD.3
|
||||||
|
POPD.1
|
||||||
|
CALL scriptCopyState
|
||||||
|
RET
|
||||||
|
|
||||||
|
scriptPop:
|
||||||
|
CALL scriptSlotAt
|
||||||
|
PSHD.3
|
||||||
|
POPD.0
|
||||||
|
SETD.1 ScriptName
|
||||||
|
CALL scriptCopyState
|
||||||
|
; ScriptAt points into the block buffer, which now holds somebody else's block. Reading
|
||||||
|
; it back is what makes the saved pointer mean what it meant.
|
||||||
|
CALL scriptReread
|
||||||
|
RET
|
||||||
|
|
||||||
|
; DP3 = where the script one level up is remembered. Reached by stepping rather than by
|
||||||
|
; multiplying, because this machine cannot multiply and the depth is never more than three
|
||||||
|
; steps. DP3 because RET puts the others back.
|
||||||
|
scriptSlotAt:
|
||||||
|
SETD.3 ScriptSaved
|
||||||
|
SETD.2 ScriptDepth
|
||||||
|
LDA.2
|
||||||
|
DECA
|
||||||
|
BRA scriptSlotDone
|
||||||
|
scriptSlotStep:
|
||||||
|
DPUP.3 0d71
|
||||||
|
DECA
|
||||||
|
BNA scriptSlotStep
|
||||||
|
scriptSlotDone:
|
||||||
|
RET
|
||||||
|
|
||||||
|
; Seventy bytes, DP0 to DP1.
|
||||||
|
scriptCopyState:
|
||||||
|
INIB 0d71
|
||||||
|
scriptCopyByte:
|
||||||
|
LDA.0
|
||||||
|
STA.1
|
||||||
|
INCD.0
|
||||||
|
INCD.1
|
||||||
|
DECB
|
||||||
|
BNB scriptCopyByte
|
||||||
|
RET
|
||||||
|
|
||||||
|
; The block that is meant to be in the buffer, back in the buffer. ScriptIndex is the NEXT
|
||||||
|
; one, so the one being read from is the one before it.
|
||||||
|
scriptReread:
|
||||||
|
SETD.1 ScriptIndex
|
||||||
|
LDA.1
|
||||||
|
INCD.1
|
||||||
|
LDB.1
|
||||||
|
DECB
|
||||||
|
BNC scriptRereadGo
|
||||||
|
DECA
|
||||||
|
scriptRereadGo:
|
||||||
|
SETD.0 ScriptName
|
||||||
|
SETD.1 ScriptBlock
|
||||||
|
SWI osFileBlock
|
||||||
|
SETD.1 ScriptBlock
|
||||||
|
PSHD.3
|
||||||
|
POPB
|
||||||
|
POPA
|
||||||
|
DPUW.1
|
||||||
|
RSTA
|
||||||
|
STA.1
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- Opening ----
|
||||||
|
;
|
||||||
|
; The name is COPIED rather than remembered by address. osFileBlock is given the name again
|
||||||
|
; for every block, and the caller's copy is CommandLine, which the next line typed will
|
||||||
|
; overwrite. fileStream remembers an address and says in its own comment that the address
|
||||||
|
; must stay valid; here it cannot, because the thing that reads the next line is the reason
|
||||||
|
; the name is needed.
|
||||||
|
scriptOpen:
|
||||||
|
; ---- Four deep and no further ----
|
||||||
|
SETD.1 ScriptDepth
|
||||||
|
LDA.1
|
||||||
|
INIB 0d4
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ scriptOpenTooDeep
|
||||||
|
|
||||||
|
; ---- The one already running is put somewhere safe FIRST ----
|
||||||
|
;
|
||||||
|
; Before anything below overwrites it, and put back again on every way out of here that is
|
||||||
|
; not success. Opening writes the name into the live state to ask the disk about it, so by
|
||||||
|
; the time the answer is known the caller's place is already gone.
|
||||||
|
;
|
||||||
|
; A still holds the depth from the check above: SUB writes Q and leaves it alone.
|
||||||
|
BRA scriptOpenOutermost
|
||||||
|
CALL scriptPush
|
||||||
|
BRI scriptOpenFirst
|
||||||
|
scriptOpenOutermost:
|
||||||
|
; ---- The one place a stale quiet would matter ----
|
||||||
|
;
|
||||||
|
; A script started from the prompt begins loud whatever the last one left behind, which is
|
||||||
|
; what makes this the only place the flag has to be put back. Nested scripts INHERIT
|
||||||
|
; instead: a build that asked for quiet meant its helpers too.
|
||||||
|
RSTA
|
||||||
|
SETD.1 ScriptQuiet
|
||||||
|
STA.1
|
||||||
|
scriptOpenFirst:
|
||||||
|
|
||||||
|
SETD.1 ScriptName
|
||||||
|
INIB 0d63
|
||||||
|
CALL copyText
|
||||||
|
|
||||||
|
SETD.0 ScriptName
|
||||||
|
SWI osFileInfo
|
||||||
|
BRQ scriptOpenThere
|
||||||
|
INIA 0x01
|
||||||
|
BRI scriptOpenFailed
|
||||||
|
scriptOpenThere:
|
||||||
|
; DP3 is how many blocks. A file of none is not a script.
|
||||||
|
PSHD.3
|
||||||
|
POPB
|
||||||
|
POPA
|
||||||
|
SETD.1 ScriptBlocks
|
||||||
|
STA.1
|
||||||
|
INCD.1
|
||||||
|
STB.1
|
||||||
|
OR
|
||||||
|
BRQ scriptOpenNotOne
|
||||||
|
|
||||||
|
SETD.1 ScriptIndex
|
||||||
|
RSTA
|
||||||
|
STA.1
|
||||||
|
INCD.1
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
; The first block, so that the #! can be looked at before anything is promised.
|
||||||
|
CALL scriptFill
|
||||||
|
BNQ scriptOpenNotOne
|
||||||
|
|
||||||
|
; ---- What makes a file a script ----
|
||||||
|
;
|
||||||
|
; Two bytes, and the rest of the line ignored. A directive rather than a comment, the way
|
||||||
|
; #Program is in assembly: the shell reads it and refuses the file without it, so calling
|
||||||
|
; it a comment would be a lie about what it does. What follows the #! is where the name of
|
||||||
|
; an interpreter goes when there is a second one; today there is one and it is this shell.
|
||||||
|
SETD.1 ScriptBlock
|
||||||
|
LDA.1
|
||||||
|
INIB 0d35 ; '#'
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ scriptOpenNotOne
|
||||||
|
INCD.1
|
||||||
|
LDA.1
|
||||||
|
INIB 0d33 ; '!'
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ scriptOpenNotOne
|
||||||
|
|
||||||
|
; Past the shebang line, wherever it ends.
|
||||||
|
CALL scriptSkipLine
|
||||||
|
|
||||||
|
SETD.1 ScriptDepth
|
||||||
|
LDA.1
|
||||||
|
INCA
|
||||||
|
STA.1
|
||||||
|
RSTA
|
||||||
|
BRI scriptOpenAnswer ; A is nought, which is the answer for "it opened".
|
||||||
|
|
||||||
|
scriptOpenTooDeep:
|
||||||
|
INIA 0x03
|
||||||
|
BRI scriptOpenAnswer ; Nothing was pushed, so there is nothing to put back.
|
||||||
|
|
||||||
|
scriptOpenNotOne:
|
||||||
|
INIA 0x02
|
||||||
|
scriptOpenFailed:
|
||||||
|
; Whatever was running is still running, and its place is in the slot rather than in the
|
||||||
|
; live state. A is the answer and must survive being put back.
|
||||||
|
SETD.1 ScriptDepth
|
||||||
|
LDB.1
|
||||||
|
BRB scriptOpenAnswer
|
||||||
|
PSHA
|
||||||
|
CALL scriptPop
|
||||||
|
POPA
|
||||||
|
scriptOpenAnswer:
|
||||||
|
; Q is the answer, and A holds it. Adding nought is how a register becomes Q.
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- One line ----
|
||||||
|
;
|
||||||
|
; Comments and blank lines never reach the shell. The reader drops them, so the echo does not
|
||||||
|
; print them and the dispatch never sees a line it would have to know to ignore.
|
||||||
|
scriptLine:
|
||||||
|
SETD.1 ScriptRoom
|
||||||
|
STB.1
|
||||||
|
SETD.1 ScriptInto
|
||||||
|
STD.0.1
|
||||||
|
|
||||||
|
scriptLineAgain:
|
||||||
|
SETD.1 ScriptLength
|
||||||
|
RSTA
|
||||||
|
STA.1
|
||||||
|
SETD.0 ScriptInto
|
||||||
|
LDD.0.0
|
||||||
|
|
||||||
|
scriptLineNext:
|
||||||
|
CALL scriptByte
|
||||||
|
BNQ scriptLineEnded
|
||||||
|
|
||||||
|
SETD.1 ScriptChar
|
||||||
|
LDA.1
|
||||||
|
INIB 0d10
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ scriptLineDone
|
||||||
|
; A still holds the character: SUB writes Q and leaves it alone.
|
||||||
|
INIB 0d13 ; Carriage return, from a file written on a host that uses them.
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ scriptLineNext
|
||||||
|
|
||||||
|
; Room?
|
||||||
|
SETD.1 ScriptLength
|
||||||
|
LDA.1
|
||||||
|
SETD.1 ScriptRoom
|
||||||
|
LDB.1
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ scriptLineNext ; Full. Read on and drop what comes, the way readLine does.
|
||||||
|
|
||||||
|
SETD.1 ScriptChar
|
||||||
|
LDA.1
|
||||||
|
STA.0
|
||||||
|
INCD.0
|
||||||
|
SETD.1 ScriptLength
|
||||||
|
LDA.1
|
||||||
|
INCA
|
||||||
|
STA.1
|
||||||
|
BRI scriptLineNext
|
||||||
|
|
||||||
|
scriptLineDone:
|
||||||
|
; Terminate it, then decide whether the shell wants to see it.
|
||||||
|
RSTA
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
SETD.0 ScriptInto
|
||||||
|
LDD.0.0
|
||||||
|
LDA.0
|
||||||
|
BRA scriptLineAgain ; Empty.
|
||||||
|
INIB 0d59 ; ';' - a comment, the same as everywhere else on this machine.
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ scriptLineAgain
|
||||||
|
|
||||||
|
; ---- A directive, which is about the file rather than for the shell ----
|
||||||
|
;
|
||||||
|
; '#' the way the assembler means it. #quiet stops each line being echoed as it runs, for a
|
||||||
|
; script whose own output is the point and which the prompts get in the way of; #loud puts
|
||||||
|
; it back.
|
||||||
|
;
|
||||||
|
; ANYTHING ELSE BEGINNING WITH # IS HANDED TO THE SHELL, which does not know it and says
|
||||||
|
; so and stops the script. That is deliberate and it is free: a script that asked for
|
||||||
|
; something this shell cannot do should not carry on as though it had been given it, and
|
||||||
|
; the machinery for saying so already exists.
|
||||||
|
INIB 0d35 ; '#'
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ scriptLineGive
|
||||||
|
|
||||||
|
SETD.1 QuietWord
|
||||||
|
CALL textSame
|
||||||
|
BRQ scriptLineQuiet
|
||||||
|
SETD.0 ScriptInto
|
||||||
|
LDD.0.0
|
||||||
|
SETD.1 LoudWord
|
||||||
|
CALL textSame
|
||||||
|
BRQ scriptLineLoud
|
||||||
|
SETD.0 ScriptInto
|
||||||
|
LDD.0.0
|
||||||
|
BRI scriptLineGive
|
||||||
|
|
||||||
|
scriptLineQuiet:
|
||||||
|
INIA 0x01
|
||||||
|
SETD.1 ScriptQuiet
|
||||||
|
STA.1
|
||||||
|
BRI scriptLineAgain
|
||||||
|
scriptLineLoud:
|
||||||
|
RSTA
|
||||||
|
SETD.1 ScriptQuiet
|
||||||
|
STA.1
|
||||||
|
BRI scriptLineAgain
|
||||||
|
|
||||||
|
scriptLineGive:
|
||||||
|
RSTA ; Q = 0: there is a line.
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
scriptLineEnded:
|
||||||
|
; ---- A last line with no newline on it is still a line ----
|
||||||
|
;
|
||||||
|
; Text files do not reliably end with one, and a script whose final command silently did
|
||||||
|
; not run because somebody's editor left the newline off is a bad way to find that out.
|
||||||
|
; If anything has been gathered, finish it the ordinary way; the next call comes back here
|
||||||
|
; with nothing gathered and ends for real.
|
||||||
|
SETD.1 ScriptLength
|
||||||
|
LDA.1
|
||||||
|
BRA scriptLineNoMore
|
||||||
|
BRI scriptLineDone
|
||||||
|
|
||||||
|
scriptLineNoMore:
|
||||||
|
CALL scriptClose
|
||||||
|
INIA 0x01
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- One character, or the end ----
|
||||||
|
;
|
||||||
|
; Q = 0 and the character is in ScriptChar, or Q is one and there are no more.
|
||||||
|
;
|
||||||
|
; IN MEMORY RATHER THAN IN A REGISTER, because RET puts A and B back the way the caller had
|
||||||
|
; them - only Q and Data Pointer 3 survive a CALL. Handing the character back in A looked
|
||||||
|
; right, assembled, and returned the caller's own A every time.
|
||||||
|
scriptByte:
|
||||||
|
SETD.1 ScriptAt
|
||||||
|
LDD.1.1
|
||||||
|
LDA.1
|
||||||
|
BRA scriptByteRefill ; The nought at the end of the block.
|
||||||
|
|
||||||
|
; Step the saved pointer past it.
|
||||||
|
SETD.1 ScriptChar
|
||||||
|
STA.1
|
||||||
|
SETD.1 ScriptAt
|
||||||
|
LDD.0.1
|
||||||
|
INCD.0
|
||||||
|
STD.0.1
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD ; Q = 0.
|
||||||
|
RET
|
||||||
|
|
||||||
|
scriptByteRefill:
|
||||||
|
CALL scriptFill
|
||||||
|
BNQ scriptByteNoMore
|
||||||
|
BRI scriptByte
|
||||||
|
scriptByteNoMore:
|
||||||
|
INIA 0x01
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- The next block, with a nought written after it ----
|
||||||
|
;
|
||||||
|
; Q = 0 if there is one.
|
||||||
|
scriptFill:
|
||||||
|
SETD.1 ScriptBlocks
|
||||||
|
LDA.1
|
||||||
|
INCD.1
|
||||||
|
LDB.1
|
||||||
|
OR
|
||||||
|
BRQ scriptFillNoMore
|
||||||
|
|
||||||
|
SETD.0 ScriptName
|
||||||
|
SETD.1 ScriptBlock
|
||||||
|
SETD.2 ScriptIndex
|
||||||
|
LDA.2
|
||||||
|
INCD.2
|
||||||
|
LDB.2
|
||||||
|
SWI osFileBlock
|
||||||
|
BNQ scriptFillNoMore
|
||||||
|
|
||||||
|
; DP3 is how many bytes came back. The nought goes after them.
|
||||||
|
SETD.1 ScriptBlock
|
||||||
|
PSHD.3
|
||||||
|
POPB
|
||||||
|
POPA
|
||||||
|
DPUW.1
|
||||||
|
RSTA
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
SETD.1 ScriptAt
|
||||||
|
SETD.0 ScriptBlock
|
||||||
|
STD.0.1
|
||||||
|
|
||||||
|
; Index++, blocks--.
|
||||||
|
SETD.1 ScriptIndex
|
||||||
|
INCD.1
|
||||||
|
LDA.1
|
||||||
|
INCA
|
||||||
|
STA.1
|
||||||
|
BNC scriptFillCount
|
||||||
|
DECD.1
|
||||||
|
LDA.1
|
||||||
|
INCA
|
||||||
|
STA.1
|
||||||
|
scriptFillCount:
|
||||||
|
SETD.1 ScriptBlocks
|
||||||
|
INCD.1
|
||||||
|
LDA.1
|
||||||
|
BRA scriptFillBorrow
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
BRI scriptFillGot
|
||||||
|
scriptFillBorrow:
|
||||||
|
INIA 0xFF
|
||||||
|
STA.1
|
||||||
|
DECD.1
|
||||||
|
LDA.1
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
scriptFillGot:
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
scriptFillNoMore:
|
||||||
|
INIA 0x01
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; Everything up to and including the next line feed, thrown away. Used for the shebang.
|
||||||
|
scriptSkipLine:
|
||||||
|
CALL scriptByte
|
||||||
|
BNQ scriptSkipDone
|
||||||
|
SETD.1 ScriptChar
|
||||||
|
LDA.1
|
||||||
|
INIB 0d10
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ scriptSkipLine
|
||||||
|
scriptSkipDone:
|
||||||
|
RET
|
||||||
|
|
||||||
|
; One script ending. Whatever asked for it carries on, if anything did.
|
||||||
|
scriptClose:
|
||||||
|
SETD.1 ScriptDepth
|
||||||
|
LDA.1
|
||||||
|
BRA scriptCloseNone
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
BRA scriptCloseNone
|
||||||
|
CALL scriptPop
|
||||||
|
RET
|
||||||
|
|
||||||
|
scriptCloseNone:
|
||||||
|
; ---- Nothing to put back here ----
|
||||||
|
;
|
||||||
|
; The flag is only ever READ while a script is running: the console's own path says its
|
||||||
|
; prompt whatever this holds, and the first script started from the prompt sets it to loud
|
||||||
|
; on the way in. So a stale quiet cannot be observed, and clearing it here would be a line
|
||||||
|
; no test could tell the difference about - which is how it was written the first time, with
|
||||||
|
; a comment claiming it fixed something. It fixed something that a later change had already
|
||||||
|
; made impossible.
|
||||||
|
RET
|
||||||
|
|
||||||
|
; Every script ending at once, which is what a line that did not work means. A build whose
|
||||||
|
; helper failed should not carry on in the script that called the helper either.
|
||||||
|
scriptAbandon:
|
||||||
|
RSTA
|
||||||
|
SETD.1 ScriptDepth
|
||||||
|
STA.1
|
||||||
|
RET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
QuietWord:
|
||||||
|
"#quiet"
|
||||||
|
LoudWord:
|
||||||
|
"#loud"
|
||||||
|
|
||||||
|
ScriptDepth:
|
||||||
|
0x00
|
||||||
|
|
||||||
|
; ---- Seventy bytes, and they are next to each other on purpose ----
|
||||||
|
;
|
||||||
|
; Name, blocks left, next block, where in the block, and whether it is echoing: the whole of
|
||||||
|
; where a script has got to. Saving it is one copy because of this order, and nothing else
|
||||||
|
; may be put between them.
|
||||||
|
ScriptName:
|
||||||
|
#Reserve 0d64
|
||||||
|
ScriptBlocks:
|
||||||
|
0x00 0x00
|
||||||
|
ScriptIndex:
|
||||||
|
0x00 0x00
|
||||||
|
ScriptAt:
|
||||||
|
0x00 0x00
|
||||||
|
; Saved with the rest, so that a quiet script calling a loud one gets its quiet back when
|
||||||
|
; the loud one finishes. A new script INHERITS it rather than resetting, because a build
|
||||||
|
; that asked for quiet meant its helpers too; only the first script started from the prompt
|
||||||
|
; begins loud.
|
||||||
|
ScriptQuiet:
|
||||||
|
0x00
|
||||||
|
|
||||||
|
; Three would do - a save happens on the second script and not the first - but four costs
|
||||||
|
; seventy bytes and removes an off-by-one from the only place it could hide.
|
||||||
|
ScriptSaved:
|
||||||
|
#Reserve 0d284
|
||||||
|
ScriptInto:
|
||||||
|
0x00 0x00
|
||||||
|
ScriptRoom:
|
||||||
|
0x00
|
||||||
|
ScriptLength:
|
||||||
|
0x00
|
||||||
|
ScriptChar:
|
||||||
|
0x00
|
||||||
|
ScriptBlock:
|
||||||
|
#Reserve 0d257
|
||||||
@@ -167,3 +167,14 @@
|
|||||||
; print it: a program that failed has already said so in words, and a number beside that
|
; print it: a program that failed has already said so in words, and a number beside that
|
||||||
; would be noise. This is for the thing that cannot read words.
|
; would be noise. This is for the thing that cannot read words.
|
||||||
osLastStatus 0d35
|
osLastStatus 0d35
|
||||||
|
|
||||||
|
; ---- Taking the screen, and giving it back ----
|
||||||
|
;
|
||||||
|
; Says that this program is about to use the whole screen and would like what is on it now put
|
||||||
|
; back when it exits. Q is zero if that was arranged; anything else means it was not, which is
|
||||||
|
; the ordinary answer on a machine with no volatile drive to keep it on - and a program told
|
||||||
|
; no should carry on regardless, because it was going to before this existed.
|
||||||
|
;
|
||||||
|
; NOT AUTOMATIC, and that is the point: dir and Say print and stop, and their output is the
|
||||||
|
; reason you ran them. Only a program that says it took the screen gets the screen put back.
|
||||||
|
osTakeScreen 0d36
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
; colours.asm
|
||||||
|
; Every colour the machine wakes up with, and how to change one.
|
||||||
|
; Written by Anachronaut
|
||||||
|
;
|
||||||
|
; ---- What a colour is on this machine ----
|
||||||
|
;
|
||||||
|
; The screen draws CELLS, and a cell is two bytes: which tile, and an attribute. A tile is
|
||||||
|
; eight by eight pixels and every pixel is a byte - a number, not a colour. What colour that
|
||||||
|
; number means is looked up in the PALETTE, which is 256 entries of four bytes: red, green,
|
||||||
|
; blue, and one spare. Four rather than three so that entry n begins at n times four, which
|
||||||
|
; is a shift; three would need a multiply and this machine has none.
|
||||||
|
;
|
||||||
|
; A cell's attribute nibble is ADDED to every number in its tile, sixteen at a time. So the
|
||||||
|
; same tile drawn with attribute 0 reads palette entries 0 and 1, with attribute 1 it reads
|
||||||
|
; 16 and 17, and so on. Sixteen banks of sixteen.
|
||||||
|
;
|
||||||
|
; The console's glyphs are drawn in numbers 0 and 1 - paper and ink - so for text those
|
||||||
|
; sixteen banks are sixteen INK AND PAPER PAIRS. Writing the attribute register at port 0x06
|
||||||
|
; says which one to use.
|
||||||
|
;
|
||||||
|
; The palette a machine wakes up with is laid out so one bit inverts a pair:
|
||||||
|
;
|
||||||
|
; banks 0 to 7 a colour on black
|
||||||
|
; banks 8 to 15 the same colour AS the background, with black text on it
|
||||||
|
;
|
||||||
|
; So attribute XOR 8 highlights, which is all a cursor is.
|
||||||
|
;
|
||||||
|
; ---- Reaching the palette ----
|
||||||
|
;
|
||||||
|
; Video memory belongs to the screen, not to the program, so the CPU cannot write it with a
|
||||||
|
; store. It is reached the way every device's memory is reached: registered as a bank, and
|
||||||
|
; written through the memory controller. That is what the last part of this program does.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
; The console wants sixteen columns for the name and a bit more, so the wide screen is
|
||||||
|
; not needed. This is the mode the machine wakes up in and is here to be seen.
|
||||||
|
RSTA
|
||||||
|
OUTA 0x31
|
||||||
|
|
||||||
|
; ---- Sixteen pairs, one line each ----
|
||||||
|
;
|
||||||
|
; Counting in memory rather than in a register, because the loop below uses A and B for
|
||||||
|
; the arithmetic and there is nowhere else to keep it.
|
||||||
|
SETD.0 Bank
|
||||||
|
STA.0 ; Still the zero from the mode write above: SETD does not touch A
|
||||||
|
|
||||||
|
nextBank:
|
||||||
|
LDA.0
|
||||||
|
OUTA 0x06 ; Draw in this pair from now on
|
||||||
|
SETD.1 SampleText
|
||||||
|
RCAL say
|
||||||
|
|
||||||
|
; The same bank with bit 3 set, which is the same colour inside out.
|
||||||
|
LDA.0
|
||||||
|
INIB 0x08
|
||||||
|
XOR
|
||||||
|
MVQA
|
||||||
|
OUTA 0x06
|
||||||
|
SETD.1 HighlightText
|
||||||
|
RCAL say
|
||||||
|
|
||||||
|
RSTA
|
||||||
|
OUTA 0x06 ; Back to plain for the newline
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
INIB 0d8 ; Eight banks; the other eight are their reverses
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ nextBank
|
||||||
|
|
||||||
|
; ---- And one written by hand ----
|
||||||
|
;
|
||||||
|
; Bank 2 is green when the machine starts. This makes its ink orange instead, by writing
|
||||||
|
; three bytes into the palette - which means reaching video memory, which means the
|
||||||
|
; controller.
|
||||||
|
|
||||||
|
; Give the screen's memory a bank number. The screen answers on port 0x30, and bank 3 is
|
||||||
|
; the first number software is allowed to hand out: 0, 1 and 2 belong to the machine.
|
||||||
|
INIA 0d3
|
||||||
|
OUTA 0xE3 ; DestBank: the number being given
|
||||||
|
INIA 0x30
|
||||||
|
OUTA 0xE2 ; SourceLow: the port that owns the memory
|
||||||
|
INIA 0x03
|
||||||
|
OUTA 0xE8 ; Command: RegisterBank
|
||||||
|
|
||||||
|
; The palette sits at the top of video memory, at 0xFC00, and entry n is at n times
|
||||||
|
; four. Bank 2's ink is entry 2 * 16 + 1, which is 33, and 33 * 4 is 132 - so 0xFC84.
|
||||||
|
INIA 0xFC
|
||||||
|
OUTA 0xE4 ; DestHigh
|
||||||
|
INIA 0x84
|
||||||
|
OUTA 0xE5 ; DestLow
|
||||||
|
|
||||||
|
; Writing the controller's Data port puts a byte at the destination and steps it on, so
|
||||||
|
; three writes are red, green and blue in order.
|
||||||
|
INIA 0xF0
|
||||||
|
OUTA 0xE9 ; red
|
||||||
|
INIA 0x80
|
||||||
|
OUTA 0xE9 ; green
|
||||||
|
INIA 0x20
|
||||||
|
OUTA 0xE9 ; blue
|
||||||
|
|
||||||
|
INIA 0x02
|
||||||
|
OUTA 0x06 ; That pair again, now that it has been changed
|
||||||
|
SETD.1 ChangedText
|
||||||
|
RCAL say
|
||||||
|
RSTA
|
||||||
|
OUTA 0x06
|
||||||
|
HALT
|
||||||
|
|
||||||
|
; DP1 names a string. Printing is one byte at a time out of port 0x00, which is the oldest
|
||||||
|
; thing on this machine and has never changed.
|
||||||
|
say:
|
||||||
|
LDA.1
|
||||||
|
BRA sayDone
|
||||||
|
OUTA 0x00
|
||||||
|
INCD.1
|
||||||
|
BRI say
|
||||||
|
sayDone:
|
||||||
|
RRET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
Bank:
|
||||||
|
0x00
|
||||||
|
|
||||||
|
SampleText:
|
||||||
|
" ordinary "
|
||||||
|
HighlightText:
|
||||||
|
" highlighted "
|
||||||
|
ChangedText:
|
||||||
|
"
|
||||||
|
bank 2's ink is orange now, because this program said so
|
||||||
|
"
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
Boot start
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
; frames.asm
|
||||||
|
; Waiting for the screen, which is the only regular beat this machine has.
|
||||||
|
; Written by Anachronaut
|
||||||
|
;
|
||||||
|
; ---- There is no clock ----
|
||||||
|
;
|
||||||
|
; Nothing on a SplitBit can tell you how long a second is. Every program that wanted to
|
||||||
|
; happen at a certain speed has counted instructions and hoped - which is why Snake's pause
|
||||||
|
; quietly halved the day a cycle stopped being an instruction and became a memory access.
|
||||||
|
; The program was right; the thing it was counting had changed underneath it.
|
||||||
|
;
|
||||||
|
; A screen finishes drawing sixty times a second, and that is a real beat. It is counted in
|
||||||
|
; the machine's own cycles rather than the host's, so this program sees sixty frames a second
|
||||||
|
; whether the emulator is running at its proper rate or as fast as it possibly can.
|
||||||
|
;
|
||||||
|
; ---- Waiting rather than spinning ----
|
||||||
|
;
|
||||||
|
; WAIT stops the machine until something interrupts it. That is not the same as looping until
|
||||||
|
; a flag goes up, even though both take the same time and print the same thing: a machine in
|
||||||
|
; WAIT is not using memory, so its cycles are counted as idle rather than as bus. Run this
|
||||||
|
; and the last line says so - nearly every cycle it spent, it spent asleep.
|
||||||
|
;
|
||||||
|
; Which is the whole argument for having a frame to wait for. On real hardware that is a
|
||||||
|
; machine that could be doing something else, or nothing at all and drawing less current.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
SETD.0 Frames
|
||||||
|
RSTA
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
; Ask the screen to interrupt at each frame, then let interrupts in. The screen does not do
|
||||||
|
; this unless it is asked: an interrupt with nothing installed to catch it is a fault, so a
|
||||||
|
; machine that started interrupting on its own would take down every program that had never
|
||||||
|
; heard of frames.
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x35
|
||||||
|
SIF
|
||||||
|
|
||||||
|
everyFrame:
|
||||||
|
; A dot a frame, so there is something to watch.
|
||||||
|
INIA 0d46
|
||||||
|
OUTA 0x00
|
||||||
|
|
||||||
|
; And nothing at all until the next one.
|
||||||
|
WAIT
|
||||||
|
|
||||||
|
SETD.0 Frames
|
||||||
|
LDA.0
|
||||||
|
INIB 0d60 ; One second of them
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ everyFrame
|
||||||
|
|
||||||
|
; Put the screen back the way it was found, and stop asking to be interrupted before
|
||||||
|
; taking away the thing that would catch it.
|
||||||
|
CIF
|
||||||
|
RSTA
|
||||||
|
OUTA 0x35
|
||||||
|
SETD.0 Done
|
||||||
|
RCAL say
|
||||||
|
HALT
|
||||||
|
|
||||||
|
; ---- Called sixty times a second ----
|
||||||
|
;
|
||||||
|
; A handler runs between two instructions of whatever was going on, so it saves everything it
|
||||||
|
; touches - which for an interrupt the machine does itself. RETI puts it all back.
|
||||||
|
frame:
|
||||||
|
SETD.0 Frames
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
RETI
|
||||||
|
|
||||||
|
say:
|
||||||
|
LDA.0
|
||||||
|
BRA sayDone
|
||||||
|
OUTA 0x00
|
||||||
|
INCD.0
|
||||||
|
BRI say
|
||||||
|
sayDone:
|
||||||
|
RRET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
Frames:
|
||||||
|
0x00
|
||||||
|
Done:
|
||||||
|
"
|
||||||
|
that was a second
|
||||||
|
"
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Boot start
|
||||||
|
Device 0x30 frame
|
||||||
@@ -14,8 +14,8 @@
|
|||||||
|
|
||||||
start:
|
start:
|
||||||
CALL seedGlider
|
CALL seedGlider
|
||||||
SETD ClearScreen
|
INIA 0x01
|
||||||
CALL printString
|
OUTA 0x05 ; Console command: clear the screen
|
||||||
|
|
||||||
generationLoop:
|
generationLoop:
|
||||||
CALL renderBoard
|
CALL renderBoard
|
||||||
@@ -48,8 +48,9 @@ seedGlider:
|
|||||||
RET
|
RET
|
||||||
|
|
||||||
renderBoard:
|
renderBoard:
|
||||||
SETD CursorHome
|
RSTA
|
||||||
CALL printString
|
OUTA 0x03
|
||||||
|
OUTA 0x04 ; Cursor to row 0, column 0
|
||||||
SETD Board
|
SETD Board
|
||||||
DPUP 0d38
|
DPUP 0d38
|
||||||
INIA 0d16
|
INIA 0d16
|
||||||
@@ -306,12 +307,6 @@ RowCount:
|
|||||||
ColCount:
|
ColCount:
|
||||||
0x00
|
0x00
|
||||||
|
|
||||||
ClearScreen:
|
|
||||||
0x1B
|
|
||||||
"[2J"
|
|
||||||
CursorHome:
|
|
||||||
0x1B
|
|
||||||
"[H"
|
|
||||||
|
|
||||||
; The emulator zero-fills the remainder of Data Memory. Board names the first
|
; The emulator zero-fills the remainder of Data Memory. Board names the first
|
||||||
; byte of a 648-byte logical allocation (18 * 18 * 2).
|
; byte of a 648-byte logical allocation (18 * 18 * 2).
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
|
|
||||||
start:
|
start:
|
||||||
CALL seedGlider
|
CALL seedGlider
|
||||||
SETD.0 ClearScreen
|
INIA 0x01
|
||||||
CALL printString
|
OUTA 0x05 ; Console command: clear the screen
|
||||||
|
|
||||||
generationLoop:
|
generationLoop:
|
||||||
CALL renderBoard
|
CALL renderBoard
|
||||||
@@ -38,8 +38,9 @@ seedGlider:
|
|||||||
RET
|
RET
|
||||||
|
|
||||||
renderBoard:
|
renderBoard:
|
||||||
SETD.0 CursorHome
|
RSTA
|
||||||
CALL printString
|
OUTA 0x03
|
||||||
|
OUTA 0x04 ; Cursor to row 0, column 0
|
||||||
SETD.1 RowCount
|
SETD.1 RowCount
|
||||||
SETD.2 ColCount
|
SETD.2 ColCount
|
||||||
INIA 0d16
|
INIA 0d16
|
||||||
@@ -250,12 +251,6 @@ RowCount:
|
|||||||
ColCount:
|
ColCount:
|
||||||
0x00
|
0x00
|
||||||
|
|
||||||
ClearScreen:
|
|
||||||
0x1B
|
|
||||||
"[2J"
|
|
||||||
CursorHome:
|
|
||||||
0x1B
|
|
||||||
"[H"
|
|
||||||
|
|
||||||
; 18 by 18 cells with the current and next states interleaved, so 648 bytes. The
|
; 18 by 18 cells with the current and next states interleaved, so 648 bytes. The
|
||||||
; original leaves this implicit and leans on Data Memory being zero, which works but
|
; original leaves this implicit and leans on Data Memory being zero, which works but
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
; picture.asm
|
||||||
|
; The other kind of screen: a byte a pixel.
|
||||||
|
; Written by Anachronaut
|
||||||
|
;
|
||||||
|
; ---- Two ways to have a screen ----
|
||||||
|
;
|
||||||
|
; A tile mode costs the machine the number of CELLS that changed. Forty by twenty-five is
|
||||||
|
; two thousand bytes for a whole screen and four bytes for two cells, which is why text on
|
||||||
|
; this machine is affordable at all.
|
||||||
|
;
|
||||||
|
; A bitmap costs it the number of PIXELS. Three hundred and twenty by two hundred is 64,000
|
||||||
|
; bytes - four frames of work at a megahertz to replace all of it. So this is not the mode to
|
||||||
|
; animate a whole screen in; it is the mode to draw a picture in and then leave alone, or to
|
||||||
|
; change a corner of.
|
||||||
|
;
|
||||||
|
; It lives over the top of tile memory and the map, because there is nowhere else for it: the
|
||||||
|
; bank is 65,536 bytes and the picture is 64,000 of them. Going to bitmap mode does not clear
|
||||||
|
; the text screen, it stops calling it one - and coming back finds the tiles holding whatever
|
||||||
|
; the picture put there. Taking the screen means taking it.
|
||||||
|
;
|
||||||
|
; The palette is the one thing that means the same in both, which is why it sits at the very
|
||||||
|
; top, out of the way of everything.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
; Video memory is the screen's, not this program's, so it is reached the way every device's
|
||||||
|
; memory is: given a bank number, then written through the memory controller. Banks 0, 1
|
||||||
|
; and 2 belong to the machine, so 3 is the first one software may hand out.
|
||||||
|
INIA 0d3
|
||||||
|
OUTA 0xE3 ; DestBank: the number being given
|
||||||
|
INIA 0x30
|
||||||
|
OUTA 0xE2 ; SourceLow: the port that owns the memory
|
||||||
|
INIA 0x03
|
||||||
|
OUTA 0xE8 ; Command: RegisterBank
|
||||||
|
|
||||||
|
; ---- Two hundred and fifty six colours ----
|
||||||
|
;
|
||||||
|
; Entry n at 0xFC00 plus n times four. Writing the controller's Data port puts a byte at
|
||||||
|
; the destination and steps it on, so the whole palette is one address and a loop.
|
||||||
|
INIA 0xFC
|
||||||
|
OUTA 0xE4
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE5
|
||||||
|
SETD.0 Count
|
||||||
|
STA.0 ; Still the zero from DestLow above: SETD does not touch A
|
||||||
|
palette:
|
||||||
|
LDA.0
|
||||||
|
OUTA 0xE9 ; red climbs
|
||||||
|
LDA.0
|
||||||
|
OUTA 0xE9 ; green with it
|
||||||
|
LDA.0
|
||||||
|
INIB 0xFF
|
||||||
|
XOR
|
||||||
|
MVQA
|
||||||
|
OUTA 0xE9 ; and blue falls away, so it runs blue to white to yellow
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE9 ; the fourth byte is spare
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
BNA palette ; A comes back to zero after 256 of them
|
||||||
|
|
||||||
|
; ---- The picture ----
|
||||||
|
;
|
||||||
|
; Two hundred rows of three hundred and twenty pixels, each row one colour. FILL LEAVES THE
|
||||||
|
; DESTINATION PAST WHAT IT TOUCHED, so the address is set once here and never worked out
|
||||||
|
; again - which matters, because working out where row n begins would be n times 320 and
|
||||||
|
; this machine has no multiply.
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE4
|
||||||
|
OUTA 0xE5 ; Dest 0x0000, the top left corner
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0xE6
|
||||||
|
INIA 0x40
|
||||||
|
OUTA 0xE7 ; 320 bytes, which is one row
|
||||||
|
SETD.0 Count
|
||||||
|
RSTA
|
||||||
|
STA.0
|
||||||
|
rows:
|
||||||
|
LDA.0
|
||||||
|
OUTA 0xE2 ; Fill takes its byte from SourceLow: the row number is the colour
|
||||||
|
INIA 0x02
|
||||||
|
OUTA 0xE8 ; Command: Fill
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
INIB 0d200
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ rows
|
||||||
|
|
||||||
|
; And now show it. Nothing above cared which mode the screen was in - the bytes were
|
||||||
|
; already there, waiting to be called a picture.
|
||||||
|
INIA 0x02
|
||||||
|
OUTA 0x31
|
||||||
|
HALT
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
Count:
|
||||||
|
0x00
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
Boot start
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
; The 16-bit segmented sieve rewritten for SplitBit's four-Data-Pointer ISA.
|
; The 16-bit segmented sieve rewritten for SplitBit's four-Data-Pointer ISA.
|
||||||
;
|
;
|
||||||
; This deliberately implements the same algorithm and emits the same text as
|
; This deliberately implements the same algorithm and emits the same text as
|
||||||
; 16bitSegmentedSieve.asm, making the two versions useful as a direct comparison.
|
; 16bitSieve.asm, making the two versions useful as a direct comparison.
|
||||||
; DP0 walks PrimeStates, DP1 holds Page, DP2 walks Segment, and volatile DP3
|
; DP0 walks PrimeStates, DP1 holds Page, DP2 walks Segment, and volatile DP3
|
||||||
; marks multiples. CALL preserves the first three pointers automatically.
|
; marks multiples. CALL preserves the first three pointers automatically.
|
||||||
|
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
; tune.asm
|
||||||
|
; Playing a melody, which needs a sound device and a clock and has neither by halves.
|
||||||
|
; Written by Anachronaut
|
||||||
|
;
|
||||||
|
; ---- Two devices, because one is not enough ----
|
||||||
|
;
|
||||||
|
; The sound device knows how to make a note and knows nothing about when. It has no timer and
|
||||||
|
; does not interrupt, so a program that only had the sound device could play a tune at
|
||||||
|
; whatever speed the machine happened to run at, which is not a tune.
|
||||||
|
;
|
||||||
|
; The screen finishes a frame sixty times a second and will say so. That is the only regular
|
||||||
|
; beat on this machine, and it is counted in the machine's own cycles, so this plays at the
|
||||||
|
; same speed whether the emulator is running at a megahertz or as fast as it can go. Every
|
||||||
|
; duration below is in frames: 30 is half a second.
|
||||||
|
;
|
||||||
|
; A programmable timer is the device that ought to be doing this, and it does not exist yet.
|
||||||
|
; Borrowing the screen's frame costs nothing and works, which is the whole reason to notice
|
||||||
|
; that a beat is a beat wherever it comes from.
|
||||||
|
;
|
||||||
|
; ---- What borrowing it costs ----
|
||||||
|
;
|
||||||
|
; The frame is not slow. It is FIXED, and that is the different complaint. Every duration here
|
||||||
|
; is a whole number of 16.67 ms, so a note worth a third of a beat cannot be written at all -
|
||||||
|
; and the way round it is to pick a tempo whose subdivisions happen to land on whole frames,
|
||||||
|
; which is making the tune fit the machine rather than the other way round. The theme below
|
||||||
|
; was written to the frame and is a few cents of tempo away from what it wants to be.
|
||||||
|
;
|
||||||
|
; So what the timer wants is an ARBITRARY tick rather than a faster fixed one, and the reason
|
||||||
|
; the screen should not be the clock is that a display refresh and a music routine have no
|
||||||
|
; reason to share a rate.
|
||||||
|
;
|
||||||
|
; ---- What a patch costs and what a note costs ----
|
||||||
|
;
|
||||||
|
; Setting the sound up is twenty-odd writes, done once before a single note is played. After
|
||||||
|
; that the inner loop is two: the note, and letting go of it. That split is what the selector
|
||||||
|
; and value registers are for - see Making A Noise in the Programming Manual.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
|
||||||
|
; ---- The instrument ----
|
||||||
|
;
|
||||||
|
; Channel 0, selected once. Every parameter write below lands on it.
|
||||||
|
RSTA
|
||||||
|
OUTA 0x41
|
||||||
|
|
||||||
|
; A saw wave, which has all the harmonics and so is the one to hear a filter on. Writing a
|
||||||
|
; port leaves A alone, so the nothing that selected the channel also selects parameter 0,
|
||||||
|
; which is oscillator 0's waveform. SplitLint will point out any attempt to put it there
|
||||||
|
; twice.
|
||||||
|
OUTA 0x42
|
||||||
|
INIA 0d2 ; Saw
|
||||||
|
OUTA 0x43
|
||||||
|
|
||||||
|
INIA 0x01 ; Oscillator 0, gain
|
||||||
|
OUTA 0x42
|
||||||
|
INIA 0xFF ; All of it. A channel arrives at full gain already, so this is
|
||||||
|
OUTA 0x43 ; saying so rather than changing it.
|
||||||
|
|
||||||
|
; A second oscillator a little out of tune with the first, which is the oldest trick there
|
||||||
|
; is for making one voice sound like more than one.
|
||||||
|
;
|
||||||
|
; SWITCHING IT ON IS A SEPARATE WRITE from setting its gain, and it is the one that matters:
|
||||||
|
; the two oscillators are averaged rather than added, so `active` is structural. Setting a
|
||||||
|
; gain on an oscillator that is off does nothing at all, silently, which is how the first
|
||||||
|
; draft of this program came to have a detune in it that could not be heard.
|
||||||
|
INIA 0x15 ; Oscillator 1, on
|
||||||
|
OUTA 0x42
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x43
|
||||||
|
INIA 0x11 ; Oscillator 1, gain
|
||||||
|
OUTA 0x42
|
||||||
|
INIA 0xC0
|
||||||
|
OUTA 0x43
|
||||||
|
INIA 0x13 ; Oscillator 1, detune
|
||||||
|
OUTA 0x42
|
||||||
|
INIA 0d129 ; Centred on 128, and a step is about nine cents, so this is
|
||||||
|
; nine cents sharp - a shimmer rather than a wrong note.
|
||||||
|
OUTA 0x43
|
||||||
|
|
||||||
|
; Plucked: no attack to speak of, most of a second of decay, and nothing held.
|
||||||
|
;
|
||||||
|
; A sustain of nothing does NOT end the note. It goes quiet and keeps sounding, because a
|
||||||
|
; voice holding at nothing is what a held key is. Dropping the gate is the only thing that
|
||||||
|
; ends a note, which is why the loop below does it whether the sound has faded or not.
|
||||||
|
INIA 0x20 ; Amplitude envelope, attack
|
||||||
|
OUTA 0x42
|
||||||
|
INIA 0d10
|
||||||
|
OUTA 0x43
|
||||||
|
INIA 0x21 ; Decay
|
||||||
|
OUTA 0x42
|
||||||
|
INIA 0d120
|
||||||
|
OUTA 0x43
|
||||||
|
INIA 0x22 ; Sustain: nothing
|
||||||
|
OUTA 0x42
|
||||||
|
RSTA
|
||||||
|
OUTA 0x43
|
||||||
|
INIA 0x23 ; Release
|
||||||
|
OUTA 0x42
|
||||||
|
INIA 0d40
|
||||||
|
OUTA 0x43
|
||||||
|
|
||||||
|
; A low pass with the modulation envelope opening it, so each note starts bright and closes
|
||||||
|
; down. This is what the second envelope is for, and it can only be spent this way because
|
||||||
|
; the level is shaped by the first one and not by whichever happens to be wired to the
|
||||||
|
; output.
|
||||||
|
INIA 0x40 ; Filter, on
|
||||||
|
OUTA 0x42
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x43
|
||||||
|
INIA 0x42 ; Cutoff, low to start with
|
||||||
|
OUTA 0x42
|
||||||
|
INIA 0d90
|
||||||
|
OUTA 0x43
|
||||||
|
INIA 0x43 ; A little resonance, to hear it move
|
||||||
|
OUTA 0x42
|
||||||
|
INIA 0d150
|
||||||
|
OUTA 0x43
|
||||||
|
INIA 0x44 ; What opens it: the modulation envelope
|
||||||
|
OUTA 0x42
|
||||||
|
INIA 0d2
|
||||||
|
OUTA 0x43
|
||||||
|
INIA 0x45 ; And how far, upwards from centre
|
||||||
|
OUTA 0x42
|
||||||
|
INIA 0d220
|
||||||
|
OUTA 0x43
|
||||||
|
INIA 0x31 ; That envelope's decay, which is the sweep's length
|
||||||
|
OUTA 0x42
|
||||||
|
INIA 0d70
|
||||||
|
OUTA 0x43
|
||||||
|
INIA 0x32 ; and it closes all the way
|
||||||
|
OUTA 0x42
|
||||||
|
RSTA
|
||||||
|
OUTA 0x43
|
||||||
|
|
||||||
|
INIA 0xC0 ; The device's volume, with room left over the top
|
||||||
|
OUTA 0x46
|
||||||
|
|
||||||
|
; ---- The beat ----
|
||||||
|
;
|
||||||
|
; Ask the screen to interrupt at each frame, and let interrupts in. The screen does not do
|
||||||
|
; this unless it is asked.
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x35
|
||||||
|
SIF
|
||||||
|
|
||||||
|
; ---- The tune ----
|
||||||
|
;
|
||||||
|
; Data Pointer 0 walks the table, and nothing in this loop is a CALL, so it stays where it
|
||||||
|
; was left without being saved anywhere.
|
||||||
|
SETD.0 Theme
|
||||||
|
|
||||||
|
nextNote:
|
||||||
|
LDA.0 ; The note. Zero is the end of the tune.
|
||||||
|
BRA finished
|
||||||
|
OUTA 0x44 ; Writing the note is what starts it.
|
||||||
|
INCD.0
|
||||||
|
LDB.0 ; How many frames it lasts.
|
||||||
|
INCD.0
|
||||||
|
|
||||||
|
holdNote:
|
||||||
|
WAIT ; Nothing at all until the screen says a frame has gone by.
|
||||||
|
DECB
|
||||||
|
BNB holdNote
|
||||||
|
|
||||||
|
; Let go. The note is already fading on its own decay, but dropping the gate is what a
|
||||||
|
; keyboard does and what the release time is waiting for.
|
||||||
|
RSTA
|
||||||
|
OUTA 0x45
|
||||||
|
BRI nextNote
|
||||||
|
|
||||||
|
finished:
|
||||||
|
; Let the last note ring out rather than cutting it off, then put the screen back the way it
|
||||||
|
; was found - and stop asking to be interrupted before taking away what catches it.
|
||||||
|
INIB 0d45
|
||||||
|
lastRing:
|
||||||
|
WAIT
|
||||||
|
DECB
|
||||||
|
BNB lastRing
|
||||||
|
|
||||||
|
CIF
|
||||||
|
RSTA
|
||||||
|
OUTA 0x35
|
||||||
|
HALT
|
||||||
|
|
||||||
|
; 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
|
||||||
|
; to catch it is a fault.
|
||||||
|
frame:
|
||||||
|
RETI
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
; ---- 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
|
||||||
|
; note ends it, which is why there are no rests in here - a rest would want a duration with no
|
||||||
|
; note, and this table has no way to say that. Adding one is a byte of flag or a note number
|
||||||
|
; nothing plays, and this program did not need it.
|
||||||
|
Tune:
|
||||||
|
0d60 0d15 ; C
|
||||||
|
0d64 0d15 ; E
|
||||||
|
0d67 0d15 ; G
|
||||||
|
0d72 0d30 ; C, an octave up, held twice as long
|
||||||
|
0d71 0d15 ; B
|
||||||
|
0d67 0d15 ; G
|
||||||
|
0d64 0d15 ; E
|
||||||
|
0d60 0d45 ; and home
|
||||||
|
0x00
|
||||||
|
|
||||||
|
Theme:
|
||||||
|
0x30 0d15 ; C
|
||||||
|
0x35 0d15 ; F
|
||||||
|
0x3C 0d15 ; C+
|
||||||
|
0x40 0d30 ; E+
|
||||||
|
0x48 0d30 ; C++
|
||||||
|
0x45 0d30 ; A+
|
||||||
|
0x47 0d30 ; B+
|
||||||
|
0x43 0d45 ; G+
|
||||||
|
; repeat four times.
|
||||||
|
0x2A 0d7 ; Gs
|
||||||
|
0x33 0d7 ; Ef
|
||||||
|
0x3C 0d7 ; C+
|
||||||
|
;
|
||||||
|
0x2A 0d7 ; Gs
|
||||||
|
0x33 0d7 ; Ef
|
||||||
|
0x3C 0d7 ; C+
|
||||||
|
;
|
||||||
|
0x2A 0d7 ; Gs
|
||||||
|
0x33 0d7 ; Ef
|
||||||
|
0x3C 0d7 ; C+
|
||||||
|
;
|
||||||
|
0x2A 0d7 ; Gs
|
||||||
|
0x33 0d7 ; Ef
|
||||||
|
0x3C 0d7 ; C+
|
||||||
|
; next chord
|
||||||
|
0x2E 0d7 ; Bf
|
||||||
|
0x35 0d7 ; F
|
||||||
|
0x3E 0d7 ; D+
|
||||||
|
; next chord
|
||||||
|
0x2E 0d7 ; Bf
|
||||||
|
0x35 0d7 ; F
|
||||||
|
0x3E 0d7 ; D+
|
||||||
|
;
|
||||||
|
; next chord
|
||||||
|
0x2E 0d7 ; Bf
|
||||||
|
0x35 0d7 ; F
|
||||||
|
0x3E 0d7 ; D+
|
||||||
|
;
|
||||||
|
; next chord
|
||||||
|
0x2E 0d7 ; Bf
|
||||||
|
0x35 0d7 ; F
|
||||||
|
0x3E 0d7 ; D+
|
||||||
|
; Finally on the C major
|
||||||
|
0x30 0d7 ; C
|
||||||
|
0x37 0d7 ; G
|
||||||
|
0x40 0d7 ; E+
|
||||||
|
;
|
||||||
|
0x30 0d7 ; C
|
||||||
|
0x37 0d7 ; G
|
||||||
|
0x40 0d7 ; E+
|
||||||
|
;
|
||||||
|
0x30 0d7 ; C
|
||||||
|
0x37 0d7 ; G
|
||||||
|
0x40 0d7 ; E+
|
||||||
|
;
|
||||||
|
0x30 0d7 ; C
|
||||||
|
0x37 0d7 ; G
|
||||||
|
0x40 0d7 ; E+
|
||||||
|
;
|
||||||
|
0x18 0d60 ; C bass
|
||||||
|
0x00
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Boot start
|
||||||
|
Device 0x30 frame
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
# SplitBit Programs Makefile
|
|
||||||
# Anachronaut
|
|
||||||
#
|
|
||||||
# Builds every SplitBit program into build/, and keeps track of which libraries
|
|
||||||
# each one includes so that editing a library reassembles whatever depends on it.
|
|
||||||
#
|
|
||||||
# make Assemble everything.
|
|
||||||
# make clean Throw away build/.
|
|
||||||
# make run-hello Assemble and run one program.
|
|
||||||
|
|
||||||
ASM ?= ../Assembler
|
|
||||||
EMU ?= ../SplitBit
|
|
||||||
BUILD ?= build
|
|
||||||
|
|
||||||
# Libraries are included by bare name, so the assembler is told where to find them.
|
|
||||||
# CosmOS owns the filesystem library and the service names, so it is a place to look too.
|
|
||||||
INCLUDES = -I Libraries -I CosmOS/Source
|
|
||||||
|
|
||||||
# The programs worth building. Every one lives in a directory that says what kind it is:
|
|
||||||
# Examples/ is what you read to learn, Loader/ is the standalone loader CosmOS grew out of,
|
|
||||||
# CosmOS/ is the system. Files in Libraries/ are left out because they have no entry point
|
|
||||||
# of their own, and the ones in testPrograms/ are covered by 'make test' in the parent
|
|
||||||
# directory.
|
|
||||||
PROGRAMS = \
|
|
||||||
CosmOS/Source/cosmos.asm \
|
|
||||||
Examples/hello.asm \
|
|
||||||
Examples/printHello.asm \
|
|
||||||
Examples/inputTest.asm \
|
|
||||||
Examples/replCalculator.asm \
|
|
||||||
Examples/Fibonacci/8bitFibonacci.asm \
|
|
||||||
Examples/Fibonacci/16bitFibonacci.asm \
|
|
||||||
Examples/Fibonacci/32bitFibonacci.asm \
|
|
||||||
Examples/primeSieve/8bitSieve.asm \
|
|
||||||
Examples/primeSieve/16bitSegmentedSieve.asm \
|
|
||||||
Examples/primeSieve/16bitSegmentedSieveModern.asm \
|
|
||||||
Examples/gameOfLife/16x16Life.asm \
|
|
||||||
Examples/gameOfLife/16x16LifeModern.asm \
|
|
||||||
Loader/loader.asm \
|
|
||||||
Loader/loadable.asm
|
|
||||||
|
|
||||||
BINARIES = $(PROGRAMS:%.asm=$(BUILD)/%.bin)
|
|
||||||
DEPENDENCIES = $(BINARIES:.bin=.d)
|
|
||||||
|
|
||||||
all: $(BINARIES)
|
|
||||||
|
|
||||||
# -M writes out which source files went into the binary, in the form of a make rule.
|
|
||||||
$(BUILD)/%.bin: %.asm
|
|
||||||
@mkdir -p $(@D)
|
|
||||||
$(ASM) $(INCLUDES) -M $(@:.bin=.d) -o $@ $<
|
|
||||||
|
|
||||||
# Assemble and run a single program, as in 'make run-hello'.
|
|
||||||
#
|
|
||||||
# THE NAME IS THE PROGRAM'S, NOT ITS PATH. This used to be a pattern rule against
|
|
||||||
# $(BUILD)/%.bin, which worked while every program sat at the top of this directory and
|
|
||||||
# stopped working the moment they were filed into Examples/ - 'make run-hello' had nothing
|
|
||||||
# to match. Somebody trying the one command the header advertises should not be the way
|
|
||||||
# that is discovered, so the name is now looked up among the programs instead.
|
|
||||||
run-%:
|
|
||||||
@target=`echo $(BINARIES) | tr ' ' '\n' | grep -E "(^|/)$*\.bin$$" | head -1`; \
|
|
||||||
if [ -z "$$target" ]; then \
|
|
||||||
echo "There is no program called '$*'. What there is:"; \
|
|
||||||
echo $(PROGRAMS) | tr ' ' '\n' | sed 's|.*/||;s|\.asm$$||;s|^| |'; \
|
|
||||||
exit 1; \
|
|
||||||
fi; \
|
|
||||||
$(MAKE) --no-print-directory "$$target" && $(EMU) "$$target"
|
|
||||||
|
|
||||||
# ---- CosmOS ----
|
|
||||||
#
|
|
||||||
# make cosmos Assemble the system and everything it can load.
|
|
||||||
# make cosmos-disk ... and put the loadable programs on a disk image.
|
|
||||||
# make run-cosmos ... and boot the machine with that disk in the drive.
|
|
||||||
#
|
|
||||||
# Programs in Apps/ say where they live with #Base, so the assembler writes them out as
|
|
||||||
# loadable programs rather than as boot images. They are named .sbx to keep that
|
|
||||||
# difference visible: a .bin is something the machine boots, a .sbx is something a
|
|
||||||
# running system loads.
|
|
||||||
|
|
||||||
DISKTOOL ?= ../SplitDisk
|
|
||||||
COSMOS = $(BUILD)/CosmOS/Source/cosmos.bin
|
|
||||||
APPS = $(patsubst CosmOS/Apps/%.asm,$(BUILD)/CosmOS/Apps/%.sbx,$(wildcard CosmOS/Apps/*.asm))
|
|
||||||
COSMOS_DISK = $(BUILD)/cosmos.img
|
|
||||||
|
|
||||||
# ---- What starts the machine ----
|
|
||||||
#
|
|
||||||
# Stage two goes into a boot slot as RAW BYTES: stage one reads blocks into Program Memory
|
|
||||||
# and jumps to the first one, so a sixteen byte header would be sixteen bytes of nonsense
|
|
||||||
# executed first. Its Data Segment travels with it and it copies that down itself.
|
|
||||||
#
|
|
||||||
# Stage one is not here at all. It is the ROM, built into the emulator by the top level
|
|
||||||
# makefile from the same source, which is what makes it the one part of this that a disk
|
|
||||||
# cannot replace.
|
|
||||||
STAGE2 = $(BUILD)/Boot/stage2.raw
|
|
||||||
DEPENDENCIES += $(BUILD)/Boot/stage2.d
|
|
||||||
|
|
||||||
$(STAGE2): Boot/stage2.asm
|
|
||||||
@mkdir -p $(@D)
|
|
||||||
$(ASM) $(INCLUDES) -M $(BUILD)/Boot/stage2.d -o $(BUILD)/Boot/stage2.sbx $<
|
|
||||||
tail -c +17 $(BUILD)/Boot/stage2.sbx > $@
|
|
||||||
DEPENDENCIES += $(APPS:.sbx=.d)
|
|
||||||
|
|
||||||
$(BUILD)/CosmOS/Apps/%.sbx: CosmOS/Apps/%.asm
|
|
||||||
@mkdir -p $(@D)
|
|
||||||
$(ASM) $(INCLUDES) -M $(@:.sbx=.d) -o $@ $<
|
|
||||||
|
|
||||||
# The assembler that runs on the machine. It is not in Apps/ because it is not one file:
|
|
||||||
# it has a directory of its own, the way the C assembler does. Its own pieces are found
|
|
||||||
# beside it without being told, since an include is looked for next to the file that asked
|
|
||||||
# for it before anywhere else; only services.asm needs the include path.
|
|
||||||
NATIVE_ASM = $(BUILD)/CosmOS/Assembler/Asm.sbx
|
|
||||||
DEPENDENCIES += $(NATIVE_ASM:.sbx=.d)
|
|
||||||
|
|
||||||
$(NATIVE_ASM): CosmOS/Assembler/Asm.asm
|
|
||||||
@mkdir -p $(@D)
|
|
||||||
$(ASM) $(INCLUDES) -M $(@:.sbx=.d) -o $@ $<
|
|
||||||
|
|
||||||
cosmos: $(COSMOS) $(APPS) $(NATIVE_ASM)
|
|
||||||
|
|
||||||
# Made from scratch every time, so that what is on it is what is in Apps/ now and not
|
|
||||||
# also whatever used to be.
|
|
||||||
#
|
|
||||||
# TWENTY FOUR DIRECTORY BLOCKS, WHICH IS ONE HUNDRED AND NINETY TWO NAMES. It was eight,
|
|
||||||
# and that is sixty four, of which thirty nine were already spoken for. The two ceilings a
|
|
||||||
# disk has were nowhere near each other: at the average file on here, twenty six blocks,
|
|
||||||
# sixty four names run out with the disk forty one per cent full. Names were going to be
|
|
||||||
# gone long before space was.
|
|
||||||
#
|
|
||||||
# A directory block is 256 bytes and holds eight entries, so the difference costs sixteen
|
|
||||||
# blocks of four thousand and ninety six - three tenths of one per cent - to buy a hundred
|
|
||||||
# and twenty eight more names. The superblock has carried this number per disk since the
|
|
||||||
# format was written, so nothing but this line knows what it is.
|
|
||||||
$(COSMOS_DISK): $(APPS) $(NATIVE_ASM) $(COSMOS) $(STAGE2) testPrograms/stringKeyword.asm \
|
|
||||||
$(wildcard CosmOS/Apps/*.asm) \
|
|
||||||
$(wildcard CosmOS/Source/*.asm) $(wildcard CosmOS/Assembler/*.asm)
|
|
||||||
@mkdir -p $(@D)
|
|
||||||
rm -f $@
|
|
||||||
@# A BOOT AREA, so that this is a disk the machine can start itself from rather than
|
|
||||||
@# one it has to be handed. Forty blocks a slot and two slots: stage two is about
|
|
||||||
@# eight thousand bytes, and the second slot is what makes replacing it survivable,
|
|
||||||
@# since raw blocks have no name and so nothing to rename.
|
|
||||||
$(DISKTOOL) format $@ 4096 24 40
|
|
||||||
$(DISKTOOL) boot $@ $(STAGE2) 0
|
|
||||||
@# THREE DIRECTORIES, WHICH IS WHAT A CLEAN INSTALL LOOKS LIKE: what you run, what you
|
|
||||||
@# assemble, and what those include. It was thirty nine files in one list with
|
|
||||||
@# cosmos.asm sitting between fileStream.asm and sbfs.asm.
|
|
||||||
@#
|
|
||||||
@# The split is by ROLE rather than by which directory the host keeps them in. /Source
|
|
||||||
@# holds the things you name to the assembler and /Lib the things they pull in, which is
|
|
||||||
@# a distinction the host makes with -I and the machine now makes with a search path of
|
|
||||||
@# its own: an include is looked for beside you and then in /Lib. Without that, every
|
|
||||||
@# source that calls a service would have to sit in the same directory as services.asm
|
|
||||||
@# and there would be nothing to organise.
|
|
||||||
$(DISKTOOL) mkdir $@ /Apps
|
|
||||||
$(DISKTOOL) mkdir $@ /Source
|
|
||||||
$(DISKTOOL) mkdir $@ /Lib
|
|
||||||
$(DISKTOOL) mkdir $@ /System
|
|
||||||
$(DISKTOOL) mkdir $@ /System/Boot
|
|
||||||
@# The system itself, as a file, which is the whole of what boot.cfg chooses between.
|
|
||||||
@# No boot.cfg is written: stage two falls back to this name when there is none, and a
|
|
||||||
@# clean install having nothing to configure is the right default.
|
|
||||||
$(DISKTOOL) put $@ $(COSMOS) /System/Boot/cosmos.bin
|
|
||||||
@# What you run. /Apps is the second place the shell looks when a word it does not know
|
|
||||||
@# turns out to be a program, so anything in here starts by name from anywhere.
|
|
||||||
@for app in $(APPS); do \
|
|
||||||
$(DISKTOOL) put $@ $$app /Apps/`basename $$app` >/dev/null || exit 1; done
|
|
||||||
$(DISKTOOL) put $@ $(NATIVE_ASM) /Apps/Asm.sbx
|
|
||||||
@# What you assemble. All of it, because an assembler with nothing to assemble is a
|
|
||||||
@# demonstration of nothing:
|
|
||||||
@#
|
|
||||||
@# > cd /Source
|
|
||||||
@# /Source> Asm cosmos.asm the system it is running on
|
|
||||||
@# /Source> Asm Asm.asm and the thing that built it
|
|
||||||
@#
|
|
||||||
@# Both come out byte for byte what the host tool makes from the same source.
|
|
||||||
@#
|
|
||||||
@# Keys.asm brings a vector of its own, so assembling it exercises the version two
|
|
||||||
@# header and the Vector Segment: the loader installs its handler, the console
|
|
||||||
@# interrupts into it, and the shell takes the vector back at exit.
|
|
||||||
@#
|
|
||||||
@# strings.asm is the odd one out on purpose. It has no #Include and no #Base, so it
|
|
||||||
@# comes out as a boot image rather than a loadable program, and the difference between
|
|
||||||
@# the two is visible on one disk.
|
|
||||||
$(DISKTOOL) put $@ CosmOS/Source/cosmos.asm /Source/cosmos.asm
|
|
||||||
$(DISKTOOL) put $@ CosmOS/Assembler/Asm.asm /Source/Asm.asm
|
|
||||||
$(DISKTOOL) put $@ CosmOS/Assembler/readTest.asm /Source/readTest.asm
|
|
||||||
$(DISKTOOL) put $@ CosmOS/Assembler/tokenTest.asm /Source/tokenTest.asm
|
|
||||||
$(DISKTOOL) put $@ CosmOS/Apps/hello.asm /Source/hello.asm
|
|
||||||
$(DISKTOOL) put $@ CosmOS/Apps/Say.asm /Source/Say.asm
|
|
||||||
$(DISKTOOL) put $@ CosmOS/Apps/Keys.asm /Source/Keys.asm
|
|
||||||
$(DISKTOOL) put $@ testPrograms/stringKeyword.asm /Source/strings.asm
|
|
||||||
@# And the loader, so the machine can rebuild what starts it. Assembling stage2.asm on
|
|
||||||
@# the machine and writing the result into the other boot slot is the whole of a
|
|
||||||
@# self-hosted boot chain, and everything it includes is already in /Lib.
|
|
||||||
$(DISKTOOL) put $@ Boot/stage1.asm /Source/stage1.asm
|
|
||||||
$(DISKTOOL) put $@ Boot/stage2.asm /Source/stage2.asm
|
|
||||||
@# And what those include. Everything here is named by an #Include somewhere and by
|
|
||||||
@# nothing else, which is exactly what makes it a library rather than a source.
|
|
||||||
@for f in CosmOS/Source/console.asm CosmOS/Source/fileStream.asm \
|
|
||||||
CosmOS/Source/sbfs.asm CosmOS/Source/services.asm CosmOS/Source/text.asm \
|
|
||||||
CosmOS/Source/config.asm \
|
|
||||||
CosmOS/Assembler/classify.asm CosmOS/Assembler/labels.asm \
|
|
||||||
CosmOS/Assembler/numbers.asm CosmOS/Assembler/scratch.asm \
|
|
||||||
CosmOS/Assembler/source.asm CosmOS/Assembler/table.asm \
|
|
||||||
CosmOS/Assembler/token.asm CosmOS/Assembler/vectors.asm; do \
|
|
||||||
$(DISKTOOL) put $@ $$f /Lib/`basename $$f` >/dev/null || exit 1; done
|
|
||||||
|
|
||||||
# The system as well as the disk. Building only the image leaves whatever cosmos.bin was
|
|
||||||
# there before, or none at all, and then the disk is booted with a system that does not
|
|
||||||
# match the programs on it.
|
|
||||||
cosmos-disk: $(COSMOS) $(COSMOS_DISK)
|
|
||||||
|
|
||||||
# THE MACHINE STARTS ITSELF. No image is named, so the emulator shadows its ROM into
|
|
||||||
# Program Memory and that reads the disk for everything else - a boot slot, then a loader,
|
|
||||||
# then whatever /System/Boot/boot.cfg names, or cosmos.bin when it names nothing.
|
|
||||||
run-cosmos: $(COSMOS_DISK)
|
|
||||||
$(EMU) --disk $(COSMOS_DISK)
|
|
||||||
|
|
||||||
# The same disk with the system handed over directly instead, which is what a debugger
|
|
||||||
# does: memory is placed from outside and nothing on the disk is consulted about it. Useful
|
|
||||||
# when the thing being debugged is the boot chain itself, since it skips the boot chain.
|
|
||||||
run-cosmos-direct: $(COSMOS) $(COSMOS_DISK)
|
|
||||||
$(EMU) --disk $(COSMOS_DISK) $(COSMOS)
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -rf $(BUILD)
|
|
||||||
|
|
||||||
# Pull in the dependency rules written by -M above, so that touching a library
|
|
||||||
# reassembles every program that includes it.
|
|
||||||
-include $(DEPENDENCIES)
|
|
||||||
|
|
||||||
.PHONY: all clean cosmos cosmos-disk run-cosmos run-cosmos-direct
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#! shell
|
||||||
|
#quiet
|
||||||
|
clear
|
||||||
|
echo Welcome to CosmOS.
|
||||||
|
echo Ready.
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
;
|
;
|
||||||
; There is a fourth bit, for whether the console interrupts on input, and nothing here
|
; There is a fourth bit, for whether the console interrupts on input, and nothing here
|
||||||
; sets it. Polling and interrupting are the two ways to get a byte and this is the one
|
; sets it. Polling and interrupting are the two ways to get a byte and this is the one
|
||||||
; about polling; consoleInterruptTest.asm is the other.
|
; about polling; consoleInterrupt.asm is the other.
|
||||||
;
|
;
|
||||||
; This runs with input from a file rather than a terminal, so key mode has no terminal to
|
; This runs with input from a file rather than a terminal, so key mode has no terminal to
|
||||||
; put into another state and the mode bit is the only thing that changes. That is on
|
; put into another state and the mode bit is the only thing that changes. That is on
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
; diskLineTest.asm
|
||||||
|
; A device's line comes down when its status port is read.
|
||||||
|
; Written by Anachronaut
|
||||||
|
;
|
||||||
|
; ---- The idiom with the race in it ----
|
||||||
|
;
|
||||||
|
; This is the shape the manual gives for waiting on a device, and it has a hole. It reads the
|
||||||
|
; status, branches out if the device has already finished, and only WAITs otherwise. On a disk
|
||||||
|
; fast enough to finish before the first look - which is every disk here - the WAIT is never
|
||||||
|
; reached, and WAIT was the only thing that took the line down for a program with no handler.
|
||||||
|
;
|
||||||
|
; The line then stood for the rest of the machine's life. Nothing else was going to answer it:
|
||||||
|
; the program is masked and has nowhere to dispatch to. So the next program to set the
|
||||||
|
; Interrupt Flag was interrupted on behalf of a read that finished before it was loaded, and
|
||||||
|
; faulted on the instruction after the SIF.
|
||||||
|
;
|
||||||
|
; That is not a story about the disk. It is what happens to any program CosmOS loads, because
|
||||||
|
; the boot chain reads the disk to load it. Playing Examples/tune.asm through Once is how it
|
||||||
|
; was found: the program set its whole patch up and died four bytes before its first note.
|
||||||
|
;
|
||||||
|
; So reading the status takes the line down, the same way taking the byte takes the console's
|
||||||
|
; down. This program is the check on that: read the disk without ever waiting, then let
|
||||||
|
; interrupts in with no handler installed anywhere. If a line were standing, SIF would find it
|
||||||
|
; and there would be nothing to catch it.
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
; Block 0, read.
|
||||||
|
RSTA
|
||||||
|
OUTA 0x20
|
||||||
|
OUTA 0x21
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x22
|
||||||
|
|
||||||
|
waitDisk:
|
||||||
|
INA 0x23
|
||||||
|
INIB 0x01
|
||||||
|
AND
|
||||||
|
BRQ readDone ; Already finished, so the WAIT below never runs.
|
||||||
|
WAIT
|
||||||
|
BRI waitDisk
|
||||||
|
|
||||||
|
readDone:
|
||||||
|
; No handler is installed for anything, and none is installed below either. This is the
|
||||||
|
; instruction the fault used to land on.
|
||||||
|
SIF
|
||||||
|
|
||||||
|
INIA 0d110
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0d111
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0d32
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0d108
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0d105
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0d110
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0d101
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0d10
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Boot start
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
; Choosing which disk the registers mean.
|
||||||
|
;
|
||||||
|
; SEVERAL DISKS ARE ONE CONTROLLER AND NOT SEVERAL DEVICES, and the instruction set is the
|
||||||
|
; reason. A port is an immediate byte inside the OUT that names it, so a program cannot
|
||||||
|
; compute one - "the disk on port 0x20 plus drive times four" is not a thing this machine can
|
||||||
|
; say. Two disks as two devices would mean a branch per access in every place the filesystem
|
||||||
|
; names a disk port.
|
||||||
|
;
|
||||||
|
; So the drive is a register, the way a floppy controller has always done it.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
; How many are plugged in. A fact about the machine, so it is read only.
|
||||||
|
INA 0x25
|
||||||
|
INIB 0d48
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
OUTQ 0x00
|
||||||
|
|
||||||
|
INIA 0d1
|
||||||
|
OUTA 0x24
|
||||||
|
INA 0x24
|
||||||
|
CCF
|
||||||
|
ADD ; B is still 48, from the count above.
|
||||||
|
OUTQ 0x00 ; It took.
|
||||||
|
|
||||||
|
; ---- A drive that is not there is refused, not wrapped ----
|
||||||
|
;
|
||||||
|
; Wrapping to drive 0 would mean a program asking for a drive this machine does not have
|
||||||
|
; quietly reading the one it does. That is the same shape of fault as taking a bank number
|
||||||
|
; somebody else is using: it succeeds, and the wrong disk answers.
|
||||||
|
INIA 0d9
|
||||||
|
OUTA 0x24
|
||||||
|
INA 0x24
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
OUTQ 0x00 ; Still 1.
|
||||||
|
|
||||||
|
; ---- And reading follows the selection ----
|
||||||
|
;
|
||||||
|
; Drive 1 has no disk in it here, which is not the same as not existing: the controller has
|
||||||
|
; four drives whether or not there are disks in them, so this selects, reads, and fails.
|
||||||
|
INIA 0d1
|
||||||
|
OUTA 0x24
|
||||||
|
RSTA
|
||||||
|
OUTA 0x20
|
||||||
|
OUTA 0x21
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x22
|
||||||
|
waitDisk:
|
||||||
|
INA 0x23
|
||||||
|
INIB 0x01
|
||||||
|
AND
|
||||||
|
BNQ waitDisk
|
||||||
|
INA 0x23
|
||||||
|
INIB 0x02
|
||||||
|
AND
|
||||||
|
BRQ readWorked
|
||||||
|
INIA 0d78 ; 'N', which is what an empty drive should give.
|
||||||
|
OUTA 0x00
|
||||||
|
BRI done
|
||||||
|
readWorked:
|
||||||
|
INIA 0d89
|
||||||
|
OUTA 0x00
|
||||||
|
done:
|
||||||
|
INIA 0d10
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Boot start
|
||||||
@@ -6,10 +6,15 @@
|
|||||||
; about, which is the whole point: reading port 0x00 to find out what it is would take
|
; about, which is the whole point: reading port 0x00 to find out what it is would take
|
||||||
; a character off standard input and wait for one that never comes.
|
; a character off standard input and wait for one that never comes.
|
||||||
;
|
;
|
||||||
; Port 0x05 has nothing on it, and reads as class 0. That is the same answer a machine
|
; Port 0x80 has nothing on it, and reads as class 0. That is the same answer a machine
|
||||||
; with no registry at all would give, so software finds out whether it can enumerate by
|
; with no registry at all would give, so software finds out whether it can enumerate by
|
||||||
; enumerating.
|
; enumerating.
|
||||||
;
|
;
|
||||||
|
; It used to ask about 0x05, until the console grew cursor registers and took it. The empty
|
||||||
|
; port has to be one nothing is likely to want: 0x80 is clear of the console below it, the
|
||||||
|
; disk and the screen, the memory controller at the top, and the sound device that is coming
|
||||||
|
; to 0x40.
|
||||||
|
;
|
||||||
; Correct output is:
|
; Correct output is:
|
||||||
; 00 02 00 the console, class 2, no flags
|
; 00 02 00 the console, class 2, no flags
|
||||||
; 10 10 00 the test device, class 0x10, no flags
|
; 10 10 00 the test device, class 0x10, no flags
|
||||||
@@ -27,7 +32,7 @@ start:
|
|||||||
CALL reportPort
|
CALL reportPort
|
||||||
INIA 0xFF
|
INIA 0xFF
|
||||||
CALL reportPort
|
CALL reportPort
|
||||||
INIA 0x05
|
INIA 0x80
|
||||||
CALL reportPort
|
CALL reportPort
|
||||||
HALT
|
HALT
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
; The timer interrupting, which is what a music routine actually wants.
|
||||||
|
;
|
||||||
|
; 125,000 cycles is a sixteenth note at 120 beats a minute. THE SCREEN CANNOT EXPRESS IT: a
|
||||||
|
; frame is 16,667 cycles, so that beat is seven and a half of them, and a program timing music
|
||||||
|
; on frames has to pick a tempo whose subdivisions happen to land on whole ones.
|
||||||
|
;
|
||||||
|
; Eight of them is a second, and what is recorded is that it took one - and that the machine
|
||||||
|
; slept through nearly all of it, which is the difference between waiting for a beat and
|
||||||
|
; counting up to it.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Program
|
||||||
|
start:
|
||||||
|
; 125,000 cycles: a sixteenth note at 120 beats a minute, which the screen's frame
|
||||||
|
; cannot express at all.
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x52
|
||||||
|
INIA 0xE8
|
||||||
|
OUTA 0x53
|
||||||
|
INIA 0x48
|
||||||
|
OUTA 0x54 ; 0x01E848 = 125,000
|
||||||
|
|
||||||
|
INIA 0x07
|
||||||
|
OUTA 0x51 ; Run, repeat, interrupt.
|
||||||
|
SIF
|
||||||
|
|
||||||
|
INIB 0d8
|
||||||
|
everyBeat:
|
||||||
|
WAIT
|
||||||
|
INIA 0d46
|
||||||
|
OUTA 0x00
|
||||||
|
DECB
|
||||||
|
BNB everyBeat
|
||||||
|
|
||||||
|
CIF
|
||||||
|
RSTA
|
||||||
|
OUTA 0x51
|
||||||
|
INIA 0d10
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
|
|
||||||
|
beat:
|
||||||
|
RETI
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
Boot start
|
||||||
|
Device 0x50 beat
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
; A beat a program sets for itself.
|
||||||
|
;
|
||||||
|
; The only regular thing this machine had was the screen finishing a frame, sixty times a
|
||||||
|
; second and not negotiable. That is a clock a program BORROWS: every duration becomes a
|
||||||
|
; multiple of 16.67 ms, so a sixteenth note at 120 beats a minute - 125,000 cycles, which is
|
||||||
|
; seven and a half frames - cannot be asked for at all.
|
||||||
|
;
|
||||||
|
; Counted in cycles, because that is what everything else on this machine is counted in.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
; ---- Repeating, and polled ----
|
||||||
|
;
|
||||||
|
; 100,000 cycles, a tenth of a second. Four of them, so the whole thing is 400,000 and the
|
||||||
|
; recorded cycle count is the check that the period is what it says.
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x52
|
||||||
|
INIA 0x86
|
||||||
|
OUTA 0x53
|
||||||
|
INIA 0xA0
|
||||||
|
OUTA 0x54 ; 0x0186A0
|
||||||
|
|
||||||
|
INIA 0x03
|
||||||
|
OUTA 0x51 ; Run, repeat, no interrupt.
|
||||||
|
|
||||||
|
INIB 0d4
|
||||||
|
everyTick:
|
||||||
|
INA 0x50
|
||||||
|
PSHB
|
||||||
|
INIB 0x01
|
||||||
|
AND
|
||||||
|
POPB
|
||||||
|
BRQ everyTick ; Not yet.
|
||||||
|
INIA 0d46
|
||||||
|
OUTA 0x00
|
||||||
|
DECB
|
||||||
|
BNB everyTick
|
||||||
|
|
||||||
|
; ---- And looking is what answered it ----
|
||||||
|
;
|
||||||
|
; The bit came down when it was read, so asking again immediately says nothing has happened
|
||||||
|
; since. A timer whose flag stayed up would look like a beat every time round the loop.
|
||||||
|
INA 0x50
|
||||||
|
INIB 0x01
|
||||||
|
AND
|
||||||
|
BRQ tickCleared
|
||||||
|
INIA 0d78 ; 'N'
|
||||||
|
OUTA 0x00
|
||||||
|
BRI oneShot
|
||||||
|
tickCleared:
|
||||||
|
INIA 0d89 ; 'Y'
|
||||||
|
OUTA 0x00
|
||||||
|
|
||||||
|
oneShot:
|
||||||
|
; ---- Once, and then stopped ----
|
||||||
|
;
|
||||||
|
; Without the repeat bit it runs its period out and turns itself off, which the status port
|
||||||
|
; says: bit 1 is whether it is running.
|
||||||
|
RSTA
|
||||||
|
OUTA 0x51 ; Stop first, so starting below is a start.
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x51 ; Run, no repeat.
|
||||||
|
|
||||||
|
waitOnce:
|
||||||
|
INA 0x50
|
||||||
|
INIB 0x01
|
||||||
|
AND
|
||||||
|
BRQ waitOnce
|
||||||
|
INIA 0d46
|
||||||
|
OUTA 0x00
|
||||||
|
|
||||||
|
INA 0x50
|
||||||
|
INIB 0x02 ; RUNNING
|
||||||
|
AND
|
||||||
|
BRQ stopped
|
||||||
|
INIA 0d78
|
||||||
|
OUTA 0x00
|
||||||
|
BRI done
|
||||||
|
stopped:
|
||||||
|
INIA 0d89
|
||||||
|
OUTA 0x00
|
||||||
|
|
||||||
|
done:
|
||||||
|
INIA 0d10
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Boot start
|
||||||
@@ -20,17 +20,81 @@ wrote Asm.sbx: program 7533, data 4099, labels 555
|
|||||||
|
|
||||||
| Directory | What it holds |
|
| Directory | What it holds |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [`Source/Emulator`](Source/Emulator) | The machine: CPU, memory controller, devices, console, disk |
|
| [`Source/Emulator`](Source/Emulator) | The machine: CPU, memory controller, devices, console, disk - and the two front ends that present it |
|
||||||
| [`Source/Assembler`](Source/Assembler) | The assembler that runs on a host |
|
| [`Source/Assembler`](Source/Assembler) | The assembler that runs on a host |
|
||||||
| [`Source/DiskTool`](Source/DiskTool) | SplitDisk, which reads and writes SplitBit's filesystem |
|
| [`Source/DiskTool`](Source/DiskTool) | SplitDisk, which reads and writes SplitBit's filesystem |
|
||||||
| [`Source/Linter`](Source/Linter) | SplitLint, which points out needlessly long assembly forms |
|
| [`Source/Linter`](Source/Linter) | SplitLint, which points out needlessly long assembly forms |
|
||||||
| [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life |
|
| [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life, the colours |
|
||||||
| [`Programs/Libraries`](Programs/Libraries) | Code included by name rather than linked, since there is no linker |
|
| [`Programs/Libraries`](Programs/Libraries) | Code included by name rather than linked, since there is no linker |
|
||||||
| [`Programs/Loader`](Programs/Loader) | The standalone loader CosmOS grew out of |
|
| [`Programs/Loader`](Programs/Loader) | The standalone loader CosmOS grew out of |
|
||||||
| [`Programs/CosmOS`](Programs/CosmOS) | The operating system, its applications, and the native assembler |
|
| [`Programs/CosmOS`](Programs/CosmOS) | The operating system, its applications, and the native assembler |
|
||||||
| [`Programs/testPrograms`](Programs/testPrograms) | What the test suite drives |
|
| [`Programs/testPrograms`](Programs/testPrograms) | What the test suite drives |
|
||||||
| [`Tests`](Tests) | The suite: the manifest, the recorded output, and the scripts that check it |
|
| [`Tests`](Tests) | The suite: the manifest, the recorded output, and the scripts that check it |
|
||||||
|
|
||||||
|
## Two Front Ends:
|
||||||
|
|
||||||
|
`make` builds **SplitBit**, which is the machine with a terminal attached, and where Raylib
|
||||||
|
is installed it also builds **Voyager**, which is the same machine with a screen and a
|
||||||
|
speaker. Everything that is actually the machine - every instruction, every device, every
|
||||||
|
cycle - is shared between them, and each brings one file of its own: a terminal or a window.
|
||||||
|
|
||||||
|
Voyager is deliberately not required. The machine, the assembler, the disk tool, the linter
|
||||||
|
and the whole test suite build and run on a host with no graphics library at all, because a
|
||||||
|
project about a small understandable CPU should not need OpenGL to run its tests. Where
|
||||||
|
Raylib is missing, `make` says so once and builds everything else.
|
||||||
|
|
||||||
|
The suite holds the two to being the same machine rather than taking it on trust: it runs
|
||||||
|
the entire manifest through Voyager as well, with `--headless`, and requires it to satisfy
|
||||||
|
every recorded result byte for byte.
|
||||||
|
|
||||||
|
**Control, Shift and R is the reset button.** On real hardware it is not a key at all - a
|
||||||
|
Voyager has a button on the case, and what a window has instead of a case is a gesture. Three
|
||||||
|
things follow from that. It must not be a key software might want, because a machine with a
|
||||||
|
keyboard has function keys and something will eventually have a use for them. It must not be
|
||||||
|
reachable by accident, because restarting throws away everything in memory and a single key
|
||||||
|
that does that sits one mistake away from losing work. And **it must be a gesture the host
|
||||||
|
has no opinion about**.
|
||||||
|
|
||||||
|
That last one rules out the obvious answer. Control, Alt and Delete has meant this since 1981
|
||||||
|
and cannot be used: it is a secure attention key, reserved by every serious operating system
|
||||||
|
so that it always reaches the system and never an application - precisely so a program cannot
|
||||||
|
imitate a login screen. On Windows an application cannot see it without a kernel driver, and
|
||||||
|
on Linux the desktop takes it. It is unavailable for the same reason it seemed right.
|
||||||
|
|
||||||
|
It does exactly what writing to the machine port does: the machine starts the way it started,
|
||||||
|
so the boot chain runs again and finds whatever the disk now says to run. **And it works on a
|
||||||
|
machine that has stopped** - one that halted, or faulted, or is a bare metal demo that ended.
|
||||||
|
That is the whole point of a button: a machine which is not going anywhere is exactly the one
|
||||||
|
worth restarting, and it is the one that cannot notice a request by itself, because a reset is
|
||||||
|
otherwise seen between instructions and a halted machine runs none.
|
||||||
|
|
||||||
|
This part is **emulator magic and known to be**. There is no reset line on this machine yet
|
||||||
|
and no keyboard controller to assert one; the window reaches in and sets the same flag the
|
||||||
|
machine port sets. When those are designed, a keyboard controller will have to see the gesture
|
||||||
|
and pull reset regardless of what the CPU is doing - which is the property that matters, and
|
||||||
|
the one a port write can never have, since a port write needs a program willing and able to
|
||||||
|
make it.
|
||||||
|
|
||||||
|
That is what makes a bare metal program escapable. `Once` puts one in front of the next start
|
||||||
|
and deletes the request before jumping, so a demo that has taken the whole machine is one
|
||||||
|
gesture away from the system coming back, without closing the window and opening it again. It
|
||||||
|
works on a machine that is stuck waiting for a key, too, which is when a reset button earns
|
||||||
|
its keep.
|
||||||
|
|
||||||
|
**Escape reaches the machine.** Raylib closes a window on Escape unless it is told not to,
|
||||||
|
and this machine sends Escape to the console like any other key - so a program reading keys
|
||||||
|
could be ended by one of them, taking whatever was in memory with it.
|
||||||
|
|
||||||
|
**The screen belongs to the machine, not to the window.** The video device is a tile engine
|
||||||
|
on ports 0x30 to 0x3F that brings its own bank of video memory, and it renders into a buffer
|
||||||
|
that is a pure function of that memory - so the same program draws the same picture whether
|
||||||
|
or not anybody is watching. The console draws on it: it is a display controller as well as a
|
||||||
|
port, with a font, a cursor and scrollback, which is why CosmOS runs in a window without a
|
||||||
|
line of it being changed. Voyager puts that buffer on the glass and decides nothing about
|
||||||
|
it. Either binary will save a picture of the screen with `--screen`, which is how a test
|
||||||
|
suite on a host with no display checks what was drawn. See **The Screen** in the Programming
|
||||||
|
Manual.
|
||||||
|
|
||||||
## The Machine:
|
## The Machine:
|
||||||
|
|
||||||
- **Harvard architecture.** Two 64K memories, one for instructions and one for data. An instruction can only read the second, which is why strings live there and why the memory controller exists.
|
- **Harvard architecture.** Two 64K memories, one for instructions and one for data. An instruction can only read the second, which is why strings live there and why the memory controller exists.
|
||||||
@@ -72,12 +136,27 @@ Assemble something and run it:
|
|||||||
Or boot the operating system, with a disk of programs and all of its own source on it:
|
Or boot the operating system, with a disk of programs and all of its own source on it:
|
||||||
|
|
||||||
```
|
```
|
||||||
cd Programs
|
make run-voyager
|
||||||
make run-cosmos
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`make` builds a disk as well as the tools, so there is one to boot. `make run-cosmos` is the
|
||||||
|
same system in the terminal, for a machine with no graphics library.
|
||||||
|
|
||||||
|
Both put a second disk in drive 1, at `Disks/personal.img`. It is made once and then never
|
||||||
|
rebuilt, cleaned or committed: everything else here can be thrown away and made again from
|
||||||
|
source, and that one is where anything made ON the machine lives.
|
||||||
|
|
||||||
Then `dir` to see what is there, `load Snake.sbx` and `run` to play something, or `load Asm.sbx` and `run cosmos.asm` to watch the machine build itself.
|
Then `dir` to see what is there, `load Snake.sbx` and `run` to play something, or `load Asm.sbx` and `run cosmos.asm` to watch the machine build itself.
|
||||||
|
|
||||||
|
Every source in `Programs/` is on that disk, under `/Source`, so anything not shipped as a
|
||||||
|
binary can still be assembled on the machine: `cd /Source/Examples` and `Asm colours.asm`.
|
||||||
|
|
||||||
|
`make run-voyager` boots the same disk on the machine with a screen instead of a terminal.
|
||||||
|
Both targets depend on the disk, so a disk built before a change to the machine is rebuilt
|
||||||
|
rather than booted as it stands: **what is on a disk is whatever was built when the disk was
|
||||||
|
made**, and a system whose console has changed will happily start an image full of programs
|
||||||
|
written for the old one.
|
||||||
|
|
||||||
## Running Programs: SplitBit
|
## Running Programs: SplitBit
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -131,14 +210,38 @@ anybody could build - and it is the emulator's job to be the thing the hardware
|
|||||||
against.
|
against.
|
||||||
|
|
||||||
The average SplitBit instruction costs 3.72 cycles, measured over the native assembler
|
The average SplitBit instruction costs 3.72 cycles, measured over the native assembler
|
||||||
assembling a program.
|
assembling a program. **Which is what a nominal 1 MHz means here**: about 270,000
|
||||||
|
instructions a second, not a million. The same program takes 3.72 times the wall clock it
|
||||||
|
did when a cycle was an instruction - nothing got slower, the number got honest, and the
|
||||||
|
number it replaced described no machine anybody could build.
|
||||||
|
|
||||||
|
Two measurements that put that in proportion, taken on the machine assembling its own
|
||||||
|
operating system:
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| The emulator, in `--fast` | 195 million cycles a second |
|
||||||
|
| CosmOS assembling CosmOS | 654 million cycles: 11 minutes at 1 MHz, 3.3 seconds at `--fast` |
|
||||||
|
|
||||||
|
At a hypothetical 100 MHz that build is six and a half seconds, and that is a pessimistic
|
||||||
|
figure rather than a hopeful one: it assumes hardware overlaps nothing, which is the same
|
||||||
|
conservative reading the memory controller's cost model takes and the same one that wants
|
||||||
|
measuring before it is designed.
|
||||||
|
|
||||||
**The memory controller is charged for what it moves**, on the same terms. Banks are
|
**The memory controller is charged for what it moves**, on the same terms. Banks are
|
||||||
separate memories, and that is what sets the rate: a move between two of them can overlap
|
separate memories, and that is what sets the rate: a move between two of them can overlap
|
||||||
its read and its write, so it settles at a byte a cycle, while a move within one bank cannot
|
its read and its write, while a move within one bank cannot and costs twice as much. A fill
|
||||||
and costs two. A fill has nothing to read and costs one. So a 256 byte block is 257 cycles
|
has nothing to read and goes at the between-banks rate. Against the ten cycles a transfer
|
||||||
between banks and 513 within one, against the ten it used to cost - which was the five port
|
used to cost - the five port writes that set it up, and nothing at all for the quarter of a
|
||||||
writes that set it up and nothing for the quarter of a kilobyte that moved.
|
kilobyte that moved.
|
||||||
|
|
||||||
|
**And the controller's path to memory is sixteen bits wide.** A transfer whose source,
|
||||||
|
destination and length are all even moves two bytes a cycle between banks and one within a
|
||||||
|
bank; anything odd falls back to a byte a cycle, because lining bytes up across word
|
||||||
|
boundaries is a second design and this is not it. So a 256 byte block is 129 cycles between
|
||||||
|
banks and 257 within one when it is aligned, and 257 and 513 when it is not. The CPU still
|
||||||
|
sees eight bits and no instruction means anything different: this is a peripheral being
|
||||||
|
faster, not a new machine.
|
||||||
|
|
||||||
The transfer stalls the program that asked for it. Whether hardware would let the two run at
|
The transfer stalls the program that asked for it. Whether hardware would let the two run at
|
||||||
once is left open, the same way pipelining is: the memories are separate, so it plausibly
|
once is left open, the same way pipelining is: the memories are separate, so it plausibly
|
||||||
@@ -481,11 +584,10 @@ A disk is at the lowest version that describes what is on it, so `format` makes
|
|||||||
|
|
||||||
The assembler is built to work with make. `-o` puts the output where the build system wants it, and `-M` writes out which libraries went into it, so that editing a library reassembles everything that includes it.
|
The assembler is built to work with make. `-o` puts the output where the build system wants it, and `-M` writes out which libraries went into it, so that editing a library reassembles everything that includes it.
|
||||||
|
|
||||||
`Programs/makefile` does this for the programs in this repository:
|
The makefile does this for the programs in this repository:
|
||||||
|
|
||||||
```
|
```
|
||||||
cd Programs
|
make programs
|
||||||
make
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The rule it uses is small enough to copy into your own projects:
|
The rule it uses is small enough to copy into your own projects:
|
||||||
@@ -504,45 +606,43 @@ $(BUILD)/%.bin: %.asm
|
|||||||
make test
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
The suite assembles and runs every program in `Programs/` and compares the results against recorded output. Tests are defined in `Tests/manifest`, one line per program. To record the current output as the expected result, after you have checked that it is correct:
|
Builds the four tools, checks they build clean under strict ISO C, and runs nine scripts.
|
||||||
|
`Tests/run.sh` assembles and runs every program in `Programs/` and compares the results
|
||||||
|
against recorded output; six more ask the questions a recorded file cannot answer. Between
|
||||||
|
them they check the two assemblers against each other byte for byte, the two SBFS
|
||||||
|
implementations against each other on the same disk, the disk tool against the format, the
|
||||||
|
linter against a fixture written to trip it, the terminal behaviour that a pipe makes
|
||||||
|
invisible, and the manuals against the code.
|
||||||
|
|
||||||
```
|
```
|
||||||
make bless
|
make bless
|
||||||
```
|
```
|
||||||
|
|
||||||
Programs are built inside `Tests/build`, so running the suite never overwrites anything in `Programs/`. To run only some of the tests, call the runner directly with their names:
|
Records the current output as the expected result, after you have checked that it is
|
||||||
|
correct.
|
||||||
```
|
|
||||||
./Tests/run.sh hello 8bitFibonacci
|
|
||||||
```
|
|
||||||
|
|
||||||
The disk images tests read from are built first by `Tests/makedisks.sh`, using SplitDisk. A
|
|
||||||
test that reads one is therefore checked against a filesystem written by different code from
|
|
||||||
the same written specification, rather than against itself.
|
|
||||||
|
|
||||||
`Tests/run.sh` drives that comparison. Six more scripts run alongside it, and each exists
|
|
||||||
because a recorded file cannot answer its question:
|
|
||||||
|
|
||||||
- **`Tests/disk.sh`** checks the disk tool on its own: files of every awkward size onto an image and off again, and the things the format says cannot happen refused rather than half done.
|
|
||||||
- **`Tests/terminal.sh`** checks what a recorded file cannot see. Piped output is buffered and flushed at exit, so a prompt shown before its answer is asked for and one shown an hour late produce identical files; and key mode only touches a terminal when there is one. Both have gone wrong here, and both were found by a person whose terminal stopped working rather than by anything in this suite. So it runs the emulator under a pseudo-terminal and asks directly: that a prompt arrives before input is read, that a keystroke arrives without Return, that the terminal is handed back however the machine dies, and that suspending and resuming leave it as they found it. It also asks the one question about cycles that a recorded file cannot, since the count is stripped from every one: whether a program on a slow disk slept through the wait or spun on it. Both print the same characters and take the same elapsed time, and only the split between idle and bus cycles tells them apart.
|
|
||||||
- **`Tests/native.sh`** checks the assembler that runs on SplitBit against the one that runs on the host, byte for byte, on a boot image and four loadable programs, and then on CosmOS and on itself, and then on the CosmOS that CosmOS built.
|
|
||||||
- **`Tests/agree.sh`** checks the two implementations of SBFS against each other rather than each against itself, by building the same disk with SplitDisk and with CosmOS and comparing the images byte for byte. Every field one of them writes and the other only reads is checked there and nowhere else.
|
|
||||||
- **`Tests/lint.sh`** checks SplitLint against a fixture written so that every line of it trips exactly one rule. It compares which warning came out and at which line rather than how many came out in total: a count stays right while the thing behind it goes wrong, and breaking one rule's message left the total untouched at twenty three.
|
|
||||||
- **`Tests/docs.sh`** checks the manuals against the code: that every instruction has a row and every row is an instruction, that the counts in the headings are right, that every directive is written down, that every service the system implements is described and every service described is implemented, that every routine the manuals promise exists, that CosmOS still fits in the half of the machine its memory map gives it, and that the worked examples still assemble to the bytes printed beside them.
|
|
||||||
|
|
||||||
A cycle count is deliberately **not** part of a recorded result. The last line of the emulator's output has the number taken out before anything is compared, keeping only whether the program stopped on its own or ran into its limit, which is behaviour. Two instructions added to CosmOS used to move that number in six unrelated files at once, so a real difference would have arrived in a crowd of meaningless ones. Anything that wants to measure cycles should say so in a test of its own.
|
|
||||||
|
|
||||||
To rebuild all four tools with the address and undefined behaviour sanitizers and run the suite under them:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
make sanitize
|
make sanitize
|
||||||
```
|
```
|
||||||
|
|
||||||
This catches reads and writes past the end of an array, use after free, leaks, and undefined arithmetic. It also fills fresh allocations with a junk pattern, which turns a read of uninitialised memory from something that quietly works into something the tests notice. It runs everything `make test` runs, takes about twice as long, and puts the ordinary binaries back when it finishes.
|
Rebuilds all four tools with the address and undefined behaviour sanitizers and runs the
|
||||||
|
whole suite under them. It catches reads and writes past the end of an array, use after
|
||||||
|
free, leaks, and undefined arithmetic, takes about twice as long, and puts the ordinary
|
||||||
|
binaries back when it finishes.
|
||||||
|
|
||||||
|
Everything is built inside `Tests/build`, so running the suite never overwrites anything in
|
||||||
|
`Programs/`. To run only some of the tests, call the runner directly with their names:
|
||||||
|
|
||||||
|
```
|
||||||
|
./Tests/run.sh hello 8bitFibonacci
|
||||||
|
```
|
||||||
|
|
||||||
|
What each script can and cannot answer, how to add a test, and where the suite is blind are
|
||||||
|
in the [SplitBit Test Manual](SplitBit%20Test%20Manual.md).
|
||||||
|
|
||||||
## Documentation:
|
## Documentation:
|
||||||
|
|
||||||
Three documents, divided by what they are about rather than by who reads them.
|
Four documents, divided by what they are about rather than by who reads them.
|
||||||
|
|
||||||
**[SplitBit Programming Manual](SplitBit%20Programming%20Manual.md)** describes **the machine**: the instruction set, the registers, the vector table, interrupts, devices, the memory controller, the console, storage, and faults. Everything here is true of any SplitBit, whatever is running on it.
|
**[SplitBit Programming Manual](SplitBit%20Programming%20Manual.md)** describes **the machine**: the instruction set, the registers, the vector table, interrupts, devices, the memory controller, the console, storage, and faults. Everything here is true of any SplitBit, whatever is running on it.
|
||||||
|
|
||||||
@@ -550,6 +650,8 @@ Three documents, divided by what they are about rather than by who reads them.
|
|||||||
|
|
||||||
[Programs/CosmOS/README.md](Programs/CosmOS/README.md) describes **the operating system**: its shell, its applications, what a program may ask it for, and the libraries it owns. A different system on the same machine would answer all of that differently, which is why it is documented with the system rather than with the CPU.
|
[Programs/CosmOS/README.md](Programs/CosmOS/README.md) describes **the operating system**: its shell, its applications, what a program may ask it for, and the libraries it owns. A different system on the same machine would answer all of that differently, which is why it is documented with the system rather than with the CPU.
|
||||||
|
|
||||||
|
**[SplitBit Test Manual](SplitBit%20Test%20Manual.md)** describes **the test suite**: what each of its scripts claims, which of those claims are worth the most, how to add to it, and what it is blind to. It is about this repository rather than about the machine, which is why it comes last.
|
||||||
|
|
||||||
## License:
|
## License:
|
||||||
|
|
||||||
Apache License, Version 2.0. You may obtain a copy at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0).
|
Apache License, Version 2.0. You may obtain a copy at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0).
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
// For mirroring a host directory onto a disk: walking one, and telling a directory from a
|
||||||
|
// file. POSIX rather than C, which is why the build asks for POSIX.1-2008 by name.
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
// Numbers on a SplitBit disk are most significant byte first, the same as everywhere
|
// Numbers on a SplitBit disk are most significant byte first, the same as everywhere
|
||||||
// else on the machine.
|
// else on the machine.
|
||||||
@@ -1108,6 +1112,137 @@ done:
|
|||||||
// disk - so the children of a deleted directory would reappear inside whatever took its
|
// disk - so the children of a deleted directory would reappear inside whatever took its
|
||||||
// place. Emptying it first is the only safe order, and making the caller do that is the
|
// place. Emptying it first is the only safe order, and making the caller do that is the
|
||||||
// smallest way to guarantee it.
|
// smallest way to guarantee it.
|
||||||
|
// ---- Mirroring a host directory onto a disk ----
|
||||||
|
//
|
||||||
|
// So that putting a new program where the others live is all it takes to have it on the
|
||||||
|
// machine. A list of files in a makefile is a list that goes stale the moment somebody adds
|
||||||
|
// something and forgets, and the thing they forgot is invisible until they look for it.
|
||||||
|
//
|
||||||
|
// EVERY FILE GOES THROUGH put AND EVERY DIRECTORY THROUGH mkdir, which is the point: this
|
||||||
|
// adds a walk and no filesystem code at all, so anything the format refuses here it refuses
|
||||||
|
// everywhere, in exactly the same words.
|
||||||
|
|
||||||
|
static int compareEntries(const void *left, const void *right) {
|
||||||
|
return strcmp(*(const char *const *)left, *(const char *const *)right);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int mirrorDirectory(const char *path, const char *hostDir, const char *diskDir,
|
||||||
|
int skipCount, char *const skips[]) {
|
||||||
|
DIR *open = opendir(hostDir);
|
||||||
|
if (open == NULL) {
|
||||||
|
fprintf(stderr, "Error: Couldn't read the directory \"%s\".\n", hostDir);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Read the names first, and sort them ----
|
||||||
|
//
|
||||||
|
// readdir hands them back in whatever order the host filesystem feels like, and a disk
|
||||||
|
// image that comes out different from one run to the next is an image no test can
|
||||||
|
// compare against another. Sorted, the same tree always makes the same disk.
|
||||||
|
char **names = NULL;
|
||||||
|
size_t count = 0, room = 0;
|
||||||
|
const struct dirent *entry;
|
||||||
|
while ((entry = readdir(open)) != NULL) {
|
||||||
|
// Nothing beginning with a dot: that is . and .. and every editor's leavings, and
|
||||||
|
// none of it is source anybody wants on the machine.
|
||||||
|
if (entry->d_name[0] == '.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int skipped = 0;
|
||||||
|
for (int i = 0; i < skipCount; i++) {
|
||||||
|
if (strcmp(entry->d_name, skips[i]) == 0) {
|
||||||
|
skipped = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (skipped) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (count == room) {
|
||||||
|
room = room ? room * 2 : 32;
|
||||||
|
char **grown = realloc(names, room * sizeof(*names));
|
||||||
|
if (grown == NULL) {
|
||||||
|
fprintf(stderr, "Error: Out of memory reading \"%s\".\n", hostDir);
|
||||||
|
closedir(open);
|
||||||
|
for (size_t i = 0; i < count; i++) free(names[i]);
|
||||||
|
free(names);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
names = grown;
|
||||||
|
}
|
||||||
|
names[count] = strdup(entry->d_name);
|
||||||
|
if (names[count] == NULL) {
|
||||||
|
fprintf(stderr, "Error: Out of memory reading \"%s\".\n", hostDir);
|
||||||
|
closedir(open);
|
||||||
|
for (size_t i = 0; i < count; i++) free(names[i]);
|
||||||
|
free(names);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
closedir(open);
|
||||||
|
qsort(names, count, sizeof(*names), compareEntries);
|
||||||
|
|
||||||
|
int failed = 0;
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
char hostChild[1024];
|
||||||
|
char diskChild[1024];
|
||||||
|
if (snprintf(hostChild, sizeof(hostChild), "%s/%s", hostDir, names[i])
|
||||||
|
>= (int)sizeof(hostChild)
|
||||||
|
|| snprintf(diskChild, sizeof(diskChild), "%s/%s", diskDir, names[i])
|
||||||
|
>= (int)sizeof(diskChild)) {
|
||||||
|
fprintf(stderr, "Error: \"%s/%s\" makes a path too long to follow.\n",
|
||||||
|
hostDir, names[i]);
|
||||||
|
failed = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A NAME TOO LONG IS AN ERROR RATHER THAN A SKIP. Leaving it off would mean a build
|
||||||
|
// that looks like it worked and a disk quietly missing a program, which is the exact
|
||||||
|
// failure a mirror exists to prevent. Twenty-two bytes is what a directory entry
|
||||||
|
// holds, and the fix is to call the file something shorter.
|
||||||
|
if (strlen(names[i]) > SBFS_NAME_BYTES) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" is %zu characters, and a name holds %d.\n",
|
||||||
|
names[i], strlen(names[i]), SBFS_NAME_BYTES);
|
||||||
|
failed = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct stat about;
|
||||||
|
if (stat(hostChild, &about) != 0) {
|
||||||
|
fprintf(stderr, "Error: Couldn't look at \"%s\".\n", hostChild);
|
||||||
|
failed = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (S_ISDIR(about.st_mode)) {
|
||||||
|
if (commandMakeDirectory(path, diskChild)
|
||||||
|
|| mirrorDirectory(path, hostChild, diskChild, skipCount, skips)) {
|
||||||
|
failed = 1;
|
||||||
|
}
|
||||||
|
} else if (S_ISREG(about.st_mode)) {
|
||||||
|
if (commandPut(path, hostChild, diskChild)) {
|
||||||
|
failed = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Anything else - a socket, a device, whatever a host has - is not a thing this
|
||||||
|
// filesystem has a way to be, so it is passed over without comment.
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
free(names[i]);
|
||||||
|
}
|
||||||
|
free(names);
|
||||||
|
return failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int commandMirror(const char *path, const char *hostDir, const char *diskDir,
|
||||||
|
int skipCount, char *const skips[]) {
|
||||||
|
struct stat about;
|
||||||
|
if (stat(hostDir, &about) != 0 || !S_ISDIR(about.st_mode)) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" is not a directory to mirror.\n", hostDir);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return mirrorDirectory(path, hostDir, diskDir, skipCount, skips);
|
||||||
|
}
|
||||||
|
|
||||||
static int commandRemoveDirectory(const char *path, const char *name) {
|
static int commandRemoveDirectory(const char *path, const char *name) {
|
||||||
FILE *image = openImage(path, "r+b");
|
FILE *image = openImage(path, "r+b");
|
||||||
if (image == NULL) {
|
if (image == NULL) {
|
||||||
@@ -1162,6 +1297,8 @@ static void printUsage(const char *program) {
|
|||||||
printf(" delete <image> <path> Remove a file.\n");
|
printf(" delete <image> <path> Remove a file.\n");
|
||||||
printf(" mkdir <image> <path> Make a directory.\n");
|
printf(" mkdir <image> <path> Make a directory.\n");
|
||||||
printf(" rmdir <image> <path> Remove an empty one.\n");
|
printf(" rmdir <image> <path> Remove an empty one.\n");
|
||||||
|
printf(" mirror <image> <dir> <path> [skip...] Copy a whole host directory onto it,\n");
|
||||||
|
printf(" leaving behind anything named in skip.\n");
|
||||||
printf("\n");
|
printf("\n");
|
||||||
printf("Blocks are %d bytes. A name may be %d characters, and a path is names with\n",
|
printf("Blocks are %d bytes. A name may be %d characters, and a path is names with\n",
|
||||||
SBFS_BLOCK_BYTES, SBFS_NAME_BYTES);
|
SBFS_BLOCK_BYTES, SBFS_NAME_BYTES);
|
||||||
@@ -1256,6 +1393,16 @@ int main(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
return commandGet(path, argv[3], (argc > 4) ? argv[4] : argv[3]);
|
return commandGet(path, argv[3], (argc > 4) ? argv[4] : argv[3]);
|
||||||
}
|
}
|
||||||
|
if (strcmp(command, "mirror") == 0) {
|
||||||
|
if (argc < 5) {
|
||||||
|
fprintf(stderr, "Error: mirror needs a directory to copy and somewhere to put"
|
||||||
|
" it.\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// Anything after those is a name to leave behind, which is how a project keeps what
|
||||||
|
// it builds out of what it wrote.
|
||||||
|
return commandMirror(path, argv[3], argv[4], argc - 5, &argv[5]);
|
||||||
|
}
|
||||||
if (strcmp(command, "delete") == 0) {
|
if (strcmp(command, "delete") == 0) {
|
||||||
if (argc < 4) {
|
if (argc < 4) {
|
||||||
fprintf(stderr, "Error: delete needs the name of a file on the disk.\n");
|
fprintf(stderr, "Error: delete needs the name of a file on the disk.\n");
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ uint8_t loadROM(const unsigned char *bytes, unsigned long length,
|
|||||||
return failed;
|
return failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data) {
|
uint8_t loadFile(const char *path, uint8_t *Program, uint8_t *Data) {
|
||||||
FILE *file = fopen(path, "rb");
|
FILE *file = fopen(path, "rb");
|
||||||
if (file == NULL) {
|
if (file == NULL) {
|
||||||
fprintf(stderr, "Error: Couldn't open file: %s\n", path);
|
fprintf(stderr, "Error: Couldn't open file: %s\n", path);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data);
|
uint8_t loadFile(const char *path, uint8_t *Program, uint8_t *Data);
|
||||||
|
|
||||||
// The same, from bytes the emulator carries rather than a file it opens. See loadROM.
|
// The same, from bytes the emulator carries rather than a file it opens. See loadROM.
|
||||||
uint8_t loadROM(const unsigned char *bytes, unsigned long length,
|
uint8_t loadROM(const unsigned char *bytes, unsigned long length,
|
||||||
|
|||||||
+171
-170
@@ -14,276 +14,277 @@
|
|||||||
#include "../Assembler/assembly.h" // For the fault vector numbers.
|
#include "../Assembler/assembly.h" // For the fault vector numbers.
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t *memory; // Never published. See the note in controller.h.
|
|
||||||
uint32_t capacity; // In bytes. A full bank is 65536, which is why this is not 16 bit.
|
|
||||||
uint8_t flags;
|
|
||||||
uint8_t ownerPort;
|
|
||||||
uint16_t guardStart;
|
|
||||||
uint16_t guardEnd;
|
|
||||||
} Bank;
|
|
||||||
|
|
||||||
static Bank banks[BANK_COUNT];
|
|
||||||
|
|
||||||
// Bank 2's contents: the description of every bank, for anything that wants to read it.
|
|
||||||
static uint8_t bankTable[BANK_TABLE_BYTES];
|
|
||||||
|
|
||||||
// The registers, exactly as the ports name them.
|
|
||||||
static uint8_t sourceBank, destBank, guardBank;
|
|
||||||
static uint16_t sourceAddress, destAddress, length;
|
|
||||||
static uint16_t guardStart, guardEnd;
|
|
||||||
static uint8_t status;
|
|
||||||
|
|
||||||
// Writes a bank's description into the table that bank 2 publishes. Called whenever
|
// Writes a bank's description into the table that bank 2 publishes. Called whenever
|
||||||
// anything about a bank changes, so the published table and the real one cannot drift.
|
// anything about a bank changes, so the published table and the real one cannot drift.
|
||||||
static void publishBank(int number) {
|
static void publishBank(Controller *c, int number) {
|
||||||
uint8_t *record = bankTable + number * BANK_RECORD_BYTES;
|
uint8_t *record = c->bankTable + number * BANK_RECORD_BYTES;
|
||||||
record[0] = banks[number].flags;
|
record[0] = c->banks[number].flags;
|
||||||
record[1] = banks[number].ownerPort;
|
record[1] = c->banks[number].ownerPort;
|
||||||
// Zero means the whole 64K, the same convention Length uses, because a capacity of
|
// Zero means the whole 64K, the same convention Length uses, because a capacity of
|
||||||
// nothing is never what anyone meant.
|
// nothing is never what anyone meant.
|
||||||
record[2] = (uint8_t)((banks[number].capacity >> 8) & 0xFF);
|
record[2] = (uint8_t)((c->banks[number].capacity >> 8) & 0xFF);
|
||||||
record[3] = (uint8_t)(banks[number].capacity & 0xFF);
|
record[3] = (uint8_t)(c->banks[number].capacity & 0xFF);
|
||||||
record[4] = (uint8_t)(banks[number].guardStart >> 8);
|
record[4] = (uint8_t)(c->banks[number].guardStart >> 8);
|
||||||
record[5] = (uint8_t)(banks[number].guardStart & 0xFF);
|
record[5] = (uint8_t)(c->banks[number].guardStart & 0xFF);
|
||||||
record[6] = (uint8_t)(banks[number].guardEnd >> 8);
|
record[6] = (uint8_t)(c->banks[number].guardEnd >> 8);
|
||||||
record[7] = (uint8_t)(banks[number].guardEnd & 0xFF);
|
record[7] = (uint8_t)(c->banks[number].guardEnd & 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void defineBank(int number, uint8_t *memory, uint32_t capacity, uint8_t flags, uint8_t owner) {
|
static void defineBank(Controller *c, int number, uint8_t *memory, uint32_t capacity, uint8_t flags, uint8_t owner) {
|
||||||
banks[number].memory = memory;
|
c->banks[number].memory = memory;
|
||||||
banks[number].capacity = capacity;
|
c->banks[number].capacity = capacity;
|
||||||
banks[number].flags = flags | BANK_FLAG_PRESENT;
|
c->banks[number].flags = flags | BANK_FLAG_PRESENT;
|
||||||
banks[number].ownerPort = owner;
|
c->banks[number].ownerPort = owner;
|
||||||
banks[number].guardStart = 0;
|
c->banks[number].guardStart = 0;
|
||||||
banks[number].guardEnd = 0;
|
c->banks[number].guardEnd = 0;
|
||||||
publishBank(number);
|
publishBank(c, number);
|
||||||
}
|
}
|
||||||
|
|
||||||
void initializeController(uint8_t *programMemory, uint8_t *dataMemory) {
|
void initializeController(Controller *c, uint8_t *programMemory, uint8_t *dataMemory) {
|
||||||
memset(banks, 0, sizeof(banks));
|
memset(c->banks, 0, sizeof(c->banks));
|
||||||
memset(bankTable, 0, sizeof(bankTable));
|
memset(c->bankTable, 0, sizeof(c->bankTable));
|
||||||
for (int i = 0; i < BANK_COUNT; i++) {
|
for (int i = 0; i < BANK_COUNT; i++) {
|
||||||
banks[i].ownerPort = BANK_OWNER_MACHINE;
|
c->banks[i].ownerPort = BANK_OWNER_MACHINE;
|
||||||
publishBank(i);
|
publishBank(c, i);
|
||||||
}
|
}
|
||||||
|
|
||||||
defineBank(BANK_PROGRAM, programMemory, 0x10000, 0, BANK_OWNER_MACHINE);
|
defineBank(c, BANK_PROGRAM, programMemory, 0x10000, 0, BANK_OWNER_MACHINE);
|
||||||
defineBank(BANK_DATA, dataMemory, 0x10000, 0, BANK_OWNER_MACHINE);
|
defineBank(c, BANK_DATA, dataMemory, 0x10000, 0, BANK_OWNER_MACHINE);
|
||||||
// The table describes itself, so a program that walks it finds bank 2 in there along
|
// The table describes itself, so a program that walks it finds bank 2 in there along
|
||||||
// with everything else. It is read only, which is what keeps RegisterBank the only
|
// with everything else. It is read only, which is what keeps RegisterBank the only
|
||||||
// way to change what the controller routes through.
|
// way to change what the controller routes through.
|
||||||
defineBank(BANK_TABLE, bankTable, BANK_TABLE_BYTES, BANK_FLAG_READ_ONLY, BANK_OWNER_MACHINE);
|
defineBank(c, BANK_TABLE, c->bankTable, BANK_TABLE_BYTES, BANK_FLAG_READ_ONLY, BANK_OWNER_MACHINE);
|
||||||
|
|
||||||
sourceBank = destBank = guardBank = 0;
|
c->sourceBank = c->destBank = c->guardBank = 0;
|
||||||
sourceAddress = destAddress = length = 0;
|
c->sourceAddress = c->destAddress = c->length = 0;
|
||||||
guardStart = guardEnd = 0;
|
c->guardStart = c->guardEnd = 0;
|
||||||
status = 0;
|
c->status = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refuses, remembering why so that Status can be read afterwards.
|
// Refuses, remembering why so that Status can be read afterwards.
|
||||||
static void refuse(uint8_t faultVector) {
|
static void refuse(Controller *c, uint8_t faultVector) {
|
||||||
status = faultVector;
|
c->status = faultVector;
|
||||||
refuseAccess(faultVector);
|
refuseAccess(faultVector);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is this somewhere the controller can read? A bank has to be there, and the address has
|
// Is this somewhere the controller can read? A bank has to be there, and the address has
|
||||||
// to be inside it.
|
// to be inside it.
|
||||||
static int canRead(uint8_t bank, uint16_t address) {
|
static int canRead(Controller *c, uint8_t bank, uint16_t address) {
|
||||||
if (!(banks[bank].flags & BANK_FLAG_PRESENT) || address >= banks[bank].capacity) {
|
if (!(c->banks[bank].flags & BANK_FLAG_PRESENT) || address >= c->banks[bank].capacity) {
|
||||||
refuse(VECTOR_BANK_FAULT);
|
refuse(c, VECTOR_BANK_FAULT);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The same, and then the two reasons a write in particular gets turned away.
|
// The same, and then the two reasons a write in particular gets turned away.
|
||||||
static int canWrite(uint8_t bank, uint16_t address) {
|
static int canWrite(Controller *c, uint8_t bank, uint16_t address) {
|
||||||
if (!canRead(bank, address)) {
|
if (!canRead(c, bank, address)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (banks[bank].flags & BANK_FLAG_READ_ONLY) {
|
if (c->banks[bank].flags & BANK_FLAG_READ_ONLY) {
|
||||||
refuse(VECTOR_GUARD_VIOLATION);
|
refuse(c, VECTOR_GUARD_VIOLATION);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if ((banks[bank].flags & BANK_FLAG_GUARDED)
|
if ((c->banks[bank].flags & BANK_FLAG_GUARDED)
|
||||||
&& address >= banks[bank].guardStart && address <= banks[bank].guardEnd) {
|
&& address >= c->banks[bank].guardStart && address <= c->banks[bank].guardEnd) {
|
||||||
refuse(VECTOR_GUARD_VIOLATION);
|
refuse(c, VECTOR_GUARD_VIOLATION);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A length of zero means the whole 64K, because a transfer of no bytes is never what
|
// A c->length of zero means the whole 64K, because a transfer of no bytes is never what
|
||||||
// anyone meant, and 65536 does not fit in the two bytes that carry it.
|
// anyone meant, and 65536 does not fit in the two bytes that carry it.
|
||||||
static uint32_t transferLength(void) {
|
static uint32_t transferLength(Controller *c) {
|
||||||
return (length == 0) ? 0x10000u : (uint32_t)length;
|
return (c->length == 0) ? 0x10000u : (uint32_t)c->length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everything a transfer will touch is checked before any of it moves. A blit that ran
|
// Everything a transfer will touch is checked before any of it moves. A blit that ran
|
||||||
// out of bank halfway would leave memory in a state no program asked for, and the
|
// out of bank halfway would leave memory in a state no program asked for, and the
|
||||||
// diagnostic would arrive after the damage rather than instead of it. So these answer
|
// diagnostic would arrive after the damage rather than instead of it. So these answer
|
||||||
// for the whole range or refuse the whole thing.
|
// for the whole range or refuse the whole thing.
|
||||||
static int rangeReadable(uint8_t bank, uint16_t address, uint32_t count) {
|
static int rangeReadable(Controller *c, uint8_t bank, uint16_t address, uint32_t count) {
|
||||||
if (!(banks[bank].flags & BANK_FLAG_PRESENT)
|
if (!(c->banks[bank].flags & BANK_FLAG_PRESENT)
|
||||||
|| (uint32_t)address + count > banks[bank].capacity) {
|
|| (uint32_t)address + count > c->banks[bank].capacity) {
|
||||||
refuse(VECTOR_BANK_FAULT);
|
refuse(c, VECTOR_BANK_FAULT);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int rangeWritable(uint8_t bank, uint16_t address, uint32_t count) {
|
static int rangeWritable(Controller *c, uint8_t bank, uint16_t address, uint32_t count) {
|
||||||
if (!rangeReadable(bank, address, count)) {
|
if (!rangeReadable(c, bank, address, count)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (banks[bank].flags & BANK_FLAG_READ_ONLY) {
|
if (c->banks[bank].flags & BANK_FLAG_READ_ONLY) {
|
||||||
refuse(VECTOR_GUARD_VIOLATION);
|
refuse(c, VECTOR_GUARD_VIOLATION);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (banks[bank].flags & BANK_FLAG_GUARDED) {
|
if (c->banks[bank].flags & BANK_FLAG_GUARDED) {
|
||||||
uint32_t last = (uint32_t)address + count - 1;
|
uint32_t last = (uint32_t)address + count - 1;
|
||||||
// Any overlap at all with the fence, not just a write that starts inside it.
|
// Any overlap at all with the fence, not just a write that starts inside it.
|
||||||
if (!(last < banks[bank].guardStart || address > banks[bank].guardEnd)) {
|
if (!(last < c->banks[bank].guardStart || address > c->banks[bank].guardEnd)) {
|
||||||
refuse(VECTOR_GUARD_VIOLATION);
|
refuse(c, VECTOR_GUARD_VIOLATION);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// What the moves below have cost since anybody last asked.
|
// ---- Sixteen bits wide, when the addresses let it be ----
|
||||||
static unsigned long pendingCycles = 0;
|
//
|
||||||
|
// The controller reaches bank memory two bytes at a time, so an aligned transfer moves two
|
||||||
|
// bytes in the time a misaligned one moves one. A word is read at an even address and
|
||||||
|
// written at an even address, which is why the source, the destination AND the c->length must
|
||||||
|
// all be even: an odd anything would have the controller shifting bytes across word
|
||||||
|
// boundaries to line them up, and that is a second design rather than this one.
|
||||||
|
//
|
||||||
|
// Misaligned falls back to a byte a cycle, which is exactly what the machine did before it
|
||||||
|
// was widened, so nothing already written got slower.
|
||||||
|
//
|
||||||
|
// THE RULE IS VISIBLE ON PURPOSE. A program that cares can align what it moves, and a cost
|
||||||
|
// a program cannot see is a cost it cannot avoid. It is also the honest thing to model:
|
||||||
|
// hardware this shape really does behave this way.
|
||||||
|
static int wideRun(uint32_t addressesAndLength) {
|
||||||
|
return (addressesAndLength & 1u) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
unsigned long controllerTakeCycles(void) {
|
unsigned long controllerTakeCycles(Controller *c) {
|
||||||
unsigned long taken = pendingCycles;
|
unsigned long taken = c->pendingCycles;
|
||||||
pendingCycles = 0;
|
c->pendingCycles = 0;
|
||||||
return taken;
|
return taken;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void doBlit(void) {
|
static void doBlit(Controller *c) {
|
||||||
uint32_t count = transferLength();
|
uint32_t count = transferLength(c);
|
||||||
if (!rangeReadable(sourceBank, sourceAddress, count)) {
|
if (!rangeReadable(c, c->sourceBank, c->sourceAddress, count)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!rangeWritable(destBank, destAddress, count)) {
|
if (!rangeWritable(c, c->destBank, c->destAddress, count)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// memmove rather than memcpy, because source and destination may be the same bank
|
// memmove rather than memcpy, because source and destination may be the same bank
|
||||||
// and may overlap. Sliding a buffer along itself is an ordinary thing to want, and
|
// and may overlap. Sliding a buffer along itself is an ordinary thing to want, and
|
||||||
// getting it silently wrong is exactly the sort of failure this machine keeps
|
// getting it silently wrong is exactly the sort of failure this machine keeps
|
||||||
// designing against.
|
// designing against.
|
||||||
memmove(banks[destBank].memory + destAddress,
|
memmove(c->banks[c->destBank].memory + c->destAddress,
|
||||||
banks[sourceBank].memory + sourceAddress, count);
|
c->banks[c->sourceBank].memory + c->sourceAddress, count);
|
||||||
// A byte read and a byte written. Two banks are two memories and the pair overlaps;
|
// A word read and a word written. Two c->banks are two memories and the pair overlaps;
|
||||||
// one bank is one memory and they do not. The odd cycle is the pipeline filling.
|
// one bank is one memory and they do not. The odd cycle is the pipeline filling.
|
||||||
pendingCycles += (sourceBank == destBank) ? 2 * count + 1 : count + 1;
|
//
|
||||||
sourceAddress = (uint16_t)(sourceAddress + count);
|
// Wide when everything is even, so an aligned move between c->banks settles at two bytes a
|
||||||
destAddress = (uint16_t)(destAddress + count);
|
// cycle and an aligned move within one at a byte a cycle - each twice what it was.
|
||||||
status = 0;
|
unsigned long moves = wideRun(c->sourceAddress | c->destAddress | count) ? count / 2 : count;
|
||||||
|
c->pendingCycles += (c->sourceBank == c->destBank) ? 2 * moves + 1 : moves + 1;
|
||||||
|
c->sourceAddress = (uint16_t)(c->sourceAddress + count);
|
||||||
|
c->destAddress = (uint16_t)(c->destAddress + count);
|
||||||
|
c->status = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void doFill(void) {
|
static void doFill(Controller *c) {
|
||||||
uint32_t count = transferLength();
|
uint32_t count = transferLength(c);
|
||||||
if (!rangeWritable(destBank, destAddress, count)) {
|
if (!rangeWritable(c, c->destBank, c->destAddress, count)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// A fill has nowhere to read from, only a value, so SourceLow carries the byte and
|
// A fill has nowhere to read from, only a value, so SourceLow carries the byte and the
|
||||||
// the rest of the source registers mean nothing here.
|
// rest of the source registers mean nothing here - including for the alignment, which
|
||||||
pendingCycles += count + 1;
|
// asks only about where the bytes are going and how many there are.
|
||||||
memset(banks[destBank].memory + destAddress, (int)(sourceAddress & 0xFF), count);
|
c->pendingCycles += (wideRun(c->destAddress | count) ? count / 2 : count) + 1;
|
||||||
destAddress = (uint16_t)(destAddress + count);
|
memset(c->banks[c->destBank].memory + c->destAddress, (int)(c->sourceAddress & 0xFF), count);
|
||||||
status = 0;
|
c->destAddress = (uint16_t)(c->destAddress + count);
|
||||||
|
c->status = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DestBank is the number being given out, and SourceLow says which port owns the memory.
|
// DestBank is the number being given out, and SourceLow says which port owns the memory.
|
||||||
// The capacity is asked of the device rather than supplied, because how big a bank is
|
// The capacity is asked of the device rather than supplied, because how big a bank is
|
||||||
// was settled when the machine was built.
|
// was settled when the machine was built.
|
||||||
static void doRegisterBank(void) {
|
static void doRegisterBank(Controller *c) {
|
||||||
if (destBank <= BANK_TABLE) {
|
if (c->destBank <= BANK_TABLE) {
|
||||||
// Banks 0 to 2 are the machine's own and are not anybody's to hand out.
|
// Banks 0 to 2 are the machine's own and are not anybody's to hand out.
|
||||||
refuse(VECTOR_BANK_FAULT);
|
refuse(c, VECTOR_BANK_FAULT);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
uint8_t port = (uint8_t)(sourceAddress & 0xFF);
|
uint8_t port = (uint8_t)(c->sourceAddress & 0xFF);
|
||||||
uint32_t capacity = 0;
|
uint32_t capacity = 0;
|
||||||
uint8_t *memory = deviceMemory(port, &capacity);
|
uint8_t *memory = deviceMemory(port, &capacity);
|
||||||
if (memory == NULL) {
|
if (memory == NULL) {
|
||||||
// Either nothing is on that port or what is there brings no memory. Registering
|
// Either nothing is on that port or what is there brings no memory. Registering
|
||||||
// it would put a bank in the table that leads nowhere.
|
// it would put a bank in the table that leads nowhere.
|
||||||
refuse(VECTOR_BANK_FAULT);
|
refuse(c, VECTOR_BANK_FAULT);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Registering over a bank that already has something in it is allowed. Which number
|
// Registering over a bank that already has something in it is allowed. Which number
|
||||||
// a device's memory answers to is the OS's business, and nothing was allocated that
|
// a device's memory answers to is the OS's business, and nothing was allocated that
|
||||||
// could be lost by changing its mind.
|
// could be lost by changing its mind.
|
||||||
defineBank(destBank, memory, capacity, 0, port);
|
defineBank(c, c->destBank, memory, capacity, 0, port);
|
||||||
status = 0;
|
c->status = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The guard registers stage a range; this is what commits it. Raising a fence over a
|
// The guard registers stage a range; this is what commits it. Raising a fence over a
|
||||||
// bank that is not there would protect nothing while looking like it protected
|
// bank that is not there would protect nothing while looking like it protected
|
||||||
// something, so it is refused rather than quietly accepted.
|
// something, so it is refused rather than quietly accepted.
|
||||||
static void doGuardOn(void) {
|
static void doGuardOn(Controller *c) {
|
||||||
if (!(banks[guardBank].flags & BANK_FLAG_PRESENT)) {
|
if (!(c->banks[c->guardBank].flags & BANK_FLAG_PRESENT)) {
|
||||||
refuse(VECTOR_BANK_FAULT);
|
refuse(c, VECTOR_BANK_FAULT);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (guardStart > guardEnd) {
|
if (c->guardStart > c->guardEnd) {
|
||||||
// No address can be inside a range that ends before it starts, so this fence
|
// No address can be inside a range that ends before it starts, so this fence
|
||||||
// would catch nothing. A program that raised one would believe it was protected
|
// would catch nothing. A program that raised one would believe it was protected
|
||||||
// and would not be, which is worse than having no fence at all.
|
// and would not be, which is worse than having no fence at all.
|
||||||
refuse(VECTOR_BANK_FAULT);
|
refuse(c, VECTOR_BANK_FAULT);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
banks[guardBank].guardStart = guardStart;
|
c->banks[c->guardBank].guardStart = c->guardStart;
|
||||||
banks[guardBank].guardEnd = guardEnd;
|
c->banks[c->guardBank].guardEnd = c->guardEnd;
|
||||||
banks[guardBank].flags |= BANK_FLAG_GUARDED;
|
c->banks[c->guardBank].flags |= BANK_FLAG_GUARDED;
|
||||||
publishBank(guardBank);
|
publishBank(c, c->guardBank);
|
||||||
status = 0;
|
c->status = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void doGuardOff(void) {
|
static void doGuardOff(Controller *c) {
|
||||||
if (!(banks[guardBank].flags & BANK_FLAG_PRESENT)) {
|
if (!(c->banks[c->guardBank].flags & BANK_FLAG_PRESENT)) {
|
||||||
refuse(VECTOR_BANK_FAULT);
|
refuse(c, VECTOR_BANK_FAULT);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
banks[guardBank].flags &= (uint8_t)~BANK_FLAG_GUARDED;
|
c->banks[c->guardBank].flags &= (uint8_t)~BANK_FLAG_GUARDED;
|
||||||
publishBank(guardBank);
|
publishBank(c, c->guardBank);
|
||||||
status = 0;
|
c->status = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t controllerWrite(uint8_t value, uint8_t port) {
|
uint8_t controllerWrite(Controller *c, uint8_t value, uint8_t port) {
|
||||||
switch (port) {
|
switch (port) {
|
||||||
case CTRL_SOURCE_BANK: sourceBank = value; break;
|
case CTRL_SOURCE_BANK: c->sourceBank = value; break;
|
||||||
case CTRL_SOURCE_HIGH: sourceAddress = (uint16_t)(value << 8) | (sourceAddress & 0x00FF); break;
|
case CTRL_SOURCE_HIGH: c->sourceAddress = (uint16_t)(value << 8) | (c->sourceAddress & 0x00FF); break;
|
||||||
case CTRL_SOURCE_LOW: sourceAddress = (sourceAddress & 0xFF00) | value; break;
|
case CTRL_SOURCE_LOW: c->sourceAddress = (c->sourceAddress & 0xFF00) | value; break;
|
||||||
case CTRL_DEST_BANK: destBank = value; break;
|
case CTRL_DEST_BANK: c->destBank = value; break;
|
||||||
case CTRL_DEST_HIGH: destAddress = (uint16_t)(value << 8) | (destAddress & 0x00FF); break;
|
case CTRL_DEST_HIGH: c->destAddress = (uint16_t)(value << 8) | (c->destAddress & 0x00FF); break;
|
||||||
case CTRL_DEST_LOW: destAddress = (destAddress & 0xFF00) | value; break;
|
case CTRL_DEST_LOW: c->destAddress = (c->destAddress & 0xFF00) | value; break;
|
||||||
case CTRL_LENGTH_HIGH: length = (uint16_t)(value << 8) | (length & 0x00FF); break;
|
case CTRL_LENGTH_HIGH: c->length = (uint16_t)(value << 8) | (c->length & 0x00FF); break;
|
||||||
case CTRL_LENGTH_LOW: length = (length & 0xFF00) | value; break;
|
case CTRL_LENGTH_LOW: c->length = (c->length & 0xFF00) | value; break;
|
||||||
|
|
||||||
case CTRL_GUARD_BANK: guardBank = value; break;
|
case CTRL_GUARD_BANK: c->guardBank = value; break;
|
||||||
case CTRL_GUARD_START_HIGH: guardStart = (uint16_t)(value << 8) | (guardStart & 0x00FF); break;
|
case CTRL_GUARD_START_HIGH: c->guardStart = (uint16_t)(value << 8) | (c->guardStart & 0x00FF); break;
|
||||||
case CTRL_GUARD_START_LOW: guardStart = (guardStart & 0xFF00) | value; break;
|
case CTRL_GUARD_START_LOW: c->guardStart = (c->guardStart & 0xFF00) | value; break;
|
||||||
case CTRL_GUARD_END_HIGH: guardEnd = (uint16_t)(value << 8) | (guardEnd & 0x00FF); break;
|
case CTRL_GUARD_END_HIGH: c->guardEnd = (uint16_t)(value << 8) | (c->guardEnd & 0x00FF); break;
|
||||||
case CTRL_GUARD_END_LOW: guardEnd = (guardEnd & 0xFF00) | value; break;
|
case CTRL_GUARD_END_LOW: c->guardEnd = (c->guardEnd & 0xFF00) | value; break;
|
||||||
|
|
||||||
case CTRL_DATA:
|
case CTRL_DATA:
|
||||||
// A byte into the destination, and the address steps on so that writing a
|
// A byte into the destination, and the address steps on so that writing a
|
||||||
// run of bytes is a loop over one instruction rather than four.
|
// run of bytes is a loop over one instruction rather than four.
|
||||||
if (canWrite(destBank, destAddress)) {
|
if (canWrite(c, c->destBank, c->destAddress)) {
|
||||||
pendingCycles++; // The byte itself, beyond reaching the port.
|
c->pendingCycles++; // The byte itself, beyond reaching the port.
|
||||||
banks[destBank].memory[destAddress] = value;
|
c->banks[c->destBank].memory[c->destAddress] = value;
|
||||||
if (destBank == BANK_TABLE) {
|
if (c->destBank == BANK_TABLE) {
|
||||||
// Unreachable while the table is read only, and here so that it stays
|
// Unreachable while the table is read only, and here so that it stays
|
||||||
// true if that ever changes: the published bytes are a description,
|
// true if that ever changes: the published bytes are a description,
|
||||||
// and nothing may write through them into a real bank.
|
// and nothing may write through them into a real bank.
|
||||||
publishBank(BANK_TABLE);
|
publishBank(c, BANK_TABLE);
|
||||||
}
|
}
|
||||||
destAddress++;
|
c->destAddress++;
|
||||||
status = 0;
|
c->status = 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -291,16 +292,16 @@ uint8_t controllerWrite(uint8_t value, uint8_t port) {
|
|||||||
// Both leave the addresses past whatever they touched and Length as it was,
|
// Both leave the addresses past whatever they touched and Length as it was,
|
||||||
// so asking again carries straight on from where the last one stopped.
|
// so asking again carries straight on from where the last one stopped.
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case COMMAND_BLIT: doBlit(); break;
|
case COMMAND_BLIT: doBlit(c); break;
|
||||||
case COMMAND_FILL: doFill(); break;
|
case COMMAND_FILL: doFill(c); break;
|
||||||
case COMMAND_REGISTER_BANK: doRegisterBank(); break;
|
case COMMAND_REGISTER_BANK: doRegisterBank(c); break;
|
||||||
case COMMAND_GUARD_ON: doGuardOn(); break;
|
case COMMAND_GUARD_ON: doGuardOn(c); break;
|
||||||
case COMMAND_GUARD_OFF: doGuardOff(); break;
|
case COMMAND_GUARD_OFF: doGuardOff(c); break;
|
||||||
default:
|
default:
|
||||||
// Refusing an unknown command is better than ignoring it, since a
|
// Refusing an unknown command is better than ignoring it, since a
|
||||||
// program that asked for something is entitled to find out that it
|
// program that asked for something is entitled to find out that it
|
||||||
// did not happen.
|
// did not happen.
|
||||||
refuse(VECTOR_BANK_FAULT);
|
refuse(c, VECTOR_BANK_FAULT);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -312,33 +313,33 @@ uint8_t controllerWrite(uint8_t value, uint8_t port) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t controllerRead(uint8_t port) {
|
uint8_t controllerRead(Controller *c, uint8_t port) {
|
||||||
switch (port) {
|
switch (port) {
|
||||||
case CTRL_SOURCE_BANK: return sourceBank;
|
case CTRL_SOURCE_BANK: return c->sourceBank;
|
||||||
case CTRL_SOURCE_HIGH: return (uint8_t)(sourceAddress >> 8);
|
case CTRL_SOURCE_HIGH: return (uint8_t)(c->sourceAddress >> 8);
|
||||||
case CTRL_SOURCE_LOW: return (uint8_t)(sourceAddress & 0xFF);
|
case CTRL_SOURCE_LOW: return (uint8_t)(c->sourceAddress & 0xFF);
|
||||||
case CTRL_DEST_BANK: return destBank;
|
case CTRL_DEST_BANK: return c->destBank;
|
||||||
case CTRL_DEST_HIGH: return (uint8_t)(destAddress >> 8);
|
case CTRL_DEST_HIGH: return (uint8_t)(c->destAddress >> 8);
|
||||||
case CTRL_DEST_LOW: return (uint8_t)(destAddress & 0xFF);
|
case CTRL_DEST_LOW: return (uint8_t)(c->destAddress & 0xFF);
|
||||||
case CTRL_LENGTH_HIGH: return (uint8_t)(length >> 8);
|
case CTRL_LENGTH_HIGH: return (uint8_t)(c->length >> 8);
|
||||||
case CTRL_LENGTH_LOW: return (uint8_t)(length & 0xFF);
|
case CTRL_LENGTH_LOW: return (uint8_t)(c->length & 0xFF);
|
||||||
case CTRL_STATUS: return status;
|
case CTRL_STATUS: return c->status;
|
||||||
|
|
||||||
case CTRL_GUARD_BANK: return guardBank;
|
case CTRL_GUARD_BANK: return c->guardBank;
|
||||||
case CTRL_GUARD_START_HIGH: return (uint8_t)(guardStart >> 8);
|
case CTRL_GUARD_START_HIGH: return (uint8_t)(c->guardStart >> 8);
|
||||||
case CTRL_GUARD_START_LOW: return (uint8_t)(guardStart & 0xFF);
|
case CTRL_GUARD_START_LOW: return (uint8_t)(c->guardStart & 0xFF);
|
||||||
case CTRL_GUARD_END_HIGH: return (uint8_t)(guardEnd >> 8);
|
case CTRL_GUARD_END_HIGH: return (uint8_t)(c->guardEnd >> 8);
|
||||||
case CTRL_GUARD_END_LOW: return (uint8_t)(guardEnd & 0xFF);
|
case CTRL_GUARD_END_LOW: return (uint8_t)(c->guardEnd & 0xFF);
|
||||||
|
|
||||||
case CTRL_DATA: {
|
case CTRL_DATA: {
|
||||||
// A byte out of the source, stepping on the same way a write does.
|
// A byte out of the source, stepping on the same way a write does.
|
||||||
if (!canRead(sourceBank, sourceAddress)) {
|
if (!canRead(c, c->sourceBank, c->sourceAddress)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
pendingCycles++; // As above, the other way round.
|
c->pendingCycles++; // As above, the other way round.
|
||||||
uint8_t value = banks[sourceBank].memory[sourceAddress];
|
uint8_t value = c->banks[c->sourceBank].memory[c->sourceAddress];
|
||||||
sourceAddress++;
|
c->sourceAddress++;
|
||||||
status = 0;
|
c->status = 0;
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,17 @@
|
|||||||
// a program that could write one would be setting a host address, which means nothing on
|
// a program that could write one would be setting a host address, which means nothing on
|
||||||
// hardware and everything to the emulator running it.
|
// hardware and everything to the emulator running it.
|
||||||
#define BANK_RECORD_BYTES 8
|
#define BANK_RECORD_BYTES 8
|
||||||
|
|
||||||
|
// What the controller knows about one bank. The memory pointer is never published - see the
|
||||||
|
// note above the record layout.
|
||||||
|
typedef struct {
|
||||||
|
uint8_t *memory;
|
||||||
|
uint32_t capacity; // In bytes. A full bank is 65536, which is why this is not 16 bit.
|
||||||
|
uint8_t flags;
|
||||||
|
uint8_t ownerPort;
|
||||||
|
uint16_t guardStart;
|
||||||
|
uint16_t guardEnd;
|
||||||
|
} Bank;
|
||||||
#define BANK_TABLE_BYTES (BANK_COUNT * BANK_RECORD_BYTES)
|
#define BANK_TABLE_BYTES (BANK_COUNT * BANK_RECORD_BYTES)
|
||||||
|
|
||||||
#define BANK_FLAG_PRESENT 0x01
|
#define BANK_FLAG_PRESENT 0x01
|
||||||
@@ -93,21 +104,51 @@
|
|||||||
// work look like ten cycles.
|
// work look like ten cycles.
|
||||||
//
|
//
|
||||||
// Banks are separate memories, which is what decides the rate. A move between two of them
|
// Banks are separate memories, which is what decides the rate. A move between two of them
|
||||||
// can overlap its read and its write - fetch the next byte while the last one is stored -
|
// can overlap its read and its write - fetch the next word while the last one is stored -
|
||||||
// so it settles at a byte a cycle. A move WITHIN one bank cannot, and costs two. A fill has
|
// while a move WITHIN one bank cannot and costs twice as much. A fill has nothing to read
|
||||||
// nothing to read and costs one whatever the banks are.
|
// and costs the same as a move between banks.
|
||||||
|
//
|
||||||
|
// AND THE PATH IS SIXTEEN BITS WIDE, so a transfer whose source, destination and length are
|
||||||
|
// all even moves two bytes a cycle between banks and one within a bank. Anything odd falls
|
||||||
|
// back to the byte a cycle this had before it was widened: lining up bytes across word
|
||||||
|
// boundaries is a second design, and this is not it. See wideRun in controller.c.
|
||||||
//
|
//
|
||||||
// Returned and cleared, so the caller adds it to whatever it is charging for. The CPU picks
|
// Returned and cleared, so the caller adds it to whatever it is charging for. The CPU picks
|
||||||
// it up after each port access, which makes the transfer a stall: the machine issues a blit
|
// it up after each port access, which makes the transfer a stall: the machine issues a blit
|
||||||
// and waits for it. Whether real hardware would let the two run at once is a live question -
|
// and waits for it. Whether real hardware would let the two run at once is a live question -
|
||||||
// the memories are separate, so it plausibly could - and the answer wants measuring before
|
// the memories are separate, so it plausibly could - and the answer wants measuring before
|
||||||
// it is designed.
|
// it is designed.
|
||||||
unsigned long controllerTakeCycles(void);
|
// ---- One of these to a bus ----
|
||||||
|
//
|
||||||
|
// A controller is the most stateful thing on this machine: a source bank and address, a
|
||||||
|
// destination, a length, a command, 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 amount of arbitration fixes that, because there is nothing to
|
||||||
|
// arbitrate: both writes were legal and the result belongs to neither of them.
|
||||||
|
//
|
||||||
|
// So a peripheral core gets its own, describing its own memories. The fields are here rather
|
||||||
|
// than hidden in the source file because a device that contains a core has to be able to hold
|
||||||
|
// one; nothing outside reaches into them.
|
||||||
|
typedef struct {
|
||||||
|
Bank banks[BANK_COUNT];
|
||||||
|
// Bank 2's contents: the description of every bank, for anything that wants to read it.
|
||||||
|
uint8_t bankTable[BANK_TABLE_BYTES];
|
||||||
|
// The registers, exactly as the ports name them.
|
||||||
|
uint8_t sourceBank, destBank, guardBank;
|
||||||
|
uint16_t sourceAddress, destAddress, length;
|
||||||
|
uint16_t guardStart, guardEnd;
|
||||||
|
uint8_t status;
|
||||||
|
// What the moves have cost since anybody last asked.
|
||||||
|
unsigned long pendingCycles;
|
||||||
|
} Controller;
|
||||||
|
|
||||||
void initializeController(uint8_t *programMemory, uint8_t *dataMemory);
|
unsigned long controllerTakeCycles(Controller *c);
|
||||||
|
|
||||||
uint8_t controllerWrite(uint8_t value, uint8_t port);
|
void initializeController(Controller *c, uint8_t *programMemory, uint8_t *dataMemory);
|
||||||
|
|
||||||
uint8_t controllerRead(uint8_t port);
|
uint8_t controllerWrite(Controller *c, uint8_t value, uint8_t port);
|
||||||
|
|
||||||
|
uint8_t controllerRead(Controller *c, uint8_t port);
|
||||||
|
|
||||||
#endif // CONTROLLER_H
|
#endif // CONTROLLER_H
|
||||||
|
|||||||
+30
-13
@@ -8,8 +8,6 @@
|
|||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "../Assembler/assembly.h" // For the vector table layout, which both tools share.
|
#include "../Assembler/assembly.h" // For the vector table layout, which both tools share.
|
||||||
|
|
||||||
uint16_t shiftRegister;
|
|
||||||
|
|
||||||
// Reads one entry out of a vector table. Most significant byte first, matching the
|
// Reads one entry out of a vector table. Most significant byte first, matching the
|
||||||
// branch instructions and both file formats.
|
// branch instructions and both file formats.
|
||||||
// ---- Every touch of memory, and what it costs ----
|
// ---- Every touch of memory, and what it costs ----
|
||||||
@@ -38,16 +36,20 @@ static inline void writeData(CPURegisters *cpu, uint16_t at, uint8_t value) {
|
|||||||
// otherwise.
|
// otherwise.
|
||||||
static inline void portOut(CPURegisters *cpu, uint8_t value, uint8_t port) {
|
static inline void portOut(CPURegisters *cpu, uint8_t value, uint8_t port) {
|
||||||
cpu->busCycles++;
|
cpu->busCycles++;
|
||||||
OutputHandler(value, port);
|
cpu->bus->out(value, port);
|
||||||
// And whatever memory that made the controller move. The machine waits for it, which
|
// And whatever memory that made the controller move. The machine waits for it, which
|
||||||
// is the conservative reading: a blit stalls the program that asked for one.
|
// is the conservative reading: a blit stalls the program that asked for one.
|
||||||
cpu->busCycles += controllerTakeCycles();
|
cpu->busCycles += cpu->bus->takeStall();
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline uint8_t portIn(CPURegisters *cpu, uint8_t port) {
|
static inline uint8_t portIn(CPURegisters *cpu, uint8_t port) {
|
||||||
cpu->busCycles++;
|
cpu->busCycles++;
|
||||||
uint8_t value = InputHandler(port);
|
uint8_t value = cpu->bus->in(port);
|
||||||
cpu->busCycles += controllerTakeCycles();
|
cpu->busCycles += cpu->bus->takeStall();
|
||||||
|
// 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 += cpu->bus->takeIdle();
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +129,9 @@ static uint8_t answerRefusal(CPURegisters *cpu, uint16_t site) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemory) {
|
void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemory) {
|
||||||
|
// The machine's own, which is what every processor here was on when there could only be
|
||||||
|
// one. Anything that wants a processor somewhere else changes this afterwards.
|
||||||
|
cpu->bus = machineBus();
|
||||||
cpu->A = 0;
|
cpu->A = 0;
|
||||||
cpu->B = 0;
|
cpu->B = 0;
|
||||||
cpu->Q = 0;
|
cpu->Q = 0;
|
||||||
@@ -245,17 +250,29 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
break;
|
break;
|
||||||
case 0x17:
|
case 0x17:
|
||||||
// SHL - Shift AB left.
|
// SHL - Shift AB left.
|
||||||
shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B;
|
//
|
||||||
|
// A local, and it always was one in effect: written and read inside this one
|
||||||
|
// instruction and never carried to the next. It sat at file scope until there
|
||||||
|
// was a second processor to share it with, which is a poor time to find out.
|
||||||
|
{
|
||||||
|
uint16_t shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B;
|
||||||
shiftRegister = (shiftRegister << 1) | (shiftRegister >> 15);
|
shiftRegister = (shiftRegister << 1) | (shiftRegister >> 15);
|
||||||
cpu->A = shiftRegister >> 8;
|
cpu->A = shiftRegister >> 8;
|
||||||
cpu->B = shiftRegister & 0xFF;
|
cpu->B = shiftRegister & 0xFF;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 0x18:
|
case 0x18:
|
||||||
// SHR - Shift AB right.
|
// SHR - Shift AB right.
|
||||||
shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B;
|
//
|
||||||
|
// A local, and it always was one in effect: written and read inside this one
|
||||||
|
// instruction and never carried to the next. It sat at file scope until there
|
||||||
|
// was a second processor to share it with, which is a poor time to find out.
|
||||||
|
{
|
||||||
|
uint16_t shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B;
|
||||||
shiftRegister = (shiftRegister >> 1) | (shiftRegister << 15);
|
shiftRegister = (shiftRegister >> 1) | (shiftRegister << 15);
|
||||||
cpu->A = shiftRegister >> 8;
|
cpu->A = shiftRegister >> 8;
|
||||||
cpu->B = shiftRegister & 0xFF;
|
cpu->B = shiftRegister & 0xFF;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
//
|
//
|
||||||
// 1x - Branch Operations:
|
// 1x - Branch Operations:
|
||||||
@@ -821,7 +838,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
// makes the ordinary idiom race-free: a program tests its device, finds it
|
// makes the ordinary idiom race-free: a program tests its device, finds it
|
||||||
// busy, and waits. If the device finished in between, the line is standing
|
// busy, and waits. If the device finished in between, the line is standing
|
||||||
// and this does nothing at all rather than sleeping through the answer.
|
// and this does nothing at all rather than sleeping through the answer.
|
||||||
if (nextPendingInterrupt() < 0) {
|
if (cpu->bus->nextInterrupt() < 0) {
|
||||||
cpu->Waiting = 1;
|
cpu->Waiting = 1;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -854,7 +871,7 @@ void stepCPU(CPURegisters *cpu) {
|
|||||||
// handler for and simply read its status afterwards, which is the whole reason
|
// handler for and simply read its status afterwards, which is the whole reason
|
||||||
// this is worth having and is what the filesystem does with it.
|
// this is worth having and is what the filesystem does with it.
|
||||||
if (cpu->Waiting) {
|
if (cpu->Waiting) {
|
||||||
if (nextPendingInterrupt() < 0) {
|
if (cpu->bus->nextInterrupt() < 0) {
|
||||||
cpu->idleCycles++;
|
cpu->idleCycles++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -868,7 +885,7 @@ void stepCPU(CPURegisters *cpu) {
|
|||||||
// handler faults there the way it always has. Waiting changes what the CPU
|
// handler faults there the way it always has. Waiting changes what the CPU
|
||||||
// does between instructions; it does not change interrupt policy.
|
// does between instructions; it does not change interrupt policy.
|
||||||
if (!(cpu->Status & STATUS_INTERRUPT)) {
|
if (!(cpu->Status & STATUS_INTERRUPT)) {
|
||||||
clearInterrupt((uint8_t)nextPendingInterrupt());
|
cpu->bus->clearInterrupt((uint8_t)cpu->bus->nextInterrupt());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// A device asking for attention is answered between instructions and never
|
// A device asking for attention is answered between instructions and never
|
||||||
@@ -878,9 +895,9 @@ void stepCPU(CPURegisters *cpu) {
|
|||||||
// A line that is up while the Interrupt Flag is clear stays up. Masking holds a
|
// A line that is up while the Interrupt Flag is clear stays up. Masking holds a
|
||||||
// device off; it does not lose what the device was asking for.
|
// device off; it does not lose what the device was asking for.
|
||||||
if (cpu->Status & STATUS_INTERRUPT) {
|
if (cpu->Status & STATUS_INTERRUPT) {
|
||||||
int port = nextPendingInterrupt();
|
int port = cpu->bus->nextInterrupt();
|
||||||
if (port >= 0) {
|
if (port >= 0) {
|
||||||
clearInterrupt((uint8_t)port);
|
cpu->bus->clearInterrupt((uint8_t)port);
|
||||||
if (enterInterrupt(cpu, HARDWARE_VECTOR_BASE, (uint8_t)port, cpu->ProgramCounter)) {
|
if (enterInterrupt(cpu, HARDWARE_VECTOR_BASE, (uint8_t)port, cpu->ProgramCounter)) {
|
||||||
// The device asked and nobody was listening. enterInterrupt has
|
// The device asked and nobody was listening. enterInterrupt has
|
||||||
// already stopped the machine; correct the cause, because the empty
|
// already stopped the machine; correct the cause, because the empty
|
||||||
|
|||||||
@@ -51,6 +51,29 @@ typedef enum {
|
|||||||
FAULT_DEVICE_REFUSED // A device refused, and nothing was installed to catch it.
|
FAULT_DEVICE_REFUSED // A device refused, and nothing was installed to catch it.
|
||||||
} FaultCause;
|
} FaultCause;
|
||||||
|
|
||||||
|
// ---- What a CPU is plugged into ----
|
||||||
|
//
|
||||||
|
// Five things a CPU asks of the world outside itself, and every one of them was a call to a
|
||||||
|
// function there was exactly one of. That is fine for a machine with one processor and wrong
|
||||||
|
// for a machine with two: A PERIPHERAL CORE'S BUS IS ITS OWN. It sees the devices its own
|
||||||
|
// device gives it, raises its own interrupt lines, and stalls on its own controller - none
|
||||||
|
// of which are the host's.
|
||||||
|
//
|
||||||
|
// Gathered here rather than threaded through as a bus number, because a bus is a thing a
|
||||||
|
// device provides, and a device that provides one should hand over the answers rather than
|
||||||
|
// be looked up by an index somebody has to keep right.
|
||||||
|
typedef struct {
|
||||||
|
uint8_t (*out)(uint8_t value, uint8_t port);
|
||||||
|
uint8_t (*in)(uint8_t port);
|
||||||
|
// What the memory controller on this bus has just spent moving memory, and what the
|
||||||
|
// machine spent stopped waiting on a device. Both are taken and cleared.
|
||||||
|
unsigned long (*takeStall)(void);
|
||||||
|
unsigned long (*takeIdle)(void);
|
||||||
|
// The lowest port with its line up, or below zero for none, and putting one down.
|
||||||
|
int (*nextInterrupt)(void);
|
||||||
|
void (*clearInterrupt)(uint8_t port);
|
||||||
|
} Bus;
|
||||||
|
|
||||||
// The struct containing the CPU registers.
|
// The struct containing the CPU registers.
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t A;
|
uint8_t A;
|
||||||
@@ -62,6 +85,9 @@ typedef struct {
|
|||||||
uint16_t StackPointer;
|
uint16_t StackPointer;
|
||||||
uint8_t *Program;
|
uint8_t *Program;
|
||||||
uint8_t *Data;
|
uint8_t *Data;
|
||||||
|
// Which bus this processor is on. initializeCPU puts the machine's own here, which is
|
||||||
|
// what every CPU had before there could be more than one.
|
||||||
|
const Bus *bus;
|
||||||
|
|
||||||
// ---- What the machine has cost so far ----
|
// ---- What the machine has cost so far ----
|
||||||
//
|
//
|
||||||
|
|||||||
+26
-218
@@ -4,87 +4,23 @@
|
|||||||
// Small 8-Bit Harvard Architecture CPU
|
// Small 8-Bit Harvard Architecture CPU
|
||||||
// Written by Anachronaut
|
// Written by Anachronaut
|
||||||
// 10/15/2024
|
// 10/15/2024
|
||||||
|
|
||||||
#include "rom.h"
|
|
||||||
#include "bootstrap.h"
|
|
||||||
#include "../Assembler/assembly.h"
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include "cpu.h"
|
|
||||||
#include "controller.h"
|
|
||||||
#include "io.h"
|
|
||||||
#include "utility.h"
|
|
||||||
#include <string.h>
|
|
||||||
#include <getopt.h>
|
|
||||||
#include <time.h>
|
|
||||||
|
|
||||||
// nanoseconds per second
|
|
||||||
#define NS_PER_SEC 1000000000LL
|
|
||||||
#define CYCLE_RATE 1000000
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
long long cycles_per_sec; // e.g. 1000000 for 1 MHz
|
|
||||||
long long accumulator_ns; // unspent nanoseconds
|
|
||||||
struct timespec prev;
|
|
||||||
} CycleTimer;
|
|
||||||
|
|
||||||
static inline long long timespec_diff_ns(struct timespec a, struct timespec b) {
|
|
||||||
return (a.tv_sec - b.tv_sec) * NS_PER_SEC + (a.tv_nsec - b.tv_nsec);
|
|
||||||
}
|
|
||||||
|
|
||||||
void cycle_timer_init(CycleTimer *t, long long cycles_per_sec) {
|
|
||||||
t->cycles_per_sec = cycles_per_sec;
|
|
||||||
t->accumulator_ns = 0;
|
|
||||||
clock_gettime(CLOCK_MONOTONIC, &t->prev);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call once per host frame. Returns how many SplitBit cycles to execute.
|
|
||||||
int cycle_timer_tick(CycleTimer *t) {
|
|
||||||
struct timespec now;
|
|
||||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
||||||
|
|
||||||
long long elapsed = timespec_diff_ns(now, t->prev);
|
|
||||||
t->prev = now;
|
|
||||||
|
|
||||||
// optional: clamp to avoid spiral-of-death on hitches
|
|
||||||
if (elapsed > NS_PER_SEC / 10) elapsed = NS_PER_SEC / 10;
|
|
||||||
|
|
||||||
t->accumulator_ns += elapsed;
|
|
||||||
|
|
||||||
long long period_ns = NS_PER_SEC / t->cycles_per_sec;
|
|
||||||
int cycles = (int)(t->accumulator_ns / period_ns);
|
|
||||||
t->accumulator_ns %= period_ns;
|
|
||||||
return cycles;
|
|
||||||
}
|
|
||||||
|
|
||||||
// How many cycles to run between glances at the wall clock. In fast mode there is
|
|
||||||
// no clock to keep pace with, so run a large batch before looking up.
|
|
||||||
#define FAST_BATCH 65536
|
|
||||||
|
|
||||||
unsigned long cycleCount = 0;
|
|
||||||
char *programFile = NULL;
|
|
||||||
|
|
||||||
// Memory Banks:
|
|
||||||
uint8_t Program[0x10000], Data[0x10000];
|
|
||||||
|
|
||||||
// How the run is reported. The idle half is mentioned only when there is one, so that
|
|
||||||
// every program written before WAIT existed prints exactly the line it always did.
|
|
||||||
//
|
//
|
||||||
// THE TWO ARE NOT THE SAME KIND OF TIME. A bus cycle is the machine using memory; an idle
|
// ---- The machine with a terminal attached ----
|
||||||
// cycle is the machine stopped in a WAIT while a device catches up. Added together they
|
//
|
||||||
// are elapsed time, which is what a cycle limit measures; told apart they say whether a
|
// This is a front end and nothing else. The machine itself is in machine.c, shared with
|
||||||
// program was working or waiting.
|
// Voyager, which is the same machine with a screen and a speaker instead of a terminal.
|
||||||
static void reportCycles(const CPURegisters *cpu, unsigned long cycleCount) {
|
//
|
||||||
if (cpu->idleCycles > 0) {
|
// Keeping this file small is the point rather than a side effect: anything that ends up
|
||||||
printf("Execution halted after %lu cycles, %lu of them waiting.\n",
|
// here is behaviour Voyager does not have, and the two are supposed to differ only in
|
||||||
cycleCount, cpu->idleCycles);
|
// what they present. This builds and runs anywhere, with no graphics library, which is
|
||||||
} else {
|
// what keeps the whole toolchain and the whole suite dependency free.
|
||||||
printf("Execution halted after %lu cycles.\n", cycleCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main (int argc, char *argv[]) {
|
#include "machine.h"
|
||||||
|
#include "utility.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <getopt.h>
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
EmulatorOptions options;
|
EmulatorOptions options;
|
||||||
uint8_t result = parseOptions(argc, argv, &options);
|
uint8_t result = parseOptions(argc, argv, &options);
|
||||||
if (result == OPTIONS_HELP) {
|
if (result == OPTIONS_HELP) {
|
||||||
@@ -94,6 +30,7 @@ int main (int argc, char *argv[]) {
|
|||||||
// Bad command line, don't execute.
|
// Bad command line, don't execute.
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
char *programFile = NULL;
|
||||||
if (optind < argc) {
|
if (optind < argc) {
|
||||||
programFile = argv[optind];
|
programFile = argv[optind];
|
||||||
optind++;
|
optind++;
|
||||||
@@ -102,150 +39,21 @@ int main (int argc, char *argv[]) {
|
|||||||
fprintf(stderr, "Error: Unexpected argument: %s\n", argv[optind]);
|
fprintf(stderr, "Error: Unexpected argument: %s\n", argv[optind]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
// ---- Where the machine's first instruction comes from ----
|
|
||||||
//
|
Machine machine;
|
||||||
// Named an image, it is placed into memory and started - which is what a debugger
|
uint8_t started = machineStart(&machine, &options, programFile);
|
||||||
// does, and is how every test here runs. That path is not a shortcut to apologise
|
if (started == MACHINE_NOTHING_TO_RUN) {
|
||||||
// for: placing memory from outside is a real thing real machines allow.
|
|
||||||
//
|
|
||||||
// Named none, the machine starts the way hardware would: the ROM is shadowed into
|
|
||||||
// Program Memory and it reads the disk for the rest. There has to be a disk for that
|
|
||||||
// to mean anything, and no image and no disk is a machine with nothing to run.
|
|
||||||
if (programFile == NULL && options.disk == NULL) {
|
|
||||||
fprintf(stderr, "Error: No boot image and no disk, so there is nothing to run.\n");
|
fprintf(stderr, "Error: No boot image and no disk, so there is nothing to run.\n");
|
||||||
printHelp(argv[0]);
|
printHelp(argv[0]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
} else if (started != MACHINE_OK) {
|
||||||
if (programFile != NULL) {
|
|
||||||
if (loadFile(programFile, Program, Data)) {
|
|
||||||
fprintf(stderr, "Error: Couldn't read file: %s\n", programFile);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
} else if (loadROM(bootROM, bootROMBytes, Program, Data)) {
|
|
||||||
fprintf(stderr, "Error: The boot ROM is not a boot image.\n");
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (options.disk != NULL && attachDisk(options.disk, options.writeProtect)) {
|
|
||||||
return 1;
|
while (machineRunning(&machine)) {
|
||||||
}
|
machineRunSlice(&machine);
|
||||||
CPURegisters cpu;
|
|
||||||
// The controller has to know where the memories are before anything can reach
|
|
||||||
// them through it. Banks 0 and 1 are those two arrays.
|
|
||||||
initializeController(Program, Data);
|
|
||||||
initializeCPU(&cpu, Program, Data);
|
|
||||||
if(options.debug) {
|
|
||||||
printRegisters(&cpu, Program, Data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CycleTimer timer;
|
machineStop(&machine);
|
||||||
setDiskLatency(options.diskCycles);
|
return machineReport(&machine);
|
||||||
cycle_timer_init(&timer, CYCLE_RATE);
|
|
||||||
|
|
||||||
uint8_t limitReached = 0;
|
|
||||||
while (!(cpu.Status & STATUS_HALT) && !limitReached) {
|
|
||||||
if (options.debug) {
|
|
||||||
// Wait before advancing, not after, so that a keypress is what moves the
|
|
||||||
// machine on rather than something that happens once it already has.
|
|
||||||
// Through the console rather than getchar, so that everything reading standard
|
|
||||||
// input reads it the same way and the console's pushback stays the only place
|
|
||||||
// a byte can be sitting.
|
|
||||||
consoleReadByte();
|
|
||||||
}
|
|
||||||
int cycles;
|
|
||||||
if (options.debug) {
|
|
||||||
// Debug mode advances one instruction per keypress, so the wall clock
|
|
||||||
// has no say in how many cycles to run.
|
|
||||||
cycles = 1;
|
|
||||||
} else if (options.fast) {
|
|
||||||
cycles = FAST_BATCH;
|
|
||||||
} else {
|
|
||||||
cycles = cycle_timer_tick(&timer);
|
|
||||||
}
|
|
||||||
// ---- Spending a budget of cycles, not running a count of instructions ----
|
|
||||||
//
|
|
||||||
// An instruction costs what it touches, so a batch is finished when the cycles are
|
|
||||||
// gone rather than after so many steps. In debug mode the budget is one, and any
|
|
||||||
// instruction costs at least the fetch of its own opcode, so one step still runs.
|
|
||||||
for (long spent = 0; spent < cycles; ) {
|
|
||||||
// Both kinds of cycle, because both are time passing. A step that waits
|
|
||||||
// spends no bus at all, and a budget measured only in bus cycles would never
|
|
||||||
// be spent - the machine would sit inside one batch forever and the device it
|
|
||||||
// was waiting for would never be given a moment to finish.
|
|
||||||
unsigned long before = cpu.busCycles + cpu.idleCycles;
|
|
||||||
stepCPU(&cpu);
|
|
||||||
unsigned long took = (cpu.busCycles + cpu.idleCycles) - before;
|
|
||||||
spent += (long)took;
|
|
||||||
cycleCount += took;
|
|
||||||
// Time has passed, so anything waiting on it may be finished.
|
|
||||||
deviceTick(cycleCount);
|
|
||||||
|
|
||||||
// ---- Starting over ----
|
|
||||||
//
|
|
||||||
// Between instructions, which is the only place it can happen: a device cannot
|
|
||||||
// restart the machine from inside the instruction that asked for it.
|
|
||||||
//
|
|
||||||
// WHAT A RESET REPEATS IS HOW THIS MACHINE STARTED. Named an image, it is
|
|
||||||
// placed again; named none, the ROM is shadowed again and reads the disk for
|
|
||||||
// the rest. Anything else would mean a reset changed what the machine is,
|
|
||||||
// which is the one thing a reset must not do.
|
|
||||||
//
|
|
||||||
// The disk is not unplugged and its image keeps everything written to it. That
|
|
||||||
// is what warm means: the machine starts again, the world it starts into does
|
|
||||||
// not.
|
|
||||||
if (takeResetRequest()) {
|
|
||||||
// The vector table goes, and that is a deliberate departure from leaving
|
|
||||||
// memory alone. A vector points into whatever installed it, and after this
|
|
||||||
// that program is not running - so a handler left behind would aim an
|
|
||||||
// interrupt at an address belonging to something gone. It is the argument
|
|
||||||
// CosmOS already makes when it takes a program's vectors back at exit.
|
|
||||||
memset(Program + SOFTWARE_VECTOR_BASE, 0,
|
|
||||||
(size_t)(0x10000 - SOFTWARE_VECTOR_BASE));
|
|
||||||
uint8_t failed = (programFile != NULL)
|
|
||||||
? loadFile(programFile, Program, Data)
|
|
||||||
: loadROM(bootROM, bootROMBytes, Program, Data);
|
|
||||||
if (failed) {
|
|
||||||
fprintf(stderr, "Error: The machine could not be started again.\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
initializeCPU(&cpu, Program, Data);
|
|
||||||
break; // Out of this batch; the loop above carries on with a new CPU.
|
|
||||||
}
|
|
||||||
if (cpu.Status & STATUS_HALT) {
|
|
||||||
// We've halted.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (options.cycles && cycleCount >= options.cycles) {
|
|
||||||
limitReached = 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (options.debug) {
|
|
||||||
printRegisters(&cpu, Program, Data);
|
|
||||||
printf("Cycle: %lu\n", cycleCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
detachDisk();
|
|
||||||
if (limitReached) {
|
|
||||||
printf("Execution stopped after %lu cycles. (cycle limit reached)\n", cycleCount);
|
|
||||||
} else if (cpu.Status & STATUS_FAULT) {
|
|
||||||
// The Program Counter is still pointing at whatever the CPU could not get past.
|
|
||||||
reportCycles(&cpu, cycleCount);
|
|
||||||
if (cpu.Fault == FAULT_NO_HANDLER) {
|
|
||||||
fprintf(stderr, "Fault: Software vector %u, dispatched from Program Address 0x%04X, has no handler installed.\n",
|
|
||||||
cpu.FaultVector, cpu.ProgramCounter);
|
|
||||||
} else if (cpu.Fault == FAULT_DEVICE_REFUSED) {
|
|
||||||
fprintf(stderr, "Fault: The device on port %u refused the access at Program Address 0x%04X, and nothing is installed to deal with it.\n",
|
|
||||||
cpu.FaultVector, cpu.ProgramCounter);
|
|
||||||
} else if (cpu.Fault == FAULT_NO_DEVICE_HANDLER) {
|
|
||||||
fprintf(stderr, "Fault: The device on port %u interrupted at Program Address 0x%04X, and hardware vector %u has no handler installed.\n",
|
|
||||||
cpu.FaultVector, cpu.ProgramCounter, cpu.FaultVector);
|
|
||||||
} else {
|
|
||||||
fprintf(stderr, "Fault: 0x%02X at Program Address 0x%04X is not an instruction.\n",
|
|
||||||
Program[cpu.ProgramCounter], cpu.ProgramCounter);
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
reportCycles(&cpu, cycleCount);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
// font.c
|
||||||
|
// GENERATED ONCE from the Hatchet-GPU sprite sheet, and vendored here on purpose.
|
||||||
|
//
|
||||||
|
// Hatchet was an earlier attempt at a graphics system for this machine and is not part of
|
||||||
|
// this repository. What survives of it is this font: an 8x8 sheet in ASCII order, which is
|
||||||
|
// the thing that makes it worth keeping. PETSCII's whole inconvenience was that its order
|
||||||
|
// was not ASCII's, so a machine using it needed a translation table in front of every
|
||||||
|
// string. Here the machine subtracts 32 and is done.
|
||||||
|
//
|
||||||
|
// One bit a pixel, most significant bit leftmost, eight bytes a glyph. The screen wants
|
||||||
|
// eight bits a pixel, so the machine expands this into tile memory at reset rather than
|
||||||
|
// storing it expanded: 1,088 bytes here against 16 kilobytes there.
|
||||||
|
//
|
||||||
|
// Index 0 is ASCII 32, the space, and is blank. Everything through ASCII 126 is where
|
||||||
|
// ASCII says it is; after that come the drawn extras - box corners and junctions, arrows,
|
||||||
|
// the card suits - which have no ASCII to be in order with.
|
||||||
|
|
||||||
|
#include "font.h"
|
||||||
|
|
||||||
|
const unsigned char consoleFont[CONSOLE_FONT_GLYPHS * CONSOLE_FONT_BYTES] = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 32 space
|
||||||
|
0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x30, // 33 '!'
|
||||||
|
0x00, 0x36, 0x36, 0x12, 0x00, 0x00, 0x00, 0x00, // 34 '"'
|
||||||
|
0x00, 0x24, 0x7E, 0x24, 0x24, 0x7E, 0x24, 0x00, // 35 '#'
|
||||||
|
0x00, 0x18, 0x3E, 0x58, 0x3C, 0x1A, 0x7C, 0x18, // 36 '$'
|
||||||
|
0x00, 0x62, 0x66, 0x0C, 0x18, 0x30, 0x66, 0x06, // 37 '%'
|
||||||
|
0x00, 0x38, 0x44, 0x48, 0x30, 0x4A, 0x44, 0x3A, // 38 '&'
|
||||||
|
0x00, 0x18, 0x18, 0x08, 0x00, 0x00, 0x00, 0x00, // 39 '''
|
||||||
|
0x00, 0x1E, 0x38, 0x70, 0x70, 0x70, 0x38, 0x1E, // 40 '('
|
||||||
|
0x00, 0x78, 0x1C, 0x0E, 0x0E, 0x0E, 0x1C, 0x78, // 41 ')'
|
||||||
|
0x00, 0x00, 0x5A, 0x3C, 0x7E, 0x3C, 0x5A, 0x00, // 42 '*'
|
||||||
|
0x00, 0x00, 0x18, 0x18, 0x7E, 0x7E, 0x18, 0x18, // 43 '+'
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x10, // 44 ','
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x7F, 0x7F, 0x00, 0x00, // 45 '-'
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, // 46 '.'
|
||||||
|
0x00, 0x03, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0x60, // 47 '/'
|
||||||
|
0x00, 0x3C, 0x66, 0x4E, 0x5A, 0x72, 0x66, 0x3C, // 48 '0'
|
||||||
|
0x00, 0x18, 0x38, 0x18, 0x18, 0x18, 0x18, 0x7E, // 49 '1'
|
||||||
|
0x00, 0x3C, 0x66, 0x46, 0x0C, 0x18, 0x30, 0x7E, // 50 '2'
|
||||||
|
0x00, 0x3C, 0x66, 0x06, 0x0C, 0x06, 0x66, 0x3C, // 51 '3'
|
||||||
|
0x00, 0x3C, 0x6C, 0x6C, 0x7E, 0x7E, 0x0C, 0x0C, // 52 '4'
|
||||||
|
0x00, 0x7E, 0x60, 0x7C, 0x0E, 0x66, 0x6E, 0x3C, // 53 '5'
|
||||||
|
0x00, 0x3C, 0x66, 0x60, 0x7C, 0x66, 0x66, 0x3C, // 54 '6'
|
||||||
|
0x00, 0x7E, 0x66, 0x66, 0x0E, 0x1C, 0x38, 0x30, // 55 '7'
|
||||||
|
0x00, 0x3C, 0x66, 0x66, 0x3C, 0x66, 0x66, 0x3C, // 56 '8'
|
||||||
|
0x00, 0x3E, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x06, // 57 '9'
|
||||||
|
0x00, 0x00, 0x30, 0x30, 0x00, 0x30, 0x30, 0x00, // 58 ':'
|
||||||
|
0x00, 0x00, 0x30, 0x30, 0x00, 0x30, 0x30, 0x10, // 59 ';'
|
||||||
|
0x00, 0x0E, 0x1C, 0x38, 0x70, 0x38, 0x1C, 0x0E, // 60 '<'
|
||||||
|
0x00, 0x00, 0x3E, 0x3E, 0x00, 0x3E, 0x3E, 0x00, // 61 '='
|
||||||
|
0x00, 0x70, 0x38, 0x1C, 0x0E, 0x1C, 0x38, 0x70, // 62 '>'
|
||||||
|
0x00, 0x3C, 0x66, 0x06, 0x1C, 0x18, 0x00, 0x18, // 63 '?'
|
||||||
|
0x00, 0x3C, 0x66, 0x4E, 0x4E, 0x40, 0x60, 0x3C, // 64 '@'
|
||||||
|
0x00, 0x3E, 0x36, 0x63, 0x63, 0x7F, 0x63, 0x63, // 65 'A'
|
||||||
|
0x00, 0x7C, 0x66, 0x66, 0x7C, 0x66, 0x66, 0x7C, // 66 'B'
|
||||||
|
0x00, 0x3E, 0x73, 0x60, 0x60, 0x60, 0x73, 0x3E, // 67 'C'
|
||||||
|
0x00, 0x7C, 0x66, 0x63, 0x63, 0x63, 0x66, 0x7C, // 68 'D'
|
||||||
|
0x00, 0x7E, 0x7E, 0x60, 0x78, 0x60, 0x7E, 0x7E, // 69 'E'
|
||||||
|
0x00, 0x7E, 0x7E, 0x60, 0x7C, 0x7C, 0x60, 0x60, // 70 'F'
|
||||||
|
0x00, 0x3E, 0x73, 0x60, 0x67, 0x63, 0x73, 0x3E, // 71 'G'
|
||||||
|
0x00, 0x63, 0x63, 0x63, 0x7F, 0x63, 0x63, 0x63, // 72 'H'
|
||||||
|
0x00, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, // 73 'I'
|
||||||
|
0x00, 0x7F, 0x6C, 0x0C, 0x0C, 0x6C, 0x6C, 0x38, // 74 'J'
|
||||||
|
0x00, 0x67, 0x6E, 0x7C, 0x78, 0x7E, 0x66, 0x67, // 75 'K'
|
||||||
|
0x00, 0x60, 0x60, 0x60, 0x60, 0x60, 0x7E, 0x7E, // 76 'L'
|
||||||
|
0x00, 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, // 77 'M'
|
||||||
|
0x00, 0x63, 0x73, 0x7B, 0x7B, 0x6F, 0x67, 0x67, // 78 'N'
|
||||||
|
0x00, 0x3C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, // 79 'O'
|
||||||
|
0x00, 0x7E, 0x67, 0x63, 0x67, 0x7E, 0x60, 0x60, // 80 'P'
|
||||||
|
0x00, 0x3E, 0x77, 0x63, 0x63, 0x77, 0x3E, 0x07, // 81 'Q'
|
||||||
|
0x00, 0x7E, 0x67, 0x63, 0x66, 0x7C, 0x6E, 0x67, // 82 'R'
|
||||||
|
0x00, 0x3E, 0x77, 0x70, 0x3E, 0x07, 0x77, 0x3E, // 83 'S'
|
||||||
|
0x00, 0x7F, 0x6C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, // 84 'T'
|
||||||
|
0x00, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x3E, // 85 'U'
|
||||||
|
0x00, 0x63, 0x63, 0x63, 0x77, 0x3E, 0x1C, 0x08, // 86 'V'
|
||||||
|
0x00, 0x63, 0x63, 0x63, 0x6B, 0x6B, 0x7F, 0x36, // 87 'W'
|
||||||
|
0x00, 0x66, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0x66, // 88 'X'
|
||||||
|
0x00, 0x63, 0x77, 0x3E, 0x1C, 0x38, 0x70, 0x60, // 89 'Y'
|
||||||
|
0x00, 0x7F, 0x7F, 0x0E, 0x1C, 0x38, 0x7F, 0x7F, // 90 'Z'
|
||||||
|
0x00, 0x3E, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3E, // 91 '['
|
||||||
|
0x00, 0x60, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, // 92 '\'
|
||||||
|
0x00, 0x7C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x7C, // 93 ']'
|
||||||
|
0x00, 0x08, 0x1C, 0x3E, 0x77, 0x63, 0x00, 0x00, // 94 '^'
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x7F, // 95 '_'
|
||||||
|
0x00, 0x30, 0x38, 0x18, 0x00, 0x00, 0x00, 0x00, // 96 '`'
|
||||||
|
0x00, 0x00, 0x38, 0x06, 0x3E, 0x66, 0x66, 0x3B, // 97 'a'
|
||||||
|
0x00, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x7C, // 98 'b'
|
||||||
|
0x00, 0x00, 0x3C, 0x66, 0x60, 0x60, 0x66, 0x3C, // 99 'c'
|
||||||
|
0x00, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x66, 0x3E, // 100 'd'
|
||||||
|
0x00, 0x00, 0x3C, 0x66, 0x7C, 0x60, 0x62, 0x3C, // 101 'e'
|
||||||
|
0x00, 0x1E, 0x30, 0x60, 0x7C, 0x60, 0x60, 0x60, // 102 'f'
|
||||||
|
0x00, 0x00, 0x3C, 0x66, 0x66, 0x3E, 0x06, 0x7C, // 103 'g'
|
||||||
|
0x00, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x66, // 104 'h'
|
||||||
|
0x00, 0x18, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, // 105 'i'
|
||||||
|
0x00, 0x06, 0x00, 0x06, 0x06, 0x66, 0x66, 0x3C, // 106 'j'
|
||||||
|
0x00, 0x60, 0x66, 0x6C, 0x78, 0x7C, 0x64, 0x66, // 107 'k'
|
||||||
|
0x00, 0x38, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, // 108 'l'
|
||||||
|
0x00, 0x00, 0xC2, 0x66, 0x7E, 0x7E, 0x66, 0x66, // 109 'm'
|
||||||
|
0x00, 0x00, 0xEC, 0x76, 0x66, 0x66, 0x66, 0x66, // 110 'n'
|
||||||
|
0x00, 0x00, 0x3C, 0x66, 0x66, 0x66, 0x66, 0x3C, // 111 'o'
|
||||||
|
0x00, 0x00, 0x7C, 0x66, 0x66, 0x7C, 0x60, 0x60, // 112 'p'
|
||||||
|
0x00, 0x00, 0x3C, 0x66, 0x3C, 0x04, 0x19, 0x3E, // 113 'q'
|
||||||
|
0x00, 0x00, 0x60, 0x7C, 0x6C, 0x60, 0x60, 0x60, // 114 'r'
|
||||||
|
0x00, 0x00, 0x3C, 0x66, 0x30, 0x0C, 0x66, 0x3C, // 115 's'
|
||||||
|
0x00, 0x18, 0x18, 0x7E, 0x58, 0x18, 0x18, 0x18, // 116 't'
|
||||||
|
0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x7E, 0x3D, // 117 'u'
|
||||||
|
0x00, 0x00, 0x42, 0x66, 0x66, 0x76, 0x3C, 0x18, // 118 'v'
|
||||||
|
0x00, 0x00, 0x42, 0x66, 0x66, 0x7E, 0x7E, 0x24, // 119 'w'
|
||||||
|
0x00, 0x00, 0x66, 0x76, 0x38, 0x1C, 0x6E, 0x66, // 120 'x'
|
||||||
|
0x00, 0x00, 0x66, 0x66, 0x76, 0x3E, 0x06, 0x7C, // 121 'y'
|
||||||
|
0x00, 0x00, 0x7E, 0x66, 0x0C, 0x18, 0x32, 0x7E, // 122 'z'
|
||||||
|
0x00, 0x0E, 0x38, 0x30, 0x18, 0x30, 0x38, 0x0E, // 123 '{'
|
||||||
|
0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, // 124 '|'
|
||||||
|
0x00, 0x70, 0x1C, 0x0C, 0x18, 0x0C, 0x1C, 0x78, // 125 '}'
|
||||||
|
0x00, 0x00, 0x03, 0x3B, 0x6E, 0x60, 0x00, 0x00, // 126 '~'
|
||||||
|
0x00, 0x40, 0x60, 0x70, 0x78, 0x7C, 0x10, 0x00, // 127
|
||||||
|
0x00, 0x00, 0x00, 0x0F, 0x1F, 0x1C, 0x18, 0x18, // 128
|
||||||
|
0x00, 0x00, 0x00, 0xF0, 0xF8, 0x38, 0x18, 0x18, // 129
|
||||||
|
0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, // 130
|
||||||
|
0x00, 0x00, 0x00, 0xFF, 0xFF, 0x3C, 0x18, 0x18, // 131
|
||||||
|
0x18, 0x18, 0x38, 0xF8, 0xF8, 0x38, 0x18, 0x18, // 132
|
||||||
|
0xFF, 0xFF, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, // 133
|
||||||
|
0x03, 0x03, 0x07, 0xFF, 0xFF, 0x07, 0x03, 0x03, // 134
|
||||||
|
0x01, 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, // 135
|
||||||
|
0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, // 136
|
||||||
|
0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, // 137
|
||||||
|
0xFF, 0xFF, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, // 138
|
||||||
|
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 139
|
||||||
|
0x18, 0x18, 0x1C, 0x1F, 0x0F, 0x00, 0x00, 0x00, // 140
|
||||||
|
0x18, 0x18, 0x38, 0xF8, 0xF0, 0x00, 0x00, 0x00, // 141
|
||||||
|
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, // 142
|
||||||
|
0x18, 0x18, 0x1C, 0x1F, 0x1F, 0x1C, 0x18, 0x18, // 143
|
||||||
|
0x18, 0x18, 0x3C, 0xFF, 0xFF, 0x00, 0x00, 0x00, // 144
|
||||||
|
0xC0, 0xC0, 0xE0, 0xFF, 0xFF, 0xE0, 0xC0, 0xC0, // 145
|
||||||
|
0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0xFF, 0xFF, // 146
|
||||||
|
0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, // 147
|
||||||
|
0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0x80, // 148
|
||||||
|
0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF, // 149
|
||||||
|
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xFF, 0xFF, // 150
|
||||||
|
0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, // 151
|
||||||
|
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, // 152
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, // 153
|
||||||
|
0x00, 0x36, 0x7F, 0x7F, 0x7F, 0x3E, 0x1C, 0x08, // 154
|
||||||
|
0x00, 0x08, 0x1C, 0x3E, 0x7F, 0x3E, 0x08, 0x3E, // 155
|
||||||
|
0x00, 0x08, 0x1C, 0x3E, 0x7F, 0x3E, 0x1C, 0x08, // 156
|
||||||
|
0x00, 0x1C, 0x1C, 0x7F, 0x7F, 0x7F, 0x08, 0x3E, // 157
|
||||||
|
0x00, 0x18, 0x24, 0x24, 0x42, 0x42, 0x7E, 0x00, // 158
|
||||||
|
0x00, 0x3C, 0x66, 0x42, 0x42, 0x66, 0x3C, 0x00, // 159
|
||||||
|
0x00, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x00, // 160
|
||||||
|
0x00, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x00, // 161
|
||||||
|
0x00, 0x02, 0x04, 0x44, 0x28, 0x28, 0x10, 0x00, // 162
|
||||||
|
0x08, 0x0C, 0x0E, 0xFF, 0xFF, 0x0E, 0x0C, 0x08, // 163
|
||||||
|
0x18, 0x18, 0x18, 0x18, 0xFF, 0x7E, 0x3C, 0x18, // 164
|
||||||
|
0x10, 0x30, 0x70, 0xFF, 0xFF, 0x70, 0x30, 0x10, // 165
|
||||||
|
0x18, 0x3C, 0x7E, 0xFF, 0x18, 0x18, 0x18, 0x18, // 166
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// font.h
|
||||||
|
// The console's character generator.
|
||||||
|
// Written by Anachronaut
|
||||||
|
|
||||||
|
#ifndef FONT_H
|
||||||
|
#define FONT_H
|
||||||
|
|
||||||
|
// Eight bytes a glyph, one bit a pixel. See font.c for where it came from.
|
||||||
|
#define CONSOLE_FONT_BYTES 8
|
||||||
|
#define CONSOLE_FONT_GLYPHS 135
|
||||||
|
|
||||||
|
// The first character the font has, so a byte maps to a glyph by subtracting this.
|
||||||
|
#define CONSOLE_FONT_FIRST 32
|
||||||
|
|
||||||
|
extern const unsigned char consoleFont[CONSOLE_FONT_GLYPHS * CONSOLE_FONT_BYTES];
|
||||||
|
|
||||||
|
#endif // FONT_H
|
||||||
+943
-74
File diff suppressed because it is too large
Load Diff
+248
-2
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include "cpu.h"
|
#include "cpu.h"
|
||||||
|
#include "controller.h"
|
||||||
|
|
||||||
// ---- Ports ----
|
// ---- Ports ----
|
||||||
//
|
//
|
||||||
@@ -18,11 +19,30 @@
|
|||||||
// does not change: writing sends a byte, reading takes one and waits for it. The other two
|
// does not change: writing sends a byte, reading takes one and waits for it. The other two
|
||||||
// are additions, so a program written before they existed cannot notice them.
|
// are additions, so a program written before they existed cannot notice them.
|
||||||
#define PORT_CONSOLE 0x00
|
#define PORT_CONSOLE 0x00
|
||||||
#define PORT_CONSOLE_TOP 0x02
|
#define PORT_CONSOLE_TOP 0x06
|
||||||
#define CONSOLE_DATA 0x00
|
#define CONSOLE_DATA 0x00
|
||||||
#define CONSOLE_STATUS 0x01
|
#define CONSOLE_STATUS 0x01
|
||||||
#define CONSOLE_CONTROL 0x02
|
#define CONSOLE_CONTROL 0x02
|
||||||
|
|
||||||
|
// ---- Where the cursor is, as registers ----
|
||||||
|
//
|
||||||
|
// Read as well as written, which is the thing an escape sequence cannot do without a query
|
||||||
|
// and a parse. A program that wants to put something back where it found it asks.
|
||||||
|
#define CONSOLE_CURSOR_ROW 0x03
|
||||||
|
#define CONSOLE_CURSOR_COLUMN 0x04
|
||||||
|
|
||||||
|
// Written, and it happens at once - the same shape as the memory controller's Command port
|
||||||
|
// rather than a bit in a register that otherwise holds state.
|
||||||
|
#define CONSOLE_COMMAND 0x05
|
||||||
|
#define CONSOLE_COMMAND_CLEAR 0x01
|
||||||
|
|
||||||
|
// ---- What colour to write in ----
|
||||||
|
//
|
||||||
|
// The attribute given to every cell the console draws from now on. Its low nibble picks one
|
||||||
|
// of sixteen ink and paper pairs, and the default palette is arranged so that XOR 8 turns
|
||||||
|
// any of them inside out - which is highlighting, and is also how the cursor is drawn.
|
||||||
|
#define CONSOLE_ATTRIBUTE 0x06
|
||||||
|
|
||||||
#define PORT_TEST 0x10
|
#define PORT_TEST 0x10
|
||||||
#define PORT_REFUSE 0x11
|
#define PORT_REFUSE 0x11
|
||||||
#define PORT_MEMORY 0x12
|
#define PORT_MEMORY 0x12
|
||||||
@@ -46,11 +66,81 @@
|
|||||||
// that spans more than one port raises its line on its base, which is the rule the
|
// that spans more than one port raises its line on its base, which is the rule the
|
||||||
// machine has not needed until now: the controller spans sixteen and never interrupts.
|
// machine has not needed until now: the controller spans sixteen and never interrupts.
|
||||||
#define PORT_DISK 0x20
|
#define PORT_DISK 0x20
|
||||||
#define PORT_DISK_TOP 0x23
|
#define PORT_DISK_TOP 0x28
|
||||||
#define DISK_BLOCK_HIGH 0x20
|
#define DISK_BLOCK_HIGH 0x20
|
||||||
#define DISK_BLOCK_LOW 0x21
|
#define DISK_BLOCK_LOW 0x21
|
||||||
#define DISK_COMMAND 0x22
|
#define DISK_COMMAND 0x22
|
||||||
#define DISK_STATUS 0x23
|
#define DISK_STATUS 0x23
|
||||||
|
|
||||||
|
// ---- Several disks, one controller ----
|
||||||
|
//
|
||||||
|
// NOT SEVERAL DEVICES, and the instruction set is why. A port is an immediate byte inside the
|
||||||
|
// OUT that names it, so a program cannot compute one - "the disk on port 0x20 + drive * 4" is
|
||||||
|
// not something this machine can say. Two disks as two devices would mean a branch per access
|
||||||
|
// in every one of the eleven places the filesystem names a disk port.
|
||||||
|
//
|
||||||
|
// So it is one controller with a drive register, which is what the machines this one is
|
||||||
|
// pretending to be actually had: one floppy controller and four drives behind it. The block,
|
||||||
|
// command and status registers all refer to whichever drive was last selected, and so does
|
||||||
|
// the single buffer - which is honest, and which means a program that changes drives knows
|
||||||
|
// the buffer no longer holds what it thought.
|
||||||
|
#define DISK_DRIVE 0x24
|
||||||
|
#define DISK_DRIVES 0x25
|
||||||
|
|
||||||
|
// ---- What a drive IS, which is the machine's business ----
|
||||||
|
//
|
||||||
|
// Not what is on it, which is the system's. A drive backed by memory loses everything when
|
||||||
|
// the machine stops, and that is the one fact about it a system cannot work out for itself -
|
||||||
|
// an empty disk and a volatile disk look identical from the outside.
|
||||||
|
//
|
||||||
|
// It matters because it is the difference between a drive a system may FORMAT on sight and
|
||||||
|
// one it must not. An unformatted floppy somebody put in deliberately is not an invitation;
|
||||||
|
// an unformatted RAM disk is, because there was never anything there to lose. The machine
|
||||||
|
// says which kind it is and says nothing about filesystems, so a system that would rather
|
||||||
|
// have its own reads the same bit and does as it likes.
|
||||||
|
#define DISK_FLAGS 0x26
|
||||||
|
#define DISK_FLAG_VOLATILE 0x01
|
||||||
|
|
||||||
|
// How many blocks the selected drive has. A fact about the drive rather than about what is
|
||||||
|
// on it - and the one a system needs before it can put anything on it, since the size a
|
||||||
|
// superblock states is no use on a disk that has not got one yet.
|
||||||
|
#define DISK_SIZE_HIGH 0x27
|
||||||
|
#define DISK_SIZE_LOW 0x28
|
||||||
|
|
||||||
|
// Four is a floppy controller's worth. The cost of another is a file handle.
|
||||||
|
#define DISK_DRIVE_COUNT 4
|
||||||
|
// ---- The screen ----
|
||||||
|
//
|
||||||
|
// Sixteen ports, like the controller, and it interrupts on its base the way the disk
|
||||||
|
// established for a device that spans more than one. The registers themselves are in
|
||||||
|
// video.h, with the memory layout they describe.
|
||||||
|
#define PORT_VIDEO 0x30
|
||||||
|
#define PORT_VIDEO_TOP 0x3F
|
||||||
|
|
||||||
|
// ---- The timer ----
|
||||||
|
//
|
||||||
|
// The only regular beat this machine had was the screen finishing a frame, which is fixed at
|
||||||
|
// sixty a second. That is a clock a program borrows rather than one it sets: every duration
|
||||||
|
// becomes a multiple of 16.67 ms, so a note worth a third of a beat cannot be asked for and
|
||||||
|
// the way round it is to choose a tempo whose subdivisions happen to land on whole frames -
|
||||||
|
// which is making the music fit the hardware.
|
||||||
|
#define PORT_TIMER 0x50
|
||||||
|
#define PORT_TIMER_TOP 0x54
|
||||||
|
|
||||||
|
#define TIMER_STATUS 0x50
|
||||||
|
#define TIMER_CONTROL 0x51
|
||||||
|
#define TIMER_PERIOD_HIGH 0x52
|
||||||
|
#define TIMER_PERIOD_MID 0x53
|
||||||
|
#define TIMER_PERIOD_LOW 0x54
|
||||||
|
|
||||||
|
#define TIMER_STATUS_TICKED 0x01
|
||||||
|
#define TIMER_STATUS_RUNNING 0x02
|
||||||
|
#define TIMER_STATUS_INTERRUPT 0x04
|
||||||
|
|
||||||
|
#define TIMER_CONTROL_RUN 0x01
|
||||||
|
#define TIMER_CONTROL_REPEAT 0x02
|
||||||
|
#define TIMER_CONTROL_INTERRUPT 0x04
|
||||||
|
|
||||||
#define PORT_REGISTRY 0xFF
|
#define PORT_REGISTRY 0xFF
|
||||||
|
|
||||||
// ---- The console ----
|
// ---- The console ----
|
||||||
@@ -93,6 +183,10 @@
|
|||||||
// That is not especially useful, but a control bit that quietly did nothing depending on
|
// That is not especially useful, but a control bit that quietly did nothing depending on
|
||||||
// another control bit would be worse than a burst of interrupts somebody asked for.
|
// another control bit would be worse than a burst of interrupts somebody asked for.
|
||||||
#define CONSOLE_CONTROL_INTERRUPT 0x02
|
#define CONSOLE_CONTROL_INTERRUPT 0x02
|
||||||
|
// Show a cursor where the next character will go. Off when the machine starts, because a
|
||||||
|
// machine draws what it is told to and a program painting its own screen does not want one
|
||||||
|
// blinking in the middle of it. A system that reads lines from a person turns it on.
|
||||||
|
#define CONSOLE_CONTROL_CURSOR 0x04
|
||||||
|
|
||||||
// Set when there is a byte to be had. NOT set at the end of input, although a read would
|
// Set when there is a byte to be had. NOT set at the end of input, although a read would
|
||||||
// answer at once there: what it answers is 0xFF standing in for nothing, and calling that
|
// answer at once there: what it answers is 0xFF standing in for nothing, and calling that
|
||||||
@@ -109,6 +203,57 @@
|
|||||||
// Whether the console is set to interrupt, for the same reason: everything a program can
|
// Whether the console is set to interrupt, for the same reason: everything a program can
|
||||||
// ask the console to be, it can also ask the console what it currently is.
|
// ask the console to be, it can also ask the console what it currently is.
|
||||||
#define CONSOLE_STATUS_INTERRUPT 0x08
|
#define CONSOLE_STATUS_INTERRUPT 0x08
|
||||||
|
// And whether a cursor is being shown, for the same reason as the rest: everything a program
|
||||||
|
// can ask the console to be, it can also ask the console what it currently is.
|
||||||
|
#define CONSOLE_STATUS_CURSOR 0x10
|
||||||
|
|
||||||
|
// ---- Keys that are not characters ----
|
||||||
|
//
|
||||||
|
// An arrow key is not a letter and there is no byte for it, which is why it has never
|
||||||
|
// reached this machine at all: a window threw it away for want of anywhere to put it, and
|
||||||
|
// a terminal sent an escape sequence that arrived in a command line and made it
|
||||||
|
// unrecognisable.
|
||||||
|
//
|
||||||
|
// So the console names them. These are the values it delivers, one byte each, and they are
|
||||||
|
// the console's own: NOT ASCII, and deliberately above it, so nothing that existed before
|
||||||
|
// them can collide. A program reads one the same way it reads a letter.
|
||||||
|
//
|
||||||
|
// THE CONSOLE NORMALISES, WHICH IS WHAT IT ALREADY DOES. Behind a window it turns the key
|
||||||
|
// somebody pressed into a byte; on a terminal it turns the sequence the terminal sent into
|
||||||
|
// the same byte. That is the same act it has always performed on Return and Backspace, one
|
||||||
|
// layer further along, and it is why a program does not have to know which it is talking to.
|
||||||
|
//
|
||||||
|
// WHAT IT DOES NOT DO is decide what they mean. Where the cursor goes, what a line looks
|
||||||
|
// like afterwards and what was typed before are the system's business - see the shell,
|
||||||
|
// which edits its own line - exactly as what is on a disk is the system's business and what
|
||||||
|
// a drive IS belongs to the machine.
|
||||||
|
#define CONSOLE_KEY_UP 0x80
|
||||||
|
#define CONSOLE_KEY_DOWN 0x81
|
||||||
|
#define CONSOLE_KEY_LEFT 0x82
|
||||||
|
#define CONSOLE_KEY_RIGHT 0x83
|
||||||
|
#define CONSOLE_KEY_HOME 0x84
|
||||||
|
#define CONSOLE_KEY_END 0x85
|
||||||
|
// Forward delete, which is the character UNDER the cursor and not the one before it.
|
||||||
|
// Backspace is 0x08 and always has been; these two are different keys that do different
|
||||||
|
// things, and a terminal has always sent different bytes for them.
|
||||||
|
#define CONSOLE_KEY_DELETE 0x86
|
||||||
|
|
||||||
|
// The range, so that anything wanting to know whether a byte is one of these can ask
|
||||||
|
// without naming them all. Room is left above DELETE on purpose: function keys and the
|
||||||
|
// paging keys are the obvious next ones, and adding one should disturb nothing.
|
||||||
|
#define CONSOLE_KEY_FIRST 0x80
|
||||||
|
#define CONSOLE_KEY_LAST 0x8F
|
||||||
|
|
||||||
|
// ---- Only in key mode ----
|
||||||
|
//
|
||||||
|
// LINE MODE DELIVERS CHARACTERS, and these are not characters. A program in line mode is
|
||||||
|
// being handed a line that something else has already finished editing, so a key that means
|
||||||
|
// "move the cursor left" arrived too late to mean anything and putting it in the line would
|
||||||
|
// only corrupt it - which is precisely what an untranslated escape sequence used to do.
|
||||||
|
//
|
||||||
|
// So the console drops them in line mode, wherever it is reading from. That is also what a
|
||||||
|
// real terminal does: canonical mode gives a program backspace and line kill, and has never
|
||||||
|
// given it arrow keys.
|
||||||
|
|
||||||
// Puts the terminal back the way it was found. Registered with atexit and called from a
|
// Puts the terminal back the way it was found. Registered with atexit and called from a
|
||||||
// handler for every signal that can end this process and be caught, because a machine that
|
// handler for every signal that can end this process and be caught, because a machine that
|
||||||
@@ -124,6 +269,28 @@ void consoleRestore(void);
|
|||||||
// of its own behind that would make the status port lie about what is waiting.
|
// of its own behind that would make the status port lie about what is waiting.
|
||||||
uint8_t consoleReadByte(void);
|
uint8_t consoleReadByte(void);
|
||||||
|
|
||||||
|
// Puts the cursor back in the corner. Called when the machine starts, since the screen is
|
||||||
|
// cleared then too and a cursor left where the last program stopped would be a cursor
|
||||||
|
// pointing into something that is gone.
|
||||||
|
void consoleHome(void);
|
||||||
|
|
||||||
|
// ---- How a front end with a window feeds the console ----
|
||||||
|
//
|
||||||
|
// Called while the console has nothing to give. It returns a byte, or one of the two
|
||||||
|
// answers below. They have to be told apart: a window with nobody typing yet is the normal
|
||||||
|
// case and happens sixty times a second, while a window that has gone is the end of input.
|
||||||
|
// One value for both would have made the first keystroke look like a closed machine.
|
||||||
|
//
|
||||||
|
// Without a hook the console reads standard input, which is what it has always done.
|
||||||
|
#define CONSOLE_NOTHING_YET (-1)
|
||||||
|
#define CONSOLE_GONE (-2)
|
||||||
|
//
|
||||||
|
// mayWait says which question is being asked. Zero is the status port looking, and must not
|
||||||
|
// present or sleep: a program polling in a loop would otherwise run at the frame rate. One
|
||||||
|
// is the data port blocking, where presenting is exactly right, because a machine waiting
|
||||||
|
// for a key is still a machine somebody is looking at.
|
||||||
|
void consoleSetInputHook(int (*hook)(int mayWait));
|
||||||
|
|
||||||
// ---- Device classes ----
|
// ---- Device classes ----
|
||||||
//
|
//
|
||||||
// What kind of thing is plugged into a port. Class 0 is not a device: reading an
|
// What kind of thing is plugged into a port. Class 0 is not a device: reading an
|
||||||
@@ -144,6 +311,9 @@ uint8_t consoleReadByte(void);
|
|||||||
#define DEVICE_REFUSE 0x11
|
#define DEVICE_REFUSE 0x11
|
||||||
#define DEVICE_MEMORY 0x12
|
#define DEVICE_MEMORY 0x12
|
||||||
#define DEVICE_DISK 0x13
|
#define DEVICE_DISK 0x13
|
||||||
|
#define DEVICE_VIDEO 0x14
|
||||||
|
#define DEVICE_SOUND 0x15
|
||||||
|
#define DEVICE_TIMER 0x16
|
||||||
|
|
||||||
// What a device brings besides itself. This means memory that somebody has to register
|
// What a device brings besides itself. This means memory that somebody has to register
|
||||||
// with the controller, so the controller's own bank 2 does not count: it is already there.
|
// with the controller, so the controller's own bank 2 does not count: it is already there.
|
||||||
@@ -193,6 +363,18 @@ uint8_t consoleReadByte(void);
|
|||||||
// display that refreshes, a port that waits on the host - wants exactly this shape.
|
// display that refreshes, a port that waits on the host - wants exactly this shape.
|
||||||
void deviceTick(unsigned long now);
|
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
|
// 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
|
// next instruction is, which is what this machine has always done and what every recorded
|
||||||
// test assumes.
|
// test assumes.
|
||||||
@@ -203,6 +385,10 @@ void setDiskLatency(unsigned long cycles);
|
|||||||
// equivalent of the tab on the side of a floppy. Returns 1 if it could not attach.
|
// equivalent of the tab on the side of a floppy. Returns 1 if it could not attach.
|
||||||
uint8_t attachDisk(const char *path, uint8_t writeProtect);
|
uint8_t attachDisk(const char *path, uint8_t writeProtect);
|
||||||
|
|
||||||
|
// A drive of that many blocks, backed by memory rather than by a file. It comes up as
|
||||||
|
// zeroes, which is not a filesystem - bringing it up is the system's job.
|
||||||
|
uint8_t attachRamDisk(uint32_t blocks);
|
||||||
|
|
||||||
void detachDisk(void);
|
void detachDisk(void);
|
||||||
|
|
||||||
// How many bytes a device's entry in the registry runs to. Reading past the end gives
|
// How many bytes a device's entry in the registry runs to. Reading past the end gives
|
||||||
@@ -237,13 +423,73 @@ void serviceDevices(void);
|
|||||||
// the instruction that asked - the CPU is mid-step and its state is not yet consistent.
|
// the instruction that asked - the CPU is mid-step and its state is not yet consistent.
|
||||||
int takeResetRequest(void);
|
int takeResetRequest(void);
|
||||||
|
|
||||||
|
// Whether one is waiting, without taking it. For anything that has to behave differently
|
||||||
|
// while a restart is on its way but is not the thing that performs it.
|
||||||
|
int resetIsPending(void);
|
||||||
|
|
||||||
|
// ---- The button on the front of the case ----
|
||||||
|
//
|
||||||
|
// A machine has one, and a window is the case. Writing MACHINE_RESET is how a PROGRAM asks;
|
||||||
|
// this is how a person does, without needing a program that is willing to listen - which is
|
||||||
|
// the whole point of a reset button and the reason the port exists at all.
|
||||||
|
void requestReset(void);
|
||||||
|
|
||||||
|
// Puts the console's input back to how a machine starts: nothing pushed back, no line half
|
||||||
|
// gathered, and NOT at the end of input. Called when the machine starts over, because a
|
||||||
|
// console that had run out of input would still have run out afterwards - and a reset that
|
||||||
|
// left the keyboard dead would be a reset nobody could use twice.
|
||||||
|
void consoleResetInput(void);
|
||||||
|
|
||||||
|
// ---- The lines on one bus ----
|
||||||
|
//
|
||||||
|
// One bit a port, so a device can ask for attention without anything having to poll it.
|
||||||
|
// Eight ports to the byte, low bit first.
|
||||||
|
//
|
||||||
|
// A MACHINE WITH TWO PROCESSORS HAS TWO SETS OF THESE. A peripheral core's devices raise
|
||||||
|
// lines on the core's bus and the host's devices on the host's, and they are not the same
|
||||||
|
// lines: one array indexed by port for the whole machine would have let each see the other's,
|
||||||
|
// so a disk finishing would interrupt a sound core and a sound core's tick would interrupt
|
||||||
|
// the shell. Both would arrive at a handler for something else entirely.
|
||||||
|
#define INTERRUPT_LINE_BYTES 32
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t bits[INTERRUPT_LINE_BYTES];
|
||||||
|
} InterruptLines;
|
||||||
|
|
||||||
|
void linesRaise(InterruptLines *lines, uint8_t port);
|
||||||
|
void linesClear(InterruptLines *lines, uint8_t port);
|
||||||
|
|
||||||
|
// The lowest numbered port with its line up, or below zero if none has. A scan rather than
|
||||||
|
// a priority encoder, so there is no arbitration to explain and a programmer can work out
|
||||||
|
// what happens next by reading the port numbers.
|
||||||
|
int linesNext(const InterruptLines *lines);
|
||||||
|
|
||||||
|
// The machine's own controller, which is what every device on this bus means when it moves
|
||||||
|
// memory. A peripheral core's device holds its own.
|
||||||
|
Controller *machineController(void);
|
||||||
|
|
||||||
|
// The machine's own lines, which is what every device on this bus means.
|
||||||
void raiseInterrupt(uint8_t port);
|
void raiseInterrupt(uint8_t port);
|
||||||
|
|
||||||
void clearInterrupt(uint8_t port);
|
void clearInterrupt(uint8_t port);
|
||||||
|
|
||||||
|
// Every line down at once, for a reset. The vector table is cleared when the machine starts
|
||||||
|
// over because a handler left behind would aim an interrupt into a program that is no longer
|
||||||
|
// running; a LINE left behind does exactly the same thing, and arrives at a program that
|
||||||
|
// never asked the device for anything.
|
||||||
|
void clearAllInterrupts(void);
|
||||||
|
|
||||||
|
// The timer, which counts the machine's own cycles.
|
||||||
|
void timerReset(void);
|
||||||
|
void timerTick(unsigned long now);
|
||||||
|
|
||||||
// The lowest numbered port with its line up, or -1 if none of them are.
|
// The lowest numbered port with its line up, or -1 if none of them are.
|
||||||
int nextPendingInterrupt(void);
|
int nextPendingInterrupt(void);
|
||||||
|
|
||||||
|
// The bus this machine's own processor is on: its devices, its lines, its controller. A
|
||||||
|
// peripheral core is given a different one by whatever device contains it.
|
||||||
|
const Bus *machineBus(void);
|
||||||
|
|
||||||
// ---- Refusing ----
|
// ---- Refusing ----
|
||||||
//
|
//
|
||||||
// A device can refuse what it was asked to do. Interrupting is a device asking for
|
// A device can refuse what it was asked to do. Interrupting is a device asking for
|
||||||
|
|||||||
@@ -0,0 +1,359 @@
|
|||||||
|
// machine.c
|
||||||
|
// The SplitBit machine: everything both front ends share.
|
||||||
|
// Written by Anachronaut
|
||||||
|
|
||||||
|
#include "machine.h"
|
||||||
|
#include "rom.h"
|
||||||
|
#include "bootstrap.h"
|
||||||
|
#include "cpu.h"
|
||||||
|
#include "controller.h"
|
||||||
|
#include "io.h"
|
||||||
|
#include "video.h"
|
||||||
|
#include "sound.h"
|
||||||
|
#include "utility.h"
|
||||||
|
#include "../Assembler/assembly.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
// nanoseconds per second
|
||||||
|
#define NS_PER_SEC 1000000000LL
|
||||||
|
|
||||||
|
static inline long long timespec_diff_ns(struct timespec a, struct timespec b) {
|
||||||
|
return (a.tv_sec - b.tv_sec) * NS_PER_SEC + (a.tv_nsec - b.tv_nsec);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cycle_timer_init(CycleTimer *t, long long cycles_per_sec) {
|
||||||
|
t->cycles_per_sec = cycles_per_sec;
|
||||||
|
t->accumulator_ns = 0;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &t->prev);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call once per host frame. Returns how many SplitBit cycles to execute.
|
||||||
|
int cycle_timer_tick(CycleTimer *t) {
|
||||||
|
struct timespec now;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||||
|
|
||||||
|
long long elapsed = timespec_diff_ns(now, t->prev);
|
||||||
|
t->prev = now;
|
||||||
|
|
||||||
|
// optional: clamp to avoid spiral-of-death on hitches
|
||||||
|
if (elapsed > NS_PER_SEC / 10) elapsed = NS_PER_SEC / 10;
|
||||||
|
|
||||||
|
t->accumulator_ns += elapsed;
|
||||||
|
|
||||||
|
long long period_ns = NS_PER_SEC / t->cycles_per_sec;
|
||||||
|
int cycles = (int)(t->accumulator_ns / period_ns);
|
||||||
|
t->accumulator_ns %= period_ns;
|
||||||
|
return cycles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// How many cycles to run between glances at the wall clock. In fast mode there is
|
||||||
|
// no clock to keep pace with, so run a large batch before looking up.
|
||||||
|
#define FAST_BATCH 65536
|
||||||
|
|
||||||
|
// Memory Banks. Static, because a front end has no business reaching into them: what it
|
||||||
|
// needs to know about the machine it asks the machine.
|
||||||
|
static uint8_t Program[0x10000], Data[0x10000];
|
||||||
|
|
||||||
|
// How the run is reported. The idle half is mentioned only when there is one, so that
|
||||||
|
// every program written before WAIT existed prints exactly the line it always did.
|
||||||
|
//
|
||||||
|
// THE TWO ARE NOT THE SAME KIND OF TIME. A bus cycle is the machine using memory; an idle
|
||||||
|
// cycle is the machine stopped in a WAIT while a device catches up. Added together they
|
||||||
|
// are elapsed time, which is what a cycle limit measures; told apart they say whether a
|
||||||
|
// program was working or waiting.
|
||||||
|
static void reportCycles(const CPURegisters *cpu, unsigned long cycleCount) {
|
||||||
|
if (cpu->idleCycles > 0) {
|
||||||
|
printf("Execution halted after %lu cycles, %lu of them waiting.\n",
|
||||||
|
cycleCount, cpu->idleCycles);
|
||||||
|
} else {
|
||||||
|
printf("Execution halted after %lu cycles.\n", cycleCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- A keyboard made of a file ----
|
||||||
|
//
|
||||||
|
// THE CONSOLE BEHIND A WINDOW IS NOT THE CONSOLE BEHIND A TERMINAL, and until this existed
|
||||||
|
// the difference was untestable. A terminal does the line editing; a window has none, so the
|
||||||
|
// console does it itself - gathering a line, rubbing out a backspace, handing it over only
|
||||||
|
// when Return arrives. That is real logic, it broke twice in two days, and both times it was
|
||||||
|
// found by a person typing rather than by anything here.
|
||||||
|
//
|
||||||
|
// So a file can be a keyboard. It installs the same hook a window does, which means the same
|
||||||
|
// path runs, and the suite can check what happens when a backspace arrives with nobody to
|
||||||
|
// interpret it. It does not test the window - Voyager's own key queue is still beyond reach
|
||||||
|
// - but it tests the console, which is where the logic is.
|
||||||
|
static FILE *keyboardFile = NULL;
|
||||||
|
|
||||||
|
static int keyboardHook(int mayWait) {
|
||||||
|
(void)mayWait; // There is no window to keep alive, so both questions are the same.
|
||||||
|
if (keyboardFile == NULL) {
|
||||||
|
return CONSOLE_GONE;
|
||||||
|
}
|
||||||
|
const int byte = fgetc(keyboardFile);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Starting over ----
|
||||||
|
//
|
||||||
|
// WHAT A RESET REPEATS IS HOW THIS MACHINE STARTED. Named an image, it is placed again; named
|
||||||
|
// none, the ROM is shadowed again and reads the disk for the rest. Anything else would mean a
|
||||||
|
// reset changed what the machine is, which is the one thing a reset must not do.
|
||||||
|
//
|
||||||
|
// The disk is not unplugged and its image keeps everything written to it. That is what warm
|
||||||
|
// means: the machine starts again, the world it starts into does not.
|
||||||
|
//
|
||||||
|
// The vector table goes, and that is a deliberate departure from leaving memory alone. A
|
||||||
|
// vector points into whatever installed it, and after this that program is not running - so a
|
||||||
|
// handler left behind would aim an interrupt at an address belonging to something gone. It is
|
||||||
|
// the argument CosmOS already makes when it takes a program's vectors back at exit.
|
||||||
|
static int machineRestart(Machine *m) {
|
||||||
|
memset(Program + SOFTWARE_VECTOR_BASE, 0,
|
||||||
|
(size_t)(0x10000 - SOFTWARE_VECTOR_BASE));
|
||||||
|
uint8_t failed = (m->programFile != NULL)
|
||||||
|
? loadFile(m->programFile, Program, Data)
|
||||||
|
: loadROM(bootROM, bootROMBytes, Program, Data);
|
||||||
|
if (failed) {
|
||||||
|
fprintf(stderr, "Error: The machine could not be started again.\n");
|
||||||
|
m->restartFailed = 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
videoReset();
|
||||||
|
soundReset();
|
||||||
|
timerReset();
|
||||||
|
consoleHome();
|
||||||
|
consoleResetInput();
|
||||||
|
// ---- And every line down ----
|
||||||
|
//
|
||||||
|
// The same reasoning that clears the vector table. A handler left behind would aim an
|
||||||
|
// interrupt into a program that is no longer running; a line left behind arrives at one
|
||||||
|
// that never asked the device for anything. The devices reset above take their own down,
|
||||||
|
// and this is the rest of them - the disk in particular, which is not unplugged by a
|
||||||
|
// reset and keeps whatever it was doing.
|
||||||
|
clearAllInterrupts();
|
||||||
|
initializeCPU(&m->cpu, Program, Data);
|
||||||
|
// A machine that had stopped is running again, which is the entire point of asking from
|
||||||
|
// outside: the interesting time to restart something is when it is not going anywhere.
|
||||||
|
m->limitReached = 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int machineTakeReset(Machine *m) {
|
||||||
|
if (!takeResetRequest()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return machineRestart(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t machineStart(Machine *m, const EmulatorOptions *options, const char *programFile) {
|
||||||
|
m->options = *options;
|
||||||
|
m->programFile = programFile;
|
||||||
|
m->cycleCount = 0;
|
||||||
|
m->limitReached = 0;
|
||||||
|
m->restartFailed = 0;
|
||||||
|
|
||||||
|
// ---- Where the machine's first instruction comes from ----
|
||||||
|
//
|
||||||
|
// Named an image, it is placed into memory and started - which is what a debugger
|
||||||
|
// does, and is how every test here runs. That path is not a shortcut to apologise
|
||||||
|
// for: placing memory from outside is a real thing real machines allow.
|
||||||
|
//
|
||||||
|
// Named none, the machine starts the way hardware would: the ROM is shadowed into
|
||||||
|
// Program Memory and it reads the disk for the rest. There has to be a disk for that
|
||||||
|
// to mean anything, and no image and no disk is a machine with nothing to run.
|
||||||
|
if (programFile == NULL && options->disk == NULL) {
|
||||||
|
return MACHINE_NOTHING_TO_RUN;
|
||||||
|
}
|
||||||
|
if (programFile != NULL) {
|
||||||
|
if (loadFile(programFile, Program, Data)) {
|
||||||
|
fprintf(stderr, "Error: Couldn't read file: %s\n", programFile);
|
||||||
|
return MACHINE_ERROR;
|
||||||
|
}
|
||||||
|
} else if (loadROM(bootROM, bootROMBytes, Program, Data)) {
|
||||||
|
fprintf(stderr, "Error: The boot ROM is not a boot image.\n");
|
||||||
|
return MACHINE_ERROR;
|
||||||
|
}
|
||||||
|
// Every drive named, in the order it was named. Write protection is the machine's rather
|
||||||
|
// than a drive's for now: a tab on one floppy and not another is a thing to add when
|
||||||
|
// somebody wants it, and pretending otherwise here would be a promise the option cannot
|
||||||
|
// keep.
|
||||||
|
for (int at = 0; at < options->diskCount; at++) {
|
||||||
|
if (attachDisk(options->disks[at], options->writeProtect)) {
|
||||||
|
return MACHINE_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// After the images, so the drive numbers a command line asks for are the order it asks
|
||||||
|
// in. A disk made of memory is still a drive and still has to be brought up by whatever
|
||||||
|
// system is running; the machine only supplies the blocks.
|
||||||
|
if (options->ramDisk > 0 && attachRamDisk((uint32_t)options->ramDisk)) {
|
||||||
|
return MACHINE_ERROR;
|
||||||
|
}
|
||||||
|
// The screen starts blank, and starts blank again on a warm restart: video memory is
|
||||||
|
// the device's, and a reset that left last program's screen up would be a reset that
|
||||||
|
// did not happen.
|
||||||
|
videoReset();
|
||||||
|
soundReset();
|
||||||
|
timerReset();
|
||||||
|
if (options->sound != NULL) {
|
||||||
|
soundKeepSamples();
|
||||||
|
}
|
||||||
|
consoleHome();
|
||||||
|
// The controller has to know where the memories are before anything can reach
|
||||||
|
// them through it. Banks 0 and 1 are those two arrays.
|
||||||
|
initializeController(machineController(), Program, Data);
|
||||||
|
initializeCPU(&m->cpu, Program, Data);
|
||||||
|
if (m->options.debug) {
|
||||||
|
printRegisters(&m->cpu, Program, Data);
|
||||||
|
}
|
||||||
|
if (options->keyboard != NULL) {
|
||||||
|
keyboardFile = fopen(options->keyboard, "rb");
|
||||||
|
if (keyboardFile == NULL) {
|
||||||
|
fprintf(stderr, "Error: Couldn't read the keyboard file: %s\n", options->keyboard);
|
||||||
|
return MACHINE_ERROR;
|
||||||
|
}
|
||||||
|
consoleSetInputHook(keyboardHook);
|
||||||
|
}
|
||||||
|
setDiskLatency(m->options.diskCycles);
|
||||||
|
cycle_timer_init(&m->timer, CYCLE_RATE);
|
||||||
|
return MACHINE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int machineRunning(const Machine *m) {
|
||||||
|
return !(m->cpu.Status & STATUS_HALT) && !m->limitReached && !m->restartFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void machineRunSlice(Machine *m) {
|
||||||
|
if (m->options.debug) {
|
||||||
|
// Wait before advancing, not after, so that a keypress is what moves the
|
||||||
|
// machine on rather than something that happens once it already has.
|
||||||
|
// Through the console rather than getchar, so that everything reading standard
|
||||||
|
// input reads it the same way and the console's pushback stays the only place
|
||||||
|
// a byte can be sitting.
|
||||||
|
consoleReadByte();
|
||||||
|
}
|
||||||
|
int cycles;
|
||||||
|
if (m->options.debug) {
|
||||||
|
// Debug mode advances one instruction per keypress, so the wall clock
|
||||||
|
// has no say in how many cycles to run.
|
||||||
|
cycles = 1;
|
||||||
|
} else if (m->options.fast) {
|
||||||
|
cycles = FAST_BATCH;
|
||||||
|
} else {
|
||||||
|
cycles = cycle_timer_tick(&m->timer);
|
||||||
|
}
|
||||||
|
// ---- Spending a budget of cycles, not running a count of instructions ----
|
||||||
|
//
|
||||||
|
// An instruction costs what it touches, so a batch is finished when the cycles are
|
||||||
|
// gone rather than after so many steps. In debug mode the budget is one, and any
|
||||||
|
// instruction costs at least the fetch of its own opcode, so one step still runs.
|
||||||
|
for (long spent = 0; spent < cycles; ) {
|
||||||
|
// Both kinds of cycle, because both are time passing. A step that waits
|
||||||
|
// spends no bus at all, and a budget measured only in bus cycles would never
|
||||||
|
// be spent - the machine would sit inside one batch forever and the device it
|
||||||
|
// was waiting for would never be given a moment to finish.
|
||||||
|
unsigned long before = m->cpu.busCycles + m->cpu.idleCycles;
|
||||||
|
stepCPU(&m->cpu);
|
||||||
|
unsigned long took = (m->cpu.busCycles + m->cpu.idleCycles) - before;
|
||||||
|
spent += (long)took;
|
||||||
|
m->cycleCount += took;
|
||||||
|
// Time has passed, so anything waiting on it may be finished.
|
||||||
|
deviceTick(m->cycleCount);
|
||||||
|
|
||||||
|
// ---- Starting over ----
|
||||||
|
//
|
||||||
|
// Between instructions, which is the only place it can happen: a device cannot
|
||||||
|
// restart the machine from inside the instruction that asked for it.
|
||||||
|
//
|
||||||
|
// WHAT A RESET REPEATS IS HOW THIS MACHINE STARTED. Named an image, it is
|
||||||
|
// placed again; named none, the ROM is shadowed again and reads the disk for
|
||||||
|
// the rest. Anything else would mean a reset changed what the machine is,
|
||||||
|
// which is the one thing a reset must not do.
|
||||||
|
//
|
||||||
|
// The disk is not unplugged and its image keeps everything written to it. That
|
||||||
|
// is what warm means: the machine starts again, the world it starts into does
|
||||||
|
// not.
|
||||||
|
if (machineTakeReset(m)) {
|
||||||
|
break; // Out of this batch; the loop above carries on with a new CPU.
|
||||||
|
}
|
||||||
|
if (m->cpu.Status & STATUS_HALT) {
|
||||||
|
// We've halted.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (m->options.cycles && m->cycleCount >= m->options.cycles) {
|
||||||
|
m->limitReached = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m->options.debug) {
|
||||||
|
printRegisters(&m->cpu, Program, Data);
|
||||||
|
printf("Cycle: %lu\n", m->cycleCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void machineStop(Machine *m) {
|
||||||
|
// ---- Saving the screen ----
|
||||||
|
//
|
||||||
|
// Written when the machine stops, and it is what makes the screen testable at all: a
|
||||||
|
// suite has no display, so the only way to check what was drawn is to be handed it. A
|
||||||
|
// picture out of a headless run is also the quickest way for a person to see what a
|
||||||
|
// program actually put on the screen without sitting and watching it happen.
|
||||||
|
if (m->options.screen != NULL) {
|
||||||
|
videoWriteImage(m->options.screen);
|
||||||
|
}
|
||||||
|
// Every sample the machine made, for the same reason a picture is saved: there is no
|
||||||
|
// speaker on a machine running tests, and a sound nothing can hear is a sound nothing
|
||||||
|
// can check.
|
||||||
|
if (m->options.sound != NULL) {
|
||||||
|
soundWriteSamples(m->options.sound);
|
||||||
|
}
|
||||||
|
if (keyboardFile != NULL) {
|
||||||
|
consoleSetInputHook(NULL);
|
||||||
|
fclose(keyboardFile);
|
||||||
|
keyboardFile = NULL;
|
||||||
|
}
|
||||||
|
detachDisk();
|
||||||
|
}
|
||||||
|
|
||||||
|
int machineReport(const Machine *m) {
|
||||||
|
if (m->restartFailed) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (m->limitReached) {
|
||||||
|
printf("Execution stopped after %lu cycles. (cycle limit reached)\n", m->cycleCount);
|
||||||
|
} else if (m->cpu.Status & STATUS_FAULT) {
|
||||||
|
// The Program Counter is still pointing at whatever the CPU could not get past.
|
||||||
|
reportCycles(&m->cpu, m->cycleCount);
|
||||||
|
if (m->cpu.Fault == FAULT_NO_HANDLER) {
|
||||||
|
fprintf(stderr, "Fault: Software vector %u, dispatched from Program Address 0x%04X, has no handler installed.\n",
|
||||||
|
m->cpu.FaultVector, m->cpu.ProgramCounter);
|
||||||
|
} else if (m->cpu.Fault == FAULT_DEVICE_REFUSED) {
|
||||||
|
fprintf(stderr, "Fault: The device on port %u refused the access at Program Address 0x%04X, and nothing is installed to deal with it.\n",
|
||||||
|
m->cpu.FaultVector, m->cpu.ProgramCounter);
|
||||||
|
} else if (m->cpu.Fault == FAULT_NO_DEVICE_HANDLER) {
|
||||||
|
fprintf(stderr, "Fault: The device on port %u interrupted at Program Address 0x%04X, and hardware vector %u has no handler installed.\n",
|
||||||
|
m->cpu.FaultVector, m->cpu.ProgramCounter, m->cpu.FaultVector);
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "Fault: 0x%02X at Program Address 0x%04X is not an instruction.\n",
|
||||||
|
Program[m->cpu.ProgramCounter], m->cpu.ProgramCounter);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
reportCycles(&m->cpu, m->cycleCount);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// machine.h
|
||||||
|
// The SplitBit machine, with nothing attached to look at it.
|
||||||
|
// Written by Anachronaut
|
||||||
|
|
||||||
|
#ifndef MACHINE_H
|
||||||
|
#define MACHINE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include "cpu.h"
|
||||||
|
#include "utility.h"
|
||||||
|
|
||||||
|
// ---- Why the machine is not a main ----
|
||||||
|
//
|
||||||
|
// There are two front ends: SplitBit, which is a terminal, and Voyager, which is a window
|
||||||
|
// and a speaker. THEY ARE THE SAME MACHINE. Keeping it here means the difference between
|
||||||
|
// them is presentation and nothing else - neither can quietly acquire behaviour the other
|
||||||
|
// lacks, and the suite can hold them to it by running a program through both and comparing
|
||||||
|
// what came out.
|
||||||
|
//
|
||||||
|
// It also decides where the devices live. A video device that only existed in the windowed
|
||||||
|
// binary would be untestable, because the suite has no display; here it is part of the
|
||||||
|
// machine, advances on emulated cycles, and Raylib only presents what it already produced.
|
||||||
|
|
||||||
|
// How fast the machine runs when it is not being told to hurry.
|
||||||
|
#define CYCLE_RATE 1000000
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
long long cycles_per_sec;
|
||||||
|
long long accumulator_ns;
|
||||||
|
struct timespec prev;
|
||||||
|
} CycleTimer;
|
||||||
|
|
||||||
|
void cycle_timer_init(CycleTimer *t, long long cycles_per_sec);
|
||||||
|
|
||||||
|
// Call once per host frame. Returns how many SplitBit cycles to execute.
|
||||||
|
int cycle_timer_tick(CycleTimer *t);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
CPURegisters cpu;
|
||||||
|
CycleTimer timer;
|
||||||
|
unsigned long cycleCount;
|
||||||
|
uint8_t limitReached;
|
||||||
|
uint8_t restartFailed;
|
||||||
|
const char *programFile;
|
||||||
|
EmulatorOptions options;
|
||||||
|
} Machine;
|
||||||
|
|
||||||
|
#define MACHINE_OK 0
|
||||||
|
#define MACHINE_ERROR 1
|
||||||
|
// No image and no disk. Told apart from an ordinary failure so that the front end can
|
||||||
|
// print its own help, which is the one thing here that knows what it is called.
|
||||||
|
#define MACHINE_NOTHING_TO_RUN 2
|
||||||
|
|
||||||
|
uint8_t machineStart(Machine *m, const EmulatorOptions *options, const char *programFile);
|
||||||
|
|
||||||
|
// Whether there is any more running to do. False once the machine has halted, faulted, or
|
||||||
|
// spent the cycle limit it was given.
|
||||||
|
int machineRunning(const Machine *m);
|
||||||
|
|
||||||
|
// ---- One slice, not the whole run ----
|
||||||
|
//
|
||||||
|
// A front end with a window has to get a turn: run a slice, present a frame, run another.
|
||||||
|
// A terminal simply runs slices until the machine stops. Both loops are three lines, which
|
||||||
|
// is the point of cutting it here rather than anywhere else.
|
||||||
|
void machineRunSlice(Machine *m);
|
||||||
|
|
||||||
|
// ---- Asking from outside, when nothing inside is asking ----
|
||||||
|
//
|
||||||
|
// A reset is normally noticed between instructions, which works when there are instructions.
|
||||||
|
// A HALTED MACHINE RUNS NONE, so a program that has finished - or faulted, or is a bare metal
|
||||||
|
// demo that ended with HALT - could ask to be restarted for ever and nothing would come along
|
||||||
|
// to hear it. That is exactly the machine somebody wants to restart.
|
||||||
|
//
|
||||||
|
// So a front end calls this every time round its own loop, whether the machine is running or
|
||||||
|
// not. Returns 1 if it started over.
|
||||||
|
int machineTakeReset(Machine *m);
|
||||||
|
|
||||||
|
void machineStop(Machine *m);
|
||||||
|
|
||||||
|
// Says how the run went and returns what the process should exit with.
|
||||||
|
int machineReport(const Machine *m);
|
||||||
|
|
||||||
|
#endif // MACHINE_H
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
// sound.c
|
||||||
|
// The Voyager's sound device.
|
||||||
|
// Written by Anachronaut
|
||||||
|
|
||||||
|
#include "sound.h"
|
||||||
|
#include "synth.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
// A megahertz, matching the machine. Kept here rather than reaching for machine.h, which
|
||||||
|
// would drag the whole front end into a device.
|
||||||
|
#define SOUND_CYCLE_RATE 1000000
|
||||||
|
|
||||||
|
static Synth synth;
|
||||||
|
static uint8_t channel;
|
||||||
|
static uint8_t parameter;
|
||||||
|
|
||||||
|
// Where the machine's clock was when the device started, and how many samples have been made
|
||||||
|
// since. The next sample is due at start + count * rate / samples, worked out in whole
|
||||||
|
// numbers each time rather than by adding an approximation over and over - twenty and five
|
||||||
|
// sixths does not add up to anything exact, and a drift of one part in a thousand is four
|
||||||
|
// seconds an hour.
|
||||||
|
static unsigned long startedAt;
|
||||||
|
static unsigned long samplesMade;
|
||||||
|
|
||||||
|
// ---- What has been made and not yet played ----
|
||||||
|
//
|
||||||
|
// A ring, written by the machine and read by whatever is playing it. One writer and one
|
||||||
|
// reader, which is the only sharing that needs no lock at all.
|
||||||
|
//
|
||||||
|
// IT DROPS WHEN IT IS FULL, and full means nobody is listening: a headless run makes
|
||||||
|
// forty-eight thousand samples a second of emulated time and there is nothing to take them.
|
||||||
|
// Dropping is right there. What must not drop is the COUNT, because that is the clock.
|
||||||
|
#define SOUND_RING 16384
|
||||||
|
static int16_t ring[SOUND_RING];
|
||||||
|
static int ringHead, ringTail;
|
||||||
|
|
||||||
|
// And a copy of everything, for --sound. Only kept when a file was asked for, because a long
|
||||||
|
// run makes millions of samples and a machine that hoarded them by default would be a machine
|
||||||
|
// that ran out of memory for no reason anybody asked for.
|
||||||
|
static int16_t *keeping = NULL;
|
||||||
|
static size_t keptCount, keptRoom;
|
||||||
|
|
||||||
|
void soundReset(void) {
|
||||||
|
synthInit(&synth, (float)SOUND_SAMPLE_RATE);
|
||||||
|
|
||||||
|
// ---- The device's own power-on state ----
|
||||||
|
//
|
||||||
|
// synthInit leaves soundThing's defaults, which are a patch EDITOR's: one voice set up to
|
||||||
|
// be heard and seven silent behind it, waiting for the edited patch to be copied over
|
||||||
|
// them. That is right for a program with one instrument on screen and wrong for a device
|
||||||
|
// whose four channels are four independent things.
|
||||||
|
//
|
||||||
|
// Two consequences if it were left alone, both of which the tests caught. Channels 1 to 3
|
||||||
|
// would be silent whatever gain was written to them, because their oscillators are not
|
||||||
|
// switched on. And channel 0's first oscillator would arrive at full gain while every
|
||||||
|
// other one arrived at nothing - an asymmetry with no reason a programmer could work out.
|
||||||
|
//
|
||||||
|
// So: EVERY CHANNEL ARRIVES ABLE TO MAKE A SOUND. Oscillator 0 on, at full gain;
|
||||||
|
// oscillator 1 off, because two oscillators is a choice and one is the plain case. A
|
||||||
|
// program that writes a note number hears that note, which is the shortest useful thing
|
||||||
|
// this device can be asked to do.
|
||||||
|
for (int i = 0; i < SOUND_CHANNELS; i++) {
|
||||||
|
synth.voices[i].oscillators[0].active = 1;
|
||||||
|
synth.voices[i].oscillators[0].gain = OSC_MAX_GAIN;
|
||||||
|
// Off rather than on-and-silent, because the two oscillators are AVERAGED and not
|
||||||
|
// added: a second one that is switched on halves the first whatever its gain is.
|
||||||
|
// "Active" is structural, and there is no setting of it that costs nothing.
|
||||||
|
synth.voices[i].oscillators[1].active = 0;
|
||||||
|
synth.voices[i].oscillators[1].gain = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
channel = 0;
|
||||||
|
parameter = 0;
|
||||||
|
startedAt = 0;
|
||||||
|
samplesMade = 0;
|
||||||
|
ringHead = 0;
|
||||||
|
ringTail = 0;
|
||||||
|
keptCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void soundKeepSamples(void) {
|
||||||
|
keptRoom = 1 << 16;
|
||||||
|
keeping = malloc(keptRoom * sizeof(*keeping));
|
||||||
|
keptCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void pushSample(int16_t sample) {
|
||||||
|
const int next = (ringTail + 1) % SOUND_RING;
|
||||||
|
if (next != ringHead) {
|
||||||
|
ring[ringTail] = sample;
|
||||||
|
ringTail = next;
|
||||||
|
}
|
||||||
|
if (keeping != NULL) {
|
||||||
|
if (keptCount == keptRoom) {
|
||||||
|
size_t bigger = keptRoom * 2;
|
||||||
|
int16_t *grown = realloc(keeping, bigger * sizeof(*keeping));
|
||||||
|
if (grown == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
keeping = grown;
|
||||||
|
keptRoom = bigger;
|
||||||
|
}
|
||||||
|
keeping[keptCount++] = sample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void soundTick(unsigned long now) {
|
||||||
|
if (startedAt == 0 && samplesMade == 0) {
|
||||||
|
startedAt = now;
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
// When the next one is due, in whole numbers: no accumulated fraction to drift.
|
||||||
|
// Sample n is due n periods after the device started, so sample nought is due the
|
||||||
|
// moment it starts. Making the first one a period late would put every sample after
|
||||||
|
// it a period late too, which is a whole sample of lag for nothing.
|
||||||
|
const unsigned long due = startedAt
|
||||||
|
+ (unsigned long)(samplesMade * (uint64_t)SOUND_CYCLE_RATE
|
||||||
|
/ SOUND_SAMPLE_RATE);
|
||||||
|
if (now < due) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int16_t sample;
|
||||||
|
synthFillBuffer(&synth, &sample, 1);
|
||||||
|
pushSample(sample);
|
||||||
|
samplesMade++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- A byte, and what it means ----
|
||||||
|
//
|
||||||
|
// Everything on this machine is a byte, and a synthesizer wants seconds, hertz and ratios. So
|
||||||
|
// each parameter says how its 0 to 255 becomes what the engine needs, and the shapes are
|
||||||
|
// chosen for where the USEFUL part of the range is rather than for arithmetic convenience.
|
||||||
|
//
|
||||||
|
// Times are squared, because the difference between five and fifty milliseconds is the whole
|
||||||
|
// character of a percussive sound and the difference between three and four seconds is
|
||||||
|
// nothing anybody can hear. Cutoff is exponential for the same reason: pitch is logarithmic
|
||||||
|
// and so is where a filter sounds like it is.
|
||||||
|
static float overRange(uint8_t value, float lowest, float highest) {
|
||||||
|
return lowest + (highest - lowest) * ((float)value / 255.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static float squared(uint8_t value, float highest) {
|
||||||
|
const float part = (float)value / 255.0f;
|
||||||
|
return part * part * highest;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float exponential(uint8_t value, float lowest, float highest) {
|
||||||
|
const float part = (float)value / 255.0f;
|
||||||
|
return lowest * powf(highest / lowest, part);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Centred on 128, so that half of nothing is no change and either side of it is a direction.
|
||||||
|
static float signedRange(uint8_t value, float reach) {
|
||||||
|
return ((float)value - 128.0f) / 128.0f * reach;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ModSource sourceFor(uint8_t value) {
|
||||||
|
return (value <= MOD_SOURCE_LFO2) ? (ModSource)value : MOD_SOURCE_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setOscillator(Oscillator *o, uint8_t which, uint8_t value) {
|
||||||
|
switch (which) {
|
||||||
|
case SP_OSC_WAVE: o->waveform = (Waveform)(value % WAVE_COUNT); break;
|
||||||
|
case SP_OSC_GAIN: o->gain = overRange(value, 0.0f, OSC_MAX_GAIN); break;
|
||||||
|
case SP_OSC_DUTY: o->dutyCycle = overRange(value, 0.05f, 0.95f); break;
|
||||||
|
// An octave either way, so a step of the byte is 1200/128, about nine cents. Fine
|
||||||
|
// enough for the shimmer of two oscillators just apart, which is what detune is
|
||||||
|
// mostly for, and wide enough to transpose one of them a whole octave.
|
||||||
|
case SP_OSC_DETUNE: o->detune = signedRange(value, 1200.0f); break;
|
||||||
|
case SP_OSC_OCTAVE: o->octave = (int)value - 128 < -2 ? -2
|
||||||
|
: ((int)value - 128 > 2 ? 2 : (int)value - 128); break;
|
||||||
|
case SP_OSC_ACTIVE: o->active = value != 0; break;
|
||||||
|
case SP_OSC_PWM_SRC: o->modRouting[0] = sourceFor(value); break;
|
||||||
|
case SP_OSC_PWM_DEPTH: o->modDepth[0] = signedRange(value, 0.5f); break;
|
||||||
|
case SP_OSC_DET_SRC: o->modRouting[1] = sourceFor(value); break;
|
||||||
|
case SP_OSC_DET_DEPTH: o->modDepth[1] = signedRange(value, 1200.0f); break;
|
||||||
|
case SP_OSC_GAIN_SRC: o->modRouting[2] = sourceFor(value); break;
|
||||||
|
case SP_OSC_GAIN_DEPTH: o->modDepth[2] = signedRange(value, OSC_MAX_GAIN); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setEnvelope(Envelope *e, uint8_t which, uint8_t value) {
|
||||||
|
switch (which) {
|
||||||
|
case SP_ENV_ATTACK: e->attackSec = squared(value, 4.0f); break;
|
||||||
|
case SP_ENV_DECAY: e->decaySec = squared(value, 4.0f); break;
|
||||||
|
case SP_ENV_SUSTAIN: e->sustainLevel = overRange(value, 0.0f, 1.0f); break;
|
||||||
|
case SP_ENV_RELEASE: e->releaseSec = squared(value, 4.0f); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setFilter(Filter *f, uint8_t which, uint8_t value) {
|
||||||
|
switch (which) {
|
||||||
|
case SP_FILTER_ACTIVE: f->active = value != 0; break;
|
||||||
|
case SP_FILTER_TYPE: f->type = (FilterType)(value % FILTER_COUNT); break;
|
||||||
|
case SP_FILTER_CUTOFF: f->cutoff = exponential(value, 20.0f, 20000.0f); break;
|
||||||
|
case SP_FILTER_RES: f->resonance = overRange(value, 0.0f, 0.99f); break;
|
||||||
|
case SP_FILTER_CUT_SRC: f->modRouting = sourceFor(value); break;
|
||||||
|
case SP_FILTER_CUT_DEP: f->modDepth = signedRange(value, 8000.0f); break;
|
||||||
|
case SP_FILTER_RES_SRC: f->resModRouting = sourceFor(value); break;
|
||||||
|
case SP_FILTER_RES_DEP: f->resModDepth = signedRange(value, 0.99f); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setLfo(LFO *l, uint8_t which, uint8_t value) {
|
||||||
|
switch (which) {
|
||||||
|
case SP_LFO_ACTIVE: l->active = value != 0; break;
|
||||||
|
case SP_LFO_WAVE: l->waveform = (Waveform)(value % WAVE_COUNT); break;
|
||||||
|
case SP_LFO_RATE: l->rate = exponential(value, 0.05f, 20.0f); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void soundParameter(uint8_t value) {
|
||||||
|
Voice *v = &synth.voices[channel];
|
||||||
|
const uint8_t group = parameter & 0xF0;
|
||||||
|
const uint8_t which = parameter & 0x0F;
|
||||||
|
switch (group) {
|
||||||
|
case SP_OSC0: setOscillator(&v->oscillators[0], which, value); break;
|
||||||
|
case SP_OSC1: setOscillator(&v->oscillators[1], which, value); break;
|
||||||
|
case SP_AMPENV: setEnvelope(&v->ampEnv, which, value); break;
|
||||||
|
case SP_MODENV: setEnvelope(&v->modEnv, which, value); break;
|
||||||
|
case SP_FILTER: setFilter(&v->filter, parameter, value); break;
|
||||||
|
case SP_LEVEL_SOURCE:
|
||||||
|
if (parameter == SP_LEVEL_SOURCE) {
|
||||||
|
v->levelSource = sourceFor(value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// The LFOs belong to the device rather than to a channel, so whichever channel is
|
||||||
|
// selected makes no difference to these.
|
||||||
|
case SP_LFO0: setLfo(&synth.lfos[0], which, value); break;
|
||||||
|
case SP_LFO1: setLfo(&synth.lfos[1], which, value); break;
|
||||||
|
default:
|
||||||
|
// A parameter number nothing answers to does nothing. A sound device is a poor
|
||||||
|
// place to stop the machine, the same as a screen.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t soundWrite(uint8_t value, uint8_t port) {
|
||||||
|
switch (port) {
|
||||||
|
case SOUND_CHANNEL: channel = value % SOUND_CHANNELS; break;
|
||||||
|
case SOUND_PARAMETER: parameter = value; break;
|
||||||
|
case SOUND_VALUE: soundParameter(value); break;
|
||||||
|
case SOUND_NOTE: synthChannelOn(&synth, channel, value); break;
|
||||||
|
case SOUND_GATE:
|
||||||
|
if (value) {
|
||||||
|
synthChannelOn(&synth, channel, synth.voices[channel].midiNote);
|
||||||
|
} else {
|
||||||
|
synthChannelOff(&synth, channel);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SOUND_VOLUME: synth.volume = overRange(value, 0.0f, 1.0f); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t soundRead(uint8_t port) {
|
||||||
|
switch (port) {
|
||||||
|
case SOUND_STATUS: {
|
||||||
|
uint8_t status = 0;
|
||||||
|
for (int i = 0; i < SOUND_CHANNELS; i++) {
|
||||||
|
if (synth.voices[i].active) {
|
||||||
|
status |= SOUND_STATUS_SOUNDING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
case SOUND_CHANNEL: return channel;
|
||||||
|
case SOUND_PARAMETER: return parameter;
|
||||||
|
case SOUND_NOTE: return (uint8_t)synth.voices[channel].midiNote;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int soundTake(int16_t *into, int wanted) {
|
||||||
|
int taken = 0;
|
||||||
|
while (taken < wanted && ringHead != ringTail) {
|
||||||
|
into[taken++] = ring[ringHead];
|
||||||
|
ringHead = (ringHead + 1) % SOUND_RING;
|
||||||
|
}
|
||||||
|
return taken;
|
||||||
|
}
|
||||||
|
|
||||||
|
int soundWriteSamples(const char *path) {
|
||||||
|
// Nothing was kept, which happens if the file was asked for after the machine ran. An
|
||||||
|
// empty file is the honest answer: the run made no sound anybody asked to hear.
|
||||||
|
if (keeping == NULL) {
|
||||||
|
keptCount = 0;
|
||||||
|
}
|
||||||
|
FILE *file = fopen(path, "wb");
|
||||||
|
if (file == NULL) {
|
||||||
|
fprintf(stderr, "Error: Couldn't write the sound to: %s\n", path);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const size_t written = keptCount == 0
|
||||||
|
? 0 : fwrite(keeping, sizeof(*keeping), keptCount, file);
|
||||||
|
fclose(file);
|
||||||
|
if (written != keptCount) {
|
||||||
|
fprintf(stderr, "Error: The sound was not written whole to: %s\n", path);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// sound.h
|
||||||
|
// The Voyager's sound device.
|
||||||
|
// Written by Anachronaut
|
||||||
|
|
||||||
|
#ifndef SOUND_H
|
||||||
|
#define SOUND_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// ---- What this is ----
|
||||||
|
//
|
||||||
|
// Four channels, each one a full soundThing voice: two oscillators, two envelopes and a
|
||||||
|
// filter. A channel is asked for by number and keeps its patch between notes, which is what
|
||||||
|
// makes it hardware rather than a keyboard - channel two is channel two.
|
||||||
|
//
|
||||||
|
// ---- Why it is not thirty ports ----
|
||||||
|
//
|
||||||
|
// A voice has some forty parameters and the machine has 256 ports, so giving each one a port
|
||||||
|
// of its own would spend a sixth of the whole address space on one device. Instead there is a
|
||||||
|
// SELECTOR AND A VALUE: say which channel, say which parameter, write it. Three writes to
|
||||||
|
// change one thing, which is the right price for something a program does when it loads a
|
||||||
|
// patch and not when it plays a note.
|
||||||
|
//
|
||||||
|
// What a program does per NOTE is cheap on purpose, because that happens in a music routine's
|
||||||
|
// inner loop: select the channel, write the note, write the gate. Three writes and no
|
||||||
|
// parameter machinery at all.
|
||||||
|
#define PORT_SOUND 0x40
|
||||||
|
#define PORT_SOUND_TOP 0x4F
|
||||||
|
|
||||||
|
#define SOUND_STATUS 0x40
|
||||||
|
#define SOUND_CHANNEL 0x41
|
||||||
|
#define SOUND_PARAMETER 0x42
|
||||||
|
#define SOUND_VALUE 0x43
|
||||||
|
#define SOUND_NOTE 0x44
|
||||||
|
#define SOUND_GATE 0x45
|
||||||
|
#define SOUND_VOLUME 0x46
|
||||||
|
|
||||||
|
// Set while any channel is still sounding, so a routine can wait for a note to finish
|
||||||
|
// rather than counting.
|
||||||
|
#define SOUND_STATUS_SOUNDING 0x01
|
||||||
|
|
||||||
|
#define SOUND_CHANNELS 4
|
||||||
|
|
||||||
|
// ---- The parameters ----
|
||||||
|
//
|
||||||
|
// Grouped so that the number says which part of a voice it belongs to: the high nibble picks
|
||||||
|
// the part and the low one picks the setting. Everything is a byte, because everything on
|
||||||
|
// this machine is - what each byte means is in the manual and in soundParameter below.
|
||||||
|
#define SP_OSC0 0x00 // 0x00-0x0F, and 0x10-0x1F for the second oscillator
|
||||||
|
#define SP_OSC1 0x10
|
||||||
|
#define SP_OSC_WAVE 0x00
|
||||||
|
#define SP_OSC_GAIN 0x01
|
||||||
|
#define SP_OSC_DUTY 0x02
|
||||||
|
#define SP_OSC_DETUNE 0x03
|
||||||
|
#define SP_OSC_OCTAVE 0x04
|
||||||
|
#define SP_OSC_ACTIVE 0x05
|
||||||
|
#define SP_OSC_PWM_SRC 0x06
|
||||||
|
#define SP_OSC_PWM_DEPTH 0x07
|
||||||
|
#define SP_OSC_DET_SRC 0x08
|
||||||
|
#define SP_OSC_DET_DEPTH 0x09
|
||||||
|
#define SP_OSC_GAIN_SRC 0x0A
|
||||||
|
#define SP_OSC_GAIN_DEPTH 0x0B
|
||||||
|
|
||||||
|
#define SP_AMPENV 0x20 // 0x20-0x2F amp, 0x30-0x3F mod
|
||||||
|
#define SP_MODENV 0x30
|
||||||
|
#define SP_ENV_ATTACK 0x00
|
||||||
|
#define SP_ENV_DECAY 0x01
|
||||||
|
#define SP_ENV_SUSTAIN 0x02
|
||||||
|
#define SP_ENV_RELEASE 0x03
|
||||||
|
|
||||||
|
#define SP_FILTER 0x40
|
||||||
|
#define SP_FILTER_ACTIVE 0x40
|
||||||
|
#define SP_FILTER_TYPE 0x41
|
||||||
|
#define SP_FILTER_CUTOFF 0x42
|
||||||
|
#define SP_FILTER_RES 0x43
|
||||||
|
#define SP_FILTER_CUT_SRC 0x44
|
||||||
|
#define SP_FILTER_CUT_DEP 0x45
|
||||||
|
#define SP_FILTER_RES_SRC 0x46
|
||||||
|
#define SP_FILTER_RES_DEP 0x47
|
||||||
|
|
||||||
|
// Which source shapes the channel's level: 0 none, 1 envelope 0, 2 envelope 1, 3 and 4 the
|
||||||
|
// LFOs. Nought is the one that could not be said before - see synth.h.
|
||||||
|
#define SP_LEVEL_SOURCE 0x50
|
||||||
|
|
||||||
|
// The LFOs belong to the whole device rather than to a channel, so these ignore whichever
|
||||||
|
// channel is selected.
|
||||||
|
#define SP_LFO0 0x60 // 0x60-0x6F and 0x70-0x7F
|
||||||
|
#define SP_LFO1 0x70
|
||||||
|
#define SP_LFO_ACTIVE 0x00
|
||||||
|
#define SP_LFO_WAVE 0x01
|
||||||
|
#define SP_LFO_RATE 0x02
|
||||||
|
|
||||||
|
// ---- Samples come from the machine's clock ----
|
||||||
|
//
|
||||||
|
// Forty-eight thousand a second against a million cycles: one sample every twenty and five
|
||||||
|
// sixths, worked out in whole numbers so it never drifts. THE HOST'S CLOCK IS NOT INVOLVED,
|
||||||
|
// which is what makes a recorded sound something a test can compare - the same program makes
|
||||||
|
// the same samples in the same cycles however fast anything really ran.
|
||||||
|
#define SOUND_SAMPLE_RATE 48000
|
||||||
|
|
||||||
|
void soundReset(void);
|
||||||
|
|
||||||
|
// Asks the device to keep every sample it makes, for soundWriteSamples. Off unless something
|
||||||
|
// wants a file, because a long run makes millions of them.
|
||||||
|
void soundKeepSamples(void);
|
||||||
|
|
||||||
|
// Called with the machine's clock, and generates whatever samples are due by now.
|
||||||
|
void soundTick(unsigned long now);
|
||||||
|
|
||||||
|
uint8_t soundWrite(uint8_t value, uint8_t port);
|
||||||
|
uint8_t soundRead(uint8_t port);
|
||||||
|
|
||||||
|
// Takes up to `wanted` samples for something that is going to play them, and says how many
|
||||||
|
// there were. A front end with a speaker calls this; nothing else has to.
|
||||||
|
int soundTake(int16_t *into, int wanted);
|
||||||
|
|
||||||
|
// Writes every sample generated so far to a file, as raw signed 16 bit. What --screen is for
|
||||||
|
// a picture: the only way to check a sound on a machine with no speaker.
|
||||||
|
int soundWriteSamples(const char *path);
|
||||||
|
|
||||||
|
#endif // SOUND_H
|
||||||
@@ -0,0 +1,590 @@
|
|||||||
|
// synth.c
|
||||||
|
// The Voyager's sound, vendored from soundThing.
|
||||||
|
//
|
||||||
|
// ---- Where this came from ----
|
||||||
|
//
|
||||||
|
// soundThing is a polyphonic subtractive synthesizer written by Anachronaut, and lives in its
|
||||||
|
// own repository. What is here is its VOICE ENGINE and nothing else: synth.c pulls in maths,
|
||||||
|
// stdlib, stdint and stdio and knows nothing about Raylib, MIDI, patches or the user
|
||||||
|
// interface, which is what made it liftable at all.
|
||||||
|
//
|
||||||
|
// It is copied rather than submoduled. Two files against tying this build to another
|
||||||
|
// repository's history is not a close call, and what a copy costs is that changes have to be
|
||||||
|
// carried across on purpose. So the list below is the whole of the difference, kept current.
|
||||||
|
//
|
||||||
|
// ---- What was changed ----
|
||||||
|
//
|
||||||
|
// 1. A VOICE'S LEVEL IS A ROUTING. Envelope 0 multiplied the output and there was no way to
|
||||||
|
// say otherwise, so routing it to a filter or an oscillator meant it shaped the volume as
|
||||||
|
// well whether that was wanted or not - which is most of the trouble with making
|
||||||
|
// percussion. Every other destination in this synth chooses its source; now this one does
|
||||||
|
// too, and MOD_SOURCE_NONE means the level is simply full.
|
||||||
|
//
|
||||||
|
// 2. NOISE COMES FROM A SEEDED GENERATOR. It drew from rand(), which is global state shared
|
||||||
|
// with the whole process and varies between libraries - so the same program would sound
|
||||||
|
// different on different machines and every recorded result would be worthless. It is a
|
||||||
|
// generator inside the Synth now, and a machine that starts the same way sounds the same
|
||||||
|
// way.
|
||||||
|
//
|
||||||
|
// 3. CHANNELS ARE NAMED, NOT ALLOCATED. synthNoteOn hunts for a free voice and steals
|
||||||
|
// round-robin, which is what a keyboard wants. A hardware channel is asked for by number.
|
||||||
|
// The old calls are still here and still do what they did.
|
||||||
|
//
|
||||||
|
// Written by Anachronaut
|
||||||
|
|
||||||
|
#include "synth.h"
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#ifndef M_PI
|
||||||
|
#define M_PI 3.14159265358979323846
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void synthInit(Synth *s, float sampleRate)
|
||||||
|
{
|
||||||
|
s->sampleRate = sampleRate;
|
||||||
|
s->pitchBend = 0.0f;
|
||||||
|
s->pitchBendRange = 2.0f;
|
||||||
|
s->lastStolenVoice = 0;
|
||||||
|
s->volume = 0.8f;
|
||||||
|
|
||||||
|
for (int i = 0; i < VOICE_COUNT; i++) {
|
||||||
|
s->voices[i].freqHz = 440.0f;
|
||||||
|
s->voices[i].active = 0;
|
||||||
|
s->voices[i].midiNote = -1;
|
||||||
|
|
||||||
|
oscillatorInit(&s->voices[i].oscillators[0], WAVE_TRIANGLE, 0.5f, 0.0f, OSC_MAX_GAIN);
|
||||||
|
oscillatorInit(&s->voices[i].oscillators[1], WAVE_TRIANGLE, 0.5f, 0.0f, 0.0f);
|
||||||
|
// A seed each, so that two noise oscillators sounding together are two noises rather
|
||||||
|
// than one heard twice. Any spread will do as long as none of them is zero.
|
||||||
|
s->voices[i].oscillators[0].noiseState = 0x9E3779B9u + (uint32_t)i * 2654435761u;
|
||||||
|
s->voices[i].oscillators[1].noiseState = 0x7F4A7C15u + (uint32_t)i * 2246822519u;
|
||||||
|
|
||||||
|
// Envelope 0 shapes the level, which is what it always did - the difference is that
|
||||||
|
// this now says so, and can be told not to.
|
||||||
|
s->voices[i].levelSource = MOD_SOURCE_AMP_ENV;
|
||||||
|
|
||||||
|
envelopeInit(&s->voices[i].ampEnv,
|
||||||
|
0.005f, // attack
|
||||||
|
0.10f, // decay
|
||||||
|
0.70f, // sustain
|
||||||
|
0.50f); // release
|
||||||
|
|
||||||
|
envelopeInit(&s->voices[i].modEnv,
|
||||||
|
0.005f, // attack
|
||||||
|
0.50f, // decay
|
||||||
|
0.0f, // sustain
|
||||||
|
0.10f); // release
|
||||||
|
|
||||||
|
s->voices[i].filter.cutoff = 8000.0f;
|
||||||
|
s->voices[i].filter.resonance = 0.0f;
|
||||||
|
s->voices[i].filter.type = FILTER_LOWPASS;
|
||||||
|
s->voices[i].filter.active = 0;
|
||||||
|
s->voices[i].filter.low = 0.0f;
|
||||||
|
s->voices[i].filter.band = 0.0f;
|
||||||
|
s->voices[i].filter.modRouting = MOD_SOURCE_NONE;
|
||||||
|
s->voices[i].filter.modDepth = 0.0f;
|
||||||
|
s->voices[i].filter.resModRouting = MOD_SOURCE_NONE;
|
||||||
|
s->voices[i].filter.resModDepth = 0.0f;
|
||||||
|
}
|
||||||
|
s->voices[0].oscillators[0].active = 1;
|
||||||
|
|
||||||
|
for (int l = 0; l < LFO_COUNT; l++) {
|
||||||
|
s->lfos[l].phase = 0.0f;
|
||||||
|
s->lfos[l].rate = 1.0f;
|
||||||
|
s->lfos[l].waveform = WAVE_SINE;
|
||||||
|
s->lfos[l].active = 0;
|
||||||
|
s->lfos[l].noiseHeld = 0.0f;
|
||||||
|
s->lfos[l].noisePhase = 0.0f;
|
||||||
|
s->lfos[l].noiseState = 0x2545F491u + (uint32_t)l * 3266489917u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void synthResetPatch(Synth *s)
|
||||||
|
{
|
||||||
|
Voice *v = &s->voices[0];
|
||||||
|
|
||||||
|
// The level is shaped by envelope 0 unless a patch says otherwise, which is what it
|
||||||
|
// always was - the difference is only that it can now be said otherwise.
|
||||||
|
v->levelSource = MOD_SOURCE_AMP_ENV;
|
||||||
|
|
||||||
|
oscillatorInit(&v->oscillators[0], WAVE_TRIANGLE, 0.5f, 0.0f, OSC_MAX_GAIN);
|
||||||
|
v->oscillators[0].active = 1;
|
||||||
|
for (int m = 0; m < 3; m++) {
|
||||||
|
v->oscillators[0].modRouting[m] = MOD_SOURCE_NONE;
|
||||||
|
v->oscillators[0].modDepth[m] = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
oscillatorInit(&v->oscillators[1], WAVE_TRIANGLE, 0.5f, 0.0f, 0.0f);
|
||||||
|
v->oscillators[1].active = 0;
|
||||||
|
for (int m = 0; m < 3; m++) {
|
||||||
|
v->oscillators[1].modRouting[m] = MOD_SOURCE_NONE;
|
||||||
|
v->oscillators[1].modDepth[m] = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
envelopeInit(&v->ampEnv, 0.005f, 0.10f, 0.70f, 0.50f);
|
||||||
|
envelopeInit(&v->modEnv, 0.005f, 0.50f, 0.0f, 0.10f);
|
||||||
|
|
||||||
|
v->filter.cutoff = 8000.0f;
|
||||||
|
v->filter.resonance = 0.0f;
|
||||||
|
v->filter.type = FILTER_LOWPASS;
|
||||||
|
v->filter.active = 0;
|
||||||
|
v->filter.low = 0.0f;
|
||||||
|
v->filter.band = 0.0f;
|
||||||
|
v->filter.modRouting = MOD_SOURCE_NONE;
|
||||||
|
v->filter.modDepth = 0.0f;
|
||||||
|
v->filter.resModRouting = MOD_SOURCE_NONE;
|
||||||
|
v->filter.resModDepth = 0.0f;
|
||||||
|
|
||||||
|
for (int l = 0; l < LFO_COUNT; l++) {
|
||||||
|
s->lfos[l].phase = 0.0f;
|
||||||
|
s->lfos[l].rate = 1.0f;
|
||||||
|
s->lfos[l].waveform = WAVE_SINE;
|
||||||
|
s->lfos[l].active = 0;
|
||||||
|
s->lfos[l].noiseHeld = 0.0f;
|
||||||
|
s->lfos[l].noisePhase = 0.0f;
|
||||||
|
s->lfos[l].noiseState = 0x2545F491u + (uint32_t)l * 3266489917u;
|
||||||
|
}
|
||||||
|
|
||||||
|
s->volume = 0.8f;
|
||||||
|
s->pitchBendRange = 2.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void oscillatorInit(Oscillator *o, Waveform waveform, float dutyCycle, float detune, float gain)
|
||||||
|
{
|
||||||
|
o->phase = 0.0f;
|
||||||
|
o->waveform = waveform;
|
||||||
|
o->dutyCycle = dutyCycle;
|
||||||
|
o->detune = detune;
|
||||||
|
o->noiseHeld = 0.0f;
|
||||||
|
o->noisePhase = 0.0f;
|
||||||
|
o->noiseState = 0x9E3779B9u; // Non-zero, or xorshift stays at zero and makes silence.
|
||||||
|
o->gain = gain;
|
||||||
|
o->octave = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- Noise ----
|
||||||
|
//
|
||||||
|
// A plain 32-bit xorshift, which is all a noise source needs: it has to be the same sequence
|
||||||
|
// every run and it does not have to be a good one. A state of zero stays at zero and makes
|
||||||
|
// silence rather than noise, so every seed below is non-zero on purpose.
|
||||||
|
static float nextNoise(uint32_t *state) {
|
||||||
|
*state ^= *state << 13;
|
||||||
|
*state ^= *state >> 17;
|
||||||
|
*state ^= *state << 5;
|
||||||
|
return (float)(*state / 4294967296.0) * 2.0f - 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float getModValue(float ampEnv, float modEnv, float lfo0, float lfo1, ModSource source)
|
||||||
|
{
|
||||||
|
switch (source) {
|
||||||
|
case MOD_SOURCE_AMP_ENV: return ampEnv;
|
||||||
|
case MOD_SOURCE_MOD_ENV: return modEnv;
|
||||||
|
case MOD_SOURCE_LFO: return lfo0;
|
||||||
|
case MOD_SOURCE_LFO2: return lfo1;
|
||||||
|
default: return 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float lfoTick(LFO *l, float sampleRate)
|
||||||
|
{
|
||||||
|
if (!l->active) return 0.0f;
|
||||||
|
l->phase += l->rate / sampleRate;
|
||||||
|
if (l->phase >= 1.0f) l->phase -= 1.0f;
|
||||||
|
if (l->waveform == WAVE_NOISE) {
|
||||||
|
l->noisePhase += l->rate / sampleRate;
|
||||||
|
if (l->noisePhase >= 1.0f) {
|
||||||
|
l->noisePhase -= 1.0f;
|
||||||
|
l->noiseHeld = nextNoise(&l->noiseState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return waveformSample(l->waveform, l->phase, 0.5f, l->noiseHeld);
|
||||||
|
}
|
||||||
|
|
||||||
|
float filterTick(Filter *f, float input, float cutoff, float resonance, float sampleRate)
|
||||||
|
{
|
||||||
|
if (!f->active) return input;
|
||||||
|
|
||||||
|
if (cutoff < 20.0f) cutoff = 20.0f;
|
||||||
|
if (cutoff > sampleRate * 0.499f) cutoff = sampleRate * 0.499f;
|
||||||
|
if (resonance < 0.0f) resonance = 0.0f;
|
||||||
|
if (resonance > 0.99f) resonance = 0.99f;
|
||||||
|
|
||||||
|
// Andy Simper TPT SVF (bilinear integration - unconditionally stable)
|
||||||
|
float g = tanf((float)M_PI * cutoff / sampleRate);
|
||||||
|
float Q = 0.5f + resonance * 9.5f; // resonance 0..0.99 -> Q 0.5..10.0
|
||||||
|
float k = 1.0f / Q;
|
||||||
|
float a1 = 1.0f / (1.0f + g * (g + k));
|
||||||
|
float a2 = g * a1;
|
||||||
|
float a3 = g * a2;
|
||||||
|
|
||||||
|
// f->band = s1, f->low = s2 (integrator states)
|
||||||
|
float v3 = input - f->low;
|
||||||
|
float v1 = a1 * f->band + a2 * v3;
|
||||||
|
float v2 = f->low + a2 * f->band + a3 * v3;
|
||||||
|
f->band = 2.0f * v1 - f->band;
|
||||||
|
f->low = 2.0f * v2 - f->low;
|
||||||
|
|
||||||
|
switch (f->type) {
|
||||||
|
case FILTER_LOWPASS: return v2;
|
||||||
|
case FILTER_HIGHPASS: return input - k * v1 - v2;
|
||||||
|
case FILTER_BANDPASS: return v1;
|
||||||
|
default: return v2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *filterTypeName(FilterType t)
|
||||||
|
{
|
||||||
|
switch (t) {
|
||||||
|
case FILTER_LOWPASS: return "LP";
|
||||||
|
case FILTER_HIGHPASS: return "HP";
|
||||||
|
case FILTER_BANDPASS: return "BP";
|
||||||
|
default: return "??";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// float oscillatorTick(Oscillator *o, float freqHz, float bendMultiplier, float sampleRate,
|
||||||
|
// float dutyCycle, float detune, float gain)
|
||||||
|
// {
|
||||||
|
// float detuneMultiplier = powf(2.0f, o->detune / 1200.0f);
|
||||||
|
// float freq = freqHz * detuneMultiplier * bendMultiplier;
|
||||||
|
//
|
||||||
|
// // Advance phase
|
||||||
|
// o->phase += freq / sampleRate;
|
||||||
|
// if (o->phase >= 1.0f) o->phase -= 1.0f;
|
||||||
|
//
|
||||||
|
// // Clocked noise - draw a new random value once per cycle
|
||||||
|
// if (o->waveform == WAVE_NOISE) {
|
||||||
|
// o->noisePhase += freq / sampleRate;
|
||||||
|
// if (o->noisePhase >= 1.0f) {
|
||||||
|
// o->noisePhase -= 1.0f;
|
||||||
|
// o->noiseHeld = nextNoise(&o->noiseState);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return waveformSample(o->waveform, o->phase, o->dutyCycle, o->noiseHeld) * o->gain;
|
||||||
|
// }
|
||||||
|
|
||||||
|
float oscillatorTick(Oscillator *o, float freqHz, float bendMultiplier, float sampleRate,
|
||||||
|
float dutyCycle, float detune, float gain)
|
||||||
|
{
|
||||||
|
float detuneMultiplier = powf(2.0f, detune / 1200.0f);
|
||||||
|
float freq = freqHz * detuneMultiplier * bendMultiplier;
|
||||||
|
|
||||||
|
// Advance phase
|
||||||
|
o->phase += freq / sampleRate;
|
||||||
|
if (o->phase >= 1.0f) o->phase -= 1.0f;
|
||||||
|
|
||||||
|
// Clocked noise - draw a new random value once per cycle
|
||||||
|
if (o->waveform == WAVE_NOISE) {
|
||||||
|
o->noisePhase += freq / sampleRate;
|
||||||
|
if (o->noisePhase >= 1.0f) {
|
||||||
|
o->noisePhase -= 1.0f;
|
||||||
|
o->noiseHeld = nextNoise(&o->noiseState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return waveformSample(o->waveform, o->phase, dutyCycle, o->noiseHeld) * gain;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
float waveformSample(Waveform w, float phase, float dutyCycle, float noiseHeld)
|
||||||
|
{
|
||||||
|
switch (w) {
|
||||||
|
case WAVE_SINE:
|
||||||
|
return sinf(2.0f * (float)M_PI * phase);
|
||||||
|
case WAVE_TRIANGLE:
|
||||||
|
return (phase < 0.5f)
|
||||||
|
? ( 4.0f * phase - 1.0f)
|
||||||
|
: (-4.0f * phase + 3.0f);
|
||||||
|
case WAVE_SAW:
|
||||||
|
return 2.0f * phase - 1.0f;
|
||||||
|
case WAVE_RAMP:
|
||||||
|
return 1.0f - 2.0f * phase;
|
||||||
|
case WAVE_PULSE:
|
||||||
|
return (phase < dutyCycle) ? 1.0f : -1.0f;
|
||||||
|
case WAVE_NOISE:
|
||||||
|
return noiseHeld;
|
||||||
|
default:
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *waveformName(Waveform w)
|
||||||
|
{
|
||||||
|
switch (w) {
|
||||||
|
case WAVE_SINE: return "Sine";
|
||||||
|
case WAVE_TRIANGLE: return "Triangle";
|
||||||
|
case WAVE_SAW: return "Saw";
|
||||||
|
case WAVE_RAMP: return "Ramp";
|
||||||
|
case WAVE_PULSE: return "Pulse";
|
||||||
|
case WAVE_NOISE: return "Noise";
|
||||||
|
default: return "???";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void synthNoteOn(Synth *s, int midiNote)
|
||||||
|
{
|
||||||
|
float hz = 440.0f * powf(2.0f, (midiNote - 69) / 12.0f);
|
||||||
|
|
||||||
|
for (int i = 0; i < VOICE_COUNT; i++) {
|
||||||
|
if (!s->voices[i].active) {
|
||||||
|
s->voices[i].freqHz = hz;
|
||||||
|
s->voices[i].midiNote = midiNote;
|
||||||
|
s->voices[i].active = 1;
|
||||||
|
s->voices[i].filter.low = 0.0f;
|
||||||
|
s->voices[i].filter.band = 0.0f;
|
||||||
|
envelopeNoteOn(&s->voices[i].ampEnv);
|
||||||
|
envelopeNoteOn(&s->voices[i].modEnv);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Steal round-robin
|
||||||
|
int i = s->lastStolenVoice % VOICE_COUNT;
|
||||||
|
s->lastStolenVoice++;
|
||||||
|
s->voices[i].freqHz = hz;
|
||||||
|
s->voices[i].midiNote = midiNote;
|
||||||
|
s->voices[i].active = 1;
|
||||||
|
s->voices[i].filter.low = 0.0f;
|
||||||
|
s->voices[i].filter.band = 0.0f;
|
||||||
|
envelopeNoteOn(&s->voices[i].ampEnv);
|
||||||
|
envelopeNoteOn(&s->voices[i].modEnv);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void synthNoteOff(Synth *s, int midiNote) {
|
||||||
|
for (int i = 0; i < VOICE_COUNT; i++) {
|
||||||
|
if (s->voices[i].active && s->voices[i].midiNote == midiNote) {
|
||||||
|
envelopeNoteOff(&s->voices[i].ampEnv);
|
||||||
|
envelopeNoteOff(&s->voices[i].modEnv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- A channel is the channel you asked for ----
|
||||||
|
//
|
||||||
|
// synthNoteOn hunts for a free voice and steals round-robin, which is what a keyboard wants:
|
||||||
|
// eight fingers and no say in which voice serves which. A hardware channel is not like that.
|
||||||
|
// Channel two is channel two, it holds its patch between notes, and a program driving it
|
||||||
|
// knows perfectly well what it is doing - so these say which one and nothing is stolen.
|
||||||
|
//
|
||||||
|
// The two above are left exactly as they were, because the standalone synthesizer still wants
|
||||||
|
// them and a keyboard has not stopped being a keyboard.
|
||||||
|
void synthChannelOn(Synth *s, int channel, int midiNote)
|
||||||
|
{
|
||||||
|
if (channel < 0 || channel >= VOICE_COUNT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Voice *v = &s->voices[channel];
|
||||||
|
v->freqHz = 440.0f * powf(2.0f, (midiNote - 69) / 12.0f);
|
||||||
|
v->midiNote = midiNote;
|
||||||
|
v->active = 1;
|
||||||
|
// The filter's memory of the last note is not this note's business. A note beginning
|
||||||
|
// where the last one left off is how a click gets into the front of every sound.
|
||||||
|
v->filter.low = 0.0f;
|
||||||
|
v->filter.band = 0.0f;
|
||||||
|
envelopeNoteOn(&v->ampEnv);
|
||||||
|
envelopeNoteOn(&v->modEnv);
|
||||||
|
}
|
||||||
|
|
||||||
|
void synthChannelOff(Synth *s, int channel)
|
||||||
|
{
|
||||||
|
if (channel < 0 || channel >= VOICE_COUNT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Released rather than stopped: what happens next is the envelope's business, and a note
|
||||||
|
// that ended the instant a key came up would have no release at all.
|
||||||
|
envelopeNoteOff(&s->voices[channel].ampEnv);
|
||||||
|
envelopeNoteOff(&s->voices[channel].modEnv);
|
||||||
|
}
|
||||||
|
|
||||||
|
void synthFillBuffer(Synth *s, int16_t *out, int frames) {
|
||||||
|
const float sr = s->sampleRate;
|
||||||
|
const float bendMultiplier = powf(2.0f, (s->pitchBend * s->pitchBendRange) / 12.0f);
|
||||||
|
|
||||||
|
for (int i = 0; i < frames; i++) {
|
||||||
|
float mix = 0.0f;
|
||||||
|
|
||||||
|
float lfo0 = lfoTick(&s->lfos[0], sr);
|
||||||
|
float lfo1 = lfoTick(&s->lfos[1], sr);
|
||||||
|
|
||||||
|
for (int v = 0; v < VOICE_COUNT; v++) {
|
||||||
|
Voice *vv = &s->voices[v];
|
||||||
|
if (!vv->active) continue;
|
||||||
|
|
||||||
|
float amp = envelopeTick(&vv->ampEnv, sr);
|
||||||
|
float mod = envelopeTick(&vv->modEnv, sr);
|
||||||
|
|
||||||
|
if (vv->ampEnv.stage == ENV_IDLE) {
|
||||||
|
vv->active = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float oscMix = 0.0f;
|
||||||
|
int activeOscs = 0;
|
||||||
|
for (int o = 0; o < OSC_COUNT; o++) {
|
||||||
|
Oscillator *osc = &vv->oscillators[o];
|
||||||
|
if (!osc->active) continue;
|
||||||
|
|
||||||
|
float dutyCycle = osc->dutyCycle +
|
||||||
|
getModValue(amp, mod, lfo0, lfo1, osc->modRouting[0]) * osc->modDepth[0];
|
||||||
|
float detune = osc->detune + (float)osc->octave * 1200.0f +
|
||||||
|
getModValue(amp, mod, lfo0, lfo1, osc->modRouting[1]) * osc->modDepth[1];
|
||||||
|
float gain = osc->gain +
|
||||||
|
getModValue(amp, mod, lfo0, lfo1, osc->modRouting[2]) * osc->modDepth[2];
|
||||||
|
|
||||||
|
if (dutyCycle < 0.05f) dutyCycle = 0.05f;
|
||||||
|
if (dutyCycle > 0.95f) dutyCycle = 0.95f;
|
||||||
|
if (gain < 0.0f) gain = 0.0f;
|
||||||
|
if (gain > OSC_MAX_GAIN) gain = OSC_MAX_GAIN;
|
||||||
|
|
||||||
|
oscMix += oscillatorTick(osc, vv->freqHz, bendMultiplier, sr, dutyCycle, detune, gain);
|
||||||
|
activeOscs++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeOscs > 0) oscMix /= activeOscs;
|
||||||
|
|
||||||
|
float cutoff = vv->filter.cutoff +
|
||||||
|
getModValue(amp, mod, lfo0, lfo1, vv->filter.modRouting) * vv->filter.modDepth;
|
||||||
|
float resonance = vv->filter.resonance +
|
||||||
|
getModValue(amp, mod, lfo0, lfo1, vv->filter.resModRouting) * vv->filter.resModDepth;
|
||||||
|
oscMix = filterTick(&vv->filter, oscMix, cutoff, resonance, sr);
|
||||||
|
|
||||||
|
// ---- How loud this voice is ----
|
||||||
|
//
|
||||||
|
// Envelope 0 used to be multiplied in here unconditionally, so routing it
|
||||||
|
// anywhere else meant it shaped the volume as well. Now the voice says which
|
||||||
|
// source shapes its level, and MOD_SOURCE_NONE means nothing does.
|
||||||
|
//
|
||||||
|
// Clamped at nothing, because an LFO swings either side of zero and the far side
|
||||||
|
// is not a negative volume, it is silence. Which makes an LFO here tremolo.
|
||||||
|
float level = 1.0f;
|
||||||
|
if (vv->levelSource != MOD_SOURCE_NONE) {
|
||||||
|
level = getModValue(amp, mod, lfo0, lfo1, vv->levelSource);
|
||||||
|
if (level < 0.0f) level = 0.0f;
|
||||||
|
}
|
||||||
|
mix += oscMix * level;
|
||||||
|
}
|
||||||
|
|
||||||
|
mix *= (0.2f / VOICE_COUNT) * 4.0f * s->volume;
|
||||||
|
|
||||||
|
int32_t sample = (int32_t)lrintf(mix * 32767.0f);
|
||||||
|
if (sample > 32767) sample = 32767;
|
||||||
|
if (sample < -32768) sample = -32768;
|
||||||
|
out[i] = (int16_t)sample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void envelopeInit(Envelope *e, float attackSec, float decaySec, float sustainLevel, float releaseSec)
|
||||||
|
{
|
||||||
|
e->stage = ENV_IDLE;
|
||||||
|
e->value = 0.0f;
|
||||||
|
e->attackSec = attackSec;
|
||||||
|
e->decaySec = decaySec;
|
||||||
|
e->sustainLevel = sustainLevel;
|
||||||
|
e->releaseSec = releaseSec;
|
||||||
|
}
|
||||||
|
|
||||||
|
void envelopeNoteOn(Envelope *e)
|
||||||
|
{
|
||||||
|
e->value = 0.0f;
|
||||||
|
e->stage = ENV_ATTACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void envelopeNoteOff(Envelope *e)
|
||||||
|
{
|
||||||
|
// Only trigger release if we're actually playing
|
||||||
|
if (e->stage != ENV_IDLE)
|
||||||
|
e->stage = ENV_RELEASE;
|
||||||
|
}
|
||||||
|
|
||||||
|
float envelopeTick(Envelope *e, float sampleRate)
|
||||||
|
{
|
||||||
|
switch (e->stage) {
|
||||||
|
case ENV_ATTACK: {
|
||||||
|
float inc = (e->attackSec <= 0.0f) ? 1.0f : (1.0f / (e->attackSec * sampleRate));
|
||||||
|
e->value += inc;
|
||||||
|
if (e->value >= 1.0f) {
|
||||||
|
e->value = 1.0f;
|
||||||
|
e->stage = ENV_DECAY;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ENV_DECAY: {
|
||||||
|
float inc = (e->decaySec <= 0.0f) ? 1.0f : (1.0f / (e->decaySec * sampleRate));
|
||||||
|
e->value -= inc;
|
||||||
|
if (e->value <= e->sustainLevel) {
|
||||||
|
e->value = e->sustainLevel;
|
||||||
|
e->stage = ENV_SUSTAIN;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ENV_SUSTAIN:
|
||||||
|
e->value = e->sustainLevel;
|
||||||
|
break;
|
||||||
|
case ENV_RELEASE: {
|
||||||
|
float inc = (e->releaseSec <= 0.0f) ? 1.0f : (1.0f / (e->releaseSec * sampleRate));
|
||||||
|
e->value -= inc;
|
||||||
|
if (e->value <= 0.0f) {
|
||||||
|
e->value = 0.0f;
|
||||||
|
e->stage = ENV_IDLE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ENV_IDLE:
|
||||||
|
e->value = 0.0f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return e->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void synthSyncVoices(Synth *s)
|
||||||
|
{
|
||||||
|
for (int v = 1; v < VOICE_COUNT; v++) {
|
||||||
|
// Sync oscillator settings
|
||||||
|
for (int o = 0; o < OSC_COUNT; o++) {
|
||||||
|
s->voices[v].oscillators[o].waveform = s->voices[0].oscillators[o].waveform;
|
||||||
|
s->voices[v].oscillators[o].dutyCycle = s->voices[0].oscillators[o].dutyCycle;
|
||||||
|
s->voices[v].oscillators[o].detune = s->voices[0].oscillators[o].detune;
|
||||||
|
s->voices[v].oscillators[o].gain = s->voices[0].oscillators[o].gain;
|
||||||
|
s->voices[v].oscillators[o].active = s->voices[0].oscillators[o].active;
|
||||||
|
s->voices[v].oscillators[o].octave = s->voices[0].oscillators[o].octave;
|
||||||
|
s->voices[v].oscillators[o].modRouting[0] = s->voices[0].oscillators[o].modRouting[0];
|
||||||
|
s->voices[v].oscillators[o].modRouting[1] = s->voices[0].oscillators[o].modRouting[1];
|
||||||
|
s->voices[v].oscillators[o].modRouting[2] = s->voices[0].oscillators[o].modRouting[2];
|
||||||
|
s->voices[v].oscillators[o].modDepth[0] = s->voices[0].oscillators[o].modDepth[0];
|
||||||
|
s->voices[v].oscillators[o].modDepth[1] = s->voices[0].oscillators[o].modDepth[1];
|
||||||
|
s->voices[v].oscillators[o].modDepth[2] = s->voices[0].oscillators[o].modDepth[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync filter params but not state (low/band are per-voice)
|
||||||
|
s->voices[v].filter.cutoff = s->voices[0].filter.cutoff;
|
||||||
|
s->voices[v].filter.resonance = s->voices[0].filter.resonance;
|
||||||
|
s->voices[v].filter.type = s->voices[0].filter.type;
|
||||||
|
s->voices[v].filter.active = s->voices[0].filter.active;
|
||||||
|
s->voices[v].filter.modRouting = s->voices[0].filter.modRouting;
|
||||||
|
s->voices[v].filter.modDepth = s->voices[0].filter.modDepth;
|
||||||
|
s->voices[v].filter.resModRouting = s->voices[0].filter.resModRouting;
|
||||||
|
s->voices[v].filter.resModDepth = s->voices[0].filter.resModDepth;
|
||||||
|
|
||||||
|
// Sync envelope settings but NOT runtime state
|
||||||
|
// Each voice needs its own stage, value - just copy the parameters
|
||||||
|
s->voices[v].ampEnv.attackSec = s->voices[0].ampEnv.attackSec;
|
||||||
|
s->voices[v].ampEnv.decaySec = s->voices[0].ampEnv.decaySec;
|
||||||
|
s->voices[v].ampEnv.sustainLevel = s->voices[0].ampEnv.sustainLevel;
|
||||||
|
s->voices[v].ampEnv.releaseSec = s->voices[0].ampEnv.releaseSec;
|
||||||
|
// Sync the mod envelope, too.
|
||||||
|
s->voices[v].modEnv.attackSec = s->voices[0].modEnv.attackSec;
|
||||||
|
s->voices[v].modEnv.decaySec = s->voices[0].modEnv.decaySec;
|
||||||
|
s->voices[v].modEnv.sustainLevel = s->voices[0].modEnv.sustainLevel;
|
||||||
|
s->voices[v].modEnv.releaseSec = s->voices[0].modEnv.releaseSec;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
// synth.h
|
||||||
|
// The Voyager's sound, vendored from soundThing.
|
||||||
|
//
|
||||||
|
// ---- Where this came from ----
|
||||||
|
//
|
||||||
|
// soundThing is a polyphonic subtractive synthesizer written by Anachronaut, and lives in its
|
||||||
|
// own repository. What is here is its VOICE ENGINE and nothing else: synth.c pulls in maths,
|
||||||
|
// stdlib, stdint and stdio and knows nothing about Raylib, MIDI, patches or the user
|
||||||
|
// interface, which is what made it liftable at all.
|
||||||
|
//
|
||||||
|
// It is copied rather than submoduled. Two files against tying this build to another
|
||||||
|
// repository's history is not a close call, and what a copy costs is that changes have to be
|
||||||
|
// carried across on purpose. So the list below is the whole of the difference, kept current.
|
||||||
|
//
|
||||||
|
// ---- What was changed ----
|
||||||
|
//
|
||||||
|
// 1. A VOICE'S LEVEL IS A ROUTING. Envelope 0 multiplied the output and there was no way to
|
||||||
|
// say otherwise, so routing it to a filter or an oscillator meant it shaped the volume as
|
||||||
|
// well whether that was wanted or not - which is most of the trouble with making
|
||||||
|
// percussion. Every other destination in this synth chooses its source; now this one does
|
||||||
|
// too, and MOD_SOURCE_NONE means the level is simply full.
|
||||||
|
//
|
||||||
|
// 2. NOISE COMES FROM A SEEDED GENERATOR. It drew from rand(), which is global state shared
|
||||||
|
// with the whole process and varies between libraries - so the same program would sound
|
||||||
|
// different on different machines and every recorded result would be worthless. It is a
|
||||||
|
// generator inside the Synth now, and a machine that starts the same way sounds the same
|
||||||
|
// way.
|
||||||
|
//
|
||||||
|
// 3. CHANNELS ARE NAMED, NOT ALLOCATED. synthNoteOn hunts for a free voice and steals
|
||||||
|
// round-robin, which is what a keyboard wants. A hardware channel is asked for by number.
|
||||||
|
// The old calls are still here and still do what they did.
|
||||||
|
//
|
||||||
|
// Written by Anachronaut
|
||||||
|
|
||||||
|
#ifndef SYNTH_H
|
||||||
|
#define SYNTH_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define VOICE_COUNT 8
|
||||||
|
#define OSC_COUNT 2
|
||||||
|
#define LFO_COUNT 2
|
||||||
|
#define OSC_MAX_GAIN 4.0f
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
WAVE_SINE,
|
||||||
|
WAVE_TRIANGLE,
|
||||||
|
WAVE_SAW,
|
||||||
|
WAVE_RAMP,
|
||||||
|
WAVE_PULSE,
|
||||||
|
WAVE_NOISE,
|
||||||
|
WAVE_COUNT // handy for the modulo wrap on waveform switching
|
||||||
|
} Waveform;
|
||||||
|
|
||||||
|
// Envelope structures:
|
||||||
|
typedef enum {
|
||||||
|
ENV_IDLE,
|
||||||
|
ENV_ATTACK,
|
||||||
|
ENV_DECAY,
|
||||||
|
ENV_SUSTAIN,
|
||||||
|
ENV_RELEASE
|
||||||
|
} EnvStage;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
MOD_SOURCE_NONE = 0,
|
||||||
|
MOD_SOURCE_AMP_ENV = 1,
|
||||||
|
MOD_SOURCE_MOD_ENV = 2,
|
||||||
|
MOD_SOURCE_LFO = 3,
|
||||||
|
MOD_SOURCE_LFO2 = 4
|
||||||
|
} ModSource;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
EnvStage stage;
|
||||||
|
float value; // current output value, 0.0 to 1.0
|
||||||
|
float attackSec;
|
||||||
|
float decaySec;
|
||||||
|
float sustainLevel;
|
||||||
|
float releaseSec;
|
||||||
|
} Envelope;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
float phase;
|
||||||
|
float rate; // Hz
|
||||||
|
Waveform waveform;
|
||||||
|
int active;
|
||||||
|
float noiseHeld;
|
||||||
|
float noisePhase;
|
||||||
|
// Its own noise, seeded at init. rand() is global state shared with the whole process and
|
||||||
|
// varies between C libraries, so the same program sounded different on different machines
|
||||||
|
// and no recorded result could mean anything. One generator EACH rather than one shared,
|
||||||
|
// because two noise sources drawing from the same stream are not two noise sources.
|
||||||
|
uint32_t noiseState;
|
||||||
|
} LFO;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
FILTER_LOWPASS,
|
||||||
|
FILTER_HIGHPASS,
|
||||||
|
FILTER_BANDPASS,
|
||||||
|
FILTER_COUNT
|
||||||
|
} FilterType;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
float cutoff; // Hz
|
||||||
|
float resonance; // 0.0 (flat) to 0.99 (near self-oscillation)
|
||||||
|
FilterType type;
|
||||||
|
int active;
|
||||||
|
float low, band; // TPT integrator states s1, s2
|
||||||
|
int modRouting;
|
||||||
|
float modDepth; // Hz
|
||||||
|
int resModRouting;
|
||||||
|
float resModDepth; // resonance units (-0.99..0.99)
|
||||||
|
} Filter;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
float phase;
|
||||||
|
float dutyCycle;
|
||||||
|
Waveform waveform;
|
||||||
|
float detune; // cents, 0 = no detune
|
||||||
|
float noiseHeld; // last drawn random value for clocked noise
|
||||||
|
float noisePhase; // tracks when to draw a new noise value
|
||||||
|
// Its own noise, seeded at init. rand() is global state shared with the whole process and
|
||||||
|
// varies between C libraries, so the same program sounded different on different machines
|
||||||
|
// and no recorded result could mean anything. One generator EACH rather than one shared,
|
||||||
|
// because two noise sources drawing from the same stream are not two noise sources.
|
||||||
|
uint32_t noiseState;
|
||||||
|
|
||||||
|
float gain;
|
||||||
|
int active; // whether this oscillator contributes to output
|
||||||
|
int octave; // transposition in octaves, -2 to +2
|
||||||
|
// Modulation routing: one source per destination by design.
|
||||||
|
// Each parameter (pwm, detune, gain) has exactly one mod source and one depth.
|
||||||
|
int modRouting[3]; // 0=off, 1=env0, 2=env1, 3=lfo0, 4=lfo1
|
||||||
|
float modDepth[3]; // Depth of modulation parameter
|
||||||
|
} Oscillator;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Oscillator oscillators[OSC_COUNT];
|
||||||
|
float freqHz;
|
||||||
|
int active;
|
||||||
|
int midiNote;
|
||||||
|
Envelope ampEnv;
|
||||||
|
Envelope modEnv;
|
||||||
|
Filter filter;
|
||||||
|
// ---- What shapes how loud this voice is ----
|
||||||
|
//
|
||||||
|
// Envelope 0 used to, always, with no way to say otherwise - so routing it to a filter or
|
||||||
|
// an oscillator meant it shaped the volume too, whether that was wanted or not. Every
|
||||||
|
// other destination here names its source; this one does now as well.
|
||||||
|
//
|
||||||
|
// NOT the base-and-depth pair the others use, because a level is not a deviation from a
|
||||||
|
// resting value - it is a shape from nothing to full, and multiplying is what an amplitude
|
||||||
|
// envelope does. So this names a source outright, and MOD_SOURCE_NONE means the voice is
|
||||||
|
// simply at full and whatever env0 is doing is somebody else's business.
|
||||||
|
//
|
||||||
|
// It also makes two things possible that were not: envelope 1 shaping the volume, and an
|
||||||
|
// LFO doing it, which is tremolo.
|
||||||
|
ModSource levelSource;
|
||||||
|
} Voice;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
float sampleRate;
|
||||||
|
float pitchBend;
|
||||||
|
float pitchBendRange;
|
||||||
|
int lastStolenVoice;
|
||||||
|
Voice voices[VOICE_COUNT];
|
||||||
|
float volume; // 0.0 to 1.0, master output level
|
||||||
|
LFO lfos[LFO_COUNT];
|
||||||
|
} Synth;
|
||||||
|
|
||||||
|
// Envelope functions:
|
||||||
|
void envelopeInit(Envelope *e, float attackSec, float decaySec, float sustainLevel, float releaseSec);
|
||||||
|
void envelopeNoteOn(Envelope *e);
|
||||||
|
void envelopeNoteOff(Envelope *e);
|
||||||
|
float envelopeTick(Envelope *e, float sampleRate);
|
||||||
|
|
||||||
|
// Oscillator functions:
|
||||||
|
void oscillatorInit(Oscillator *o, Waveform waveform, float dutyCycle, float detune, float gain);
|
||||||
|
float oscillatorTick(Oscillator *o, float freqHz, float bendMultiplier, float sampleRate,
|
||||||
|
float dutyCycle, float detune, float gain);
|
||||||
|
|
||||||
|
// Synth functions:
|
||||||
|
void synthInit(Synth *s, float sampleRate);
|
||||||
|
void synthResetPatch(Synth *s);
|
||||||
|
void synthNoteOn(Synth *s, int midiNote);
|
||||||
|
void synthNoteOff(Synth *s, int midiNote);
|
||||||
|
|
||||||
|
// ---- 3. A channel is asked for by number ----
|
||||||
|
//
|
||||||
|
// The two above hunt for a free voice and steal round-robin, which is what a keyboard wants
|
||||||
|
// and what the standalone synthesizer still does. A hardware channel is not allocated: it is
|
||||||
|
// the third one, and it is the third one every time.
|
||||||
|
void synthChannelOn(Synth *s, int channel, int midiNote);
|
||||||
|
void synthChannelOff(Synth *s, int channel);
|
||||||
|
void synthFillBuffer(Synth *s, int16_t *out, int frames);
|
||||||
|
void synthSyncVoices(Synth *s);
|
||||||
|
|
||||||
|
// LFO functions:
|
||||||
|
float lfoTick(LFO *l, float sampleRate);
|
||||||
|
|
||||||
|
// Filter functions:
|
||||||
|
float filterTick(Filter *f, float input, float cutoff, float resonance, float sampleRate);
|
||||||
|
const char *filterTypeName(FilterType t);
|
||||||
|
|
||||||
|
// Waveform functions:
|
||||||
|
float waveformSample(Waveform w, float phase, float dutyCycle, float noiseHeld);
|
||||||
|
const char *waveformName(Waveform w);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -27,6 +27,16 @@ void printHelp(const char *programName) {
|
|||||||
printf(" default, finishes before the next instruction starts.\n");
|
printf(" default, finishes before the next instruction starts.\n");
|
||||||
printf(" -W, --write-protect Attach the disk read only. A disk the host will not let\n");
|
printf(" -W, --write-protect Attach the disk read only. A disk the host will not let\n");
|
||||||
printf(" you write is read only whether you ask for this or not.\n");
|
printf(" you write is read only whether you ask for this or not.\n");
|
||||||
|
printf(" -S, --screen FILE Save a picture of the screen, as a PPM, when the machine\n");
|
||||||
|
printf(" stops. Works with or without a window, which is how the\n");
|
||||||
|
printf(" tests look at a screen on a host that has no display.\n");
|
||||||
|
printf(" -K, --keyboard FILE Feed the console from this file as though it were a\n");
|
||||||
|
printf(" keyboard rather than a terminal. Which means the console does\n");
|
||||||
|
printf(" its own line editing, the way it must when a window is open\n");
|
||||||
|
printf(" and there is no terminal behind it to do it.\n");
|
||||||
|
printf(" -N, --sound FILE Save every sample the machine made, as raw signed 16 bit\n");
|
||||||
|
printf(" at 48kHz. What --screen is for a picture: the only way to\n");
|
||||||
|
printf(" check a sound on a machine with no speaker.\n");
|
||||||
printf(" -h, --help Display this help message.\n");
|
printf(" -h, --help Display this help message.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,23 +46,32 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
|||||||
{"cycles", required_argument, 0, 'c'},
|
{"cycles", required_argument, 0, 'c'},
|
||||||
{"fast", no_argument, 0, 'f'},
|
{"fast", no_argument, 0, 'f'},
|
||||||
{"disk", required_argument, 0, 'D'},
|
{"disk", required_argument, 0, 'D'},
|
||||||
|
{"ram-disk", required_argument, 0, 'R'},
|
||||||
{"write-protect", no_argument, 0, 'W'},
|
{"write-protect", no_argument, 0, 'W'},
|
||||||
{"disk-cycles", required_argument, 0, 'L'},
|
{"disk-cycles", required_argument, 0, 'L'},
|
||||||
|
{"screen", required_argument, 0, 'S'},
|
||||||
|
{"keyboard", required_argument, 0, 'K'},
|
||||||
|
{"sound", required_argument, 0, 'N'},
|
||||||
{"help", no_argument, 0, 'h'},
|
{"help", no_argument, 0, 'h'},
|
||||||
{0, 0, 0, 0 }
|
{0, 0, 0, 0 }
|
||||||
};
|
};
|
||||||
int opt;
|
int opt;
|
||||||
int option_index = 0;
|
int option_index = 0;
|
||||||
|
|
||||||
options->debug = 0;
|
// ---- Everything off, in one line rather than nine ----
|
||||||
options->fast = 0;
|
//
|
||||||
options->cycles = 0;
|
// This was a list of assignments, one per field, and a list beside a struct drifts from
|
||||||
options->disk = NULL;
|
// the struct: adding `disks` and `diskCount` left them holding whatever was on the stack,
|
||||||
options->writeProtect = 0;
|
// so a machine given one disk was told it already had four drives. The same struct
|
||||||
options->diskCycles = 0;
|
// growing a field once before left Voyager linking against an object that disagreed
|
||||||
|
// about its size.
|
||||||
|
//
|
||||||
|
// Every default here is nought or nothing, and a default that is not can be written
|
||||||
|
// below this line where it will be read as the exception it is.
|
||||||
|
*options = (EmulatorOptions){0};
|
||||||
|
|
||||||
// Parse options
|
// Parse options
|
||||||
while ((opt = getopt_long(argc, argv, "dc:fhD:WL:", long_options, &option_index)) != -1) {
|
while ((opt = getopt_long(argc, argv, "dc:fhD:WL:S:K:N:R:", long_options, &option_index)) != -1) {
|
||||||
switch (opt) {
|
switch (opt) {
|
||||||
case 'd':
|
case 'd':
|
||||||
options->debug = 1;
|
options->debug = 1;
|
||||||
@@ -74,7 +93,28 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
|||||||
options->fast = 1;
|
options->fast = 1;
|
||||||
break;
|
break;
|
||||||
case 'D':
|
case 'D':
|
||||||
options->disk = optarg;
|
// Each one is the next drive. The first is also left in `disk`, because a
|
||||||
|
// machine with one disk is what almost every caller means and reading it
|
||||||
|
// that way keeps them all unchanged.
|
||||||
|
if (options->diskCount >= DISK_DRIVE_COUNT) {
|
||||||
|
fprintf(stderr, "Error: This machine has %d drives.\n",
|
||||||
|
DISK_DRIVE_COUNT);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
options->disks[options->diskCount++] = optarg;
|
||||||
|
if (options->disk == NULL) {
|
||||||
|
options->disk = optarg;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'R': {
|
||||||
|
char *end;
|
||||||
|
const unsigned long blocks = strtoul(optarg, &end, 10);
|
||||||
|
if (*optarg == '\0' || *end != '\0' || blocks == 0) {
|
||||||
|
fprintf(stderr, "Error: --ram-disk wants a number of blocks.\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
options->ramDisk = blocks;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'W':
|
case 'W':
|
||||||
options->writeProtect = 1;
|
options->writeProtect = 1;
|
||||||
@@ -82,6 +122,15 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
|||||||
case 'L':
|
case 'L':
|
||||||
options->diskCycles = strtoul(optarg, NULL, 0);
|
options->diskCycles = strtoul(optarg, NULL, 0);
|
||||||
break;
|
break;
|
||||||
|
case 'S':
|
||||||
|
options->screen = optarg;
|
||||||
|
break;
|
||||||
|
case 'K':
|
||||||
|
options->keyboard = optarg;
|
||||||
|
break;
|
||||||
|
case 'N':
|
||||||
|
options->sound = optarg;
|
||||||
|
break;
|
||||||
case 'h':
|
case 'h':
|
||||||
printHelp(argv[0]);
|
printHelp(argv[0]);
|
||||||
return OPTIONS_HELP;
|
return OPTIONS_HELP;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#define UTILITY_H
|
#define UTILITY_H
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include "io.h"
|
||||||
#include "cpu.h"
|
#include "cpu.h"
|
||||||
|
|
||||||
// Results of reading the command line.
|
// Results of reading the command line.
|
||||||
@@ -20,15 +21,28 @@ typedef struct {
|
|||||||
uint8_t fast; // Ignore the cycle rate and run as fast as the host allows.
|
uint8_t fast; // Ignore the cycle rate and run as fast as the host allows.
|
||||||
unsigned long cycles; // Stop after this many cycles. Zero means run until the program halts.
|
unsigned long cycles; // Stop after this many cycles. Zero means run until the program halts.
|
||||||
unsigned long diskCycles; // How long a block move takes. Zero is instant, and the default.
|
unsigned long diskCycles; // How long a block move takes. Zero is instant, and the default.
|
||||||
|
// ---- The drives, in the order they were named ----
|
||||||
|
//
|
||||||
|
// --disk given more than once fills them in turn, so the first is drive 0 and the machine
|
||||||
|
// has as many as were asked for. One name is the ordinary case and reads exactly as it
|
||||||
|
// did when there could only be one.
|
||||||
const char *disk; // Disk image to attach, or NULL for a machine with no disk.
|
const char *disk; // Disk image to attach, or NULL for a machine with no disk.
|
||||||
|
const char *disks[DISK_DRIVE_COUNT];
|
||||||
|
int diskCount;
|
||||||
|
// A drive of this many blocks made of memory, taking the next drive number after the
|
||||||
|
// images above. Zero for a machine without one.
|
||||||
|
unsigned long ramDisk;
|
||||||
uint8_t writeProtect; // Attach the disk read only, the way a tab on a floppy would.
|
uint8_t writeProtect; // Attach the disk read only, the way a tab on a floppy would.
|
||||||
|
const char *screen; // Where to save a picture of the screen when the machine stops.
|
||||||
|
const char *keyboard; // Feed the console from this file as a keyboard, not a terminal.
|
||||||
|
const char *sound; // Where to save the samples the machine made, as raw 16 bit.
|
||||||
} EmulatorOptions;
|
} EmulatorOptions;
|
||||||
|
|
||||||
uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options);
|
uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options);
|
||||||
|
|
||||||
void printHelp(const char *programName);
|
void printHelp(const char *programName);
|
||||||
|
|
||||||
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data);
|
uint8_t loadFile(const char *path, uint8_t *Program, uint8_t *Data);
|
||||||
|
|
||||||
void bootStrap(uint8_t *Program, uint8_t *Data);
|
void bootStrap(uint8_t *Program, uint8_t *Data);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,404 @@
|
|||||||
|
// video.c
|
||||||
|
// The Voyager's video device.
|
||||||
|
// Written by Anachronaut
|
||||||
|
|
||||||
|
#include "video.h"
|
||||||
|
#include "font.h"
|
||||||
|
#include "io.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
// The bank the device brings. Registered by whoever enumerates the hardware, reached only
|
||||||
|
// through the memory controller, and never by the CPU directly - the same arrangement the
|
||||||
|
// disk's buffer has always had.
|
||||||
|
static uint8_t videoRAM[VIDEO_MEMORY_BYTES];
|
||||||
|
|
||||||
|
static uint8_t mode;
|
||||||
|
// Which map row is drawn at the top. THE MAP IS A RING: rendering row r reads map row
|
||||||
|
// (scroll + r) wrapped, so scrolling a screen moves this byte and moves no memory at all.
|
||||||
|
//
|
||||||
|
// That is worth more than it looks. Blitting a 40 by 25 screen up one line is 1,920 bytes
|
||||||
|
// inside one bank, which is 1,920 cycles even with the controller widened - twelve percent
|
||||||
|
// of a frame, every line. A program printing one page would spend six frames shuffling
|
||||||
|
// memory. Here it costs one port write, and the rows that scrolled off are still there,
|
||||||
|
// which is where the console gets scrollback it never had.
|
||||||
|
static uint8_t scroll;
|
||||||
|
|
||||||
|
// The column origin, and the pixel remainder for each axis. Kept apart from the row origin
|
||||||
|
// above rather than folded into it, because they are read at different moments: the origins
|
||||||
|
// decide which cell a program's writes land in, and the fine offsets decide nothing at all
|
||||||
|
// except where the finished picture sits. See videoPutCell.
|
||||||
|
static uint8_t scrollColumn;
|
||||||
|
static uint8_t fineX, fineY;
|
||||||
|
|
||||||
|
static uint8_t pixels[VIDEO_MAX_WIDTH * VIDEO_MAX_HEIGHT * 3];
|
||||||
|
static int renderedWidth = 0;
|
||||||
|
static int renderedHeight = 0;
|
||||||
|
|
||||||
|
// Zero in bitmap mode, where there are no characters. Everything that draws one checks, so
|
||||||
|
// this is the single place the answer lives rather than a mode test in each of them.
|
||||||
|
static int columnsFor(uint8_t m) {
|
||||||
|
if (m == VIDEO_MODE_BITMAP) return 0;
|
||||||
|
return m == VIDEO_MODE_80x50 ? 80 : 40;
|
||||||
|
}
|
||||||
|
static int rowsFor(uint8_t m) {
|
||||||
|
if (m == VIDEO_MODE_BITMAP) return 0;
|
||||||
|
return m == VIDEO_MODE_80x50 ? 50 : 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
int videoTextRows(void) { return rowsFor(mode); }
|
||||||
|
|
||||||
|
int videoColumns(void) { return columnsFor(mode); }
|
||||||
|
int videoRows(void) { return rowsFor(mode); }
|
||||||
|
|
||||||
|
// ---- Sixteen schemes a machine wakes up with ----
|
||||||
|
//
|
||||||
|
// A glyph is drawn in palette indices 0 and 1, paper and ink, and a cell's attribute nibble
|
||||||
|
// adds sixteen to both. So bank n colours text with entries n*16 and n*16+1, and SIXTEEN
|
||||||
|
// BANKS IS SIXTEEN INK AND PAPER PAIRS - a text attribute system that costs one nibble and
|
||||||
|
// no hardware at all.
|
||||||
|
//
|
||||||
|
// The arrangement is a convention rather than a rule of the machine, and it is chosen so
|
||||||
|
// that HIGHLIGHTING IS ONE BIT. Banks 0 to 7 are colours on black; banks 8 to 15 are the
|
||||||
|
// same colours as paper with black ink. Attribute XOR 8 therefore turns any of them inside
|
||||||
|
// out, which is what a cursor and a selected line both want, and a program that disagrees
|
||||||
|
// writes its own palette over the top.
|
||||||
|
//
|
||||||
|
// Bank 0 is grey on black, which is what the machine has always woken up as.
|
||||||
|
//
|
||||||
|
// BLACK IS BLACK AND GREY IS GREY. These were tinted towards green to begin with, on the
|
||||||
|
// theory that a phosphor never was neutral, and on a real screen it read as a fault rather
|
||||||
|
// than as character - a background that is nearly black looks like a background that failed
|
||||||
|
// to be black.
|
||||||
|
static const uint8_t defaultInks[8][3] = {
|
||||||
|
{ 0xD8, 0xD8, 0xD8 }, // grey, which is what plain text has always been
|
||||||
|
{ 0xD0, 0x40, 0x38 }, // red
|
||||||
|
{ 0x50, 0xC0, 0x50 }, // green
|
||||||
|
{ 0xD8, 0xC0, 0x48 }, // yellow
|
||||||
|
{ 0x58, 0x80, 0xE0 }, // blue
|
||||||
|
{ 0xC8, 0x60, 0xC0 }, // magenta
|
||||||
|
{ 0x50, 0xC0, 0xC8 }, // cyan
|
||||||
|
{ 0xF0, 0xF0, 0xF0 }, // white
|
||||||
|
};
|
||||||
|
static const uint8_t defaultPaper[3] = { 0x00, 0x00, 0x00 };
|
||||||
|
|
||||||
|
// Where the cursor is, whether it is wanted, and what the clock says - which is what makes
|
||||||
|
// it blink without anything having to remember when it last did.
|
||||||
|
static int cursorAtRow = 0;
|
||||||
|
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;
|
||||||
|
cursorVisible = 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) {
|
||||||
|
// One bit a pixel becomes one byte a pixel: index 1 where the font has a dot and 0
|
||||||
|
// where it does not, which is what makes the two palette entries below mean ink and
|
||||||
|
// paper. Glyphs the font does not have are left blank rather than left as whatever was
|
||||||
|
// in tile memory.
|
||||||
|
memset(videoRAM + VIDEO_TILE_BASE, 0, (size_t)VIDEO_TILE_COUNT * VIDEO_TILE_BYTES);
|
||||||
|
for (int glyph = 0; glyph < CONSOLE_FONT_GLYPHS && glyph < VIDEO_TILE_COUNT; glyph++) {
|
||||||
|
uint8_t *tile = videoRAM + VIDEO_TILE_BASE + glyph * VIDEO_TILE_BYTES;
|
||||||
|
for (int y = 0; y < CONSOLE_FONT_BYTES; y++) {
|
||||||
|
const unsigned char row = consoleFont[glyph * CONSOLE_FONT_BYTES + y];
|
||||||
|
for (int x = 0; x < VIDEO_CELL_PIXELS; x++) {
|
||||||
|
tile[y * VIDEO_CELL_PIXELS + x] = (row & (0x80u >> x)) ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uint8_t *palette = videoRAM + VIDEO_PALETTE_BASE;
|
||||||
|
for (int bank = 0; bank < 8; bank++) {
|
||||||
|
// Colour on black, and then the same colour as paper with black ink, sixteen banks
|
||||||
|
// apart so that one bit turns either into the other.
|
||||||
|
memcpy(palette + (bank * 16 + 0) * VIDEO_PALETTE_BYTES, defaultPaper, 3);
|
||||||
|
memcpy(palette + (bank * 16 + 1) * VIDEO_PALETTE_BYTES, defaultInks[bank], 3);
|
||||||
|
memcpy(palette + ((bank + 8) * 16 + 0) * VIDEO_PALETTE_BYTES, defaultInks[bank], 3);
|
||||||
|
memcpy(palette + ((bank + 8) * 16 + 1) * VIDEO_PALETTE_BYTES, defaultPaper, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void videoPutCell(int screenRow, int column, uint8_t tile, uint8_t attribute) {
|
||||||
|
if (screenRow < 0 || screenRow >= rowsFor(mode)) return;
|
||||||
|
if (column < 0 || column >= columnsFor(mode)) return;
|
||||||
|
// ---- Where the caller means, not where the map begins ----
|
||||||
|
//
|
||||||
|
// Both origins, because a caller says "row three, column five OF THE SCREEN" and the
|
||||||
|
// screen is a window onto the map. The row origin has always been applied here - it is
|
||||||
|
// what makes the console's scrollback free - and the column origin has to be for the
|
||||||
|
// same reason, or text lands in the wrong cell the moment anything scrolls sideways.
|
||||||
|
//
|
||||||
|
// THE FINE OFFSETS ARE NOT APPLIED and must not be. They move the finished picture by
|
||||||
|
// less than a cell, and there is no such thing as less than a cell to write into.
|
||||||
|
const int mapRow = (scroll + screenRow) % VIDEO_MAP_ROWS;
|
||||||
|
const int mapColumn = (scrollColumn + column) % VIDEO_MAP_COLUMNS;
|
||||||
|
uint8_t *cell = videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE
|
||||||
|
+ mapColumn * VIDEO_CELL_BYTES;
|
||||||
|
cell[0] = tile;
|
||||||
|
cell[1] = attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
void videoScrollUp(void) {
|
||||||
|
scroll = (uint8_t)((scroll + 1) % VIDEO_MAP_ROWS);
|
||||||
|
// The row now at the bottom held whatever was there a ring ago, so it is cleared. The
|
||||||
|
// rows that went off the top are NOT cleared, which is the whole of the scrollback: a
|
||||||
|
// hundred rows of what has already been said, still sitting in the map.
|
||||||
|
const int bottom = rowsFor(mode) - 1;
|
||||||
|
const int mapRow = (scroll + bottom) % VIDEO_MAP_ROWS;
|
||||||
|
memset(videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE, 0, VIDEO_MAP_STRIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void videoReset(void) {
|
||||||
|
memset(videoRAM, 0, sizeof(videoRAM));
|
||||||
|
mode = VIDEO_MODE_40x25;
|
||||||
|
scroll = 0;
|
||||||
|
scrollColumn = 0;
|
||||||
|
fineX = 0;
|
||||||
|
fineY = 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t *videoMemory(uint32_t *capacity) {
|
||||||
|
*capacity = VIDEO_MEMORY_BYTES;
|
||||||
|
return videoRAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t videoWrite(uint8_t value, uint8_t port) {
|
||||||
|
switch (port) {
|
||||||
|
case VIDEO_MODE:
|
||||||
|
// A mode that does not exist is not taken. Refusing outright would be the other
|
||||||
|
// choice, but a screen is not the place to stop the machine: a program that
|
||||||
|
// asked for something impossible still has the screen it had.
|
||||||
|
if (value < VIDEO_MODE_COUNT) {
|
||||||
|
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.
|
||||||
|
scroll = (uint8_t)(value % VIDEO_MAP_ROWS);
|
||||||
|
break;
|
||||||
|
case VIDEO_SCROLL_COLUMN:
|
||||||
|
// The same ring the other way. A map row is 256 bytes and a cell is two, so
|
||||||
|
// there are 128 columns whatever the mode shows.
|
||||||
|
scrollColumn = (uint8_t)(value % VIDEO_MAP_COLUMNS);
|
||||||
|
break;
|
||||||
|
case VIDEO_FINE_X:
|
||||||
|
// The low three bits and nothing else. Eight is not one cell along, it is zero
|
||||||
|
// again - see the note by the port numbers about why this does not carry.
|
||||||
|
fineX = (uint8_t)(value & VIDEO_FINE_MASK);
|
||||||
|
break;
|
||||||
|
case VIDEO_FINE_Y:
|
||||||
|
fineY = (uint8_t)(value & VIDEO_FINE_MASK);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// 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.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t videoRead(uint8_t port) {
|
||||||
|
switch (port) {
|
||||||
|
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.
|
||||||
|
//
|
||||||
|
// The line goes with the flag, and for the stronger reason: a program that polls
|
||||||
|
// this port is not going to be the one that answers an interrupt, so a line left
|
||||||
|
// standing here is one nothing will ever take down.
|
||||||
|
frameWaiting = 0;
|
||||||
|
clearInterrupt(PORT_VIDEO);
|
||||||
|
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;
|
||||||
|
case VIDEO_SCROLL_COLUMN: return scrollColumn;
|
||||||
|
case VIDEO_FINE_X: return fineX;
|
||||||
|
case VIDEO_FINE_Y: return fineY;
|
||||||
|
// 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.
|
||||||
|
case VIDEO_COLUMNS: return (uint8_t)columnsFor(mode);
|
||||||
|
case VIDEO_ROWS: return (uint8_t)rowsFor(mode);
|
||||||
|
case VIDEO_SCROLL: return scroll;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void videoRender(void) {
|
||||||
|
if (mode == VIDEO_MODE_BITMAP) {
|
||||||
|
// ---- A byte a pixel, and nothing in the way ----
|
||||||
|
//
|
||||||
|
// No tile to look up and no attribute to add: the byte IS the palette index. Which
|
||||||
|
// is the whole difference between the two kinds of screen - a tile mode costs the
|
||||||
|
// CPU the number of cells that changed, and this costs it the number of pixels.
|
||||||
|
const uint8_t *palette = videoRAM + VIDEO_PALETTE_BASE;
|
||||||
|
const uint8_t *from = videoRAM + VIDEO_BITMAP_BASE;
|
||||||
|
uint8_t *out = pixels;
|
||||||
|
for (int at = 0; at < VIDEO_BITMAP_WIDTH * VIDEO_BITMAP_HEIGHT; at++) {
|
||||||
|
const uint8_t *entry = palette + from[at] * VIDEO_PALETTE_BYTES;
|
||||||
|
*out++ = entry[0];
|
||||||
|
*out++ = entry[1];
|
||||||
|
*out++ = entry[2];
|
||||||
|
}
|
||||||
|
renderedWidth = VIDEO_BITMAP_WIDTH;
|
||||||
|
renderedHeight = VIDEO_BITMAP_HEIGHT;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int columns = columnsFor(mode);
|
||||||
|
const int rows = rowsFor(mode);
|
||||||
|
const int width = columns * VIDEO_CELL_PIXELS;
|
||||||
|
|
||||||
|
const int height = rows * VIDEO_CELL_PIXELS;
|
||||||
|
|
||||||
|
// ---- One more row and one more column than fit ----
|
||||||
|
//
|
||||||
|
// With a fine offset the screen no longer starts on a cell boundary, so the first cell
|
||||||
|
// of each axis is partly above or left of the picture and one extra is needed at the far
|
||||||
|
// end to fill what that uncovered. Both are drawn and clipped, which is why every write
|
||||||
|
// below is guarded rather than trusted: the two edge cells are the only ones that can
|
||||||
|
// fall outside, but they fall outside on every frame that is not cell aligned.
|
||||||
|
for (int row = 0; row <= rows; row++) {
|
||||||
|
// The ring. Rows that scrolled off the top are still in the map, which is what
|
||||||
|
// makes scrollback free rather than something the console has to keep itself.
|
||||||
|
const int mapRow = (scroll + row) % VIDEO_MAP_ROWS;
|
||||||
|
const uint8_t *cells = videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE;
|
||||||
|
for (int column = 0; column <= columns; column++) {
|
||||||
|
const int mapColumn = (scrollColumn + column) % VIDEO_MAP_COLUMNS;
|
||||||
|
const uint8_t tile = cells[mapColumn * VIDEO_CELL_BYTES];
|
||||||
|
uint8_t attribute = cells[mapColumn * VIDEO_CELL_BYTES + 1];
|
||||||
|
// ---- The cursor, turned inside out ----
|
||||||
|
//
|
||||||
|
// Not a glyph of its own, because a block drawn over a cell hides what is in it
|
||||||
|
// and a person editing a line wants to see the character they are standing on.
|
||||||
|
// XOR 8 swaps a bank for its reverse, which is what the default palette is laid
|
||||||
|
// out to make possible.
|
||||||
|
//
|
||||||
|
// The phase comes from the machine's clock, so a screen saved at a given cycle
|
||||||
|
// count is the same screen every time.
|
||||||
|
if (cursorVisible && row == cursorAtRow && column == cursorAtColumn
|
||||||
|
&& ((videoNow / VIDEO_BLINK_CYCLES) & 1) == 0) {
|
||||||
|
attribute ^= 0x08;
|
||||||
|
}
|
||||||
|
// ---- The additive nibble ----
|
||||||
|
//
|
||||||
|
// The low nibble of the attribute is added to every palette index in the tile,
|
||||||
|
// sixteen at a time. A tile drawn in indices 0 to 15 therefore appears in any
|
||||||
|
// of sixteen colour schemes without a second copy of it in tile memory, and a
|
||||||
|
// tile that wants all 256 colours simply leaves the nibble at zero and gets
|
||||||
|
// them. One adder in hardware, and neither use costs the other anything.
|
||||||
|
const uint8_t bank = (uint8_t)((attribute & 0x0F) << 4);
|
||||||
|
const uint8_t *art = videoRAM + VIDEO_TILE_BASE + tile * VIDEO_TILE_BYTES;
|
||||||
|
for (int y = 0; y < VIDEO_CELL_PIXELS; y++) {
|
||||||
|
// Where this row of the cell lands once the view has been slid up by the
|
||||||
|
// fine offset. Negative means it is the part of the top cell that is off
|
||||||
|
// the screen, which is the whole point of drawing it.
|
||||||
|
const int atY = row * VIDEO_CELL_PIXELS + y - fineY;
|
||||||
|
if (atY < 0 || atY >= height) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int x = 0; x < VIDEO_CELL_PIXELS; x++) {
|
||||||
|
const int atX = column * VIDEO_CELL_PIXELS + x - fineX;
|
||||||
|
if (atX < 0 || atX >= width) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Wrapping, because a byte plus a byte is a byte. A tile using the
|
||||||
|
// high end of the palette with a nibble set comes round the bottom,
|
||||||
|
// which is what an adder does and what the manual says it does.
|
||||||
|
const uint8_t index = (uint8_t)(art[y * VIDEO_CELL_PIXELS + x] + bank);
|
||||||
|
const uint8_t *entry = videoRAM + VIDEO_PALETTE_BASE
|
||||||
|
+ index * VIDEO_PALETTE_BYTES;
|
||||||
|
uint8_t *out = pixels + (atY * width + atX) * 3;
|
||||||
|
out[0] = entry[0];
|
||||||
|
out[1] = entry[1];
|
||||||
|
out[2] = entry[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderedWidth = width;
|
||||||
|
renderedHeight = rows * VIDEO_CELL_PIXELS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t *videoPixels(int *width, int *height) {
|
||||||
|
*width = renderedWidth;
|
||||||
|
*height = renderedHeight;
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A binary PPM, because it is the smallest format that needs no library to write and no
|
||||||
|
// library to read - which matters when the thing reading it is a test script.
|
||||||
|
int videoWriteImage(const char *path) {
|
||||||
|
videoRender();
|
||||||
|
FILE *file = fopen(path, "wb");
|
||||||
|
if (file == NULL) {
|
||||||
|
fprintf(stderr, "Error: Couldn't write the screen to: %s\n", path);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fprintf(file, "P6\n%d %d\n255\n", renderedWidth, renderedHeight);
|
||||||
|
size_t bytes = (size_t)renderedWidth * (size_t)renderedHeight * 3;
|
||||||
|
size_t written = fwrite(pixels, 1, bytes, file);
|
||||||
|
fclose(file);
|
||||||
|
if (written != bytes) {
|
||||||
|
fprintf(stderr, "Error: The screen was not written whole to: %s\n", path);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
// video.h
|
||||||
|
// The Voyager's video device.
|
||||||
|
// Written by Anachronaut
|
||||||
|
|
||||||
|
#ifndef VIDEO_H
|
||||||
|
#define VIDEO_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// ---- What this is ----
|
||||||
|
//
|
||||||
|
// A tile engine. The CPU writes cell indices and the device expands them into pixels, which
|
||||||
|
// is the difference between a screen costing 2,000 bytes a frame and 64,000 - and at a
|
||||||
|
// megahertz that is the difference between a screen and no screen at all.
|
||||||
|
//
|
||||||
|
// It follows that COLOUR DEPTH IS FREE AT FRAME TIME. The map is the same size whether the
|
||||||
|
// tiles behind it are one bit deep or eight, because the depth lives in tile memory, which
|
||||||
|
// is written once when a program loads and not sixty times a second. So the tiles are eight
|
||||||
|
// bits: an 8x8 cell is 64 pixels and each one picks independently out of 256 colours, with
|
||||||
|
// no per-cell limit of the kind that made a Spectrum two and C64 multicolour four.
|
||||||
|
//
|
||||||
|
// ---- The device brings memory ----
|
||||||
|
//
|
||||||
|
// One bank, registered the way the disk's buffer is, so it costs a program nothing in Data
|
||||||
|
// Memory and keeps what is in it between frames. A program blits the region that changed
|
||||||
|
// and the rest stays as it was, which is the whole reason this is a bank rather than a
|
||||||
|
// window onto a port.
|
||||||
|
|
||||||
|
#define VIDEO_MEMORY_BYTES 0x10000
|
||||||
|
|
||||||
|
// Tile memory: 256 tiles of 8x8, one byte a pixel.
|
||||||
|
#define VIDEO_TILE_BASE 0x0000
|
||||||
|
#define VIDEO_TILE_BYTES 64
|
||||||
|
#define VIDEO_TILE_COUNT 256
|
||||||
|
|
||||||
|
// ---- The map, one page a row ----
|
||||||
|
//
|
||||||
|
// A row is padded to exactly 256 bytes whether the mode uses all of it or not, and that is
|
||||||
|
// not waste, it is arithmetic. THE MACHINE HAS NO MULTIPLY. On a 40 column screen every
|
||||||
|
// cursor move would otherwise need row times 40 in software, which is a tax on the most
|
||||||
|
// common operation in the whole system. At a page a row the address needs no arithmetic at
|
||||||
|
// all: the row number IS the high byte and the doubled column IS the low byte.
|
||||||
|
//
|
||||||
|
// It also frees the geometry from having to be a power of two, which is what lets the
|
||||||
|
// pixel resolution be whatever looks right.
|
||||||
|
#define VIDEO_MAP_BASE 0x4000
|
||||||
|
#define VIDEO_MAP_STRIDE 256
|
||||||
|
#define VIDEO_MAP_ROWS 128
|
||||||
|
#define VIDEO_MAP_COLUMNS (VIDEO_MAP_STRIDE / 2)
|
||||||
|
|
||||||
|
// Two bytes to a cell: which tile, and how to colour it.
|
||||||
|
#define VIDEO_CELL_BYTES 2
|
||||||
|
|
||||||
|
// ---- A bitmap, over the top of the tiles and the map ----
|
||||||
|
//
|
||||||
|
// THE SAME MEMORY MEANING DIFFERENT THINGS IN DIFFERENT MODES, which is what shared video
|
||||||
|
// memory has always been. There is no room for it to be anywhere else: 320 by 200 at a byte
|
||||||
|
// a pixel is 64,000 bytes and the whole bank is 65,536, so a bitmap that sat beside the
|
||||||
|
// tiles rather than on top of them would need a second bank for no reason except tidiness.
|
||||||
|
//
|
||||||
|
// What it costs is that the two do not coexist. Going to bitmap mode does not clear the text
|
||||||
|
// screen; it stops calling it a text screen. Coming back finds the tiles and the map holding
|
||||||
|
// whatever the picture put there, which is what taking the screen means.
|
||||||
|
#define VIDEO_BITMAP_BASE 0x0000
|
||||||
|
#define VIDEO_BITMAP_WIDTH 320
|
||||||
|
#define VIDEO_BITMAP_HEIGHT 200
|
||||||
|
|
||||||
|
// ---- The palette ----
|
||||||
|
//
|
||||||
|
// Four bytes an entry rather than three, for the same reason a map row is a page: entry n
|
||||||
|
// begins at n times four, which is a shift. Three would need a multiply the machine does
|
||||||
|
// not have. The fourth byte is unused and reads as whatever was put there.
|
||||||
|
//
|
||||||
|
// At the TOP of video memory, clear of everything else, because it is the one thing that has
|
||||||
|
// to mean the same in every mode - a bitmap needs colours as much as a tile does, and 64,000
|
||||||
|
// bytes of picture leaves nowhere in the middle for it to hide.
|
||||||
|
#define VIDEO_PALETTE_BASE 0xFC00
|
||||||
|
#define VIDEO_PALETTE_BYTES 4
|
||||||
|
#define VIDEO_PALETTE_SIZE 256
|
||||||
|
|
||||||
|
// ---- Modes ----
|
||||||
|
//
|
||||||
|
// Both are 8x8 cells over the same engine; only how many of them differ. The pixel count
|
||||||
|
// costs the CPU nothing, because it only ever writes the map - which is why the larger mode
|
||||||
|
// is affordable at all.
|
||||||
|
#define VIDEO_MODE_40x25 0
|
||||||
|
#define VIDEO_MODE_80x50 1
|
||||||
|
#define VIDEO_MODE_BITMAP 2
|
||||||
|
#define VIDEO_MODE_COUNT 3
|
||||||
|
|
||||||
|
#define VIDEO_CELL_PIXELS 8
|
||||||
|
#define VIDEO_MAX_WIDTH (80 * VIDEO_CELL_PIXELS)
|
||||||
|
#define VIDEO_MAX_HEIGHT (50 * VIDEO_CELL_PIXELS)
|
||||||
|
|
||||||
|
// How many characters across and down the screen is, and ZERO IN BITMAP MODE, where there is
|
||||||
|
// no such thing. The console asks, and a console told there are no columns has nowhere to
|
||||||
|
// put a glyph and does not try.
|
||||||
|
int videoTextRows(void);
|
||||||
|
|
||||||
|
// ---- Ports ----
|
||||||
|
//
|
||||||
|
// Sixteen, like the controller, and it interrupts on its base the way the disk established.
|
||||||
|
// Nothing interrupts yet; the frame interrupt is the next rung.
|
||||||
|
#define VIDEO_STATUS 0x30
|
||||||
|
#define VIDEO_MODE 0x31
|
||||||
|
#define VIDEO_COLUMNS 0x32
|
||||||
|
#define VIDEO_ROWS 0x33
|
||||||
|
#define VIDEO_SCROLL 0x34
|
||||||
|
#define VIDEO_CONTROL 0x35
|
||||||
|
|
||||||
|
// ---- The other three quarters of scrolling ----
|
||||||
|
//
|
||||||
|
// 0x34 moves the view a whole cell at a time and only downwards, which is a scrolling text
|
||||||
|
// screen and not a scrolling picture. These are the rest of it: a column origin so the map
|
||||||
|
// can be wider than the screen as well as taller, and a pixel remainder for each axis so the
|
||||||
|
// step can be one pixel rather than eight.
|
||||||
|
//
|
||||||
|
// COARSE AND FINE DO NOT CARRY INTO EACH OTHER. Fine is the low three bits of what is
|
||||||
|
// written and nothing else, so a program that scrolls past a cell edge advances the coarse
|
||||||
|
// register itself. That is what the machines this one is pretending to be did, it keeps each
|
||||||
|
// register meaning exactly one thing, and it means a program always knows where it is
|
||||||
|
// without reading anything back off the screen.
|
||||||
|
#define VIDEO_SCROLL_COLUMN 0x36
|
||||||
|
#define VIDEO_FINE_X 0x37
|
||||||
|
#define VIDEO_FINE_Y 0x38
|
||||||
|
|
||||||
|
// Eight pixels to a cell, so three bits say where inside one the view begins.
|
||||||
|
#define VIDEO_FINE_MASK 0x07
|
||||||
|
|
||||||
|
// ---- 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);
|
||||||
|
|
||||||
|
// ---- What the console needs to draw with ----
|
||||||
|
//
|
||||||
|
// The Voyager's console is a display controller: it takes a byte stream and puts glyphs on
|
||||||
|
// the screen, the way a video terminal's character generator does. That is a real kind of
|
||||||
|
// chip rather than an emulator convenience - but it does mean the console and a program
|
||||||
|
// drawing graphics are writing one screen, because a machine has one screen.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
void videoLoadFont(void);
|
||||||
|
|
||||||
|
// ---- The cursor ----
|
||||||
|
//
|
||||||
|
// Drawn by the device rather than by whatever is presenting, because on a machine with a
|
||||||
|
// screen the cursor IS a hardware feature - a display controller blinks it from a counter,
|
||||||
|
// and one drawn by the window would not be in a picture the machine saved.
|
||||||
|
//
|
||||||
|
// It blinks on the machine's own clock, so the phase is a pure function of the cycle count
|
||||||
|
// and a screen saved at a given cycle is the same screen every time.
|
||||||
|
#define VIDEO_BLINK_CYCLES 500000
|
||||||
|
|
||||||
|
void videoSetCursor(int row, int column, int visible);
|
||||||
|
|
||||||
|
// The machine's clock, for anything that has to know time has passed.
|
||||||
|
void videoTick(unsigned long now);
|
||||||
|
|
||||||
|
int videoColumns(void);
|
||||||
|
int videoRows(void);
|
||||||
|
|
||||||
|
// Screen coordinates, not map coordinates. The ring is the device's business, and a caller
|
||||||
|
// that had to know where the origin was would have to be told every time it moved.
|
||||||
|
void videoPutCell(int screenRow, int column, uint8_t tile, uint8_t attribute);
|
||||||
|
|
||||||
|
// Moves the origin on by a row and clears the one that has just come into view at the
|
||||||
|
// bottom - which is holding whatever was there 128 rows ago, since the map is a ring.
|
||||||
|
void videoScrollUp(void);
|
||||||
|
|
||||||
|
uint8_t *videoMemory(uint32_t *capacity);
|
||||||
|
|
||||||
|
uint8_t videoWrite(uint8_t value, uint8_t port);
|
||||||
|
uint8_t videoRead(uint8_t port);
|
||||||
|
|
||||||
|
// Turns what is in video memory into pixels. A pure function of that memory, so the same
|
||||||
|
// contents give the same picture with nobody watching - which is what lets the suite check
|
||||||
|
// a screen on a machine that has no display.
|
||||||
|
void videoRender(void);
|
||||||
|
|
||||||
|
// The pixels the last render produced, three bytes each, red then green then blue.
|
||||||
|
const uint8_t *videoPixels(int *width, int *height);
|
||||||
|
|
||||||
|
// Renders and writes a binary PPM. Returns 0 if it worked.
|
||||||
|
int videoWriteImage(const char *path);
|
||||||
|
|
||||||
|
#endif // VIDEO_H
|
||||||
@@ -0,0 +1,445 @@
|
|||||||
|
// voyager.c
|
||||||
|
|
||||||
|
// The Segan Voyager
|
||||||
|
// A SplitBit with a screen and a speaker attached
|
||||||
|
// Written by Anachronaut
|
||||||
|
//
|
||||||
|
// ---- What this is ----
|
||||||
|
//
|
||||||
|
// The same machine SplitBit runs, presented through a window instead of a terminal. Every
|
||||||
|
// instruction, every device and every cycle is in machine.c and shared; this file opens a
|
||||||
|
// window, gives the machine a slice of time per frame, and shows what came out.
|
||||||
|
//
|
||||||
|
// THAT ORDER MATTERS AND IS THE WHOLE DESIGN. The devices belong to the machine and advance
|
||||||
|
// on emulated cycles, so the same program produces the same frames and the same samples
|
||||||
|
// whether or not anybody is looking. Raylib presents; it does not decide. Which is what
|
||||||
|
// lets a test suite with no display hold this binary to the same behaviour as the other
|
||||||
|
// one.
|
||||||
|
//
|
||||||
|
// The window shows what the video device produced and decides nothing about it. Render is a
|
||||||
|
// pure function of video memory, so the same program draws the same picture whether or not
|
||||||
|
// anybody is watching - which is what lets a suite with no display check a screen.
|
||||||
|
|
||||||
|
#include "machine.h"
|
||||||
|
#include "video.h"
|
||||||
|
#include "sound.h"
|
||||||
|
#include "io.h"
|
||||||
|
#include "utility.h"
|
||||||
|
#include "raylib.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <getopt.h>
|
||||||
|
|
||||||
|
// The window opens at the largest screen the device can produce, doubled, because a 640 by
|
||||||
|
// 400 window is small on a modern display and a 320 by 200 one is a postage stamp.
|
||||||
|
#define SCREEN_SCALE 2
|
||||||
|
|
||||||
|
// ---- Running without a window ----
|
||||||
|
//
|
||||||
|
// Taken out of the arguments here rather than in the shared parser, because it is a fact
|
||||||
|
// about this front end and the shared parser should not learn about a window that only one
|
||||||
|
// binary has. Everything else on the command line means exactly what it means to SplitBit.
|
||||||
|
//
|
||||||
|
// It exists so the suite can run this binary at all: a test machine has no display, and a
|
||||||
|
// front end that could only be exercised by a person looking at it would be a front end
|
||||||
|
// nothing checks. Headless, Voyager must print byte for byte what SplitBit prints, and
|
||||||
|
// Tests/voyager.sh holds it to that.
|
||||||
|
static int takeHeadless(int *argc, char *argv[]) {
|
||||||
|
int headless = 0;
|
||||||
|
int out = 0;
|
||||||
|
for (int i = 0; i < *argc; i++) {
|
||||||
|
if (strcmp(argv[i], "--headless") == 0) {
|
||||||
|
headless = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
argv[out++] = argv[i];
|
||||||
|
}
|
||||||
|
argv[out] = NULL;
|
||||||
|
*argc = out;
|
||||||
|
return headless;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- The window, kept in one place ----
|
||||||
|
//
|
||||||
|
// Both the frame loop and the input hook have to be able to present, because a machine
|
||||||
|
// waiting for a key is still a machine somebody is looking at. A window that froze while a
|
||||||
|
// program asked a question would look broken every time it asked one.
|
||||||
|
static Texture2D screenTexture;
|
||||||
|
static int windowOpen = 0;
|
||||||
|
|
||||||
|
// ---- Keys are kept until they are asked for ----
|
||||||
|
//
|
||||||
|
// RAYLIB CLEARS ITS CHARACTER QUEUE ON EVERY POLL, and a poll happens inside EndDrawing, so
|
||||||
|
// a key survives exactly one frame unless something takes it. That is fine for a game that
|
||||||
|
// reads input every frame and wrong for everything else: Snake looks about ten times a
|
||||||
|
// second, so five keys in six were being thrown away by the next present before it ever
|
||||||
|
// glanced at them. The shell worked the whole time, because a blocking read presents and
|
||||||
|
// then looks immediately.
|
||||||
|
//
|
||||||
|
// So the window keeps its own queue, drained from Raylib at every present and emptied only
|
||||||
|
// when the console actually takes a byte. That is what the machine already promises - Snake's
|
||||||
|
// own comment says "the console keeps the next key until it is asked for" - and it makes the
|
||||||
|
// console's timing nobody else's business.
|
||||||
|
#define KEY_QUEUE 64
|
||||||
|
static unsigned char keyQueue[KEY_QUEUE];
|
||||||
|
static int keyHead = 0;
|
||||||
|
static int keyTail = 0;
|
||||||
|
|
||||||
|
static void keyPush(unsigned char byte) {
|
||||||
|
const int next = (keyTail + 1) % KEY_QUEUE;
|
||||||
|
if (next == keyHead) {
|
||||||
|
// Full, so the oldest goes. Somebody leaning on the keyboard while a program ignores
|
||||||
|
// it should not be able to push out what they typed most recently.
|
||||||
|
keyHead = (keyHead + 1) % KEY_QUEUE;
|
||||||
|
}
|
||||||
|
keyQueue[keyTail] = byte;
|
||||||
|
keyTail = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int keyTake(void) {
|
||||||
|
if (keyHead == keyTail) {
|
||||||
|
return CONSOLE_NOTHING_YET;
|
||||||
|
}
|
||||||
|
const int byte = keyQueue[keyHead];
|
||||||
|
keyHead = (keyHead + 1) % KEY_QUEUE;
|
||||||
|
return byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything Raylib has, taken before it can throw any of it away.
|
||||||
|
static void drainKeyboard(void) {
|
||||||
|
int character;
|
||||||
|
while ((character = GetCharPressed()) > 0) {
|
||||||
|
if (character < 128) {
|
||||||
|
keyPush((unsigned char)character);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int key;
|
||||||
|
while ((key = GetKeyPressed()) > 0) {
|
||||||
|
// Only the keys a character queue does not carry, because they are not characters.
|
||||||
|
// Everything else has already arrived above, and taking it again would double it.
|
||||||
|
switch (key) {
|
||||||
|
case KEY_ENTER: case KEY_KP_ENTER: keyPush('\n'); break;
|
||||||
|
case KEY_BACKSPACE: keyPush(0x08); break;
|
||||||
|
case KEY_TAB: keyPush('\t'); break;
|
||||||
|
case KEY_ESCAPE: keyPush(0x1B); break;
|
||||||
|
// ---- And the keys that are not characters at all ----
|
||||||
|
//
|
||||||
|
// These used to fall through the default below and vanish, because there was no
|
||||||
|
// byte to turn them into. There is now, and it is the console's rather than
|
||||||
|
// this window's - a terminal reaches the same values by a different road, and a
|
||||||
|
// program is entitled not to know which of the two it is talking to.
|
||||||
|
case KEY_UP: keyPush(CONSOLE_KEY_UP); break;
|
||||||
|
case KEY_DOWN: keyPush(CONSOLE_KEY_DOWN); break;
|
||||||
|
case KEY_LEFT: keyPush(CONSOLE_KEY_LEFT); break;
|
||||||
|
case KEY_RIGHT: keyPush(CONSOLE_KEY_RIGHT); break;
|
||||||
|
case KEY_HOME: keyPush(CONSOLE_KEY_HOME); break;
|
||||||
|
case KEY_END: keyPush(CONSOLE_KEY_END); break;
|
||||||
|
case KEY_DELETE: keyPush(CONSOLE_KEY_DELETE); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- The reset button ----
|
||||||
|
//
|
||||||
|
// EMULATOR MAGIC, AND KNOWN TO BE. There is no reset line on this machine yet and no keyboard
|
||||||
|
// controller to assert one: the window reaches in and pokes the same flag a program pokes
|
||||||
|
// through the machine port. When those are designed, a keyboard controller will have to see
|
||||||
|
// this gesture and pull reset REGARDLESS OF WHAT THE CPU IS DOING - which is the property
|
||||||
|
// that matters and the one a port write cannot have, since a port write needs a program
|
||||||
|
// willing and able to make it.
|
||||||
|
//
|
||||||
|
// The shape of that is already visible here. A reset is normally noticed between
|
||||||
|
// instructions, and a halted machine runs none - so the window asks every frame rather than
|
||||||
|
// leaving it to the machine to notice, which is what real hardware would do with a line.
|
||||||
|
//
|
||||||
|
// ON REAL HARDWARE THIS IS NOT A KEY AT ALL. A Voyager has a button on the case, and what a
|
||||||
|
// window has instead of a case is a gesture. So the gesture wants two properties a single
|
||||||
|
// key does not have.
|
||||||
|
//
|
||||||
|
// It must not be a key SOFTWARE MIGHT WANT. A machine with a keyboard has function keys on
|
||||||
|
// it, and something will eventually have a use for F12 - which is where this was, and which
|
||||||
|
// would have meant taking it away again later.
|
||||||
|
//
|
||||||
|
// And it must not be reachable BY ACCIDENT. Restarting the machine throws away everything in
|
||||||
|
// memory, and a single key that does that sits one mistake away from losing work. Three keys
|
||||||
|
// together are not pressed by mistake.
|
||||||
|
//
|
||||||
|
// Control, Shift and R. It was Control, Alt and Delete, which has meant this since 1981 and
|
||||||
|
// is the one gesture nobody has to be told the meaning of - AND WHICH CANNOT BE USED.
|
||||||
|
//
|
||||||
|
// It is a secure attention key. Every serious operating system reserves it so that it always
|
||||||
|
// reaches the system and never an application, precisely so that a program cannot imitate a
|
||||||
|
// login screen; on Windows an application cannot see it at all without a kernel driver, and
|
||||||
|
// on Linux the desktop takes it. That is not an oversight to work around - it is the same
|
||||||
|
// guarantee a reset button wants, being enforced one layer further down, and there is no
|
||||||
|
// call this program can make that would win the argument.
|
||||||
|
//
|
||||||
|
// So the gesture has to be one the host has no opinion about. Control and Shift with a
|
||||||
|
// letter is about as free as a combination gets: it is not window management, not a virtual
|
||||||
|
// terminal switch, and not a shortcut any desktop claims by default.
|
||||||
|
//
|
||||||
|
// If a platform does send a character for it, nothing comes of that either - whatever
|
||||||
|
// arrives is in memory that is about to be thrown away.
|
||||||
|
//
|
||||||
|
// What it does is what writing MACHINE_RESET does: the machine starts the way it started, so
|
||||||
|
// the boot chain runs again and finds whatever the disk now says to run. Which is what makes
|
||||||
|
// a bare metal program escapable - Once puts a demo in front of the next start and deletes
|
||||||
|
// the request before jumping, so a demo that has taken the whole machine is one gesture from
|
||||||
|
// the system coming back, rather than closing the window and opening it again.
|
||||||
|
static void checkResetButton(void) {
|
||||||
|
const int control = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL);
|
||||||
|
const int shift = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
|
||||||
|
if (control && shift && IsKeyPressed(KEY_R)) {
|
||||||
|
requestReset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- The speaker ----
|
||||||
|
//
|
||||||
|
// The device makes its samples on emulated cycles and puts them in a ring; this takes them
|
||||||
|
// out and hands them to Raylib. Nothing here decides what a sound is, the same as nothing in
|
||||||
|
// presentFrame decides what the screen looks like - which is why the headless binary and this
|
||||||
|
// one make the same sound, and why Tests/sound.sh can check a device with no speaker in it.
|
||||||
|
//
|
||||||
|
// A sub-buffer at a time, because that is the unit Raylib hands back when it has finished
|
||||||
|
// playing one. At 48,000 a second and sixty frames, a frame of machine time is 800 samples,
|
||||||
|
// so 1,024 is a little over one and there are two of them.
|
||||||
|
#define SOUND_BUFFER 1024
|
||||||
|
static AudioStream soundStream;
|
||||||
|
static int speakerOn;
|
||||||
|
|
||||||
|
// ---- When the machine cannot keep up, and when it runs away ----
|
||||||
|
//
|
||||||
|
// Both directions happen and neither should be a crash. The machine runs a slice per frame
|
||||||
|
// against the wall clock, so a host that stalls leaves the ring short and a host running
|
||||||
|
// --fast fills it faster than anything can play it. The device drops when full, which is the
|
||||||
|
// runaway case. This is the other one: what is missing is filled by HOLDING THE LAST SAMPLE
|
||||||
|
// rather than by zeroes, because a jump to silence and back is a click and a held level is
|
||||||
|
// not. It is still a glitch; it is the quieter kind.
|
||||||
|
static void feedSpeaker(void) {
|
||||||
|
static int16_t buffer[SOUND_BUFFER];
|
||||||
|
static int16_t lastSample;
|
||||||
|
while (IsAudioStreamProcessed(soundStream)) {
|
||||||
|
const int taken = soundTake(buffer, SOUND_BUFFER);
|
||||||
|
if (taken > 0) {
|
||||||
|
lastSample = buffer[taken - 1];
|
||||||
|
}
|
||||||
|
for (int i = taken; i < SOUND_BUFFER; i++) {
|
||||||
|
buffer[i] = lastSample;
|
||||||
|
}
|
||||||
|
UpdateAudioStream(soundStream, buffer, SOUND_BUFFER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void presentFrame(void) {
|
||||||
|
// The device turns video memory into pixels; this puts them on the glass. Everything
|
||||||
|
// that decides what the screen looks like is in the machine, where the suite can
|
||||||
|
// reach it.
|
||||||
|
videoRender();
|
||||||
|
int width, height;
|
||||||
|
const uint8_t *frame = videoPixels(&width, &height);
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
UpdateTextureRec(screenTexture, (Rectangle){ 0, 0, (float)width, (float)height },
|
||||||
|
frame);
|
||||||
|
}
|
||||||
|
BeginDrawing();
|
||||||
|
// Clearly not the screen. What is left over when the window's shape does not match the
|
||||||
|
// picture's is a bezel, and it should look like one rather than like more screen.
|
||||||
|
ClearBackground((Color){ 40, 40, 40, 255 });
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
// ---- Filling the window, in whole pixels ----
|
||||||
|
//
|
||||||
|
// The largest whole-number scale that still fits. Whole numbers because a 320 by 200
|
||||||
|
// picture stretched by 2.7 is a picture with some rows twice as tall as their
|
||||||
|
// neighbours, which on eight pixel glyphs is the difference between text and mush.
|
||||||
|
//
|
||||||
|
// The two modes are exactly a factor of two apart and the window opens at twice the
|
||||||
|
// larger, so both fill it exactly: 320 by 200 at four, and 640 by 400 at two.
|
||||||
|
// Changing mode therefore changes how sharp the screen is and not how big it is.
|
||||||
|
const int windowWidth = GetScreenWidth();
|
||||||
|
const int windowHeight = GetScreenHeight();
|
||||||
|
int scale = windowWidth / width;
|
||||||
|
const int fits = windowHeight / height;
|
||||||
|
if (fits < scale) scale = fits;
|
||||||
|
if (scale < 1) scale = 1;
|
||||||
|
const int drawnWidth = width * scale;
|
||||||
|
const int drawnHeight = height * scale;
|
||||||
|
Rectangle from = { 0, 0, (float)width, (float)height };
|
||||||
|
Rectangle to = {
|
||||||
|
(float)((windowWidth - drawnWidth) / 2),
|
||||||
|
(float)((windowHeight - drawnHeight) / 2),
|
||||||
|
(float)drawnWidth, (float)drawnHeight
|
||||||
|
};
|
||||||
|
DrawTexturePro(screenTexture, from, to, (Vector2){ 0, 0 }, 0.0f, WHITE);
|
||||||
|
}
|
||||||
|
EndDrawing();
|
||||||
|
// EndDrawing has just polled, which is the one moment Raylib's queues hold anything.
|
||||||
|
drainKeyboard();
|
||||||
|
checkResetButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the console asks while it is waiting. Presenting from in here is what keeps the
|
||||||
|
// window answering, and EndDrawing paces it, so waiting for a key costs a frame rather
|
||||||
|
// than a spin.
|
||||||
|
static int voyagerKey(int mayWait) {
|
||||||
|
if (!windowOpen) {
|
||||||
|
return CONSOLE_GONE;
|
||||||
|
}
|
||||||
|
// ---- The button has to reach a machine that is waiting ----
|
||||||
|
//
|
||||||
|
// A reset is acted on between instructions, and a machine blocked on a key is part way
|
||||||
|
// through one - so pressing the button while a program sits waiting would set the flag
|
||||||
|
// and nothing would ever come along to notice it. Which is precisely the moment a reset
|
||||||
|
// button earns its keep: a program that is stuck is the one you want to get out of.
|
||||||
|
//
|
||||||
|
// So the wait ends. The console treats that as the end of input, which it is for the
|
||||||
|
// machine that is about to stop existing, and the reset puts the console's input back.
|
||||||
|
//
|
||||||
|
// ASKED OF THE REQUEST ITSELF rather than remembered here. A flag of its own outlived
|
||||||
|
// the reset it belonged to: a program that never read the console - picture.bin, say,
|
||||||
|
// which draws and halts - left it set, and the NEXT machine's first read came back as
|
||||||
|
// the end of input. CosmOS booted and stopped immediately, having been told there was
|
||||||
|
// nobody there. There is one fact and it lives in one place.
|
||||||
|
if (resetIsPending()) {
|
||||||
|
return CONSOLE_GONE;
|
||||||
|
}
|
||||||
|
// Whatever is already waiting, however long ago it was typed. This is the answer to
|
||||||
|
// both questions, and asking it first is what makes a program that polls rarely see
|
||||||
|
// every key rather than one in six.
|
||||||
|
const int waiting = keyTake();
|
||||||
|
if (waiting != CONSOLE_NOTHING_YET) {
|
||||||
|
return waiting;
|
||||||
|
}
|
||||||
|
if (!mayWait) {
|
||||||
|
// A poll is a poll. Presenting here would charge a frame for every glance, and a
|
||||||
|
// program that looks in a loop would run at the frame rate.
|
||||||
|
return CONSOLE_NOTHING_YET;
|
||||||
|
}
|
||||||
|
if (WindowShouldClose()) {
|
||||||
|
windowOpen = 0;
|
||||||
|
return CONSOLE_GONE;
|
||||||
|
}
|
||||||
|
// Presenting is what keeps the window answering while the machine waits, and EndDrawing
|
||||||
|
// paces it, so waiting for a key costs a frame rather than a spin. It drains the
|
||||||
|
// keyboard on the way out, so anything just typed is here now.
|
||||||
|
presentFrame();
|
||||||
|
return keyTake();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
int headless = takeHeadless(&argc, argv);
|
||||||
|
|
||||||
|
EmulatorOptions options;
|
||||||
|
uint8_t result = parseOptions(argc, argv, &options);
|
||||||
|
if (result == OPTIONS_HELP) {
|
||||||
|
printf(" --headless Run with no window, which is how the tests run it.\n");
|
||||||
|
return 0;
|
||||||
|
} else if (result == OPTIONS_ERROR) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
char *programFile = NULL;
|
||||||
|
if (optind < argc) {
|
||||||
|
programFile = argv[optind];
|
||||||
|
optind++;
|
||||||
|
}
|
||||||
|
if (optind < argc) {
|
||||||
|
fprintf(stderr, "Error: Unexpected argument: %s\n", argv[optind]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Machine machine;
|
||||||
|
uint8_t started = machineStart(&machine, &options, programFile);
|
||||||
|
if (started == MACHINE_NOTHING_TO_RUN) {
|
||||||
|
fprintf(stderr, "Error: No boot image and no disk, so there is nothing to run.\n");
|
||||||
|
printHelp(argv[0]);
|
||||||
|
return 1;
|
||||||
|
} else if (started != MACHINE_OK) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headless) {
|
||||||
|
// The same three lines SplitBit runs, and deliberately so: a headless Voyager is
|
||||||
|
// not a reduced machine, it is the machine with nobody watching.
|
||||||
|
while (machineRunning(&machine)) {
|
||||||
|
machineRunSlice(&machine);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Resizable, because how big somebody wants a screen is not the machine's business.
|
||||||
|
// The picture is rescaled to whatever the window becomes, in whole pixels.
|
||||||
|
//
|
||||||
|
// And presented in step with the display. Without the hint the frame limiter sleeps
|
||||||
|
// towards sixty a second on its own clock, which beats against a screen refreshing on
|
||||||
|
// its own - some frames shown twice, some skipped, and the machine handed an uneven
|
||||||
|
// number of cycles each time because it takes them from the wall clock. The target
|
||||||
|
// stays as well, for a driver that ignores the hint.
|
||||||
|
SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_VSYNC_HINT);
|
||||||
|
InitWindow(VIDEO_MAX_WIDTH * SCREEN_SCALE, VIDEO_MAX_HEIGHT * SCREEN_SCALE,
|
||||||
|
"Segan Voyager");
|
||||||
|
SetTargetFPS(60);
|
||||||
|
// ---- Escape is a byte, not a way out ----
|
||||||
|
//
|
||||||
|
// Raylib closes a window on Escape unless it is told not to, and this machine sends
|
||||||
|
// Escape to the console like any other key. So a program reading keys could be
|
||||||
|
// ended by one of them, taking whatever was in memory with it - which is a poor way
|
||||||
|
// to find out that a default was left as it was found.
|
||||||
|
SetExitKey(KEY_NULL);
|
||||||
|
|
||||||
|
// ---- And a speaker, if the host has one ----
|
||||||
|
//
|
||||||
|
// Asked for rather than assumed: a machine with no audio device is a perfectly good
|
||||||
|
// machine to look at, and a front end that refused to start without one would make
|
||||||
|
// the window depend on something the picture does not need.
|
||||||
|
InitAudioDevice();
|
||||||
|
if (IsAudioDeviceReady()) {
|
||||||
|
SetAudioStreamBufferSizeDefault(SOUND_BUFFER);
|
||||||
|
soundStream = LoadAudioStream(SOUND_SAMPLE_RATE, 16, 1);
|
||||||
|
PlayAudioStream(soundStream);
|
||||||
|
speakerOn = 1;
|
||||||
|
}
|
||||||
|
// One texture, updated in place. Making a new one every frame would be a new
|
||||||
|
// allocation sixty times a second for a picture that is the same size every time.
|
||||||
|
Image blank = GenImageColor(VIDEO_MAX_WIDTH, VIDEO_MAX_HEIGHT, BLACK);
|
||||||
|
ImageFormat(&blank, PIXELFORMAT_UNCOMPRESSED_R8G8B8);
|
||||||
|
screenTexture = LoadTextureFromImage(blank);
|
||||||
|
UnloadImage(blank);
|
||||||
|
windowOpen = 1;
|
||||||
|
// The keyboard becomes the console's input, in place of a standard input the window
|
||||||
|
// does not have.
|
||||||
|
consoleSetInputHook(voyagerKey);
|
||||||
|
// ---- A slice a frame ----
|
||||||
|
//
|
||||||
|
// The machine gets its turn, then the window gets its turn. Closing the window stops
|
||||||
|
// the machine, and the machine halting leaves the window up so that whatever it drew
|
||||||
|
// is still there to look at - a program that ends should not take its output off the
|
||||||
|
// screen with it.
|
||||||
|
while (windowOpen && !WindowShouldClose()) {
|
||||||
|
// Before the running check, not after it: a machine that has stopped is the one
|
||||||
|
// worth restarting, and it is the one that cannot notice a reset by itself.
|
||||||
|
machineTakeReset(&machine);
|
||||||
|
if (machineRunning(&machine)) {
|
||||||
|
machineRunSlice(&machine);
|
||||||
|
}
|
||||||
|
presentFrame();
|
||||||
|
// After the slice, so what the machine just made is what gets played.
|
||||||
|
if (speakerOn) {
|
||||||
|
feedSpeaker();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
windowOpen = 0;
|
||||||
|
// Taken back before the machine stops, so nothing can ask a window that has gone.
|
||||||
|
consoleSetInputHook(NULL);
|
||||||
|
if (speakerOn) {
|
||||||
|
UnloadAudioStream(soundStream);
|
||||||
|
speakerOn = 0;
|
||||||
|
}
|
||||||
|
CloseAudioDevice();
|
||||||
|
UnloadTexture(screenTexture);
|
||||||
|
CloseWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
machineStop(&machine);
|
||||||
|
return machineReport(&machine);
|
||||||
|
}
|
||||||
@@ -436,7 +436,7 @@ $(BUILD)/%.bin: %.asm
|
|||||||
-include $(BINARIES:.bin=.d)
|
-include $(BINARIES:.bin=.d)
|
||||||
```
|
```
|
||||||
|
|
||||||
Programs/makefile in this repository builds every program that way, if you would like a longer example to copy.
|
The makefile in this repository builds every program that way, if you would like a longer example to copy.
|
||||||
|
|
||||||
## An Example SplitBit Assembly Program:
|
## An Example SplitBit Assembly Program:
|
||||||
|
|
||||||
|
|||||||
@@ -204,6 +204,9 @@ waitForDisk:
|
|||||||
ready:
|
ready:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Notice that on a device quick enough to finish before the first look, the `WAIT` in that loop
|
||||||
|
never runs at all. That is fine, and it is why the next section exists.
|
||||||
|
|
||||||
Test the device, then wait. If the device finishes in the gap between the two, its line is
|
Test the device, then wait. If the device finishes in the gap between the two, its line is
|
||||||
standing when WAIT runs and the wait is skipped rather than slept through.
|
standing when WAIT runs and the wait is skipped rather than slept through.
|
||||||
|
|
||||||
@@ -261,13 +264,16 @@ Nothing in that program names a Data Pointer, so all of it runs through Data Poi
|
|||||||
|
|
||||||
## The Console:
|
## The Console:
|
||||||
|
|
||||||
Port 0x00 is the oldest thing on this machine and it has not changed: writing sends a byte out, reading takes one in and waits until there is one. Every program ever written for SplitBit uses it that way and still does. What is new is that a program can say what it wants a keypress to mean, can ask whether a read would have to wait, and can arrange to be told when a byte arrives instead of having to ask at all.
|
Port 0x00 is the oldest thing on this machine and it has not changed: writing sends a byte out, reading takes one in and waits until there is one.
|
||||||
|
|
||||||
|
**On a machine with a screen, the console draws.** It is a display controller as well as a port: it owns a font, keeps a cursor, and scrolls - which is what a video terminal's character generator did, and is why a program written before there was a screen puts text on one without being changed. See Writing On The Screen below.
|
||||||
|
Every program ever written for SplitBit uses it that way and still does. What is new is that a program can say what it wants a keypress to mean, can ask whether a read would have to wait, and can arrange to be told when a byte arrives instead of having to ask at all.
|
||||||
|
|
||||||
| Port | Register |
|
| Port | Register |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 0x00 | Data. Writing sends a byte out, reading takes one in and waits for it. |
|
| 0x00 | Data. Writing sends a byte out, reading takes one in and waits for it. |
|
||||||
| 0x01 | Status. Bit 0 a byte is waiting, bit 1 input has ended, bit 2 the console is in key mode, bit 3 the console is set to interrupt. |
|
| 0x01 | Status. Bit 0 a byte is waiting, bit 1 input has ended, bit 2 the console is in key mode, bit 3 the console is set to interrupt, bit 4 a cursor is being shown. |
|
||||||
| 0x02 | Control. Bit 0 asks for key mode, bit 1 asks the console to interrupt when a byte arrives. Writing 0x00 asks for neither, which is how the console starts. |
|
| 0x02 | Control. Bit 0 asks for key mode, bit 1 asks the console to interrupt when a byte arrives, bit 2 asks for a cursor. Writing 0x00 asks for none of them, which is how the console starts. |
|
||||||
|
|
||||||
The control port's two bits are independent, and one write sets both. Everything the control port can ask for, the status port reports, so a program can put the console back the way it found it instead of assuming it knows.
|
The control port's two bits are independent, and one write sets both. Everything the control port can ask for, the status port reports, so a program can put the console back the way it found it instead of assuming it knows.
|
||||||
|
|
||||||
@@ -275,10 +281,36 @@ The control port's two bits are independent, and one write sets both. Everything
|
|||||||
|
|
||||||
In **line mode**, which is how the machine starts, the terminal holds what is typed until Return and does the echoing and the backspacing on the way. A program reading the data port gets a finished line, one byte at a time. This is what the machine has always done and what a shell wants.
|
In **line mode**, which is how the machine starts, the terminal holds what is typed until Return and does the echoing and the backspacing on the way. A program reading the data port gets a finished line, one byte at a time. This is what the machine has always done and what a shell wants.
|
||||||
|
|
||||||
In **key mode** the terminal stops holding the line. Keys arrive as they are pressed, and nothing echoes them, so a program that wants them seen has to send them back out itself. The editing goes with the echo: there is no backspace, because backspace was the terminal's doing and the terminal is no longer involved. That is not a choice this machine makes, it is what asking for keys means, and a program that wants keys is expected to want it.
|
In **key mode** the terminal stops holding the line. Keys arrive as they are pressed, and nothing echoes them, so a program that wants them seen has to send them back out itself. This is also the only mode in which the keys that are not characters arrive at all - see below. The editing goes with the echo: there is no backspace, because backspace was the terminal's doing and the terminal is no longer involved. That is not a choice this machine makes, it is what asking for keys means, and a program that wants keys is expected to want it.
|
||||||
|
|
||||||
A program is expected to put the console back in line mode before it finishes. CosmOS also does it whenever a program returns, because a program that stops early would otherwise hand back a shell with no echo, and a shell has no way to find out that happened.
|
A program is expected to put the console back in line mode before it finishes. CosmOS also does it whenever a program returns, because a program that stops early would otherwise hand back a shell with no echo, and a shell has no way to find out that happened.
|
||||||
|
|
||||||
|
### Keys That Are Not Characters:
|
||||||
|
|
||||||
|
An arrow key is not a letter, and for a long time there was no byte for one, so it did not reach this machine at all: a window threw it away for want of anywhere to put it, and a terminal sent an escape sequence which arrived in the middle of whatever was being read and made it unrecognisable.
|
||||||
|
|
||||||
|
The console names them now. Each arrives as one byte, above ASCII so that nothing written before them can collide:
|
||||||
|
|
||||||
|
| Byte | Key |
|
||||||
|
| --- | --- |
|
||||||
|
| 0x80 | Up |
|
||||||
|
| 0x81 | Down |
|
||||||
|
| 0x82 | Left |
|
||||||
|
| 0x83 | Right |
|
||||||
|
| 0x84 | Home |
|
||||||
|
| 0x85 | End |
|
||||||
|
| 0x86 | Delete, meaning the character under the cursor |
|
||||||
|
|
||||||
|
Backspace is 0x08 and always has been. It is a different key from Delete and does a different thing, which is why they are two values and not one.
|
||||||
|
|
||||||
|
0x80 to 0x8F belong to the console, so a program can tell a key from a character by testing that range. The values above 0x86 are not used yet.
|
||||||
|
|
||||||
|
**The console normalises, which is what it has always done.** Behind a window it turns the key somebody pressed into a byte; on a terminal it turns `ESC [ A` and its neighbours into the same byte. That is the same act it performs on Return and Backspace, and it is why a program does not have to know which of the two it is talking to. The translation happens only when there really is a terminal: a file or a pipe holds exactly the bytes somebody put in it, and a program reading one gets those bytes untouched - which is also how a test presses an arrow key.
|
||||||
|
|
||||||
|
**These arrive in key mode only.** Line mode delivers characters, and a program in line mode is being handed a line that something else has already finished editing, so a key meaning "move the cursor left" arrived too late to mean anything. The console drops them there. This is what a terminal does too: it has always given a program in line mode backspace and line kill, and has never given it arrow keys.
|
||||||
|
|
||||||
|
**What a key means is not the console's business.** Where the cursor goes, what the line looks like afterwards and what was typed before are all decisions, and decisions belong to whatever is reading - which on this machine is usually CosmOS, whose shell edits its own line. The console says which key was pressed and stops there, exactly as the disk says what a drive is and says nothing about what should be on it.
|
||||||
|
|
||||||
### Reading Without Waiting:
|
### Reading Without Waiting:
|
||||||
|
|
||||||
Reading the data port waits in **both** modes. The status port is how a program declines to wait, and keeping that in one place is deliberate: a read that sometimes blocked and sometimes did not, depending on a mode set somewhere else, would be a program that works until it does not.
|
Reading the data port waits in **both** modes. The status port is how a program declines to wait, and keeping that in one place is deliberate: a read that sometimes blocked and sometimes did not, depending on a mode set somewhere else, would be a program that works until it does not.
|
||||||
@@ -387,6 +419,40 @@ One last thing. An interrupt arriving while the Stack Pointer is somewhere unusu
|
|||||||
|
|
||||||
# When Something Else Wants Attention
|
# When Something Else Wants Attention
|
||||||
|
|
||||||
|
## Answering A Line:
|
||||||
|
|
||||||
|
A device raises its line when it has something to say, and something has to take it down
|
||||||
|
again. There are three things that do, and between them they cover every way a program can
|
||||||
|
find out that a device is finished.
|
||||||
|
|
||||||
|
**Being interrupted takes it down.** The dispatch does it, before the handler runs, which is
|
||||||
|
why a handler does not have to and why a handler that forgets does not spin.
|
||||||
|
|
||||||
|
**Being woken from `WAIT` with the Interrupt Flag down takes it down**, because nobody else is
|
||||||
|
going to. A masked program has nowhere to dispatch to, and a line left standing would be found
|
||||||
|
by the next `WAIT`, and the one after that, and the program would spin exactly as it did
|
||||||
|
before while appearing to sleep.
|
||||||
|
|
||||||
|
**And reading the port that answers the device takes it down.** For the console that is the
|
||||||
|
data port, because taking the byte is what answers the console. For the disk and the screen it
|
||||||
|
is the status port: the operation finished, and whether it worked is what Status is for.
|
||||||
|
|
||||||
|
That third one is the one to have in mind, because without it the loop above has a hole in it.
|
||||||
|
A program that polls, finds the device already done and never reaches its `WAIT` has used none
|
||||||
|
of the first two. The line stands - and it stands for the rest of the machine's life, because
|
||||||
|
nothing is ever going to come along and answer it.
|
||||||
|
|
||||||
|
**What that costs is not paid by the program that leaves it.** That program never set the
|
||||||
|
Interrupt Flag; it was masked throughout. The bill arrives later, at whoever does. The boot
|
||||||
|
chain reads the disk to load a program, leaves the line up, and hands over - and the loaded
|
||||||
|
program is interrupted on behalf of a read that finished before it existed, through a vector
|
||||||
|
table that has no entry for a device it never touched. It faults on the instruction after its
|
||||||
|
`SIF`. `Programs/Examples/tune.asm` is how this was found: run through `Once`, it set up its
|
||||||
|
whole sound and then died four bytes before playing a note.
|
||||||
|
|
||||||
|
So: a status read is an acknowledgement, and a program that wants to be interrupted by a
|
||||||
|
device should not poll it.
|
||||||
|
|
||||||
## Interrupts:
|
## Interrupts:
|
||||||
|
|
||||||
An interrupt is an involuntary transfer of control. A subroutine call is agreed to by the code that makes it, so CALL can leave Q and Data Pointer 3 alone and let a subroutine pass results back through them. An interrupt arrives in code that has never heard of it, where Q and DP3 are ordinary working registers, so it saves everything:
|
An interrupt is an involuntary transfer of control. A subroutine call is agreed to by the code that makes it, so CALL can leave Q and Data Pointer 3 alone and let a subroutine pass results back through them. An interrupt arrives in code that has never heard of it, where Q and DP3 are ordinary working registers, so it saves everything:
|
||||||
@@ -503,15 +569,409 @@ If nothing is installed for the vector a device refused with, the machine stops
|
|||||||
|
|
||||||
| Port | Device | Class |
|
| Port | Device | Class |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 0x00 - 0x02 | The console. See The Console. Writing to 0x00 sends a byte to standard output, reading takes one from standard input. It interrupts on 0x00, its base port, when asked to. | 0x02 |
|
| 0x00 - 0x05 | The console. See The Console. Writing to 0x00 sends a byte to standard output, reading takes one from standard input. It interrupts on 0x00, its base port, when asked to. | 0x02 |
|
||||||
| 0x10 | A test device. Writing anything to it puts its own line up, so that interrupt handling can be exercised without waiting on anything. The byte written is ignored. | 0x10 |
|
| 0x10 | A test device. Writing anything to it puts its own line up, so that interrupt handling can be exercised without waiting on anything. The byte written is ignored. | 0x10 |
|
||||||
| 0x11 | A device that refuses everything, in both directions, so that refusal can be exercised without the memory controller. | 0x11 |
|
| 0x11 | A device that refuses everything, in both directions, so that refusal can be exercised without the memory controller. | 0x11 |
|
||||||
| 0x20 - 0x23 | The disk. See Storage. It interrupts on 0x20, its base port. | 0x13 |
|
| 0x20 - 0x28 | The disk. See Storage. It interrupts on 0x20, its base port. | 0x13 |
|
||||||
| 0x13 | The machine itself. Writing 1 asks it to start over: whatever put the first instruction in memory does it again, and the CPU begins where the boot vector points. A port rather than a service, because a reset has to work when the system does not - and a program that owns the whole machine has no system to ask. The disk is not unplugged and keeps what was written to it; the vector table is cleared, because a handler left behind would aim an interrupt into a program that is no longer running. | 0x04 |
|
| 0x13 | The machine itself. Writing 1 asks it to start over: whatever put the first instruction in memory does it again, and the CPU begins where the boot vector points. A port rather than a service, because a reset has to work when the system does not - and a program that owns the whole machine has no system to ask. The disk is not unplugged and keeps what was written to it; the vector table is cleared, because a handler left behind would aim an interrupt into a program that is no longer running. | 0x04 |
|
||||||
| 0x12 | A device that owns 256 bytes of memory. Writing to its port fills that memory with the byte written, standing in for a disk controller reading a sector. Its memory is unreachable until it is registered as a bank. | 0x12 |
|
| 0x12 | A device that owns 256 bytes of memory. Writing to its port fills that memory with the byte written, standing in for a disk controller reading a sector. Its memory is unreachable until it is registered as a bank. | 0x12 |
|
||||||
|
| 0x30 - 0x3F | The screen. See The Screen. It brings video memory, which is unreachable until it is registered as a bank. | 0x14 |
|
||||||
|
| 0x40 - 0x4F | The sound device. See Making A Noise. Four channels, played by writing to ports; it brings no memory. | 0x15 |
|
||||||
|
| 0x50 - 0x54 | The timer. See Keeping Time. Counts the machine's cycles and says when a period has gone by. | 0x16 |
|
||||||
| 0xE0 - 0xEF | The memory controller. See The Memory Controller. | 0x03 |
|
| 0xE0 - 0xEF | The memory controller. See The Memory Controller. | 0x03 |
|
||||||
| 0xFF | The bus registry. See Asking What Is There. | 0x01 |
|
| 0xFF | The bus registry. See Asking What Is There. | 0x01 |
|
||||||
|
|
||||||
|
## The Screen:
|
||||||
|
|
||||||
|
A tile engine, on ports 0x30 to 0x3F. The CPU writes cell indices and the device turns them into pixels.
|
||||||
|
|
||||||
|
That indirection is the whole reason a screen is affordable here. At a megahertz a frame is 16,667 cycles, and pushing a full 320 by 200 picture a byte at a time is 64,000 bytes - four frames of work for one frame of screen. A 40 by 25 map is 2,000 bytes, and a program that changes two cells writes four. **The cost of a screen becomes the number of cells that changed rather than the number of pixels on it.**
|
||||||
|
|
||||||
|
It follows that colour depth is free. The map is the same size whatever is behind it, so the tiles are eight bits deep: an 8 by 8 cell is 64 pixels and each one picks independently out of 256 colours. There is no limit of two to a cell, or four, or sixteen.
|
||||||
|
|
||||||
|
### Video Memory:
|
||||||
|
|
||||||
|
One bank, brought by the device and reached only through the memory controller, like the disk's buffer. It keeps what is in it between frames, so a program writes the part that changed and the rest stays as it was.
|
||||||
|
|
||||||
|
| Address | Holds |
|
||||||
|
| --- | --- |
|
||||||
|
| 0x0000 - 0x3FFF | Tile memory. 256 tiles of 8 by 8, one byte a pixel, so tile n begins at n times 64. |
|
||||||
|
| 0x4000 - 0xBFFF | The map. 128 rows of 256 bytes. |
|
||||||
|
| 0x0000 - 0xF9FF | In bitmap mode, the picture instead: 64,000 bytes, one to a pixel. |
|
||||||
|
| 0xFC00 - 0xFFFF | The palette. 256 entries of four bytes: red, green, blue, and one unused. |
|
||||||
|
|
||||||
|
**The bitmap is the same memory as the tiles and the map**, which is what shared video memory has always been, and there is nowhere else it could be: 64,000 bytes of picture in a 65,536 byte bank leaves room for nothing beside it. Going to bitmap mode does not clear the text screen - it stops calling it one, and coming back finds the tiles and the map holding whatever the picture put there.
|
||||||
|
|
||||||
|
The palette is at the top, out of the way of both, because it is the one thing that means the same in every mode.
|
||||||
|
|
||||||
|
**A map row is a page whether the mode fills it or not**, and that is arithmetic rather than waste. This machine has no multiply, so on a 40 column screen every cursor move would otherwise cost a `row times 40` in software - a tax on the most common operation in the system. At a page a row there is no arithmetic at all: the row number is the high byte of the address and the doubled column is the low byte.
|
||||||
|
|
||||||
|
A palette entry is four bytes for the same reason. Entry n begins at n times four, which is a shift; three bytes would need a multiply.
|
||||||
|
|
||||||
|
### Cells:
|
||||||
|
|
||||||
|
Two bytes. The first says which tile, the second how to colour it.
|
||||||
|
|
||||||
|
The low nibble of the second byte is **added to every palette index in the tile, sixteen at a time**. A tile drawn in indices 0 to 15 therefore appears in any of sixteen colour schemes without a second copy of it in tile memory. A tile that wants all 256 colours leaves the nibble at zero and gets them. The addition wraps, because a byte plus a byte is a byte.
|
||||||
|
|
||||||
|
The high nibble is reserved and should be left at zero, so that a meaning can be given to it later without changing what already-written programs mean.
|
||||||
|
|
||||||
|
### Registers:
|
||||||
|
|
||||||
|
| Port | Register |
|
||||||
|
| --- | --- |
|
||||||
|
| 0x30 | Status. Bit 0 a frame has gone by, bit 1 the screen is set to interrupt. |
|
||||||
|
| 0x31 | Mode. |
|
||||||
|
| 0x32 | Columns, read only. |
|
||||||
|
| 0x33 | Rows, read only. |
|
||||||
|
| 0x34 | Scroll row. Which of the map's 128 rows is drawn at the top. |
|
||||||
|
| 0x35 | Control. Bit 0 asks to be interrupted at each frame. |
|
||||||
|
| 0x36 | Scroll column. Which of the map's 128 columns is drawn at the left. |
|
||||||
|
| 0x37 | Fine X. How many pixels into that column the screen begins, 0 to 7. |
|
||||||
|
| 0x38 | Fine Y. How many pixels into that row the screen begins, 0 to 7. |
|
||||||
|
|
||||||
|
| Mode | Screen | Cells |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 0 | 320 by 200 | 40 by 25 |
|
||||||
|
| 1 | 640 by 400 | 80 by 50 |
|
||||||
|
| 2 | 320 by 200 | none: a byte a pixel |
|
||||||
|
|
||||||
|
The first two are 8 by 8 cells over the same engine, and the pixel count costs a program nothing, because it only ever writes the map.
|
||||||
|
|
||||||
|
**Mode 2 is the other kind of screen**, where a byte is a palette index and there is no tile to look it up in and no attribute to add. What it costs is the other way round: a whole picture is 64,000 bytes, four frames of work at a megahertz, so it is the mode to draw in and leave alone or to change a corner of, not the mode to animate all of. `Programs/Examples/picture.asm` fills one in 127 bytes of program.
|
||||||
|
|
||||||
|
**A bitmap has no columns and no rows**, and asking says so: both registers read zero, which is the true answer rather than a leftover from the last mode. The console asks, and a console told there is no character screen has nowhere to put a glyph and draws nothing - it still says everything down the serial line. The alternative is what a machine with shared video memory really does, which is scribble marks nobody can read across somebody's picture. A mode that does not exist is not taken, and is not a fault either: a screen is a poor place to stop the machine, and a program that asked for something impossible still has the screen it had.
|
||||||
|
|
||||||
|
How big the screen is, is asked for rather than assumed. A program written once can find out what it is running on.
|
||||||
|
|
||||||
|
### The Frame:
|
||||||
|
|
||||||
|
A screen finishes drawing sixty times a second and then has a moment before it starts again. That moment is the one safe time to change what it is drawing - and it is also **the only regular beat this machine has.** There is no clock here. Every program that wanted to happen at a certain speed has until now counted instructions and hoped, which is why Snake's pause quietly halved the day a cycle stopped being an instruction and became a memory access.
|
||||||
|
|
||||||
|
Sixty a second, counted in the machine's own cycles rather than the host's. So a program sees the same number of frames in the same number of cycles however fast anything really ran, which is what makes a frame something a test can count and a recorded result can contain.
|
||||||
|
|
||||||
|
**Status bit 0 goes up when a frame has gone by, and reading the status port puts it down.** Looking is what answers it: a frame that has been noticed is not still waiting to be noticed, and a program polling in a loop would otherwise see the first frame for ever.
|
||||||
|
|
||||||
|
**Control bit 0 asks to be interrupted instead**, on hardware vector 0x30, which is the screen's base port. It is **off when the machine starts**, and that is not caution for its own sake: 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. Asking to stop takes down any request already standing, for the same reason the console's interrupt bit does.
|
||||||
|
|
||||||
|
More than one frame can go by between two looks - the machine runs in batches, and a slow host covers several at once. The flag and the line are each one thing, so several frames still mean one of each. **A missed frame is missed**, which is what missing one means.
|
||||||
|
|
||||||
|
This is what `WAIT` was built for. A program does its work, waits, and is woken:
|
||||||
|
|
||||||
|
```
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x35 ; Interrupt me at each frame
|
||||||
|
SIF
|
||||||
|
loop:
|
||||||
|
; ... draw ...
|
||||||
|
WAIT ; Nothing to do until the screen says so
|
||||||
|
BRI loop
|
||||||
|
```
|
||||||
|
|
||||||
|
A machine doing that is asleep between frames rather than spinning, and the difference is visible: the cycles it spent are counted as idle rather than as bus, so a program that waited properly and one that polled in a loop can be told apart even though they print the same thing and take the same time.
|
||||||
|
|
||||||
|
### Scrolling:
|
||||||
|
|
||||||
|
**The map is a ring, and the Scroll register says which of its 128 rows is drawn at the top.** Screen row *r* shows map row *scroll + r*, wrapped.
|
||||||
|
|
||||||
|
Scrolling therefore moves a register and no memory at all. That is not a small saving. Moving a 40 by 25 screen up one line is 1,920 bytes inside one bank, which is 1,920 cycles even with the controller widened - twelve percent of a frame, for one line. A program printing a single page would spend six frames shuffling memory. Here it is one write to a port.
|
||||||
|
|
||||||
|
And the rows that scrolled off are still in the map, which is where a terminal on this machine gets scrollback without having to keep any.
|
||||||
|
|
||||||
|
**The columns are the same ring the other way.** A map row is 256 bytes and a cell is two, so there are 128 of them whatever the mode shows - 88 more than a 40 column screen displays, and 48 more than an 80. Scroll column says which one is at the left, and screen column *c* shows map column *scroll column + c*, wrapped. A map wider than the screen costs nothing to have, because the map is that wide already.
|
||||||
|
|
||||||
|
### Scrolling By Less Than A Cell:
|
||||||
|
|
||||||
|
The two registers above move the view a whole cell at a time, which is a scrolling text screen rather than a scrolling picture: eight pixels is a long way to jump sixty times a second. **Fine X and Fine Y are the remainder** - how far into the cell at the origin the screen actually starts. Together the four registers place the view anywhere in the map to the pixel.
|
||||||
|
|
||||||
|
The screen no longer begins on a cell boundary when a fine register is not zero, so the cells at two edges are partly off it. That is the device's problem and not a program's: it draws one more row and one more column than fit and clips them.
|
||||||
|
|
||||||
|
**Fine does not carry into coarse.** Writing 8 to a fine register is writing 0, because only the low three bits of it mean anything - it is not one cell along. A program scrolling past a cell edge advances the coarse register itself:
|
||||||
|
|
||||||
|
```asm
|
||||||
|
; One pixel to the left, carrying when it runs out of cell.
|
||||||
|
SETD.0 FineX
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
INIB 0x07
|
||||||
|
AND
|
||||||
|
STQ.0
|
||||||
|
BNQ scrolled ; Still inside the cell.
|
||||||
|
SETD.0 CoarseX
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
OUTA 0x36
|
||||||
|
scrolled:
|
||||||
|
```
|
||||||
|
|
||||||
|
The alternative was to let a write of 8 step the column and set the fine part to zero, and it was rejected for one reason: a program that scrolls has to know where it has got to, and if the hardware carries then the only way to find out is to read the register back. Keeping them apart means the program already knows, because it did the arithmetic.
|
||||||
|
|
||||||
|
**The fine registers move the picture and nothing else.** Writing a character still lands in a whole cell, because there is no such thing as less than a cell to write into - so a program may scroll to any pixel and the console's idea of where row three, column five is does not move underneath it. The coarse registers are the ones the console follows, and it has always followed the row.
|
||||||
|
|
||||||
|
**None of the four does anything in bitmap mode**, which has no map to slide.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
At reset the font is expanded into tile memory and the palette is given sixteen ink and paper pairs. See Colour below.
|
||||||
|
|
||||||
|
The font is in ASCII order, so a byte becomes a glyph by subtracting 32. Bytes below that have no glyph and are not drawn; three of them do something instead.
|
||||||
|
|
||||||
|
| Byte | Does |
|
||||||
|
| --- | --- |
|
||||||
|
| 0x0A | Newline. The cursor goes to the start of the next row, and at the last row the screen scrolls instead. |
|
||||||
|
| 0x0D | Carriage return. The cursor goes to the start of the row it is on. |
|
||||||
|
| 0x08 | Backspace. The cursor steps back and rubs out what was there. |
|
||||||
|
|
||||||
|
Writing past the last column wraps to the next row, the same as a newline.
|
||||||
|
|
||||||
|
### Colour:
|
||||||
|
|
||||||
|
A glyph is drawn in palette indices 0 and 1 - paper and ink - and a cell's attribute nibble adds sixteen to both. **So sixteen banks is sixteen ink and paper pairs**, and a text attribute system costs one nibble and no hardware at all.
|
||||||
|
|
||||||
|
Which pair the console draws in is the Attribute register, 0x06. Everything written after it is drawn that way, until it changes.
|
||||||
|
|
||||||
|
The palette a machine wakes up with is arranged so that **highlighting is one bit**:
|
||||||
|
|
||||||
|
| Attribute | Paper | Ink |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 0 | Black | Grey |
|
||||||
|
| 1 to 7 | Black | Red, green, yellow, blue, magenta, cyan, white |
|
||||||
|
| 8 to 15 | The same seven and grey | Black |
|
||||||
|
|
||||||
|
So `attribute XOR 8` turns any pair inside out, which is what a highlighted line wants and how the cursor is drawn. Bank 0 is grey on black, which is what plain text has always been.
|
||||||
|
|
||||||
|
**That arrangement is a convention rather than a rule of the machine.** A program that wants different colours writes its own palette, and one that wants thirty-two of something rather than sixteen pairs can have that too - the device only ever adds the nibble and looks the answer up.
|
||||||
|
|
||||||
|
The palette lives at 0xFC00 in video memory, four bytes an entry - red, green, blue, and one spare - so entry *n* begins at 0xFC00 plus *n* times four. Video memory belongs to the screen rather than to the program, so it is written the way every device's memory is written: registered as a bank, and reached through the memory controller.
|
||||||
|
|
||||||
|
`Programs/Examples/colours.asm` does all of that in eighty lines and prints the result. It shows the sixteen pairs, shows what XOR 8 does to each, and then changes one of them by writing three bytes into the palette, so that the difference between using the colours a machine wakes up with and choosing your own is visible in one program.
|
||||||
|
|
||||||
|
### Moving The Cursor:
|
||||||
|
|
||||||
|
Three more registers, because that is how this machine talks to everything else.
|
||||||
|
|
||||||
|
| Port | Register |
|
||||||
|
| --- | --- |
|
||||||
|
| 0x03 | Cursor row. Read and write. |
|
||||||
|
| 0x04 | Cursor column. Read and write. |
|
||||||
|
| 0x05 | Command. Write 1 to clear the screen, which also puts the cursor at the top left. |
|
||||||
|
| 0x06 | Attribute. Read and write. |
|
||||||
|
|
||||||
|
**A cursor is shown only when it is asked for**, with bit 2 of the Control port, and status bit 4 says whether one is being shown. Off is the right default for a machine: a program painting its own screen does not want something blinking in the middle of it, and a system that reads lines from a person turns it on.
|
||||||
|
|
||||||
|
It is drawn by turning its cell inside out rather than by putting a block over it, so the character underneath stays readable - which matters to somebody editing a line. And it blinks **on the machine's own clock**, half a second on and half a second off, so the picture at a given cycle count is the same picture every time and a saved screen is not a matter of luck.
|
||||||
|
|
||||||
|
Both counted from zero, and both **readable**, which is the thing worth having: a routine that wants to put the cursor back where it found it asks where that was.
|
||||||
|
|
||||||
|
A cursor sent past the edge is clamped rather than refused. It has an obvious place to be, and stopping the machine over one would be a poor trade.
|
||||||
|
|
||||||
|
Clearing does not touch the scrollback. It clears what is on the screen, and what has already gone off the top is still in the map where the Scroll register can find it.
|
||||||
|
|
||||||
|
**There is no escape sequence here, and there should not be.** ANSI exists because a screen used to be on the other end of a serial line and a byte stream was the only channel there was. This screen is memory the program can already address, and reaching it by sending characters for a parser to take apart is a middleman for something the machine does better - clearing by writing 1 to a port costs one command, against a thousand cells walked one at a time.
|
||||||
|
|
||||||
|
What a program on the other end of an actual serial line sees is a different question, and the answer is that the console sends it the escapes it needs. That is the emulator bridging to a host terminal, the same job it does reading standard input, and it is not part of this machine.
|
||||||
|
|
||||||
|
**Scrolling moves the video device's Scroll register and no memory at all.** The row that comes into view at the bottom is cleared, because the map is a ring and it is holding whatever was there 128 rows ago. The rows that go off the top are *not* cleared, and that is the point: a hundred rows of what has already been said are still in the map, so a machine has scrollback without anything having to keep it.
|
||||||
|
|
||||||
|
**It is one screen.** A program that writes its own tiles and its own map has taken the screen, and a console still writing characters into it will scribble on what that program drew. This is not an oversight to be worked around - it is what one screen means, and it is why a program that wants the screen takes it.
|
||||||
|
|
||||||
|
## Making A Noise:
|
||||||
|
|
||||||
|
Four channels on ports 0x40 to 0x4F. Each one is a whole voice - two oscillators, two
|
||||||
|
envelopes, a filter and the routing between them - and it keeps its settings between notes.
|
||||||
|
Channel two is channel two: a program sets up a sound once and then plays it, the same way it
|
||||||
|
sets up a tile once and then places it.
|
||||||
|
|
||||||
|
### Why It Is Six Ports And Not Forty:
|
||||||
|
|
||||||
|
A voice has around forty settings and there are four of them, so a port for each would spend
|
||||||
|
more than half of the machine's whole port space on one device. Instead there is a **selector
|
||||||
|
and a value**: say which channel, say which setting, write it. Three writes to change one
|
||||||
|
thing.
|
||||||
|
|
||||||
|
That is the right price because of *when* a program pays it. Patches are loaded; notes are
|
||||||
|
played. Changing a setting happens when a program starts or when an instrument changes, and
|
||||||
|
three writes there costs nothing anybody can hear. Playing a note happens in the inner loop of
|
||||||
|
a music routine, and that is two writes with no selector machinery at all.
|
||||||
|
|
||||||
|
### Registers:
|
||||||
|
|
||||||
|
| Port | Register |
|
||||||
|
| --- | --- |
|
||||||
|
| 0x40 | Status. Bit 0, some channel is still sounding. |
|
||||||
|
| 0x41 | Channel, 0 to 3. Anything larger wraps, so a program cannot select a channel that is not there. |
|
||||||
|
| 0x42 | Which setting the next write to 0x43 means. |
|
||||||
|
| 0x43 | The value of that setting, for the selected channel. |
|
||||||
|
| 0x44 | Note. Writing a MIDI note number **starts it**: 60 is middle C, and every 12 is an octave. |
|
||||||
|
| 0x45 | Gate. Writing zero releases the note and lets it fade; writing anything else starts the last note again. |
|
||||||
|
| 0x46 | Volume, for the whole device. |
|
||||||
|
|
||||||
|
Reading 0x41, 0x42 and 0x44 gives back what is in them, so a routine can save and restore the
|
||||||
|
selection around an interrupt.
|
||||||
|
|
||||||
|
### The Shortest Program That Makes A Sound:
|
||||||
|
|
||||||
|
```
|
||||||
|
RSTA
|
||||||
|
OUTA 0x41 ; Channel 0
|
||||||
|
INIA 0d60
|
||||||
|
OUTA 0x44 ; Middle C, which starts it
|
||||||
|
```
|
||||||
|
|
||||||
|
**Every channel arrives able to make a sound**: one oscillator switched on at full gain, a
|
||||||
|
plain triangle wave, an envelope that fades in and holds. Writing a note number is the whole
|
||||||
|
of playing a note, and a program only reaches for the settings when it wants a different
|
||||||
|
sound rather than a sound at all.
|
||||||
|
|
||||||
|
The second oscillator arrives switched off, and that is not the same as arriving silent. **The
|
||||||
|
two oscillators are averaged rather than added**, so switching the second one on halves the
|
||||||
|
first whatever gain it has - which is what keeps two of them from clipping, and which means
|
||||||
|
there is no setting of `active` that costs nothing. One oscillator is the plain case, and
|
||||||
|
asking for two is something a program says out loud:
|
||||||
|
|
||||||
|
```
|
||||||
|
INIA 0x15
|
||||||
|
OUTA 0x42 ; Oscillator 1, on
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x43
|
||||||
|
INIA 0x11
|
||||||
|
OUTA 0x42 ; and how loud
|
||||||
|
INIA 0xC0
|
||||||
|
OUTA 0x43
|
||||||
|
```
|
||||||
|
|
||||||
|
### Settings:
|
||||||
|
|
||||||
|
The high nibble says which part of the voice, the low nibble which setting of it.
|
||||||
|
|
||||||
|
| Number | Part |
|
||||||
|
| --- | --- |
|
||||||
|
| 0x00 - 0x0F | Oscillator 0. |
|
||||||
|
| 0x10 - 0x1F | Oscillator 1. |
|
||||||
|
| 0x20 - 0x2F | The amplitude envelope. |
|
||||||
|
| 0x30 - 0x3F | The modulation envelope. |
|
||||||
|
| 0x40 - 0x4F | The filter. |
|
||||||
|
| 0x50 | What shapes the channel's level. |
|
||||||
|
| 0x60 - 0x6F | LFO 0. |
|
||||||
|
| 0x70 - 0x7F | LFO 1. |
|
||||||
|
|
||||||
|
| Oscillator | Setting |
|
||||||
|
| --- | --- |
|
||||||
|
| 0 | Waveform: 0 sine, 1 triangle, 2 saw, 3 ramp, 4 pulse, 5 noise. Anything larger wraps. |
|
||||||
|
| 1 | Gain. Silent at zero, which is where it starts. |
|
||||||
|
| 2 | Pulse width, for the pulse wave. |
|
||||||
|
| 3 | Detune, centred on 128, an octave either way. A step is about nine cents. |
|
||||||
|
| 4 | Octave, centred on 128, two either way. |
|
||||||
|
| 5 | On, or off at zero. |
|
||||||
|
| 6, 7 | What modulates the pulse width, and how much. |
|
||||||
|
| 8, 9 | What modulates the detune, and how much. |
|
||||||
|
| 10, 11 | What modulates the gain, and how much. |
|
||||||
|
|
||||||
|
| Envelope | Setting |
|
||||||
|
| --- | --- |
|
||||||
|
| 0 | Attack. |
|
||||||
|
| 1 | Decay. |
|
||||||
|
| 2 | Sustain, the level it holds at while the note is held. |
|
||||||
|
| 3 | Release. |
|
||||||
|
|
||||||
|
| Filter | Setting |
|
||||||
|
| --- | --- |
|
||||||
|
| 0x40 | On, or off at zero. |
|
||||||
|
| 0x41 | Type: 0 low pass, 1 high pass, 2 band pass. Anything larger wraps. |
|
||||||
|
| 0x42 | Cutoff. |
|
||||||
|
| 0x43 | Resonance. |
|
||||||
|
| 0x44, 0x45 | What modulates the cutoff, and how much. |
|
||||||
|
| 0x46, 0x47 | What modulates the resonance, and how much. |
|
||||||
|
|
||||||
|
| LFO | Setting |
|
||||||
|
| --- | --- |
|
||||||
|
| 0 | On, or off at zero. |
|
||||||
|
| 1 | Waveform, from the same six. |
|
||||||
|
| 2 | Rate. |
|
||||||
|
|
||||||
|
Anywhere a setting asks *what modulates* something, the answer is one of these:
|
||||||
|
|
||||||
|
| Value | Source |
|
||||||
|
| --- | --- |
|
||||||
|
| 0 | Nothing. |
|
||||||
|
| 1 | The amplitude envelope. |
|
||||||
|
| 2 | The modulation envelope. |
|
||||||
|
| 3 | LFO 0. |
|
||||||
|
| 4 | LFO 1. |
|
||||||
|
|
||||||
|
**The two LFOs belong to the device and not to a channel**, so writing 0x60 to 0x7F ignores
|
||||||
|
whichever channel is selected. That is what makes them useful: a vibrato that every voice
|
||||||
|
shares is one wobble rather than four that drift apart.
|
||||||
|
|
||||||
|
### What A Byte Means:
|
||||||
|
|
||||||
|
Everything here is a byte, and a synthesizer wants seconds and hertz. How the one becomes the
|
||||||
|
other is chosen for **where the useful part of the range is**, not for whatever arithmetic is
|
||||||
|
tidiest.
|
||||||
|
|
||||||
|
| Kind of setting | 0 to 255 becomes |
|
||||||
|
| --- | --- |
|
||||||
|
| Times: attack, decay, release | Nought to four seconds, squared. |
|
||||||
|
| Levels: gain, sustain, resonance, volume | Nought to the most there is, evenly. |
|
||||||
|
| Cutoff, LFO rate | 20 Hz to 20 kHz, and 0.05 Hz to 20 Hz: exponential. |
|
||||||
|
| Detune, octave, and every modulation depth | Centred on 128, so half is no change and either side is a direction. |
|
||||||
|
|
||||||
|
Times are squared because the difference between five and fifty milliseconds is the whole
|
||||||
|
character of a percussive sound, and the difference between three seconds and four is nothing
|
||||||
|
anybody can hear. A byte spread evenly over four seconds would spend nine tenths of itself on
|
||||||
|
the part that does not matter. Cutoff and rate are exponential for the same reason, since
|
||||||
|
pitch is logarithmic and so is where a filter sounds like it is.
|
||||||
|
|
||||||
|
### Level:
|
||||||
|
|
||||||
|
Setting 0x50 says what shapes the channel's level, out of the same list of sources. It is
|
||||||
|
normally the amplitude envelope, which is what an amplitude envelope is for, and it can be set
|
||||||
|
to **nothing** - a channel whose level nothing shapes plays flat out until it is gated off.
|
||||||
|
|
||||||
|
That sounds like a small thing and is not. Without it the amplitude envelope is welded to the
|
||||||
|
output, so an envelope routed somewhere useful - opening the filter, bending a pitch - still
|
||||||
|
has to be shaped like something you would want to hear, and a snare that wants a click of
|
||||||
|
filter sweep and a flat body cannot have both.
|
||||||
|
|
||||||
|
### Knowing When It Has Finished:
|
||||||
|
|
||||||
|
The status port's bit 0 is set while any channel is still sounding, so a routine can wait for
|
||||||
|
a sound to end rather than counting cycles.
|
||||||
|
|
||||||
|
There is one rule about when a note ends, and it is worth stating on its own because the
|
||||||
|
obvious guess is wrong. **A note sounds until the gate is dropped.** What the envelope is
|
||||||
|
doing does not come into it.
|
||||||
|
|
||||||
|
In particular, a note whose sustain is nothing goes quiet and *keeps sounding*. Silence and
|
||||||
|
being finished look identical from outside and are not the same thing: the voice is holding at
|
||||||
|
nothing, which is exactly what a held key does on any instrument. A program that plays such a
|
||||||
|
note and then waits for the status bit waits for ever.
|
||||||
|
|
||||||
|
So a routine that means to wait for a sound does this, in this order: play the note, wait
|
||||||
|
however long the note is meant to last, write nothing to the gate at 0x45, and *then* wait for
|
||||||
|
the bit to come down - which it does when the release has finished. `Programs/Examples/tune.asm`
|
||||||
|
is that loop with the waiting done on the screen's frame.
|
||||||
|
|
||||||
|
### The Sound Comes From The Machine's Clock:
|
||||||
|
|
||||||
|
Samples are made against cycles, not against however fast the host really ran: forty-eight
|
||||||
|
thousand a second of *emulated* time, worked out in whole numbers so it never drifts. Three
|
||||||
|
million cycles make exactly one hundred and forty-four thousand samples.
|
||||||
|
|
||||||
|
This is the same decision as the screen writing a picture out, and it buys the same thing. A
|
||||||
|
sound is **something a test can compare**: the same program makes the same samples every time,
|
||||||
|
on any host, at any speed, and `Tests/sound.sh` reads them back and measures the pitch. It
|
||||||
|
also means a machine that is paused makes no sound rather than a held note, which is right - a
|
||||||
|
stopped machine's oscillators are stopped too.
|
||||||
|
|
||||||
|
**The device does not interrupt.** Nothing about a note finishing needs the CPU's attention
|
||||||
|
urgently enough to be worth a line, and a program that wants to play in time has the screen's
|
||||||
|
frame interrupt, which is 60 a second and already there. A programmable timer is the proper
|
||||||
|
answer and is a device that does not exist yet.
|
||||||
|
|
||||||
## Asking What Is There:
|
## Asking What Is There:
|
||||||
|
|
||||||
A program that only ever runs on one machine can be told where everything is. A program meant to run on more than one has to ask, and the bus registry on port 0xFF is what it asks.
|
A program that only ever runs on one machine can be told where everything is. A program meant to run on more than one has to ask, and the bus registry on port 0xFF is what it asks.
|
||||||
@@ -553,7 +1013,60 @@ One thing to be careful of: the registry remembers which port it was asked about
|
|||||||
| 0x11 | Test device, which refuses everything. |
|
| 0x11 | Test device, which refuses everything. |
|
||||||
| 0x12 | Test device, which owns memory. |
|
| 0x12 | Test device, which owns memory. |
|
||||||
| 0x13 | Disk. |
|
| 0x13 | Disk. |
|
||||||
| 0x14 - 0xFF | Peripherals. |
|
| 0x14 | Screen. |
|
||||||
|
| 0x15 | Sound. |
|
||||||
|
| 0x16 | Timer. |
|
||||||
|
| 0x17 - 0xFF | Peripherals. |
|
||||||
|
|
||||||
|
## Keeping Time:
|
||||||
|
|
||||||
|
A period, in cycles, and a bit that says when one has gone by.
|
||||||
|
|
||||||
|
Before this the only regular beat on the machine was the screen finishing a frame, and that is
|
||||||
|
a clock a program **borrows** rather than one it sets. A frame is 16,667 cycles and not
|
||||||
|
negotiable, so every duration becomes a multiple of it - and a sixteenth note at 120 beats a
|
||||||
|
minute is 125,000 cycles, which is seven and a half frames. It cannot be asked for at all. The
|
||||||
|
way round it is to choose a tempo whose subdivisions happen to land on whole frames, which is
|
||||||
|
making the music fit the machine.
|
||||||
|
|
||||||
|
| Port | Register |
|
||||||
|
| --- | --- |
|
||||||
|
| 0x50 | Status. Bit 0 a period has gone by, bit 1 it is running, bit 2 it is set to interrupt. |
|
||||||
|
| 0x51 | Control. Bit 0 run, bit 1 repeat, bit 2 interrupt. |
|
||||||
|
| 0x52 - 0x54 | The period, in cycles, most significant byte first. |
|
||||||
|
|
||||||
|
**The period is in cycles**, because that is what everything else here is counted in: it is
|
||||||
|
what the cost model counts and what a frame is measured in, so a timer counting anything else
|
||||||
|
would be a second unit to remember. Twenty four bits reaches from one cycle to sixteen and a
|
||||||
|
half seconds, and 120 beats a minute sits at 500,000 in the middle of it. There is no
|
||||||
|
prescaler, because there is no range left for one to buy.
|
||||||
|
|
||||||
|
**Starting it loads the period.** Writing the control byte with the run bit already set does
|
||||||
|
not, so a program that turns interrupts on half way through a period does not silently move
|
||||||
|
the beat it was keeping.
|
||||||
|
|
||||||
|
**With the repeat bit it reloads; without it, it stops** and the status port says so. What is
|
||||||
|
left over carries into the next period, so a timer asked for 1,000 cycles ticks every 1,000
|
||||||
|
and not every 1,000 plus however late anybody looked.
|
||||||
|
|
||||||
|
**Reading the status is what answers it**: the tick comes down when it is read, and the line
|
||||||
|
with it. A program that polls is not one that will answer a handler.
|
||||||
|
|
||||||
|
```asm
|
||||||
|
; A sixteenth note at 120 beats a minute, waited for rather than counted.
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x52
|
||||||
|
INIA 0xE8
|
||||||
|
OUTA 0x53
|
||||||
|
INIA 0x48
|
||||||
|
OUTA 0x54 ; 0x01E848, which is 125,000
|
||||||
|
INIA 0x07
|
||||||
|
OUTA 0x51 ; Run, repeat, interrupt
|
||||||
|
SIF
|
||||||
|
WAIT
|
||||||
|
```
|
||||||
|
|
||||||
|
Eight of those is one second, and a machine doing it spends 999,720 of those cycles asleep.
|
||||||
|
|
||||||
## The Memory Controller:
|
## The Memory Controller:
|
||||||
|
|
||||||
@@ -604,6 +1117,26 @@ Everything a transfer would touch is checked before any of it moves. A transfer
|
|||||||
|
|
||||||
Filling is worth reaching for. Clearing a page with one Fill instead of a store and a loop takes about a tenth off the running time of the segmented sieve, which spends most of its life zeroing its window.
|
Filling is worth reaching for. Clearing a page with one Fill instead of a store and a loop takes about a tenth off the running time of the segmented sieve, which spends most of its life zeroing its window.
|
||||||
|
|
||||||
|
### What A Transfer Costs:
|
||||||
|
|
||||||
|
A transfer does not wait on anything, but it is not free. The controller is charged for every byte it moves, and the program that asked stalls until it is done, so these are cycles out of that program's budget.
|
||||||
|
|
||||||
|
Two things set the rate. **Banks are separate memories**, so a move between two of them can fetch the next word while the last one is stored, and a move within a single bank cannot and costs twice as much. And **the controller's path to memory is sixteen bits wide**, so it moves two bytes at a time when the addresses allow.
|
||||||
|
|
||||||
|
They allow it when the source, the destination and the length are **all even**. A word is read at an even address and written at an even address; an odd anything would mean shifting bytes across word boundaries to line them up, which is a different machine. A misaligned transfer falls back to a byte a cycle, which is what this cost before the path was widened.
|
||||||
|
|
||||||
|
| Moving 256 bytes | Aligned | Not aligned |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Between two banks | 129 | 257 |
|
||||||
|
| Within one bank | 257 | 513 |
|
||||||
|
| Fill | 129 | 257 |
|
||||||
|
|
||||||
|
The odd cycle in each is the pipeline filling. A fill has nothing to read, so it goes at the between-banks rate whatever bank it writes, and only its destination and length decide whether it can be paired - the byte it writes lives in SourceLow and is a value rather than an address.
|
||||||
|
|
||||||
|
**The rule is visible so that a program can act on it.** Aligning a buffer costs nothing and halves what moving it costs, and a cost a program cannot see is a cost it cannot avoid.
|
||||||
|
|
||||||
|
None of this changes the CPU. It still sees eight bits, a Data Pointer still addresses a byte, and no instruction means anything different than it did. What got wider is the controller's own path to the memories it moves between.
|
||||||
|
|
||||||
### Banks:
|
### Banks:
|
||||||
|
|
||||||
Memory the controller can reach is divided into banks of up to 64K each, numbered 0 to 255. Program and Data are banks like any other; being 0 and 1 is the only thing special about them.
|
Memory the controller can reach is divided into banks of up to 64K each, numbered 0 to 255. Program and Data are banks like any other; being 0 and 1 is the only thing special about them.
|
||||||
@@ -730,6 +1263,59 @@ The disk owns one block of memory, its buffer. Reading fills it and writing take
|
|||||||
|
|
||||||
A device that answers on more than one port raises its line on the first of them, so the disk interrupts on 0x20.
|
A device that answers on more than one port raises its line on the first of them, so the disk interrupts on 0x20.
|
||||||
|
|
||||||
|
### Several Disks:
|
||||||
|
|
||||||
|
**One controller with four drives, not four devices**, and the instruction set is the reason.
|
||||||
|
A port is an immediate byte inside the `OUT` that names it, so a program cannot compute one -
|
||||||
|
*the disk on port 0x20 plus drive times four* is not something this machine can say. Two disks
|
||||||
|
as two devices would mean a branch on the drive number in every place a program touches a
|
||||||
|
disk port. So the drive is a register, which is what a floppy controller has always been.
|
||||||
|
|
||||||
|
| Port | Register |
|
||||||
|
| --- | --- |
|
||||||
|
| 0x24 | Drive. Which one the block, command and status registers refer to. Reads back. |
|
||||||
|
| 0x25 | Drives, read only. How many are plugged in. |
|
||||||
|
| 0x26 | What the selected drive is, read only. Bit 0: its contents do not survive the machine stopping. |
|
||||||
|
| 0x27, 0x28 | How many blocks the selected drive has, read only. |
|
||||||
|
|
||||||
|
### A Drive Made Of Memory:
|
||||||
|
|
||||||
|
A drive may have memory behind it instead of a file. It selects, reads, writes and has a size
|
||||||
|
like any other, and a filesystem on it is a filesystem - **a program cannot tell the
|
||||||
|
difference except by how fast it was.** What it has not got is anything that survives the
|
||||||
|
machine stopping.
|
||||||
|
|
||||||
|
That difference is the one thing a system cannot work out for itself, because an empty disk
|
||||||
|
and a volatile disk look identical from outside. So the machine says it, in bit 0 of 0x26, and
|
||||||
|
says nothing whatever about filesystems.
|
||||||
|
|
||||||
|
**Which is the whole point of saying it that way.** The bit is what separates a drive a system
|
||||||
|
may format on sight from one it must not: an unformatted floppy somebody put in deliberately
|
||||||
|
is not an invitation, while an unformatted drive made of memory never had anything to lose. A
|
||||||
|
system reads the bit and draws its own conclusion - and a system that would rather have a
|
||||||
|
different filesystem entirely reads the same bit and writes whatever it likes. **The machine
|
||||||
|
supplies blocks. Bringing them up is the system's job.**
|
||||||
|
|
||||||
|
The size registers exist for the same reason. A superblock states a disk's size too, and that
|
||||||
|
is no use at all on a disk which has not got one yet.
|
||||||
|
|
||||||
|
The block, command and status registers, **and the single buffer**, all belong to whichever
|
||||||
|
drive is selected. A program that changes drives is holding a buffer that no longer contains
|
||||||
|
what it thought, and has to say so to itself - the controller cannot know what the program
|
||||||
|
believed.
|
||||||
|
|
||||||
|
A drive that is not there is **refused rather than wrapped**: writing 9 to the drive register
|
||||||
|
leaves the selection where it was, and reading the register says so. Wrapping would mean a
|
||||||
|
program asking for a drive this machine does not have quietly reading the one it does.
|
||||||
|
|
||||||
|
**Selecting an empty drive is allowed**, because a controller has its drives whether or not
|
||||||
|
there are disks in them. Reads from one fail with the error bit, which is what an empty drive
|
||||||
|
should do. `Drives` says how many have disks; the drive register accepts any of the four.
|
||||||
|
|
||||||
|
Changing drives finishes whatever the drive being left was in the middle of. A transfer waits
|
||||||
|
for the clock, so one may be owed at any moment, and running it against the disk that is
|
||||||
|
arriving instead of the one that asked for it would be a fault with no owner.
|
||||||
|
|
||||||
### Waiting:
|
### Waiting:
|
||||||
|
|
||||||
A command returns at once and the line goes up when the block has moved. Status bit 0 says the disk is still working.
|
A command returns at once and the line goes up when the block has moved. Status bit 0 says the disk is still working.
|
||||||
|
|||||||
@@ -0,0 +1,584 @@
|
|||||||
|
# The SplitBit Test Manual
|
||||||
|
|
||||||
|
This describes the test suite: what it is for, what each part of it can and cannot answer,
|
||||||
|
how to add to it, and where it is blind. It is the fourth of the manuals, and the only one
|
||||||
|
about the repository rather than about the machine.
|
||||||
|
|
||||||
|
The other three describe things a program can rely on. This one describes the reason to
|
||||||
|
believe them.
|
||||||
|
|
||||||
|
## What The Suite Claims:
|
||||||
|
|
||||||
|
The suite is not one thing. It is eleven scripts making five different kinds of claim, and
|
||||||
|
knowing which claim you are relying on is the whole point of this document. A recorded
|
||||||
|
transcript and a byte-for-byte comparison against a second implementation both print
|
||||||
|
`[ok ]`, and they are worth wildly different amounts.
|
||||||
|
|
||||||
|
The strongest thing the suite says is this: **on the source it is given, the assembler that
|
||||||
|
runs on SplitBit and the one that runs on the host produce identical bytes; and on the
|
||||||
|
operations it performs, the filesystem written by the tool and the filesystem written by the
|
||||||
|
machine are identical disks.** Those are two programs written from one specification,
|
||||||
|
sharing no code, checking each other.
|
||||||
|
|
||||||
|
Note the qualifiers, because they are the whole difference between a strong claim and an
|
||||||
|
untrue one. This is evidence about a corpus and a sequence of operations, not a proof about
|
||||||
|
every program that could be assembled or every disk that could be built. No suite says more
|
||||||
|
than that. But nothing else here is even that strong.
|
||||||
|
|
||||||
|
The weakest thing it says is that a program prints what it printed last time. That is worth
|
||||||
|
having and it is worth having a lot of, but it only ever catches change. It cannot catch a
|
||||||
|
thing that was wrong the first time it was recorded.
|
||||||
|
|
||||||
|
Everything in between is somewhere on that line.
|
||||||
|
|
||||||
|
## Running It:
|
||||||
|
|
||||||
|
```
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
Builds the four tools - and Voyager, where Raylib is installed - checks they compile under
|
||||||
|
strict ISO C, and runs the scripts in order. Takes a few seconds. Everything must pass; there are no expected failures at the
|
||||||
|
level of the suite, only tests that record an expected failure of the assembler.
|
||||||
|
|
||||||
|
```
|
||||||
|
make sanitize
|
||||||
|
```
|
||||||
|
|
||||||
|
The same suite with the four tools rebuilt under AddressSanitizer and
|
||||||
|
UndefinedBehaviorSanitizer. See [The Sanitizer Run](#the-sanitizer-run).
|
||||||
|
|
||||||
|
Individual scripts can be run on their own, from anywhere:
|
||||||
|
|
||||||
|
```
|
||||||
|
./Tests/run.sh Every program in the manifest.
|
||||||
|
./Tests/run.sh hello waitTest Only the named ones.
|
||||||
|
./Tests/run.sh --bless Record current output as expected. See below.
|
||||||
|
./Tests/voyager.sh The same manifest, through the other front end.
|
||||||
|
./Tests/disk.sh The disk tool against the format.
|
||||||
|
./Tests/cycles.sh What the memory controller charges.
|
||||||
|
./Tests/video.sh What the video device draws.
|
||||||
|
./Tests/sound.sh What the sound device makes.
|
||||||
|
./Tests/terminal.sh The things a recorded file cannot see.
|
||||||
|
./Tests/native.sh The two assemblers against each other.
|
||||||
|
./Tests/agree.sh The two filesystems against each other.
|
||||||
|
./Tests/lint.sh SplitLint against its fixture and the corpus baseline.
|
||||||
|
./Tests/lint.sh --bless Record the corpus baseline. See below.
|
||||||
|
./Tests/docs.sh The manuals against the code.
|
||||||
|
```
|
||||||
|
|
||||||
|
`Tests/makedisks.sh` is not in that list because it checks nothing. It builds the disk
|
||||||
|
images the other scripts read, and `run.sh` calls it.
|
||||||
|
|
||||||
|
Everything is built into `Tests/build`, which is removed and remade at the start of every
|
||||||
|
run. **The suite never writes into `Programs/`.** A binary sitting next to its source came
|
||||||
|
from `make`, not from here.
|
||||||
|
|
||||||
|
## The Five Kinds Of Check:
|
||||||
|
|
||||||
|
### 1. Recorded output
|
||||||
|
|
||||||
|
`Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares
|
||||||
|
everything it printed against a file in `Tests/expected`. 189 tests, of which 127 run, 35
|
||||||
|
only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image
|
||||||
|
given at all.
|
||||||
|
|
||||||
|
This is the bulk of the suite and the shallowest part of it. It answers "did this change"
|
||||||
|
and nothing else. A recorded file is only as correct as the day somebody looked at it,
|
||||||
|
which is why blessing has a protocol.
|
||||||
|
|
||||||
|
The 16 `xfail` tests are worth calling out separately: each one is a piece of source the
|
||||||
|
assembler is supposed to refuse, and the recorded result is the refusal message. They catch
|
||||||
|
two things at once - an error that stops being detected, and an error message that changes
|
||||||
|
without anybody meaning it to.
|
||||||
|
|
||||||
|
### 2. A second implementation
|
||||||
|
|
||||||
|
`Tests/native.sh`, `Tests/agree.sh` and `Tests/voyager.sh` are the checks that do not
|
||||||
|
compare the code against a memory of itself.
|
||||||
|
|
||||||
|
`native.sh` assembles the same source with the host assembler and with `Asm.sbx` running on
|
||||||
|
the emulated machine, and compares the two binaries byte for byte. **The only honest test of
|
||||||
|
an assembler is the bytes it produces:** "it ran" and "the sizes look right" both pass for a
|
||||||
|
binary with a label one byte out, which is a program that jumps into the middle of an
|
||||||
|
instruction. It does a boot image, four loadable programs, CosmOS, and the assembler
|
||||||
|
itself - and then the CosmOS that CosmOS built, which is where it stops, because a second
|
||||||
|
generation that matches the first is a fixed point.
|
||||||
|
|
||||||
|
`agree.sh` builds the same directory tree twice, once with SplitDisk and once by driving
|
||||||
|
CosmOS, and compares the images byte for byte. Every field one implementation writes and
|
||||||
|
the other only reads is checked here and nowhere else: which entry a file lands in, which
|
||||||
|
block, what a directory's unused fields hold, the version in the superblock, the free
|
||||||
|
count. A disagreement in any of those is a disk one of them can read and the other cannot,
|
||||||
|
and the usual way that gets discovered is somebody's file coming back wrong months later.
|
||||||
|
|
||||||
|
`native.sh` and `agree.sh` both depend on the two sides being driven in the same order,
|
||||||
|
because both allocate first fit and both take the first free entry. Given the same
|
||||||
|
operations in the same sequence they should reach the same bytes, and any difference is real
|
||||||
|
rather than an artefact of the script.
|
||||||
|
|
||||||
|
`Tests/voyager.sh` is the cheapest of the three, because it reuses the runner rather than
|
||||||
|
repeating it. SplitBit and Voyager share every line of the machine and differ only in what
|
||||||
|
they present - a terminal, or a window and a speaker - and the way to keep that true is to
|
||||||
|
make the claim testable. So it runs the **whole manifest** through Voyager with
|
||||||
|
`--headless`, held to the same recorded results SplitBit is held to. The claim is not that
|
||||||
|
the two look alike: it is that one satisfies every recording the other does, byte for byte,
|
||||||
|
exit status included.
|
||||||
|
|
||||||
|
`Tests/run.sh` takes the machine to run from `SPLITBIT_EMULATOR`, which is what makes that
|
||||||
|
possible without a second copy of the runner. A copy would drift, and the first thing to go
|
||||||
|
would be whichever awkward case got added to only one of them.
|
||||||
|
|
||||||
|
**Voyager not being built is not a failure.** It needs Raylib and nothing else here does,
|
||||||
|
which is the whole point of there being two binaries; a suite that failed on a machine with
|
||||||
|
no graphics library would be enforcing exactly the dependency the split exists to avoid. The
|
||||||
|
script says it was skipped, and passes.
|
||||||
|
|
||||||
|
### 3. Named properties
|
||||||
|
|
||||||
|
`Tests/disk.sh` and `Tests/lint.sh` do not compare transcripts. Each check is a named
|
||||||
|
assertion that either holds or does not, and the name is the failure message.
|
||||||
|
|
||||||
|
`disk.sh` puts files of every awkward size onto an image and takes them off again -
|
||||||
|
nothing at all, less than a block, exactly a block, a part block, an exact multiple - and
|
||||||
|
then asks for the things the format says cannot happen and requires them to be refused
|
||||||
|
rather than half done. Roughly half of its checks are `refuses`, which is the shape
|
||||||
|
worth copying: **a tool that never says no is not finished.**
|
||||||
|
|
||||||
|
`Tests/video.sh` belongs here too, and exists for the same reason as the two above: the
|
||||||
|
suite has no display, and a screen nothing can look at is a screen nothing checks. The
|
||||||
|
device renders into a buffer that is a pure function of video memory, and the machine can be
|
||||||
|
asked to save it with `--screen`, so every check runs a program, saves the picture and reads
|
||||||
|
pixels back out of it. No window, no display server, and the same answer every time.
|
||||||
|
|
||||||
|
**It checks named behaviours rather than a recorded image**, which for a screen matters more
|
||||||
|
than usual. A recorded image would say "something changed" and leave which of the palette,
|
||||||
|
the tile, the attribute, the map or the scroll register broke to be found by hand. Instead
|
||||||
|
each check is one claim: that a tile lands where it is put and stops at the cell edge, that
|
||||||
|
the palette is what colours it, that the attribute nibble adds sixteen, that scrolling moves
|
||||||
|
which row is on top, that the map wraps, and that an impossible mode is refused without
|
||||||
|
stopping the machine.
|
||||||
|
|
||||||
|
`Tests/sound.sh` is the same argument again, one device along: the suite has no speaker, and
|
||||||
|
a sound nothing can hear is a sound nothing checks. Its samples come from the machine's clock
|
||||||
|
rather than the host's, so `--sound` writes a file that is a pure function of the program and
|
||||||
|
the cycle count - a million cycles is exactly forty-eight thousand samples, on any host, at
|
||||||
|
any speed. The checks read that file back and measure it: that a note is the note that was
|
||||||
|
asked for, that twelve of them is an octave, that gain is a level and volume is over the top
|
||||||
|
of it, and that two runs are identical byte for byte.
|
||||||
|
|
||||||
|
**It found three real defects the first time it ran**, all of them the same shape - a
|
||||||
|
synthesizer written for a patch editor, wired up as hardware and inheriting the editor's
|
||||||
|
assumptions. Only one voice had an oscillator switched on, so three of the four channels could
|
||||||
|
not make a sound whatever was written to them. That voice's oscillator arrived at full gain
|
||||||
|
while every other one arrived at nothing. And a note with no sustain is silent but not over,
|
||||||
|
so the obvious way to wait for a sound to end waits for ever.
|
||||||
|
|
||||||
|
The last of those had already caught the person writing the device, an hour before the check
|
||||||
|
existed, and had been written into the manual as advice. That is the argument for a suite in
|
||||||
|
one sentence: **the check disagreed with the documentation, and the check was right.**
|
||||||
|
|
||||||
|
**Half of `video.sh` is about the console rather than the device.** Those programs ask the video
|
||||||
|
device for nothing at all: they write bytes to port 0x00, the way every SplitBit program
|
||||||
|
always has, and the picture is what is checked. A character lands at the cursor and the
|
||||||
|
cursor moves along, a newline starts the next row, backspace rubs out, the line wraps at the
|
||||||
|
last column, and the screen scrolls by moving the origin rather than by moving 1,920 bytes -
|
||||||
|
which is checked by reading the register back, because a console that blitted rows instead
|
||||||
|
would look identical on the screen and cost twelve percent of a frame a line.
|
||||||
|
|
||||||
|
Breaking the additive nibble fails exactly one check. Breaking the scroll origin fails
|
||||||
|
exactly two. Moving every cell one pixel sideways fails the four about placement. That is
|
||||||
|
what a screen test is supposed to do.
|
||||||
|
|
||||||
|
`lint.sh` builds a fixture in which every line trips exactly one rule, and checks which
|
||||||
|
warning came out at which line. It used to compare a total, and a total is a number that
|
||||||
|
stays right while the thing behind it goes wrong: a change that stopped one rule firing and
|
||||||
|
made another fire twice would have passed without a murmur. The fixture is written inside
|
||||||
|
the script rather than kept as a file because a reader wants the pattern and the
|
||||||
|
expectation side by side.
|
||||||
|
|
||||||
|
### 4. What a recorded file cannot see
|
||||||
|
|
||||||
|
`Tests/terminal.sh` runs the emulator under a pseudo-terminal, which is what makes its
|
||||||
|
questions askable at all. Two whole classes of behaviour are invisible to every other check
|
||||||
|
here:
|
||||||
|
|
||||||
|
**When something is printed.** Piped output is fully buffered and flushed when the process
|
||||||
|
ends, so a prompt shown before its answer is read and a prompt shown an hour late produce
|
||||||
|
byte-identical files. A prompt printed *after* the thing it was asking for is invisible to
|
||||||
|
the entire rest of the suite.
|
||||||
|
|
||||||
|
**What happens to the terminal.** Key mode only touches a terminal when there is one, so
|
||||||
|
with input from a file there is nothing to put into another state and nothing to put back.
|
||||||
|
A machine that leaves the terminal with echo off passes every other test in this
|
||||||
|
repository.
|
||||||
|
|
||||||
|
Both of those went wrong here, and both were found by a person whose terminal stopped
|
||||||
|
working rather than by anything in this suite. That is why the script exists. It checks
|
||||||
|
that a prompt arrives before input is read, that a keystroke arrives without Return, that
|
||||||
|
the terminal is handed back however the machine dies - SIGHUP, SIGINT, SIGQUIT, SIGABRT,
|
||||||
|
SIGSEGV, SIGTERM - and that suspending and resuming leave it as they found it.
|
||||||
|
|
||||||
|
**And it is the only place an escape sequence is ever read as one.** A terminal sends
|
||||||
|
`ESC [ A` for the Up key and the console turns that into a byte of its own, but only when
|
||||||
|
standard input really is a terminal - everywhere else in this suite the input is a file,
|
||||||
|
which holds exactly the bytes somebody put in it and goes straight past the translation. So
|
||||||
|
three checks here type at a pseudo-terminal: that the sequences arrive as keys, that Escape
|
||||||
|
pressed on its own is still Escape, and that an ordinary character typed straight after an
|
||||||
|
escape is held rather than swallowed with the sequence that never was. The recorded tests
|
||||||
|
cover the other half - what a program does with the key values - by writing them into the
|
||||||
|
input file directly.
|
||||||
|
|
||||||
|
It also asks the one question about *waiting* that nothing else can, since the count is
|
||||||
|
stripped from every recorded result: whether a program on a slow disk slept through the wait
|
||||||
|
or spun on it. Both print the same characters and take the same elapsed time. Only the split
|
||||||
|
between idle and bus cycles tells them apart.
|
||||||
|
|
||||||
|
`Tests/cycles.sh` is the other half of the same bargain, and exists because the determinism
|
||||||
|
rules below throw the cycle count away. It measures what the memory controller charges for
|
||||||
|
moving memory - which is real time out of a program's budget, and is invisible everywhere
|
||||||
|
else in this suite.
|
||||||
|
|
||||||
|
**It pins the rate rather than a total.** Each case runs twice, from programs whose
|
||||||
|
instructions are identical except for the byte written to the Command port: once asking for
|
||||||
|
the transfer, and once asking for `GuardOff`, which lowers a fence that was never raised and
|
||||||
|
costs nothing beyond the port write. The difference between the two runs is the transfer and
|
||||||
|
nothing else - no instruction count, no setup, no startup - so the check survives every
|
||||||
|
change to the machine that is not a change to what a transfer costs.
|
||||||
|
|
||||||
|
### 5. The documents against the code
|
||||||
|
|
||||||
|
`Tests/docs.sh` settles, every time the suite runs, every claim in the manuals that can be
|
||||||
|
settled by looking at the source. Documentation goes stale quietly: an instruction added
|
||||||
|
without a table row, or a count in a heading that nobody updated, is wrong in a way nothing
|
||||||
|
notices until somebody trusts it.
|
||||||
|
|
||||||
|
It checks that every tracked file is plain ASCII; that every link in every document goes
|
||||||
|
somewhere, and carries `%20` rather than a raw space; that every instruction has a row and
|
||||||
|
every row is an instruction; that the counts in the group headings are right, and the count
|
||||||
|
of instructions that take a Data Pointer; that every device class has a row in the Devices
|
||||||
|
table; that the vector ranges the manuals quote are the ones the assembler uses; that the
|
||||||
|
loadable header table matches the offsets the assembler writes; that every console status
|
||||||
|
bit is described; that every service CosmOS implements has a row and every row is
|
||||||
|
implemented; that every application the CosmOS README describes exists; that the monitor's
|
||||||
|
instruction table is the assembler's, generated rather than typed; that every directive is
|
||||||
|
written down; that every routine the manuals promise is defined; that CosmOS still fits in
|
||||||
|
the half of the machine its memory map gives it; that the sizes quoted for applications are
|
||||||
|
their real sizes; and that the worked examples still assemble to the bytes printed beside
|
||||||
|
them.
|
||||||
|
|
||||||
|
It also checks this document: that every script the suite runs has a bullet here saying why
|
||||||
|
it exists, and that the number quoted is the number there are.
|
||||||
|
|
||||||
|
**A manual is a claim somebody may check by typing it.** Everything here is one that gets
|
||||||
|
checked automatically instead.
|
||||||
|
|
||||||
|
## Determinism:
|
||||||
|
|
||||||
|
A test that passes four times out of five is worse than no test, because the failure gets
|
||||||
|
attributed to the suite rather than to the code. Several rules exist only to keep results
|
||||||
|
identical from one run to the next, and each of them is a bug that already happened.
|
||||||
|
|
||||||
|
**The cycle count is stripped from every recorded result.** The last line of the emulator's
|
||||||
|
output has the number taken out before anything is compared, keeping only *whether* the
|
||||||
|
program stopped on its own or ran into its limit, which is behaviour. Two instructions
|
||||||
|
added to CosmOS used to move that number in six unrelated files at once, so a real
|
||||||
|
difference would have arrived in a crowd of meaningless ones and had to be picked out by
|
||||||
|
hand. Anything that genuinely wants to measure cycles should say so out loud in a test of
|
||||||
|
its own, the way `terminal.sh` does.
|
||||||
|
|
||||||
|
The substitution is deliberately **not anchored to the start of a line**. A program whose
|
||||||
|
last output has no newline on it leaves the cursor mid-line and the halt message is printed
|
||||||
|
there, so the count sat inside a line rather than at the head of one and survived.
|
||||||
|
`replCalculator` is the program that does that, and it was the only test to churn when the
|
||||||
|
machine started charging for memory accesses instead of counting instructions.
|
||||||
|
|
||||||
|
**Every program runs with `--fast`.** The emulated clock rate has no bearing on what a
|
||||||
|
program prints, and waiting out real time makes the suite slower for nothing.
|
||||||
|
|
||||||
|
**A program that never halts is bounded by `--cycles`, not by wall clock.** A cycle limit
|
||||||
|
produces the same output on a fast machine and a slow one. A time limit does not.
|
||||||
|
|
||||||
|
**Every run has a ten second timeout** on top of that, which is a failure rather than a
|
||||||
|
result. A test that hangs says so instead of hanging the suite.
|
||||||
|
|
||||||
|
**A scratch disk is removed before every run**, so nothing a test writes can be seen by the
|
||||||
|
next one. The emulator makes a blank image in its place.
|
||||||
|
|
||||||
|
**The build directory is removed and remade at the start of every run**, so a stale binary
|
||||||
|
cannot pass for a fresh one.
|
||||||
|
|
||||||
|
**The exit status is recorded with the output.** What a program exits with is part of what
|
||||||
|
it does. A program that faults is supposed to exit non-zero, and that should be as pinned
|
||||||
|
down as what it printed.
|
||||||
|
|
||||||
|
## The Manifest:
|
||||||
|
|
||||||
|
`Tests/manifest` is one test per line, fields separated by `|`. Blank lines and lines
|
||||||
|
starting with `#` are ignored, and the file is heavily commented by section.
|
||||||
|
|
||||||
|
```
|
||||||
|
name | source | mode | stdin | limit | disk
|
||||||
|
```
|
||||||
|
|
||||||
|
**name** is what the test is called and what `Tests/expected/<name>.out` is named after.
|
||||||
|
|
||||||
|
**source** is relative to `Programs/`. Everything assembles from there with `Libraries/`
|
||||||
|
and `CosmOS/Source/` on the include path. CosmOS is on the path because it owns the
|
||||||
|
filesystem library and the service names, which test programs outside it include.
|
||||||
|
|
||||||
|
**mode** is one of:
|
||||||
|
|
||||||
|
| Mode | What happens |
|
||||||
|
| --- | --- |
|
||||||
|
| `run` | Assemble, execute, compare all output against the recorded result |
|
||||||
|
| `assemble` | Assemble only, for library files with no entry point to run |
|
||||||
|
| `xfail` | Assembly is expected to fail, and the message is recorded |
|
||||||
|
| `rom` | As `run`, but no image is given, so the machine boots from its ROM |
|
||||||
|
|
||||||
|
`xfail` records a known breakage, so that fixing one is noticed and so that an accidental
|
||||||
|
new one is too. `rom` is what a machine with no debugger attached does: the emulator
|
||||||
|
shadows its built-in stage one into Program Memory and reads the disk for everything else.
|
||||||
|
|
||||||
|
**stdin** names a file in `Tests/input`, or `-` for none.
|
||||||
|
|
||||||
|
**limit** is a cycle count for programs that never halt on their own, or `-`.
|
||||||
|
|
||||||
|
**disk** names an image to attach, or `-` for a machine with no disk, which is most of
|
||||||
|
them. Four forms:
|
||||||
|
|
||||||
|
| Form | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `name.img` | Scratch. Removed before the run, so the emulator makes a blank one |
|
||||||
|
| `disks/name.img` | A fixture built by `makedisks.sh`, used as it stands |
|
||||||
|
| `...:ro` | Attached write protected, so a test can check the *device* bars writes |
|
||||||
|
| `...@N` | Given a latency of N cycles, so a test can check the filesystem waits |
|
||||||
|
|
||||||
|
The `@N` form deserves a note. Every other test runs with the disk's answer available
|
||||||
|
before the next instruction, which is the one condition under which failing to wait looks
|
||||||
|
exactly like working.
|
||||||
|
|
||||||
|
**keys** names a file in `Tests/input` to be fed to the console as a *keyboard* rather than
|
||||||
|
as standard input, and the difference between those is the whole reason the field exists.
|
||||||
|
|
||||||
|
Standard input reaches a console that believes a terminal is doing the line editing, which
|
||||||
|
is true when there is one: the terminal collects a line, rubs out a backspace, and hands
|
||||||
|
over the finished thing at Return. **Behind a window there is no terminal**, so the console
|
||||||
|
does that itself, and that is real logic which nothing could reach. It broke twice in two
|
||||||
|
days and a person typing found it both times - once as keys that never arrived, once as a
|
||||||
|
corrected line that reached the shell with the backspaces still in it, looking perfectly
|
||||||
|
right on screen and matching no command at all.
|
||||||
|
|
||||||
|
A keyboard file installs the same hook a window does, so the same path runs. It does not
|
||||||
|
test the window: Voyager's own key queue is still out of reach, and so is anything about
|
||||||
|
presenting frames. It tests the console, which is where the logic is.
|
||||||
|
|
||||||
|
## Fixture Disks:
|
||||||
|
|
||||||
|
`Tests/makedisks.sh` builds 27 images with SplitDisk before anything runs, into
|
||||||
|
`Tests/build/disks`. **That is the point of them.** A SplitBit program reading one of these
|
||||||
|
is being checked against a filesystem written by different code from the same written
|
||||||
|
specification, rather than against itself.
|
||||||
|
|
||||||
|
They are shaped to be awkward on purpose. `sbfs.img` has two directory blocks and eight
|
||||||
|
files that fill the first one exactly, so everything after that lands in the second and the
|
||||||
|
walk from block to block gets exercised rather than assumed. It carries a file longer than
|
||||||
|
a block whose pattern repeats every twenty-six bytes, which makes a misplaced block obvious
|
||||||
|
to read; an empty file; and a name of exactly the maximum length. `deep.img` has eight
|
||||||
|
directory blocks. `chain.img` carries a different payload in each of its two boot slots, so
|
||||||
|
a test reading the right word is reading the slot that was chosen rather than whatever
|
||||||
|
happened to be left in Program Memory, and `chainAlt.img` is the same disk with the other
|
||||||
|
slot selected. `selfboot.img` has stage two in a boot slot and the system as an ordinary
|
||||||
|
file, which is the whole chain end to end, and `nosystem.img` has the second stage with
|
||||||
|
nothing for it to find, so that a loader which cannot start anything says so rather than
|
||||||
|
jumping somewhere.
|
||||||
|
|
||||||
|
`cfgbare.img`, `cfgbroken.img` and `cfgfallback.img` are the same disk three ways,
|
||||||
|
differing only in `/System/Boot/boot.cfg`. Nothing else about any of them changes, which is
|
||||||
|
what makes those tests of the **file** rather than of the machinery under it. That is the
|
||||||
|
shape to copy when a fixture is added: change one thing, and keep everything else
|
||||||
|
identical to a disk that already works.
|
||||||
|
|
||||||
|
The script makes its paths absolute before doing anything else, because it changes
|
||||||
|
directory partway down. When that was wrong the failure was not an error - it was a disk
|
||||||
|
quietly missing some of the files it was supposed to have, which is a much worse thing to
|
||||||
|
debug.
|
||||||
|
|
||||||
|
## Recording Expected Output:
|
||||||
|
|
||||||
|
```
|
||||||
|
make bless
|
||||||
|
./Tests/run.sh --bless
|
||||||
|
```
|
||||||
|
|
||||||
|
This overwrites every file in `Tests/expected` with the current output.
|
||||||
|
|
||||||
|
**Blessing unexamined output records a bug as correct, permanently and silently.** It is
|
||||||
|
the one operation in this repository that can make the suite confidently wrong, and it does
|
||||||
|
it without printing anything alarming. The protocol is:
|
||||||
|
|
||||||
|
1. Run the suite and read the diffs. `run.sh` prints the first twenty lines of each.
|
||||||
|
2. Account for every changed file. Not "that looks like the change I made" - know why each
|
||||||
|
one moved, and know why the ones that did not move did not.
|
||||||
|
3. Bless.
|
||||||
|
4. Look at `git diff Tests/expected` before committing, which is the last chance to see a
|
||||||
|
recorded result that should not have changed.
|
||||||
|
|
||||||
|
A single test can be re-run by name to see one diff on its own, which is usually faster
|
||||||
|
than reading all of them:
|
||||||
|
|
||||||
|
```
|
||||||
|
./Tests/run.sh cosmosCwd
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Lint Baseline:
|
||||||
|
|
||||||
|
`Tests/lint-baseline.txt` records how many warnings of each rule SplitLint finds in each
|
||||||
|
file across the whole corpus - 34 file-and-rule pairs. It is checked on every run, and it
|
||||||
|
is checked **in both directions.**
|
||||||
|
|
||||||
|
A new warning appearing is a regression. A recorded warning *disappearing* is also
|
||||||
|
reported, and that is the half people do not expect: it means either that somebody fixed
|
||||||
|
something and did not record it, which is fine and takes one command, or that a rule
|
||||||
|
stopped firing, which is not fine at all and is otherwise completely silent.
|
||||||
|
|
||||||
|
```
|
||||||
|
./Tests/lint.sh --bless
|
||||||
|
```
|
||||||
|
|
||||||
|
records the corpus as it stands, for when warnings have been deliberately fixed or
|
||||||
|
deliberately accepted. Same shape as `run.sh --bless`, and it wants the same care.
|
||||||
|
|
||||||
|
A warning that is correct but deliberate should be suppressed in the source rather than
|
||||||
|
carried in the baseline, with a comment saying `splitlint: <reason>` or
|
||||||
|
`splitlint[rule]: <reason>`. The reason is required, so that a deliberate exception says
|
||||||
|
what makes it one. A marker that silences nothing is itself reported, as `dead-suppression`,
|
||||||
|
which stops the markers outliving the code they were about.
|
||||||
|
|
||||||
|
## The Sanitizer Run:
|
||||||
|
|
||||||
|
```
|
||||||
|
make sanitize
|
||||||
|
```
|
||||||
|
|
||||||
|
Rebuilds all four tools with `-fsanitize=address,undefined` and runs **the whole suite**
|
||||||
|
under them. What it reliably catches is invalid access: reads and writes off the end of an
|
||||||
|
array, use after free, leaks, and arithmetic the standard does not define.
|
||||||
|
|
||||||
|
AddressSanitizer also fills fresh heap allocations with a junk pattern, and it is worth
|
||||||
|
knowing why that buys almost nothing here. It is a default of the toolchain rather than
|
||||||
|
anything this build configures, so it is not something to rely on; and there are **six heap
|
||||||
|
allocations in the whole repository**, all of them in the assembler, the largest a
|
||||||
|
deliberate `calloc`. The machine's Program and Data memories are static arrays, which the
|
||||||
|
sanitizers neither fill nor bound-check - which is the same fact, seen from a
|
||||||
|
different side, as the overrun blind spot below.
|
||||||
|
|
||||||
|
It runs everything because it used to not. It built all four tools sanitized and then ran
|
||||||
|
only `run.sh` and `terminal.sh`, so SplitDisk was compiled with the sanitizers and never
|
||||||
|
exercised, and `native.sh` - which drives the assembler and the emulator harder than
|
||||||
|
anything else here - was skipped entirely. Those are exactly where block arithmetic on disk
|
||||||
|
images and buffer indexing in two assemblers live. Adding the other scripts cost about six
|
||||||
|
seconds.
|
||||||
|
|
||||||
|
If the suite fails, the sanitizer binaries are deliberately left in place so the failing
|
||||||
|
case can be run again by hand. Plain `make` puts the normal ones back.
|
||||||
|
|
||||||
|
Worth running before a release, and after anything that touches memory handling.
|
||||||
|
|
||||||
|
## The Strict Build:
|
||||||
|
|
||||||
|
`make test` depends on `make strict`, which compiles every source file with
|
||||||
|
`-std=c11 -pedantic -Wall -Wextra -Werror` **and `-D_XOPEN_SOURCE=700`**, then throws away
|
||||||
|
the object.
|
||||||
|
|
||||||
|
That last flag is part of the check rather than a hole in it, and the distinction matters:
|
||||||
|
this is strict C11 with the POSIX interfaces the code actually uses explicitly selected, not
|
||||||
|
freestanding ISO C. The sources call `realpath`, `strdup`, `dirname` and `getopt`, and
|
||||||
|
asking for POSIX.1-2008 by name is what makes a strict C11 build declare them rather than
|
||||||
|
guess.
|
||||||
|
|
||||||
|
This exists because the README says "the sources are ISO C and build clean under
|
||||||
|
`-std=c11 -pedantic` with `-Wall -Wextra`", and that is a claim somebody may check by
|
||||||
|
typing it. It was false when the target was written: `realpath` went undeclared under a
|
||||||
|
feature test macro that did not reach far enough, which the ordinary `-Os` build never saw.
|
||||||
|
|
||||||
|
## Adding A Test:
|
||||||
|
|
||||||
|
**A program.** Put the source in `Programs/`, add a line to `Tests/manifest`, run
|
||||||
|
`./Tests/run.sh <name>` and read what it printed. If it is right, `make bless` - which
|
||||||
|
records every test, so make sure the rest of the suite is clean first. If the program needs
|
||||||
|
input, put a file in `Tests/input` and name it in the manifest. If it needs a
|
||||||
|
disk, name a scratch image; if it needs a disk with something already on it, add it to
|
||||||
|
`makedisks.sh` and name it as `disks/whatever.img`.
|
||||||
|
|
||||||
|
**A property of a host tool.** Add a `check` or a `refuses` line to `Tests/disk.sh`. The
|
||||||
|
name is the failure message, so write it as a statement of what should be true: "no
|
||||||
|
removing an occupied one", not "test 47".
|
||||||
|
|
||||||
|
**Something a recorded file cannot see.** `Tests/terminal.sh` is the place, and it is the
|
||||||
|
hardest script to add to because each check has to arrange a situation and then observe it
|
||||||
|
from outside. Small assembly programs are written inline in the script for this, and only
|
||||||
|
this - everything else assembles from the repository.
|
||||||
|
|
||||||
|
**A linter rule.** Add its line to the fixture in `Tests/lint.sh` next to the other lines
|
||||||
|
for its family, with the expected line number and message. Every line of the fixture exists
|
||||||
|
to trip exactly one rule; keep it that way, or a rule that stops working will be masked by
|
||||||
|
the one next to it.
|
||||||
|
|
||||||
|
**A claim in a manual.** If you write a number, a count, or a list into any of the four
|
||||||
|
documents, consider whether `Tests/docs.sh` can settle it from the source. Most of them
|
||||||
|
can. The ones already there are all claims that were true when written and quietly stopped
|
||||||
|
being.
|
||||||
|
|
||||||
|
## Checking A Check:
|
||||||
|
|
||||||
|
**A new check is not finished until it has been seen to fail.** The discipline is to break
|
||||||
|
the thing on purpose, run the check, watch it report, and put the thing back. This has
|
||||||
|
caught several checks in this repository that passed for the wrong reason and would have
|
||||||
|
passed for ever.
|
||||||
|
|
||||||
|
Two of them are worth remembering:
|
||||||
|
|
||||||
|
An agreement test passed because both of its cases were on one disk, and the first save ate
|
||||||
|
the file that was the second case's *source*. It compared two things that were both empty.
|
||||||
|
Splitting them onto separate disks made it fail, which is when it started being a test.
|
||||||
|
|
||||||
|
The lint fixture passed with a rule's message broken, because the total was still
|
||||||
|
twenty-three.
|
||||||
|
|
||||||
|
A check that has only ever been seen to pass is a check you are trusting on faith. That is
|
||||||
|
the same category of thing as an unread recorded result.
|
||||||
|
|
||||||
|
## What The Suite Does Not Catch:
|
||||||
|
|
||||||
|
Written down because a document that lists only strengths teaches the wrong lesson.
|
||||||
|
|
||||||
|
**Buffer overruns into an adjacent variable.** This has happened three times: the shell's
|
||||||
|
prompt into its command name table, the assembler's ceilings, and the line editor's `Entry`
|
||||||
|
buffer into `TextHead` and `ArenaFree`. **All three were found by a person using the
|
||||||
|
machine, and none of them by the suite.** They are hard for it structurally: the write
|
||||||
|
succeeds, nothing faults, and the damage shows up somewhere else entirely, at a time that
|
||||||
|
depends on the memory map. The sanitizers cannot see them either, because the overrun is in
|
||||||
|
emulated Data Memory, which is one legitimate host array from end to end. The only defences
|
||||||
|
are reading every `#Reserve` that something copies into, and bounding the loop that fills
|
||||||
|
it.
|
||||||
|
|
||||||
|
**Anything that needs somebody at the keyboard.** `terminal.sh` gets closer than a recorded
|
||||||
|
file can, but it drives a pseudo-terminal on a script. Nobody types slowly, changes their
|
||||||
|
mind, resizes the window, or holds a key down. The worst bugs of the last stretch - the
|
||||||
|
editor misreading a source file, the machine wedging afterwards, and both terminal failures
|
||||||
|
above - all came from somebody sitting down and using CosmOS for an hour.
|
||||||
|
|
||||||
|
**Performance.** Cycle counts are deliberately excluded from every recorded result, so
|
||||||
|
nothing here notices a program becoming four times slower. That is the correct trade for
|
||||||
|
now and it is a real gap.
|
||||||
|
|
||||||
|
**The emulator being wrong about the machine.** Every check here compares SplitBit against
|
||||||
|
SplitBit. `native.sh` and `agree.sh` are second opinions about the *assembler* and the
|
||||||
|
*filesystem*, and there is no second opinion about the CPU at all. Timing, bus behaviour,
|
||||||
|
what an undriven port reads as, and what happens when two things want memory at once are
|
||||||
|
all decided by one implementation and checked against nothing.
|
||||||
|
|
||||||
|
**Whether the design is any good.** The suite can say that CosmOS still fits in its half of
|
||||||
|
the machine. It cannot say that the memory map is the right one.
|
||||||
|
|
||||||
+4
-1
@@ -169,7 +169,10 @@ if cmp -s toCopy.dat copiedBack.dat; then
|
|||||||
else
|
else
|
||||||
report FAIL "large native copy" "the host read back different bytes"
|
report FAIL "large native copy" "the host read back different bytes"
|
||||||
fi
|
fi
|
||||||
if grep -q '^> the same$' copied.txt; then
|
# Not anchored to a prompt any more. The shell echoes the line it was given and then ends
|
||||||
|
# it, so what a command prints now starts at the beginning of a line instead of following
|
||||||
|
# the "> " that asked for it.
|
||||||
|
if grep -q '^the same$' copied.txt; then
|
||||||
report ok "native compare" "the streamed files agree"
|
report ok "native compare" "the streamed files agree"
|
||||||
else
|
else
|
||||||
report FAIL "native compare" "Compare did not call the copied files equal"
|
report FAIL "native compare" "Compare did not call the copied files equal"
|
||||||
|
|||||||
Executable
+144
@@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Checks what the memory controller charges for moving memory.
|
||||||
|
#
|
||||||
|
# EVERY OTHER TEST HERE IS BLIND TO THIS. run.sh strips the cycle count out of every
|
||||||
|
# recorded result on purpose, because a number that moves whenever anything changes turns
|
||||||
|
# real differences into a crowd of meaningless ones. The Test Manual says the other half of
|
||||||
|
# that bargain out loud: anything that genuinely wants to measure cycles has to say so in a
|
||||||
|
# test of its own. This is that test.
|
||||||
|
#
|
||||||
|
# What it pins is the RATE rather than a total. Each case runs twice, from programs whose
|
||||||
|
# instructions are identical except for the byte written to the Command port: once asking
|
||||||
|
# for the transfer, and once asking for GuardOff, which lowers a fence that was never raised
|
||||||
|
# and costs nothing beyond the port write. The difference between the two runs is therefore
|
||||||
|
# the transfer and nothing else - no instruction count, no setup, no startup.
|
||||||
|
#
|
||||||
|
# Written by Anachronaut
|
||||||
|
|
||||||
|
set -u
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
BUILD="$ROOT/Tests/build/cycles"
|
||||||
|
ASM="$ROOT/Assembler"
|
||||||
|
EMU="$ROOT/SplitBit"
|
||||||
|
|
||||||
|
for tool in "$ASM" "$EMU"; do
|
||||||
|
[ -x "$tool" ] || { echo "$(basename "$tool") is not built."; exit 1; }
|
||||||
|
done
|
||||||
|
|
||||||
|
rm -rf "$BUILD"; mkdir -p "$BUILD"
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
FAILED_NAMES=()
|
||||||
|
|
||||||
|
GREEN=$'\033[32m'; RED=$'\033[31m'; RESET=$'\033[0m'
|
||||||
|
[ -t 1 ] || { GREEN=""; RED=""; RESET=""; }
|
||||||
|
|
||||||
|
# A program that sets the controller up and then writes one byte to the Command port.
|
||||||
|
# The eight registers are written the same way every time, so two programs built from this
|
||||||
|
# differ by exactly one immediate.
|
||||||
|
program() {
|
||||||
|
# program <sourceBank> <srcHigh> <srcLow> <destBank> <dstHigh> <dstLow> <lenHigh> <lenLow> <command>
|
||||||
|
cat <<ASM
|
||||||
|
#Program
|
||||||
|
start:
|
||||||
|
INIA $1
|
||||||
|
OUTA 0xE0
|
||||||
|
INIA $2
|
||||||
|
OUTA 0xE1
|
||||||
|
INIA $3
|
||||||
|
OUTA 0xE2
|
||||||
|
INIA $4
|
||||||
|
OUTA 0xE3
|
||||||
|
INIA $5
|
||||||
|
OUTA 0xE4
|
||||||
|
INIA $6
|
||||||
|
OUTA 0xE5
|
||||||
|
INIA $7
|
||||||
|
OUTA 0xE6
|
||||||
|
INIA $8
|
||||||
|
OUTA 0xE7
|
||||||
|
INIA $9
|
||||||
|
OUTA 0xE8
|
||||||
|
HALT
|
||||||
|
#Vectors
|
||||||
|
Boot start
|
||||||
|
ASM
|
||||||
|
}
|
||||||
|
|
||||||
|
# Runs one program and says how many cycles the machine used.
|
||||||
|
cycles() {
|
||||||
|
local name="$1"; shift
|
||||||
|
program "$@" > "$BUILD/$name.asm"
|
||||||
|
"$ASM" "$BUILD/$name.asm" -o "$BUILD/$name.bin" >/dev/null 2>&1 || {
|
||||||
|
echo " could not assemble $name"; return 1; }
|
||||||
|
"$EMU" --fast "$BUILD/$name.bin" 2>&1 | grep -oE 'after [0-9]+ cycles' | grep -oE '[0-9]+'
|
||||||
|
}
|
||||||
|
|
||||||
|
# The cost of one transfer: the same program asking for it, less the same program asking
|
||||||
|
# for GuardOff instead. 0x01 is Blit, 0x02 is Fill, 0x11 is GuardOff.
|
||||||
|
charged() {
|
||||||
|
# charged <name> <command> <sourceBank..lenLow>
|
||||||
|
local name="$1" command="$2"; shift 2
|
||||||
|
local withIt withoutIt
|
||||||
|
withIt="$(cycles "$name" "$@" "$command")" || return 1
|
||||||
|
withoutIt="$(cycles "$name-idle" "$@" 0x11)" || return 1
|
||||||
|
[ -n "$withIt" ] && [ -n "$withoutIt" ] || { echo " no cycle count for $name"; return 1; }
|
||||||
|
echo $((withIt - withoutIt))
|
||||||
|
}
|
||||||
|
|
||||||
|
check() {
|
||||||
|
# check <name> <expected> <command> <registers...>
|
||||||
|
local name="$1" expected="$2"; shift 2
|
||||||
|
local got
|
||||||
|
got="$(charged "$name" "$@")" || {
|
||||||
|
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
|
||||||
|
printf " [%sFAIL%s] %-34s could not measure it\n" "$RED" "$RESET" "$name"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if [ "$got" = "$expected" ]; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
printf " [%sok %s] %-34s %s cycles\n" "$GREEN" "$RESET" "$name" "$got"
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
|
||||||
|
printf " [%sFAIL%s] %-34s %s cycles, expected %s\n" "$RED" "$RESET" "$name" "$got" "$expected"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Checking what the memory controller charges."
|
||||||
|
|
||||||
|
# ---- Between two banks ----
|
||||||
|
#
|
||||||
|
# Data Memory to Program Memory, well above where the program itself sits and well below
|
||||||
|
# the vector table. 256 bytes: 128 words and the cycle the pipeline takes to fill.
|
||||||
|
check "256 bytes between banks" 129 0x01 0d1 0x10 0x00 0d0 0x80 0x00 0x01 0x00
|
||||||
|
# The same move with an odd source. Nothing about it can be paired, so it runs at the byte
|
||||||
|
# a cycle the machine had before the path was widened.
|
||||||
|
check "and again from an odd address" 257 0x01 0d1 0x10 0x01 0d0 0x80 0x00 0x01 0x00
|
||||||
|
# An odd destination is just as disqualifying, and so is an odd length: all three have to
|
||||||
|
# line up or none of it does.
|
||||||
|
check "an odd destination is the same" 257 0x01 0d1 0x10 0x00 0d0 0x80 0x01 0x01 0x00
|
||||||
|
check "so is an odd length" 256 0x01 0d1 0x10 0x00 0d0 0x80 0x00 0x00 0xFF
|
||||||
|
|
||||||
|
# ---- Within one bank ----
|
||||||
|
#
|
||||||
|
# One memory cannot overlap its own read and its own write, so it costs twice what the same
|
||||||
|
# move between two banks costs - widened or not.
|
||||||
|
check "256 bytes within one bank" 257 0x01 0d1 0x10 0x00 0d1 0x20 0x00 0x01 0x00
|
||||||
|
check "and misaligned, within one" 513 0x01 0d1 0x10 0x01 0d1 0x20 0x00 0x01 0x00
|
||||||
|
|
||||||
|
# ---- Filling ----
|
||||||
|
#
|
||||||
|
# Nothing to read, so it goes at the between-banks rate whatever the banks are. SourceLow
|
||||||
|
# carries the byte rather than an address, which is why only the destination and the length
|
||||||
|
# decide whether it can be paired.
|
||||||
|
check "256 bytes filled" 129 0x02 0d0 0x00 0xAA 0d1 0x30 0x00 0x01 0x00
|
||||||
|
check "and filled at an odd address" 257 0x02 0d0 0x00 0xAA 0d1 0x30 0x01 0x01 0x00
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ "$FAIL" -eq 0 ]; then
|
||||||
|
echo "All $PASS controller cost checks passed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "$PASS passed, $FAIL failed: ${FAILED_NAMES[*]}"
|
||||||
|
exit 1
|
||||||
+41
-1
@@ -76,7 +76,7 @@ for f in empty.bin one.bin exact.bin part.bin whole.bin; do
|
|||||||
check "$f comes back byte for byte" roundTrip "$f"
|
check "$f comes back byte for byte" roundTrip "$f"
|
||||||
done
|
done
|
||||||
|
|
||||||
refuses "refuse a name of 29 characters" "$TOOL" put work.img part.bin 16bitSegmentedSieveModern.asm
|
refuses "refuse a name of 29 characters" "$TOOL" put work.img part.bin twentyNineCharactersLong.asm
|
||||||
refuses "refuse a duplicate name" "$TOOL" put work.img one.bin
|
refuses "refuse a duplicate name" "$TOOL" put work.img one.bin
|
||||||
refuses "refuse a file that is not there" "$TOOL" get work.img nosuch.bin out.bin
|
refuses "refuse a file that is not there" "$TOOL" get work.img nosuch.bin out.bin
|
||||||
check "delete" "$TOOL" delete work.img one.bin
|
check "delete" "$TOOL" delete work.img one.bin
|
||||||
@@ -228,6 +228,46 @@ check "a fresh disk is settled" python3 -c "
|
|||||||
import sys
|
import sys
|
||||||
sys.exit(0 if open('plain.img','rb').read()[17] == 0 else 1)"
|
sys.exit(0 if open('plain.img','rb').read()[17] == 0 else 1)"
|
||||||
|
|
||||||
|
# ---- Mirroring a host directory ----
|
||||||
|
#
|
||||||
|
# What the system disk is built with. Every file goes through put and every directory
|
||||||
|
# through mkdir, so this is a walk over machinery already checked above - what wants
|
||||||
|
# checking is the walk: that it goes all the way down, that it leaves behind what it was
|
||||||
|
# told to, and that it REFUSES a name the format cannot hold rather than skipping it, since
|
||||||
|
# a disk quietly missing a file is the failure a mirror exists to prevent.
|
||||||
|
mkdir -p tree/inner/deeper tree/leave
|
||||||
|
printf 'top' > tree/top.txt
|
||||||
|
printf 'inner' > tree/inner/middle.txt
|
||||||
|
printf 'deep' > tree/inner/deeper/bottom.txt
|
||||||
|
printf 'not this' > tree/leave/ignored.txt
|
||||||
|
: > tree/.hidden
|
||||||
|
|
||||||
|
"$TOOL" format mirror.img 256 8 >/dev/null
|
||||||
|
check "mirror a directory tree" "$TOOL" mirror mirror.img tree /
|
||||||
|
|
||||||
|
"$TOOL" list mirror.img > mirrored.txt 2>&1
|
||||||
|
grep -q '/inner/deeper/bottom.txt' mirrored.txt \
|
||||||
|
&& { PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "it goes all the way down"; } \
|
||||||
|
|| { FAIL=$((FAIL + 1)); FAILED_NAMES+=("depth"); printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "it goes all the way down"; }
|
||||||
|
grep -q 'hidden' mirrored.txt \
|
||||||
|
&& { FAIL=$((FAIL + 1)); FAILED_NAMES+=("hidden"); printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "and leaves dotfiles behind"; } \
|
||||||
|
|| { PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "and leaves dotfiles behind"; }
|
||||||
|
|
||||||
|
# Named on the command line, which is how a project keeps what it builds out of what it
|
||||||
|
# wrote.
|
||||||
|
"$TOOL" format skipped.img 256 8 >/dev/null
|
||||||
|
check "mirror with something left out" "$TOOL" mirror skipped.img tree / leave
|
||||||
|
"$TOOL" list skipped.img > skipped.txt 2>&1
|
||||||
|
grep -q 'ignored.txt' skipped.txt \
|
||||||
|
&& { FAIL=$((FAIL + 1)); FAILED_NAMES+=("skip"); printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "and the skipped one is not there"; } \
|
||||||
|
|| { PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "and the skipped one is not there"; }
|
||||||
|
|
||||||
|
# Twenty-three characters, one more than a directory entry holds.
|
||||||
|
printf 'too long' > tree/aNameOfTwentyThreeChars
|
||||||
|
"$TOOL" format refused.img 256 8 >/dev/null
|
||||||
|
refuses "a name too long stops the mirror" "$TOOL" mirror refused.img tree /
|
||||||
|
rm -f tree/aNameOfTwentyThreeChars
|
||||||
|
|
||||||
echo
|
echo
|
||||||
if [ "$FAIL" -eq 0 ]; then
|
if [ "$FAIL" -eq 0 ]; then
|
||||||
echo "All $PASS disk tool checks passed."
|
echo "All $PASS disk tool checks passed."
|
||||||
|
|||||||
+100
-24
@@ -510,7 +510,15 @@ else:
|
|||||||
# The minimal application in the same section is what somebody copies, so it is the
|
# The minimal application in the same section is what somebody copies, so it is the
|
||||||
# part of the map most worth being right. It went stale across the doubling while the
|
# part of the map most worth being right. It went stale across the doubling while the
|
||||||
# table above it was corrected.
|
# table above it was corrected.
|
||||||
example = re.search(r"```asm\n(.*?)```", readme, re.S)
|
# ---- Found by its section, not by being first ----
|
||||||
|
#
|
||||||
|
# This took the first asm block in the file, which was the minimal application right up
|
||||||
|
# until somebody documented a program with an assembly example above it - and then this
|
||||||
|
# said the minimal application had no #Base, about a block that was never claiming to be
|
||||||
|
# one. The example lives under System Services; that is what identifies it.
|
||||||
|
services = readme.split("### System Services:", 1)
|
||||||
|
example = (re.search(r"```asm\n(.*?)```", services[1], re.S)
|
||||||
|
if len(services) > 1 else None)
|
||||||
if not example:
|
if not example:
|
||||||
problems.append("the CosmOS README no longer shows a minimal application")
|
problems.append("the CosmOS README no longer shows a minimal application")
|
||||||
else:
|
else:
|
||||||
@@ -581,42 +589,110 @@ else:
|
|||||||
" 0x%s - the buffers are on top of the variables"
|
" 0x%s - the buffers are on top of the variables"
|
||||||
% (ends - 1, first.group(1).upper()))
|
% (ends - 1, first.group(1).upper()))
|
||||||
|
|
||||||
# ---- Every test script the suite runs has a bullet saying why it exists ----
|
# ---- The Test Manual against the suite it describes ----
|
||||||
#
|
#
|
||||||
# Two claims in the README went stale at once and neither was noticed: it said FIVE more
|
# The suite documents itself, and a document about the suite goes stale the same way every
|
||||||
# scripts run alongside run.sh when there were six, and "all three tools" when there were
|
# other one does. Two claims in the README went stale at once before this check existed and
|
||||||
# four. Both are the kind of number that is written once, is true for months, and is then
|
# neither was noticed: it said FIVE more scripts run alongside run.sh when there were six,
|
||||||
# quietly wrong - which is the entire subject of this file.
|
# and "all three tools" when there were four. Both are the kind of number that is written
|
||||||
|
# once, is true for months, and is then quietly wrong - which is the entire subject of this
|
||||||
|
# file.
|
||||||
#
|
#
|
||||||
# run.sh is the driver rather than one of the others, and makedisks.sh makes the images
|
# The bullets now live in the Test Manual rather than the README, so that is what is read.
|
||||||
# rather than checking anything, so neither is counted.
|
# makedisks.sh is not counted, because it builds the images rather than checking anything;
|
||||||
# The repository README rather than CosmOS's, which is what `readme` above holds.
|
# run.sh is counted, because the manual describes it alongside the rest.
|
||||||
rootReadme = open("README.md").read()
|
rootReadme = open("README.md").read()
|
||||||
|
manual = open("SplitBit Test Manual.md").read()
|
||||||
|
# THE MANUAL IS WRAPPED, so a number and the noun it counts are regularly on two different
|
||||||
|
# lines. Every pattern below runs against a copy with its whitespace flattened.
|
||||||
|
flat = re.sub(r"\s+", " ", manual)
|
||||||
|
|
||||||
scripts = sorted(os.path.basename(p) for p in glob.glob("Tests/*.sh")
|
scripts = sorted(os.path.basename(p) for p in glob.glob("Tests/*.sh")
|
||||||
if os.path.basename(p) not in ("run.sh", "makedisks.sh"))
|
if os.path.basename(p) != "makedisks.sh")
|
||||||
words = {"three": 3, "four": 4, "five": 5, "six": 6, "seven": 7, "eight": 8, "nine": 9}
|
# Spelled out, because that is how the documents say them. Kept a few ahead of the count so
|
||||||
said = re.search(r"([A-Za-z]+) more scripts run alongside it", rootReadme)
|
# that adding a script fails on the number being wrong rather than on the word being unknown,
|
||||||
|
# which is a much less helpful thing to be told.
|
||||||
|
words = {"three": 3, "four": 4, "five": 5, "six": 6, "seven": 7, "eight": 8, "nine": 9,
|
||||||
|
"ten": 10, "eleven": 11, "twelve": 12, "thirteen": 13, "fourteen": 14}
|
||||||
|
said = re.search(r"It is ([a-z]+) scripts making", flat)
|
||||||
if not said:
|
if not said:
|
||||||
problems.append("the README no longer says how many scripts run alongside run.sh")
|
problems.append("the Test Manual no longer says how many scripts the suite is")
|
||||||
elif words.get(said.group(1).lower()) != len(scripts):
|
elif words.get(said.group(1)) != len(scripts):
|
||||||
problems.append("the README says %s scripts run alongside run.sh, and there are %d: %s"
|
problems.append("the Test Manual says the suite is %s scripts, and there are %d: %s"
|
||||||
% (said.group(1), len(scripts), ", ".join(scripts)))
|
% (said.group(1), len(scripts), ", ".join(scripts)))
|
||||||
for name in scripts:
|
for name in scripts:
|
||||||
if ("`Tests/%s`" % name) not in rootReadme:
|
if ("`Tests/%s`" % name) not in manual:
|
||||||
problems.append("Tests/%s runs in the suite and the README does not say what it is"
|
problems.append("Tests/%s runs in the suite and the Test Manual does not say what"
|
||||||
" for" % name)
|
" it is for" % name)
|
||||||
|
|
||||||
|
# ---- The shape of the manifest, which the manual states outright ----
|
||||||
|
#
|
||||||
|
# Five numbers in one sentence, all of them countable from the file they describe. This is
|
||||||
|
# the most quotable thing in the manual and the least likely to be recounted by hand.
|
||||||
|
modes = {}
|
||||||
|
total = 0
|
||||||
|
for line in open("Tests/manifest"):
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
total += 1
|
||||||
|
fields = line.split("|")
|
||||||
|
if len(fields) > 2:
|
||||||
|
modes[fields[2].strip()] = modes.get(fields[2].strip(), 0) + 1
|
||||||
|
|
||||||
|
said = re.search(r"(\d+) tests, of which (\d+) run, (\d+) only assemble,"
|
||||||
|
r" (\d+) are expected to fail to assemble, and (\d+) boot from ROM", flat)
|
||||||
|
if not said:
|
||||||
|
problems.append("the Test Manual no longer states the shape of the manifest")
|
||||||
|
else:
|
||||||
|
for index, (what, count) in enumerate((("tests", total),
|
||||||
|
("run tests", modes.get("run", 0)),
|
||||||
|
("assemble-only tests", modes.get("assemble", 0)),
|
||||||
|
("xfail tests", modes.get("xfail", 0)),
|
||||||
|
("rom tests", modes.get("rom", 0)))):
|
||||||
|
if int(said.group(index + 1)) != count:
|
||||||
|
problems.append("the Test Manual says there are %s %s, and there are %d"
|
||||||
|
% (said.group(index + 1), what, count))
|
||||||
|
|
||||||
|
said = re.search(r"The (\d+) `xfail` tests", flat)
|
||||||
|
if said and int(said.group(1)) != modes.get("xfail", 0):
|
||||||
|
problems.append("the Test Manual says %s xfail tests in one place and %d in another"
|
||||||
|
% (said.group(1), modes.get("xfail", 0)))
|
||||||
|
|
||||||
|
# ---- And the fixtures and the baseline ----
|
||||||
|
disks = len(re.findall(r'format "\$DISKS/', open("Tests/makedisks.sh").read()))
|
||||||
|
said = re.search(r"builds (\d+) images with SplitDisk", flat)
|
||||||
|
if not said:
|
||||||
|
problems.append("the Test Manual no longer says how many fixture disks are built")
|
||||||
|
elif int(said.group(1)) != disks:
|
||||||
|
problems.append("the Test Manual says %s fixture disks are built, and makedisks.sh"
|
||||||
|
" builds %d" % (said.group(1), disks))
|
||||||
|
|
||||||
|
pairs = sum(1 for line in open("Tests/lint-baseline.txt") if line.strip())
|
||||||
|
said = re.search(r"(\d+) file-and-rule pairs", flat)
|
||||||
|
if not said:
|
||||||
|
problems.append("the Test Manual no longer says how large the lint baseline is")
|
||||||
|
elif int(said.group(1)) != pairs:
|
||||||
|
problems.append("the Test Manual says the lint baseline holds %s file and rule pairs,"
|
||||||
|
" and it holds %d" % (said.group(1), pairs))
|
||||||
|
|
||||||
# ---- And the tool count is the number of things the makefile builds ----
|
# ---- And the tool count is the number of things the makefile builds ----
|
||||||
|
#
|
||||||
|
# Claimed in both documents, so both are read.
|
||||||
makefile = open("makefile").read()
|
makefile = open("makefile").read()
|
||||||
built = re.search(r"^all:(.*)$", makefile, re.M)
|
# TOOLS rather than the all target, which now depends on whether Raylib is installed.
|
||||||
|
# What "the tools" means should be a fact in one place, not read off a conditional.
|
||||||
|
built = re.search(r"^TOOLS = (.*)$", makefile, re.M)
|
||||||
if not built:
|
if not built:
|
||||||
problems.append("the makefile no longer has an all target this can count")
|
problems.append("the makefile no longer has a TOOLS list this can count")
|
||||||
else:
|
else:
|
||||||
tools = len(built.group(1).split())
|
tools = len(built.group(1).split())
|
||||||
for said in re.findall(r"(?:build|rebuild) (?:all )?(?:the )?([a-z]+) tools", rootReadme):
|
for where, text in (("README", rootReadme), ("Test Manual", manual)):
|
||||||
if words.get(said) != tools:
|
for said in re.findall(r"(?:build|rebuild)s? (?:all )?(?:the )?([a-z]+) tools",
|
||||||
problems.append("the README says the %s tools and the makefile builds %d"
|
text, re.I):
|
||||||
% (said, tools))
|
if words.get(said.lower()) != tools:
|
||||||
|
problems.append("the %s says the %s tools and the makefile builds %d"
|
||||||
|
% (where, said, tools))
|
||||||
|
|
||||||
# ---- The sizes the CosmOS README quotes for its own programs ----
|
# ---- The sizes the CosmOS README quotes for its own programs ----
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[2J[H #
|
[2J[H[1;1H #
|
||||||
#
|
#
|
||||||
###
|
###
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
# #
|
# #
|
||||||
##
|
##
|
||||||
#
|
#
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
#
|
#
|
||||||
# #
|
# #
|
||||||
##
|
##
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
#
|
#
|
||||||
##
|
##
|
||||||
##
|
##
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
###
|
###
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
# #
|
# #
|
||||||
##
|
##
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
#
|
#
|
||||||
# #
|
# #
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
#
|
#
|
||||||
##
|
##
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
# #
|
# #
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -190,7 +190,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -270,7 +270,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -286,7 +286,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -318,7 +318,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -334,7 +334,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -350,7 +350,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -366,7 +366,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -382,7 +382,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -398,7 +398,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -414,7 +414,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -430,7 +430,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -446,7 +446,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[2J[H #
|
[2J[H[1;1H #
|
||||||
#
|
#
|
||||||
###
|
###
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
# #
|
# #
|
||||||
##
|
##
|
||||||
#
|
#
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
#
|
#
|
||||||
# #
|
# #
|
||||||
##
|
##
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
#
|
#
|
||||||
##
|
##
|
||||||
##
|
##
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
###
|
###
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
# #
|
# #
|
||||||
##
|
##
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
#
|
#
|
||||||
# #
|
# #
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
#
|
#
|
||||||
##
|
##
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
# #
|
# #
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -190,7 +190,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -270,7 +270,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -286,7 +286,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -318,7 +318,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -334,7 +334,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -350,7 +350,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -366,7 +366,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -382,7 +382,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -398,7 +398,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -414,7 +414,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -430,7 +430,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -446,7 +446,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -462,7 +462,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,20 @@ stage two
|
|||||||
no /System/Boot/missing.bin
|
no /System/Boot/missing.bin
|
||||||
trying the fallback
|
trying the fallback
|
||||||
CosmOS
|
CosmOS
|
||||||
> saved it
|
> Files
|
||||||
|
saved it
|
||||||
read it back, 22 bytes:
|
read it back, 22 bytes:
|
||||||
a file kept by asking
|
a file kept by asking
|
||||||
renamed it
|
renamed it
|
||||||
deleted it
|
deleted it
|
||||||
and it is gone
|
and it is gone
|
||||||
finished
|
finished
|
||||||
> made
|
> mkdir Notes
|
||||||
> /Notes> 0 files
|
made
|
||||||
/Notes> halted
|
> cd Notes
|
||||||
|
/Notes> dir
|
||||||
|
0 files
|
||||||
|
/Notes> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
ordinary highlighted
|
||||||
|
ordinary highlighted
|
||||||
|
ordinary highlighted
|
||||||
|
ordinary highlighted
|
||||||
|
ordinary highlighted
|
||||||
|
ordinary highlighted
|
||||||
|
ordinary highlighted
|
||||||
|
ordinary highlighted
|
||||||
|
|
||||||
|
bank 2's ink is orange now, because this program said so
|
||||||
|
Execution halted.
|
||||||
|
[exit 0]
|
||||||
@@ -1,17 +1,22 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> dir list what is on the disk
|
> help
|
||||||
|
dir list what is on the disk
|
||||||
load <file> read a program off the disk
|
load <file> read a program off the disk
|
||||||
run [words] start what was loaded, and tell it those words
|
run [words] start what was loaded, and tell it those words
|
||||||
<name> [words] look where you are and then in /Apps, and start that
|
<name> [words] look where you are and then in /Apps, and start that
|
||||||
cd [path] go to a directory, or to the root with nothing after it
|
cd [path] go to a directory, or to the root with nothing after it
|
||||||
mkdir <path> make a directory
|
mkdir <path> make a directory
|
||||||
rmdir <path> remove an empty one
|
rmdir <path> remove an empty one
|
||||||
|
do <file> run the lines in a file, which must start with #!
|
||||||
|
echo [words] say them
|
||||||
|
clear empty the screen
|
||||||
delete <file> take it off the disk
|
delete <file> take it off the disk
|
||||||
rename <file> <to> call it something else
|
rename <file> <to> call it something else
|
||||||
monitor look at memory, change it, and jump into it
|
monitor look at memory, change it, and jump into it
|
||||||
help this
|
help this
|
||||||
exit stop, or leave the monitor if you are in it
|
exit stop, or leave the monitor if you are in it
|
||||||
> greeting.txt 17
|
> dir
|
||||||
|
greeting.txt 17
|
||||||
filler1.txt 8
|
filler1.txt 8
|
||||||
filler2.txt 8
|
filler2.txt 8
|
||||||
filler3.txt 8
|
filler3.txt 8
|
||||||
@@ -24,7 +29,10 @@ across.txt 700
|
|||||||
empty.txt 0
|
empty.txt 0
|
||||||
aName22CharactersLong! 22
|
aName22CharactersLong! 22
|
||||||
12 files
|
12 files
|
||||||
> > I do not know: frobnicate
|
>
|
||||||
> halted
|
> frobnicate
|
||||||
|
I do not know: frobnicate
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
no filesystem on the disk
|
no filesystem on the disk
|
||||||
> no filesystem on the disk
|
> dir
|
||||||
> halted
|
no filesystem on the disk
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CosmOS
|
||||||
|
> drive 1
|
||||||
|
drive: nothing this can read is in that drive
|
||||||
|
> drive
|
||||||
|
0
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
|
Execution halted.
|
||||||
|
[exit 0]
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> loaded, starting at 4000
|
> load Break.sbx
|
||||||
> two stops, and what the registers were at each
|
loaded, starting at 4000
|
||||||
|
> run
|
||||||
|
two stops, and what the registers were at each
|
||||||
break at 4016
|
break at 4016
|
||||||
A 11 B 22 Q 00 status 00
|
A 11 B 22 Q 00 status 00
|
||||||
DP0 2030 DP1 2000 DP2 2037 DP3 4000 SP FFFF
|
DP0 2030 DP1 2000 DP2 2037 DP3 4000 SP FFFF
|
||||||
@@ -11,6 +13,7 @@ DP0 2000 DP1 2037 DP2 2030 DP3 4000 SP FFF5
|
|||||||
press a key
|
press a key
|
||||||
carried on to the end
|
carried on to the end
|
||||||
finished
|
finished
|
||||||
> halted
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -1,27 +1,50 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> 0 files
|
> dir
|
||||||
> made
|
0 files
|
||||||
> made
|
> mkdir Apps
|
||||||
> made
|
made
|
||||||
> Apps <dir>
|
> mkdir Apps/Deep
|
||||||
|
made
|
||||||
|
> mkdir Notes
|
||||||
|
made
|
||||||
|
> dir
|
||||||
|
Apps <dir>
|
||||||
Notes <dir>
|
Notes <dir>
|
||||||
0 files, 2 directories
|
0 files, 2 directories
|
||||||
> /Apps> Deep <dir>
|
> cd Apps
|
||||||
|
/Apps> dir
|
||||||
|
Deep <dir>
|
||||||
0 files, 1 directory
|
0 files, 1 directory
|
||||||
/Apps> cannot make that: check the path, the name, and whether it is taken
|
/Apps> mkdir Deep
|
||||||
/Apps> made
|
cannot make that: check the path, the name, and whether it is taken
|
||||||
/Apps> /Notes> Deep <dir>
|
/Apps> mkdir /Notes/Deep
|
||||||
|
made
|
||||||
|
/Apps> cd /Notes
|
||||||
|
/Notes> dir
|
||||||
|
Deep <dir>
|
||||||
0 files, 1 directory
|
0 files, 1 directory
|
||||||
/Notes> cannot remove that: it must be a directory, and empty
|
/Notes> rmdir /Apps
|
||||||
/Notes> removed
|
cannot remove that: it must be a directory, and empty
|
||||||
/Notes> that is a directory
|
/Notes> rmdir /Apps/Deep
|
||||||
/Notes> removed
|
removed
|
||||||
/Notes> > cannot make that: check the path, the name, and whether it is taken
|
/Notes> delete /Apps
|
||||||
> cannot remove that: it must be a directory, and empty
|
that is a directory
|
||||||
> cannot remove that: it must be a directory, and empty
|
/Notes> rmdir /Notes/Deep
|
||||||
> removed
|
removed
|
||||||
> removed
|
/Notes> cd /
|
||||||
> 0 files
|
> mkdir Apps/Deep/Inner
|
||||||
> halted
|
cannot make that: check the path, the name, and whether it is taken
|
||||||
|
> rmdir Apps/Deep/Inner
|
||||||
|
cannot remove that: it must be a directory, and empty
|
||||||
|
> rmdir Apps/Deep
|
||||||
|
cannot remove that: it must be a directory, and empty
|
||||||
|
> rmdir Apps
|
||||||
|
removed
|
||||||
|
> rmdir Notes
|
||||||
|
removed
|
||||||
|
> dir
|
||||||
|
0 files
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> claiming more than was reserved was refused
|
> Claim
|
||||||
|
claiming more than was reserved was refused
|
||||||
and the size it really came to was taken
|
and the size it really came to was taken
|
||||||
finished
|
finished
|
||||||
> Claim.sbx 580
|
> dir
|
||||||
|
Claim.sbx 580
|
||||||
claim.dat 266
|
claim.dat 266
|
||||||
2 files
|
2 files
|
||||||
> halted
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -1,30 +1,44 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> copied
|
> Copy /Input/empty.dat /Output/empty.dat
|
||||||
|
copied
|
||||||
finished
|
finished
|
||||||
> the same
|
> Compare /Input/empty.dat /Output/empty.dat
|
||||||
|
the same
|
||||||
finished
|
finished
|
||||||
> copied
|
> Copy /Input/exact.dat /Output/exact.dat
|
||||||
|
copied
|
||||||
finished
|
finished
|
||||||
> the same
|
> Compare /Input/exact.dat /Output/exact.dat
|
||||||
|
the same
|
||||||
finished
|
finished
|
||||||
> copied
|
> Copy /Input/tail.dat /Output/tail.dat
|
||||||
|
copied
|
||||||
finished
|
finished
|
||||||
> the same
|
> Compare /Input/tail.dat /Output/tail.dat
|
||||||
|
the same
|
||||||
finished
|
finished
|
||||||
> copied
|
> Copy /Input/large.dat /Output/large.dat
|
||||||
|
copied
|
||||||
finished
|
finished
|
||||||
> the same
|
> Compare /Input/large.dat /Output/large.dat
|
||||||
|
the same
|
||||||
finished
|
finished
|
||||||
> different
|
> Compare /Input/large.dat /Input/different.dat
|
||||||
|
different
|
||||||
finished
|
finished
|
||||||
> copy: cannot find the source
|
> Copy /Input/missing.dat /Output/missing.dat
|
||||||
|
copy: cannot find the source
|
||||||
finished
|
finished
|
||||||
> compare: cannot find the second file
|
> Compare /Input/large.dat /Input/missing.dat
|
||||||
|
compare: cannot find the second file
|
||||||
finished
|
finished
|
||||||
> copy: give me a source and destination
|
> Copy
|
||||||
|
copy: give me a source and destination
|
||||||
finished
|
finished
|
||||||
> compare: give me two files
|
> Compare /Input/large.dat
|
||||||
|
compare: give me two files
|
||||||
finished
|
finished
|
||||||
> halted
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
CosmOS
|
||||||
|
> drive 1
|
||||||
|
> Copy 1:/twoblocks.txt 0:/crossed.txt
|
||||||
|
copied
|
||||||
|
finished
|
||||||
|
> drive
|
||||||
|
1
|
||||||
|
> Say from drive one
|
||||||
|
it says: from drive one
|
||||||
|
finished
|
||||||
|
> drive
|
||||||
|
1
|
||||||
|
> cd 0:/
|
||||||
|
> dir
|
||||||
|
greet.sbx 211
|
||||||
|
hello.sbx 53
|
||||||
|
Life.sbx 1396
|
||||||
|
Snake.sbx 2164
|
||||||
|
Keys.sbx 664
|
||||||
|
Say.sbx 156
|
||||||
|
Break.sbx 149
|
||||||
|
Grid.sbx 559
|
||||||
|
Press.sbx 872
|
||||||
|
notes.txt 21
|
||||||
|
Apps <dir>
|
||||||
|
hi.script 121
|
||||||
|
bad.script 45
|
||||||
|
plain.script 24
|
||||||
|
cross.script 280
|
||||||
|
nonl.script 38
|
||||||
|
outer.script 376
|
||||||
|
inner.script 44
|
||||||
|
loop.script 35
|
||||||
|
crossed.txt 560
|
||||||
|
19 files, 1 directory
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
|
Execution halted.
|
||||||
|
[exit 0]
|
||||||
@@ -1,36 +1,56 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> Apps <dir>
|
> dir
|
||||||
|
Apps <dir>
|
||||||
A <dir>
|
A <dir>
|
||||||
B <dir>
|
B <dir>
|
||||||
0 files, 3 directories
|
0 files, 3 directories
|
||||||
> /A> Say.sbx 53
|
> cd /A
|
||||||
|
/A> dir
|
||||||
|
Say.sbx 53
|
||||||
notes.txt 25
|
notes.txt 25
|
||||||
2 files
|
2 files
|
||||||
/A> these are the notes in A
|
/A> Type notes.txt
|
||||||
finished
|
|
||||||
/A> /B> and these are the very different notes in B
|
|
||||||
finished
|
|
||||||
/B> it says: reached the one in Apps
|
|
||||||
finished
|
|
||||||
/B> /A> Hello, World!
|
|
||||||
finished
|
|
||||||
/A> > Apps <dir>
|
|
||||||
A <dir>
|
|
||||||
B <dir>
|
|
||||||
0 files, 3 directories
|
|
||||||
> /B> these are the notes in A
|
|
||||||
finished
|
|
||||||
/B> moved, and reading a bare name from there:
|
|
||||||
these are the notes in A
|
these are the notes in A
|
||||||
finished
|
finished
|
||||||
/B> and these are the very different notes in B
|
/A> cd /B
|
||||||
|
/B> Type notes.txt
|
||||||
|
and these are the very different notes in B
|
||||||
finished
|
finished
|
||||||
/B> no such file
|
/B> Say reached the one in Apps
|
||||||
/B> that is not a directory
|
it says: reached the one in Apps
|
||||||
/B> > Apps <dir>
|
finished
|
||||||
|
/B> cd /A
|
||||||
|
/A> Say reached the one in A
|
||||||
|
Hello, World!
|
||||||
|
finished
|
||||||
|
/A> cd ..
|
||||||
|
> dir
|
||||||
|
Apps <dir>
|
||||||
A <dir>
|
A <dir>
|
||||||
B <dir>
|
B <dir>
|
||||||
0 files, 3 directories
|
0 files, 3 directories
|
||||||
> halted
|
> cd /B
|
||||||
|
/B> Type ../A/notes.txt
|
||||||
|
these are the notes in A
|
||||||
|
finished
|
||||||
|
/B> Wander /A
|
||||||
|
moved, and reading a bare name from there:
|
||||||
|
these are the notes in A
|
||||||
|
finished
|
||||||
|
/B> Type notes.txt
|
||||||
|
and these are the very different notes in B
|
||||||
|
finished
|
||||||
|
/B> cd nosuchplace
|
||||||
|
no such file
|
||||||
|
/B> cd notes.txt
|
||||||
|
that is not a directory
|
||||||
|
/B> cd /
|
||||||
|
> dir
|
||||||
|
Apps <dir>
|
||||||
|
A <dir>
|
||||||
|
B <dir>
|
||||||
|
0 files, 3 directories
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> /abcdefghijklmnopqrst01> /abcdefghijklmnopqrst01/abcdefghijklmnopqrst02> /abcdefghijklmnopqrst01/abcdefghijklmnopqrst02/abcdefghijklmnopqrst03> /abcdefghijklmnopqrst01/abcdefghijklmnopqrst02/abcdefghijklmnopqrst03/abcdefghijklmnopqrst04> /abcdefghijklmnopqrst01/abcdefghijklmnopqrst02/abcdefghijklmnopqrst03/abcdefghijklmnopqrst04/abcdefghijklmnopqrst05> ...opqrst01/abcdefghijklmnopqrst02/abcdefghijklmnopqrst03/abcdefghijklmnopqrst04/abcdefghijklmnopqrst05/abcdefghijklmnopqrst06> ...opqrst02/abcdefghijklmnopqrst03/abcdefghijklmnopqrst04/abcdefghijklmnopqrst05/abcdefghijklmnopqrst06/abcdefghijklmnopqrst07> ...opqrst03/abcdefghijklmnopqrst04/abcdefghijklmnopqrst05/abcdefghijklmnopqrst06/abcdefghijklmnopqrst07/abcdefghijklmnopqrst08> dir list what is on the disk
|
> cd abcdefghijklmnopqrst01
|
||||||
|
/abcdefghijklmnopqrst01> cd abcdefghijklmnopqrst02
|
||||||
|
/abcdefghijklmnopqrst01/abcdefghijklmnopqrst02> cd abcdefghijklmnopqrst03
|
||||||
|
/abcdefghijklmnopqrst01/abcdefghijklmnopqrst02/abcdefghijklmnopqrst03> cd abcdefghijklmnopqrst04
|
||||||
|
/abcdefghijklmnopqrst01/abcdefghijklmnopqrst02/abcdefghijklmnopqrst03/abcdefghijklmnopqrst04> cd abcdefghijklmnopqrst05
|
||||||
|
/abcdefghijklmnopqrst01/abcdefghijklmnopqrst02/abcdefghijklmnopqrst03/abcdefghijklmnopqrst04/abcdefghijklmnopqrst05> cd abcdefghijklmnopqrst06
|
||||||
|
...opqrst01/abcdefghijklmnopqrst02/abcdefghijklmnopqrst03/abcdefghijklmnopqrst04/abcdefghijklmnopqrst05/abcdefghijklmnopqrst06> cd abcdefghijklmnopqrst07
|
||||||
|
...opqrst02/abcdefghijklmnopqrst03/abcdefghijklmnopqrst04/abcdefghijklmnopqrst05/abcdefghijklmnopqrst06/abcdefghijklmnopqrst07> cd abcdefghijklmnopqrst08
|
||||||
|
...opqrst03/abcdefghijklmnopqrst04/abcdefghijklmnopqrst05/abcdefghijklmnopqrst06/abcdefghijklmnopqrst07/abcdefghijklmnopqrst08> help
|
||||||
|
dir list what is on the disk
|
||||||
load <file> read a program off the disk
|
load <file> read a program off the disk
|
||||||
run [words] start what was loaded, and tell it those words
|
run [words] start what was loaded, and tell it those words
|
||||||
<name> [words] look where you are and then in /Apps, and start that
|
<name> [words] look where you are and then in /Apps, and start that
|
||||||
cd [path] go to a directory, or to the root with nothing after it
|
cd [path] go to a directory, or to the root with nothing after it
|
||||||
mkdir <path> make a directory
|
mkdir <path> make a directory
|
||||||
rmdir <path> remove an empty one
|
rmdir <path> remove an empty one
|
||||||
|
do <file> run the lines in a file, which must start with #!
|
||||||
|
echo [words] say them
|
||||||
|
clear empty the screen
|
||||||
delete <file> take it off the disk
|
delete <file> take it off the disk
|
||||||
rename <file> <to> call it something else
|
rename <file> <to> call it something else
|
||||||
monitor look at memory, change it, and jump into it
|
monitor look at memory, change it, and jump into it
|
||||||
help this
|
help this
|
||||||
exit stop, or leave the monitor if you are in it
|
exit stop, or leave the monitor if you are in it
|
||||||
...opqrst03/abcdefghijklmnopqrst04/abcdefghijklmnopqrst05/abcdefghijklmnopqrst06/abcdefghijklmnopqrst07/abcdefghijklmnopqrst08> > halted
|
...opqrst03/abcdefghijklmnopqrst04/abcdefghijklmnopqrst05/abcdefghijklmnopqrst06/abcdefghijklmnopqrst07/abcdefghijklmnopqrst08> cd
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
CosmOS
|
||||||
|
> cd 1:/notes
|
||||||
|
/notes> drive
|
||||||
|
1
|
||||||
|
/notes> cd 0:/
|
||||||
|
> drive
|
||||||
|
0
|
||||||
|
> cd 1:/2things
|
||||||
|
/2things> drive
|
||||||
|
1
|
||||||
|
/2things> cd /
|
||||||
|
> cd 2things
|
||||||
|
/2things> drive
|
||||||
|
1
|
||||||
|
/2things> cd 9:/
|
||||||
|
no such file
|
||||||
|
/2things> exit
|
||||||
|
halted
|
||||||
|
Execution halted.
|
||||||
|
[exit 0]
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
CosmOS
|
||||||
|
> drive
|
||||||
|
0
|
||||||
|
> dir
|
||||||
|
greet.sbx 211
|
||||||
|
hello.sbx 53
|
||||||
|
Life.sbx 1396
|
||||||
|
Snake.sbx 2164
|
||||||
|
Keys.sbx 664
|
||||||
|
Say.sbx 156
|
||||||
|
Break.sbx 149
|
||||||
|
Grid.sbx 559
|
||||||
|
Press.sbx 872
|
||||||
|
notes.txt 21
|
||||||
|
Apps <dir>
|
||||||
|
hi.script 121
|
||||||
|
bad.script 45
|
||||||
|
plain.script 24
|
||||||
|
cross.script 280
|
||||||
|
nonl.script 38
|
||||||
|
outer.script 376
|
||||||
|
inner.script 44
|
||||||
|
loop.script 35
|
||||||
|
18 files, 1 directory
|
||||||
|
> drive 1
|
||||||
|
> dir
|
||||||
|
other.txt 28
|
||||||
|
notes <dir>
|
||||||
|
2things <dir>
|
||||||
|
twoblocks.txt 560
|
||||||
|
2 files, 2 directories
|
||||||
|
> cd /notes
|
||||||
|
/notes> drive 0
|
||||||
|
> drive 1
|
||||||
|
/notes> drive 0
|
||||||
|
> drive 1
|
||||||
|
/notes> drive 3
|
||||||
|
drive: this machine has no such drive
|
||||||
|
/notes> exit
|
||||||
|
halted
|
||||||
|
Execution halted.
|
||||||
|
[exit 0]
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> loaded, starting at 4000
|
> load Edit.sbx
|
||||||
> poem.txt, 0 lines
|
loaded, starting at 4000
|
||||||
|
> run poem.txt
|
||||||
|
poem.txt, new file
|
||||||
> : : : : > : : > 1: alpha
|
> : : : : > : : > 1: alpha
|
||||||
2: INSERTED
|
2: INSERTED
|
||||||
3: beta
|
3: beta
|
||||||
@@ -10,12 +12,14 @@ CosmOS
|
|||||||
3: beta
|
3: beta
|
||||||
> written, 22 bytes
|
> written, 22 bytes
|
||||||
> finished
|
> finished
|
||||||
> poem.txt, 3 lines
|
> run poem.txt
|
||||||
|
poem.txt, 3 lines
|
||||||
> 1: CHANGED
|
> 1: CHANGED
|
||||||
2: INSERTED
|
2: INSERTED
|
||||||
3: beta
|
3: beta
|
||||||
> there is no such line
|
> there is no such line
|
||||||
> finished
|
> finished
|
||||||
> halted
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
CosmOS
|
||||||
|
> eco ok[2;3Hecho ok [2;10H
|
||||||
|
ok
|
||||||
|
> Xecho ok[4;3Hecho ok [4;10H
|
||||||
|
ok
|
||||||
|
> echo o[6;9Hk
|
||||||
|
ok
|
||||||
|
> echo okX
|
||||||
|
ok
|
||||||
|
> echo k[10;3Hecho ok [10;10H
|
||||||
|
ok
|
||||||
|
> Xecho aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[12;3Hecho aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa [13;8H!
|
||||||
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
|
Execution halted.
|
||||||
|
[exit 0]
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> hello.asm, 31 lines
|
> Edit hello.asm
|
||||||
|
hello.asm, 31 lines
|
||||||
> finished
|
> finished
|
||||||
> hello.asm, 31 lines
|
> Edit hello.asm
|
||||||
|
hello.asm, 31 lines
|
||||||
> finished
|
> finished
|
||||||
> the last program left 0, which is: it did what it was asked
|
> Status
|
||||||
|
the last program left 0, which is: it did what it was asked
|
||||||
finished
|
finished
|
||||||
> a line in it is longer than this can edit, so it has not been opened
|
> Edit long.txt
|
||||||
|
a line in it is longer than this can edit, so it has not been opened
|
||||||
finished
|
finished
|
||||||
> the last program left 1, which is: it did not
|
> Status
|
||||||
|
the last program left 1, which is: it did not
|
||||||
finished
|
finished
|
||||||
> halted
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> one.txt 13
|
> dir
|
||||||
|
one.txt 13
|
||||||
two.txt 14
|
two.txt 14
|
||||||
2 files
|
2 files
|
||||||
> renamed
|
> rename one.txt first.txt
|
||||||
> first.txt 13
|
renamed
|
||||||
|
> dir
|
||||||
|
first.txt 13
|
||||||
two.txt 14
|
two.txt 14
|
||||||
2 files
|
2 files
|
||||||
> gone
|
> delete first.txt
|
||||||
> two.txt 14
|
gone
|
||||||
|
> dir
|
||||||
|
two.txt 14
|
||||||
1 file
|
1 file
|
||||||
> no such file
|
> delete first.txt
|
||||||
> rename what to what?
|
no such file
|
||||||
> there is no such file, or that name is taken
|
> rename two.txt
|
||||||
> halted
|
rename what to what?
|
||||||
|
> rename two.txt two.txt
|
||||||
|
there is no such file, or that name is taken
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
CosmOS
|
||||||
|
> Say before Grid
|
||||||
|
it says: before Grid
|
||||||
|
finished
|
||||||
|
> Grid
|
||||||
|
[2J[Hfinished
|
||||||
|
>
|
||||||
|
> Say after Grid
|
||||||
|
it says: after Grid
|
||||||
|
finished
|
||||||
|
> dir
|
||||||
|
greet.sbx 211
|
||||||
|
hello.sbx 53
|
||||||
|
Life.sbx 1396
|
||||||
|
Snake.sbx 2164
|
||||||
|
Keys.sbx 664
|
||||||
|
Say.sbx 156
|
||||||
|
Break.sbx 149
|
||||||
|
Grid.sbx 559
|
||||||
|
Press.sbx 872
|
||||||
|
notes.txt 21
|
||||||
|
Apps <dir>
|
||||||
|
hi.script 121
|
||||||
|
bad.script 45
|
||||||
|
plain.script 24
|
||||||
|
cross.script 280
|
||||||
|
nonl.script 38
|
||||||
|
outer.script 376
|
||||||
|
inner.script 44
|
||||||
|
loop.script 35
|
||||||
|
18 files, 1 directory
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
|
Execution halted.
|
||||||
|
[exit 0]
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> loaded, starting at 4000
|
> load hello.sbx
|
||||||
> Hello, World!
|
loaded, starting at 4000
|
||||||
|
> run
|
||||||
|
Hello, World!
|
||||||
finished
|
finished
|
||||||
> Hello, World!
|
> run
|
||||||
|
Hello, World!
|
||||||
finished
|
finished
|
||||||
> halted
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -1,33 +1,48 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> it says: hello there
|
> Say hello there
|
||||||
|
it says: hello there
|
||||||
finished
|
finished
|
||||||
> it says: spelled out in full
|
> Say.sbx spelled out in full
|
||||||
|
it says: spelled out in full
|
||||||
finished
|
finished
|
||||||
> it says: once more
|
> run once more
|
||||||
|
it says: once more
|
||||||
finished
|
finished
|
||||||
> Say.sbx 156
|
> dir
|
||||||
|
Say.sbx 156
|
||||||
dir.sbx 156
|
dir.sbx 156
|
||||||
notes.txt 21
|
notes.txt 21
|
||||||
notes.sbx 21
|
notes.sbx 21
|
||||||
4 files
|
4 files
|
||||||
> not a program
|
> notes
|
||||||
> not a program
|
not a program
|
||||||
> I do not know: notes.txt
|
> notes.sbx
|
||||||
> I do not know: nosuchprogram
|
not a program
|
||||||
> I do not know: abcdefghijklmnopqr
|
> notes.txt
|
||||||
> I do not know: abcdefghijklmnopqrs
|
I do not know: notes.txt
|
||||||
> dir list what is on the disk
|
> nosuchprogram
|
||||||
|
I do not know: nosuchprogram
|
||||||
|
> abcdefghijklmnopqr
|
||||||
|
I do not know: abcdefghijklmnopqr
|
||||||
|
> abcdefghijklmnopqrs
|
||||||
|
I do not know: abcdefghijklmnopqrs
|
||||||
|
> help
|
||||||
|
dir list what is on the disk
|
||||||
load <file> read a program off the disk
|
load <file> read a program off the disk
|
||||||
run [words] start what was loaded, and tell it those words
|
run [words] start what was loaded, and tell it those words
|
||||||
<name> [words] look where you are and then in /Apps, and start that
|
<name> [words] look where you are and then in /Apps, and start that
|
||||||
cd [path] go to a directory, or to the root with nothing after it
|
cd [path] go to a directory, or to the root with nothing after it
|
||||||
mkdir <path> make a directory
|
mkdir <path> make a directory
|
||||||
rmdir <path> remove an empty one
|
rmdir <path> remove an empty one
|
||||||
|
do <file> run the lines in a file, which must start with #!
|
||||||
|
echo [words] say them
|
||||||
|
clear empty the screen
|
||||||
delete <file> take it off the disk
|
delete <file> take it off the disk
|
||||||
rename <file> <to> call it something else
|
rename <file> <to> call it something else
|
||||||
monitor look at memory, change it, and jump into it
|
monitor look at memory, change it, and jump into it
|
||||||
help this
|
help this
|
||||||
exit stop, or leave the monitor if you are in it
|
exit stop, or leave the monitor if you are in it
|
||||||
> halted
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> loaded, starting at 4000
|
> load Keys.sbx
|
||||||
> keys, by interrupt. q stops.
|
loaded, starting at 4000
|
||||||
|
> run
|
||||||
|
keys, by interrupt. q stops.
|
||||||
ab
|
ab
|
||||||
the console has been handed back
|
the console has been handed back
|
||||||
finished
|
finished
|
||||||
> > keys, by interrupt. q stops.
|
>
|
||||||
|
> run
|
||||||
|
keys, by interrupt. q stops.
|
||||||
cd
|
cd
|
||||||
the console has been handed back
|
the console has been handed back
|
||||||
finished
|
finished
|
||||||
> > x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves
|
>
|
||||||
* bank 00
|
> monitor
|
||||||
* FE00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves
|
||||||
|
* b program
|
||||||
|
bank 00
|
||||||
|
* x fe00
|
||||||
|
FE00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||||
FE10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
FE10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||||
FE20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
FE20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||||
FE30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
FE30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||||
* > halted
|
* exit
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> loaded, starting at 4000
|
> load Life.sbx
|
||||||
> [2J[H #
|
loaded, starting at 4000
|
||||||
|
> run
|
||||||
|
[2J[H[1;1H #
|
||||||
#
|
#
|
||||||
###
|
###
|
||||||
|
|
||||||
@@ -16,7 +18,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
# #
|
# #
|
||||||
##
|
##
|
||||||
#
|
#
|
||||||
@@ -32,7 +34,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
#
|
#
|
||||||
# #
|
# #
|
||||||
##
|
##
|
||||||
@@ -48,7 +50,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
#
|
#
|
||||||
##
|
##
|
||||||
##
|
##
|
||||||
@@ -64,7 +66,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
###
|
###
|
||||||
@@ -80,7 +82,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
# #
|
# #
|
||||||
##
|
##
|
||||||
@@ -96,7 +98,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
#
|
#
|
||||||
# #
|
# #
|
||||||
@@ -112,7 +114,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
#
|
#
|
||||||
##
|
##
|
||||||
@@ -128,7 +130,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
@@ -144,7 +146,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
# #
|
# #
|
||||||
@@ -160,7 +162,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -176,7 +178,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -192,7 +194,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -208,7 +210,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -224,7 +226,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -240,7 +242,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -256,7 +258,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -272,7 +274,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -288,7 +290,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -304,7 +306,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -320,7 +322,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -336,7 +338,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -352,7 +354,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -368,7 +370,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -384,7 +386,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -400,7 +402,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -416,7 +418,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -432,7 +434,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -448,7 +450,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -464,7 +466,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -480,7 +482,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -496,7 +498,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -512,7 +514,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -528,7 +530,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -544,7 +546,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -560,7 +562,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -576,7 +578,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -592,7 +594,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -608,7 +610,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -624,7 +626,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -640,7 +642,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -656,7 +658,7 @@ CosmOS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -672,7 +674,7 @@ CosmOS
|
|||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -688,7 +690,7 @@ CosmOS
|
|||||||
##
|
##
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -704,7 +706,7 @@ CosmOS
|
|||||||
##
|
##
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -720,7 +722,7 @@ CosmOS
|
|||||||
###
|
###
|
||||||
|
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -736,7 +738,7 @@ CosmOS
|
|||||||
##
|
##
|
||||||
#
|
#
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -752,7 +754,7 @@ CosmOS
|
|||||||
# #
|
# #
|
||||||
##
|
##
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -768,7 +770,7 @@ CosmOS
|
|||||||
##
|
##
|
||||||
##
|
##
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -784,7 +786,7 @@ CosmOS
|
|||||||
#
|
#
|
||||||
###
|
###
|
||||||
|
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -800,7 +802,7 @@ CosmOS
|
|||||||
# #
|
# #
|
||||||
##
|
##
|
||||||
#
|
#
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -816,7 +818,7 @@ CosmOS
|
|||||||
#
|
#
|
||||||
# #
|
# #
|
||||||
##
|
##
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -832,7 +834,7 @@ CosmOS
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
##
|
##
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -848,7 +850,7 @@ CosmOS
|
|||||||
|
|
||||||
#
|
#
|
||||||
##
|
##
|
||||||
[H
|
[1;1H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> loaded, starting at 4000
|
> load Life.sbx
|
||||||
> [2J[H #
|
loaded, starting at 4000
|
||||||
|
> run
|
||||||
|
[2J[H[1;1H #
|
||||||
#
|
#
|
||||||
###
|
###
|
||||||
|
|
||||||
@@ -19,7 +21,8 @@ CosmOS
|
|||||||
|
|
||||||
stopped
|
stopped
|
||||||
finished
|
finished
|
||||||
> >
|
>
|
||||||
|
>
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves
|
> monitor
|
||||||
* b <program|data|number>
|
x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves
|
||||||
* there is no such bank
|
* b nonsense
|
||||||
* loaded, starting at 4000
|
b <program|data|number>
|
||||||
* bank 00
|
* b 9
|
||||||
* 4000 47 00 20 00 SETD.0 2000
|
there is no such bank
|
||||||
|
* load greet.sbx
|
||||||
|
loaded, starting at 4000
|
||||||
|
* b program
|
||||||
|
bank 00
|
||||||
|
* d 4000
|
||||||
|
4000 47 00 20 00 SETD.0 2000
|
||||||
4004 72 10 SWI 10
|
4004 72 10 SWI 10
|
||||||
4006 47 00 20 44 SETD.0 2044
|
4006 47 00 20 44 SETD.0 2044
|
||||||
400A 72 10 SWI 10
|
400A 72 10 SWI 10
|
||||||
@@ -12,23 +18,32 @@ CosmOS
|
|||||||
4010 27 1F INIB 1F
|
4010 27 1F INIB 1F
|
||||||
4012 72 11 SWI 11
|
4012 72 11 SWI 11
|
||||||
4014 47 00 20 5D SETD.0 205D
|
4014 47 00 20 5D SETD.0 205D
|
||||||
* 4000 47 00 20 00 72 10 47 00 20 44 72 10 47 00 20 7A G. .r.G. Dr.G. z
|
* x 4000
|
||||||
|
4000 47 00 20 00 72 10 47 00 20 44 72 10 47 00 20 7A G. .r.G. Dr.G. z
|
||||||
4010 27 1F 72 11 47 00 20 5D 72 10 47 00 20 7A 72 10 '.r.G. ]r.G. zr.
|
4010 27 1F 72 11 47 00 20 5D 72 10 47 00 20 7A 72 10 '.r.G. ]r.G. zr.
|
||||||
4020 47 00 20 65 72 10 20 72 12 00 00 00 00 00 00 00 G. er. r........
|
4020 47 00 20 65 72 10 20 72 12 00 00 00 00 00 00 00 G. er. r........
|
||||||
4030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
4030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||||
* bank 01
|
* b data
|
||||||
* 2000 61 20 70 72 6F 67 72 61 6D 2C 20 6C 6F 61 64 65 a program, loade
|
bank 01
|
||||||
|
* x 2000
|
||||||
|
2000 61 20 70 72 6F 67 72 61 6D 2C 20 6C 6F 61 64 65 a program, loade
|
||||||
2010 64 20 6F 66 66 20 61 20 64 69 73 6B 2C 20 72 75 d off a disk, ru
|
2010 64 20 6F 66 66 20 61 20 64 69 73 6B 2C 20 72 75 d off a disk, ru
|
||||||
2020 6E 6E 69 6E 67 20 6F 6E 20 74 68 65 20 73 79 73 nning on the sys
|
2020 6E 6E 69 6E 67 20 6F 6E 20 74 68 65 20 73 79 73 nning on the sys
|
||||||
2030 74 65 6D 20 74 68 61 74 20 6C 6F 61 64 65 64 20 tem that loaded
|
2030 74 65 6D 20 74 68 61 74 20 6C 6F 61 64 65 64 20 tem that loaded
|
||||||
* bank 02
|
* b 2
|
||||||
* 0000 01 FF 00 00 00 00 00 00 01 FF 00 00 00 00 00 00 ................
|
bank 02
|
||||||
|
* x 0
|
||||||
|
0000 01 FF 00 00 00 00 00 00 01 FF 00 00 00 00 00 00 ................
|
||||||
0010 03 FF 08 00 00 00 00 00 01 20 01 00 00 00 00 00 ......... ......
|
0010 03 FF 08 00 00 00 00 00 01 20 01 00 00 00 00 00 ......... ......
|
||||||
0020 00 FF 00 00 00 00 00 00 00 FF 00 00 00 00 00 00 ................
|
0020 00 FF 00 00 00 00 00 00 00 FF 00 00 00 00 00 00 ................
|
||||||
0030 00 FF 00 00 00 00 00 00 00 FF 00 00 00 00 00 00 ................
|
0030 00 FF 00 00 00 00 00 00 00 FF 00 00 00 00 00 00 ................
|
||||||
* that bank will not be written
|
* s 0 FF
|
||||||
* bank 00
|
that bank will not be written
|
||||||
* * 8000 26 48 INIA 48
|
* b program
|
||||||
|
bank 00
|
||||||
|
* s 8000 26 48 D1 00 26 0A D1 00 18 12
|
||||||
|
* d 8000
|
||||||
|
8000 26 48 INIA 48
|
||||||
8002 D1 00 OUTA 00
|
8002 D1 00 OUTA 00
|
||||||
8004 26 0A INIA 0A
|
8004 26 0A INIA 0A
|
||||||
8006 D1 00 OUTA 00
|
8006 D1 00 OUTA 00
|
||||||
@@ -36,7 +51,8 @@ CosmOS
|
|||||||
8009 12 AND
|
8009 12 AND
|
||||||
800A 00 ?
|
800A 00 ?
|
||||||
800B 00 ?
|
800B 00 ?
|
||||||
* Fault: 0x00 at Program Address 0x800A is not an instruction.
|
* g 8000Fault: 0x00 at Program Address 0x800A is not an instruction.
|
||||||
|
|
||||||
H
|
H
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 1]
|
[exit 1]
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> loaded, starting at 4000
|
> load More.sbx
|
||||||
> first line
|
loaded, starting at 4000
|
||||||
|
> run readable.txt
|
||||||
|
first line
|
||||||
second line
|
second line
|
||||||
line 00: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 00: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
line 01: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 01: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
@@ -24,7 +26,8 @@ line 18: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
|||||||
line 19: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 19: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
-- more --
|
-- more --
|
||||||
finished
|
finished
|
||||||
> first line
|
> run readable.txt
|
||||||
|
first line
|
||||||
second line
|
second line
|
||||||
line 00: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 00: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
line 01: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 01: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
@@ -58,7 +61,8 @@ line 27: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
|||||||
line 28: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 28: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
line 29: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 29: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
finished
|
finished
|
||||||
> first line
|
> run readable.txt
|
||||||
|
first line
|
||||||
second line
|
second line
|
||||||
line 00: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 00: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
line 01: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 01: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
@@ -84,6 +88,7 @@ line 19: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
|||||||
line 20: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
line 20: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
-- more --
|
-- more --
|
||||||
finished
|
finished
|
||||||
> halted
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
no filesystem on the disk
|
no filesystem on the disk
|
||||||
> no filesystem on the disk
|
> dir
|
||||||
|
no filesystem on the disk
|
||||||
>
|
>
|
||||||
halted
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
CosmOS
|
||||||
|
> Press
|
||||||
|
a line, then keys. q stops.
|
||||||
|
61 62 63 64
|
||||||
|
keys:
|
||||||
|
80 up
|
||||||
|
82 left
|
||||||
|
86 delete
|
||||||
|
5A Z
|
||||||
|
done
|
||||||
|
finished
|
||||||
|
>
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
|
Execution halted.
|
||||||
|
[exit 0]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
CosmOS
|
||||||
|
> Press
|
||||||
|
a line, then keys. q stops.
|
||||||
|
61 62 63 64
|
||||||
|
keys:
|
||||||
|
80 up
|
||||||
|
82 left
|
||||||
|
86 delete
|
||||||
|
5A Z
|
||||||
|
done
|
||||||
|
finished
|
||||||
|
>
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
|
Execution halted.
|
||||||
|
[exit 0]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
CosmOS
|
||||||
|
> drive 1
|
||||||
|
> dir
|
||||||
|
0 files
|
||||||
|
> Copy 0:/Say.sbx 1:/Say.sbx
|
||||||
|
copied
|
||||||
|
finished
|
||||||
|
> dir
|
||||||
|
Say.sbx 156
|
||||||
|
1 file
|
||||||
|
> drive
|
||||||
|
1
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
|
Execution halted.
|
||||||
|
[exit 0]
|
||||||
@@ -1,24 +1,44 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> nothing is loaded
|
> run
|
||||||
> greet.sbx 211
|
nothing is loaded
|
||||||
|
> dir
|
||||||
|
greet.sbx 211
|
||||||
hello.sbx 53
|
hello.sbx 53
|
||||||
Life.sbx 1410
|
Life.sbx 1396
|
||||||
Snake.sbx 2168
|
Snake.sbx 2164
|
||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
|
Grid.sbx 559
|
||||||
|
Press.sbx 872
|
||||||
notes.txt 21
|
notes.txt 21
|
||||||
8 files
|
Apps <dir>
|
||||||
> load what?
|
hi.script 121
|
||||||
> no such file
|
bad.script 45
|
||||||
> not a program
|
plain.script 24
|
||||||
> loaded, starting at 4000
|
cross.script 280
|
||||||
> a program, loaded off a disk, running on the system that loaded it
|
nonl.script 38
|
||||||
|
outer.script 376
|
||||||
|
inner.script 44
|
||||||
|
loop.script 35
|
||||||
|
18 files, 1 directory
|
||||||
|
> load
|
||||||
|
load what?
|
||||||
|
> load nosuch.sbx
|
||||||
|
no such file
|
||||||
|
> load notes.txt
|
||||||
|
not a program
|
||||||
|
> load greet.sbx
|
||||||
|
loaded, starting at 4000
|
||||||
|
> run
|
||||||
|
a program, loaded off a disk, running on the system that loaded it
|
||||||
what should I call you? hello, Anachronaut. that is all I do.
|
what should I call you? hello, Anachronaut. that is all I do.
|
||||||
finished
|
finished
|
||||||
> a program, loaded off a disk, running on the system that loaded it
|
> run
|
||||||
|
a program, loaded off a disk, running on the system that loaded it
|
||||||
what should I call you? hello, Claude. that is all I do.
|
what should I call you? hello, Claude. that is all I do.
|
||||||
finished
|
finished
|
||||||
> halted
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
CosmOS
|
CosmOS
|
||||||
> loaded, starting at 4000
|
> load Say.sbx
|
||||||
> nothing was said
|
loaded, starting at 4000
|
||||||
|
> run
|
||||||
|
nothing was said
|
||||||
finished
|
finished
|
||||||
> it says: notes.txt
|
> run notes.txt
|
||||||
|
it says: notes.txt
|
||||||
finished
|
finished
|
||||||
> it says: a longer thing with spaces
|
> run a longer thing with spaces
|
||||||
|
it says: a longer thing with spaces
|
||||||
finished
|
finished
|
||||||
> halted
|
> exit
|
||||||
|
halted
|
||||||
Execution halted.
|
Execution halted.
|
||||||
[exit 0]
|
[exit 0]
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
CosmOS
|
||||||
|
> do hi.script
|
||||||
|
> echo saying what it is doing
|
||||||
|
saying what it is doing
|
||||||
|
> Say from a script
|
||||||
|
it says: from a script
|
||||||
|
finished
|
||||||
|
> echo
|
||||||
|
|
||||||
|
> Say and again
|
||||||
|
it says: and again
|
||||||
|
finished
|
||||||
|
> exit
|
||||||
|
halted
|
||||||
|
Execution halted.
|
||||||
|
[exit 0]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user