Compare commits
75
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e85356245 | ||
|
|
388faafd04 | ||
|
|
6fe898b5fc | ||
|
|
de765267c5 | ||
|
|
b2ff8d64e5 | ||
|
|
a8b6b09a59 | ||
|
|
9ae59bfccb | ||
|
|
0264b19a3d | ||
|
|
115efa1fa1 | ||
|
|
c408fc6cf6 | ||
|
|
418631a221 | ||
|
|
7e82b64d47 | ||
|
|
1928275f87 | ||
|
|
804dd3040e | ||
|
|
922511c8a7 | ||
|
|
800f555ffb | ||
|
|
32559ce872 | ||
|
|
437ddf8ebe | ||
|
|
a02d701efe | ||
|
|
85029d3b85 | ||
|
|
c514d5328e | ||
|
|
f5642f52b5 | ||
|
|
d896c9d433 | ||
|
|
a069ee7a00 | ||
|
|
4b1c3d8e3f | ||
|
|
60e0196fe2 | ||
|
|
ee77d79780 | ||
|
|
de1857f5f7 | ||
|
|
eb695a3f3b | ||
|
|
a163c670d0 | ||
|
|
caf5e1f99d | ||
|
|
bfb515e23b | ||
|
|
7257ad369c | ||
|
|
6073086584 | ||
|
|
2274be4b68 | ||
|
|
17c8da111f | ||
|
|
e3eccd17a8 | ||
|
|
a074a831f0 | ||
|
|
f7be843ed9 | ||
|
|
6def343e97 | ||
|
|
df50c2f0f8 | ||
|
|
8eb4e4d67e | ||
|
|
db0c26e13f | ||
|
|
b1dde7908c | ||
|
|
fed1453e6e | ||
|
|
88ecb208f4 | ||
|
|
d6c81fa32c | ||
|
|
5222c85100 | ||
|
|
fba1b553d2 | ||
|
|
cb898450b5 | ||
|
|
f8c3db5d56 | ||
|
|
9eed23120f | ||
|
|
a916103a7f | ||
|
|
eee95ef0ce | ||
|
|
023362b05a | ||
|
|
66e7b84272 | ||
|
|
2abc8281df | ||
|
|
16f8232a35 | ||
|
|
4b109f704c | ||
|
|
a8707f29f0 | ||
|
|
3c76934a9a | ||
|
|
bb065fe221 | ||
|
|
749fef8ce2 | ||
|
|
b04e4b7d1c | ||
|
|
3449405b18 | ||
|
|
f97d15de08 | ||
|
|
925388c2f2 | ||
|
|
c1b3c4c156 | ||
|
|
66be42d7bb | ||
|
|
fd9c4c75f8 | ||
|
|
000a6d39cb | ||
|
|
4d976fc22a | ||
|
|
71f6e215f9 | ||
|
|
81e544eb3d | ||
|
|
373454ec00 |
@@ -39,7 +39,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SETD.0 Banner
|
||||
@@ -72,7 +72,7 @@ deeper:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
Banner:
|
||||
"two stops, and what the registers were at each
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
#Include services.asm
|
||||
#Program
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
; One block, and ten bytes after it.
|
||||
@@ -81,7 +81,7 @@ noWrite:
|
||||
SWI osExit
|
||||
|
||||
#Data
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
Name:
|
||||
"claim.dat"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#Include services.asm
|
||||
|
||||
#Program
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SETD.0 Arguments
|
||||
@@ -236,7 +236,7 @@ secondReadFailed:
|
||||
SWI osExit
|
||||
|
||||
#Data
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
Arguments:
|
||||
#Reserve 0d256
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#Include services.asm
|
||||
|
||||
#Program
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SETD.0 Arguments
|
||||
@@ -232,7 +232,7 @@ doneFailed:
|
||||
SWI osExit
|
||||
|
||||
#Data
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
Arguments:
|
||||
#Reserve 0d256
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
; Breaks on purpose, in whichever of the four ways it is asked for.
|
||||
;
|
||||
; Every one of these used to stop the machine and print a line to a standard error that
|
||||
; nobody behind a window is looking at, so the machine appeared to hang. The system catches
|
||||
; all of them now and says what happened and where, and this is what says so - and what a
|
||||
; person can run when they want to see the fault screen without having written a bug first.
|
||||
;
|
||||
; Crash opcode a byte in the middle of the code that does not decode
|
||||
; Crash service a SWI naming a service the system does not implement
|
||||
; Crash bank a transfer out of a bank that has nothing registered in it
|
||||
; Crash device asking the console to interrupt, with no handler installed
|
||||
; Crash blind the same bad byte, but from a screen with nowhere to print at all
|
||||
;
|
||||
; The last one is the odd one: it is not something this program does wrong, it is something
|
||||
; it fails to have done. The interrupt arrives from outside once the console has anything to
|
||||
; say, and with no input it is the END of input that arrives.
|
||||
;
|
||||
; Written by Anachronaut
|
||||
|
||||
#Include services.asm
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SETD.0 Argument
|
||||
INIB 0d15
|
||||
SWI osArgument
|
||||
|
||||
SETD.0 Argument
|
||||
SETD.1 WordOpcode
|
||||
CALL textSame
|
||||
BRQ crashOpcode
|
||||
SETD.0 Argument
|
||||
SETD.1 WordService
|
||||
CALL textSame
|
||||
BRQ crashService
|
||||
SETD.0 Argument
|
||||
SETD.1 WordBank
|
||||
CALL textSame
|
||||
BRQ crashBank
|
||||
SETD.0 Argument
|
||||
SETD.1 WordDevice
|
||||
CALL textSame
|
||||
BRQ crashDevice
|
||||
SETD.0 Argument
|
||||
SETD.1 WordBlind
|
||||
CALL textSame
|
||||
BRQ crashBlind
|
||||
|
||||
SETD.0 Usage
|
||||
SWI osPrintString
|
||||
INIA 0x0A
|
||||
OUTA 0x00
|
||||
INIA 0x01
|
||||
SWI osExit
|
||||
|
||||
; ---- The case the fault screen exists for ----
|
||||
;
|
||||
; Bitmap mode has no text rows, so the console draws NOTHING there: a program that faults
|
||||
; here leaves the system with a message to print and nowhere to print it. Putting the screen
|
||||
; back into a mode that has characters in it is the difference between a diagnosis and a
|
||||
; machine that appears to have hung.
|
||||
crashBlind:
|
||||
; ---- And with the colours ruined as well ----
|
||||
;
|
||||
; A known mode is only half a known screen. This makes the ink of attribute one the same as
|
||||
; its paper, which is what a program that wrote its own palette can easily leave behind -
|
||||
; and a message printed into that is perfectly present and completely invisible.
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0x30
|
||||
OUTA 0xE2
|
||||
INIA 0x03
|
||||
OUTA 0xE8 ; Video memory as bank four.
|
||||
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0xFC
|
||||
OUTA 0xE4
|
||||
INIA 0x40
|
||||
OUTA 0xE5 ; 0xFC40, the two entries attribute one draws from.
|
||||
RSTA
|
||||
INIB 0d8
|
||||
crashBlindWipe:
|
||||
OUTA 0xE9
|
||||
DECB
|
||||
BNB crashBlindWipe ; Both of them black, ink and paper alike.
|
||||
|
||||
INIA 0x02
|
||||
OUTA 0x31
|
||||
|
||||
crashOpcode:
|
||||
0x00 ; Not an instruction, and never will be.
|
||||
|
||||
crashService:
|
||||
SWI 0d40 ; Forty is nobody's.
|
||||
|
||||
crashBank:
|
||||
INIA 0d9 ; Nothing is registered there.
|
||||
OUTA 0xE0
|
||||
RSTA
|
||||
OUTA 0xE1
|
||||
OUTA 0xE2
|
||||
INIA 0d1
|
||||
OUTA 0xE3 ; Into Data Memory.
|
||||
RSTA
|
||||
OUTA 0xE4
|
||||
OUTA 0xE5
|
||||
OUTA 0xE6
|
||||
INIA 0d16
|
||||
OUTA 0xE7
|
||||
INIA 0x01
|
||||
OUTA 0xE8 ; Blit, from a bank that is not there.
|
||||
|
||||
crashDevice:
|
||||
INIA 0x02 ; Interrupt me when the console has something to say.
|
||||
OUTA 0x02
|
||||
SIF
|
||||
crashWait:
|
||||
; Never touches the console, so whatever happens next came from outside.
|
||||
BRI crashWait
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x3000
|
||||
|
||||
WordOpcode:
|
||||
"opcode"
|
||||
WordService:
|
||||
"service"
|
||||
WordBank:
|
||||
"bank"
|
||||
WordDevice:
|
||||
"device"
|
||||
WordBlind:
|
||||
"blind"
|
||||
Usage:
|
||||
"Crash opcode | service | bank | device | blind"
|
||||
|
||||
Argument:
|
||||
#Reserve 0d16
|
||||
|
||||
#Include text.asm
|
||||
@@ -0,0 +1,377 @@
|
||||
; A ball that goes behind some pillars and in front of others.
|
||||
;
|
||||
; Four pillars, each ONE 8 by 8 TILE stretched to its own width and height, at four different
|
||||
; distances. One ball, moving across all of them. Which pillars it passes behind is decided
|
||||
; per screen column by the depth buffer, and that is the thing worth watching: the ball is in
|
||||
; front of the far pillars and behind the near ones IN THE SAME FRAME, without the program
|
||||
; sorting anything or drawing anything twice.
|
||||
;
|
||||
; ---- Why that needs a buffer rather than an ordering ----
|
||||
;
|
||||
; Sprites are drawn in table order, so a program can put one in front of another by numbering
|
||||
; them. That is enough when things are flat. It stops being enough the moment a thing is
|
||||
; nearer than one part of the scenery and further than another - a ball halfway past a pillar
|
||||
; is in front of the floor beside it and behind the pillar itself, and there is no order of
|
||||
; two sprites that means "behind, on those columns only".
|
||||
;
|
||||
; So the scenery says how far away it is, a column at a time, and the ball says how far away
|
||||
; IT is. The device compares them per pixel.
|
||||
;
|
||||
; Written by Anachronaut
|
||||
|
||||
#Include services.asm
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
; The atlas: the tiles, the sprite table and the depth buffer are all in it.
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0x30
|
||||
OUTA 0xE2
|
||||
INIA 0x03
|
||||
OUTA 0xE8
|
||||
|
||||
SETD.0 Message
|
||||
SWI osPrintString
|
||||
INIA 0x0A
|
||||
OUTA 0x00
|
||||
|
||||
CALL putArt
|
||||
CALL clearDepth
|
||||
CALL putPillars
|
||||
CALL putBall
|
||||
|
||||
INIA 0x01
|
||||
OUTA 0x02 ; Key mode.
|
||||
|
||||
everyFrame:
|
||||
CALL waitFrame
|
||||
CALL stepBall
|
||||
CALL moveBall
|
||||
INA 0x01
|
||||
INIB 0x01 ; READY
|
||||
AND
|
||||
BRQ everyFrame
|
||||
INA 0x00
|
||||
|
||||
RSTA
|
||||
OUTA 0x02 ; Line mode. The sprites and the buffer are the system's to clear.
|
||||
SWI osExit
|
||||
|
||||
; ---- Two tiles ----
|
||||
;
|
||||
; Tile 200 is the ball, which has a shape and so comes out of the Data Segment. Tile 201 is
|
||||
; the pillar, which is a solid block and so is a Fill: 64 bytes of index one, no art needed.
|
||||
putArt:
|
||||
INIA 0x01
|
||||
OUTA 0xE0 ; SourceBank: Data Memory.
|
||||
SETD.1 BallArtAt
|
||||
SETD.0 BallArt
|
||||
STD.0.1
|
||||
LDA.1
|
||||
OUTA 0xE1
|
||||
INCD.1
|
||||
LDA.1
|
||||
OUTA 0xE2
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0x32
|
||||
OUTA 0xE4 ; Tile 200 is at 200 times 64, which is 0x3200.
|
||||
RSTA
|
||||
OUTA 0xE5
|
||||
OUTA 0xE6
|
||||
INIA 0x40
|
||||
OUTA 0xE7
|
||||
INIA 0x01
|
||||
OUTA 0xE8 ; Blit.
|
||||
|
||||
INIA 0x32
|
||||
OUTA 0xE4
|
||||
INIA 0x40
|
||||
OUTA 0xE5 ; Tile 201 is 64 bytes further on, at 0x3240.
|
||||
INIA 0x01
|
||||
OUTA 0xE2 ; Index ONE, which is the only index a scheme gives a colour to:
|
||||
; the default palette sets each scheme's paper and ink and
|
||||
; nothing between them.
|
||||
RSTA
|
||||
OUTA 0xE6
|
||||
INIA 0x40
|
||||
OUTA 0xE7
|
||||
INIA 0x02
|
||||
OUTA 0xE8 ; Fill.
|
||||
RET
|
||||
|
||||
; ---- The buffer, emptied ----
|
||||
;
|
||||
; Nought in a column means nothing is there. It is the program's buffer and nobody clears it
|
||||
; between programs, so a program that means "nothing yet" has to say so.
|
||||
clearDepth:
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0xD0
|
||||
OUTA 0xE4
|
||||
RSTA
|
||||
OUTA 0xE5
|
||||
OUTA 0xE2 ; Fill takes the byte it writes from SourceLow.
|
||||
INIA 0x02
|
||||
OUTA 0xE6
|
||||
INIA 0x80
|
||||
OUTA 0xE7 ; 640 bytes, one a column.
|
||||
INIA 0x02
|
||||
OUTA 0xE8
|
||||
RET
|
||||
|
||||
; ---- The pillars ----
|
||||
;
|
||||
; Four records of seven bytes: x, y, height, depth. Each becomes a sprite entry and a run of
|
||||
; the depth buffer saying how far away that part of the scenery is.
|
||||
putPillars:
|
||||
INIA 0d4
|
||||
SETD.0 PillarLeft
|
||||
STA.0 ; Sprite one is at 0xC010; sprite nought is the ball.
|
||||
INIA 0x10
|
||||
SETD.0 EntryLow
|
||||
STA.0
|
||||
SETD.3 Pillars
|
||||
|
||||
putOnePillar:
|
||||
; ---- The entry, written straight through ----
|
||||
;
|
||||
; Sixteen bytes out of one port with the address named once, because the controller's Data
|
||||
; port steps on after every byte. DP3 walks the record and survives the calls below.
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0xC0
|
||||
OUTA 0xE4
|
||||
SETD.0 EntryLow
|
||||
LDA.0
|
||||
OUTA 0xE5
|
||||
|
||||
INIA 0xC9
|
||||
OUTA 0xE9 ; Tile 201, the pillar block.
|
||||
INIA 0x01
|
||||
OUTA 0xE9 ; Attribute one.
|
||||
; Kept as they go past, because putPillarDepth needs the column and a Data Pointer cannot
|
||||
; be walked backwards to find it again.
|
||||
LDA.3
|
||||
OUTA 0xE9 ; X low.
|
||||
SETD.0 PillarXLow
|
||||
STA.0
|
||||
INCD.3
|
||||
LDA.3
|
||||
OUTA 0xE9 ; X high.
|
||||
SETD.0 PillarXHigh
|
||||
STA.0
|
||||
INCD.3
|
||||
LDA.3
|
||||
OUTA 0xE9 ; Y low.
|
||||
INCD.3
|
||||
LDA.3
|
||||
OUTA 0xE9 ; Y high.
|
||||
INCD.3
|
||||
INIA 0x11
|
||||
OUTA 0xE9 ; One tile by one, which is the art. The size below is the look.
|
||||
RSTA
|
||||
OUTA 0xE9 ; No flags.
|
||||
INIA 0d32
|
||||
OUTA 0xE9
|
||||
RSTA
|
||||
OUTA 0xE9 ; Thirty two pixels wide, whatever the tile is.
|
||||
LDA.3
|
||||
OUTA 0xE9 ; Height low.
|
||||
INCD.3
|
||||
LDA.3
|
||||
OUTA 0xE9 ; Height high.
|
||||
INCD.3
|
||||
|
||||
; ---- And a depth of NOUGHT, which is not the pillar's distance ----
|
||||
;
|
||||
; It is tempting to put the pillar's own distance here, and it is wrong: the depth buffer
|
||||
; is what has already been DRAWN, and a sprite's depth is a QUESTION ASKED OF IT. A pillar
|
||||
; that both wrote 20 into its own columns and carried 20 would be asked whether it was in
|
||||
; front of itself, and 20 is not nearer than 20, so it would vanish. Which it did.
|
||||
;
|
||||
; Scenery writes the buffer. It does not ask.
|
||||
RSTA
|
||||
OUTA 0xE9
|
||||
|
||||
CALL putPillarDepth
|
||||
|
||||
; On to the next record and the next entry.
|
||||
INCD.3
|
||||
SETD.0 EntryLow
|
||||
LDA.0
|
||||
INIB 0d16
|
||||
CCF
|
||||
ADD
|
||||
STQ.0
|
||||
SETD.0 PillarLeft
|
||||
LDA.0
|
||||
DECA
|
||||
STA.0
|
||||
BNA putOnePillar
|
||||
RET
|
||||
|
||||
; ---- How far away those thirty two columns are ----
|
||||
;
|
||||
; A Fill, because the whole run is one number: the pillar is flat on, so every column of it
|
||||
; is the same distance. DP3 is on the depth byte of the record and stays there.
|
||||
;
|
||||
; The buffer begins at 0xD000, whose low byte is nought - so the column's low byte IS the
|
||||
; address's low byte, and its high byte only has to be added to 0xD0. No sixteen bit sum.
|
||||
putPillarDepth:
|
||||
LDA.3
|
||||
OUTA 0xE2 ; The byte to fill with, which is the depth.
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
SETD.0 PillarXHigh
|
||||
LDA.0
|
||||
INIB 0xD0
|
||||
CCF
|
||||
ADD
|
||||
OUTQ 0xE4
|
||||
SETD.0 PillarXLow
|
||||
LDA.0
|
||||
OUTA 0xE5
|
||||
RSTA
|
||||
OUTA 0xE6
|
||||
INIA 0d32
|
||||
OUTA 0xE7 ; Thirty two columns of it.
|
||||
INIA 0x02
|
||||
OUTA 0xE8
|
||||
RET
|
||||
|
||||
; ---- The ball ----
|
||||
;
|
||||
; Sprite nought, so it is in front of every pillar as far as the TABLE is concerned. What
|
||||
; puts it behind some of them is its depth and nothing else, which is the whole point.
|
||||
putBall:
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0xC0
|
||||
OUTA 0xE4
|
||||
RSTA
|
||||
OUTA 0xE5
|
||||
INIA 0xC8
|
||||
OUTA 0xE9 ; Tile 200.
|
||||
INIA 0x02
|
||||
OUTA 0xE9 ; Attribute two.
|
||||
RSTA
|
||||
OUTA 0xE9
|
||||
OUTA 0xE9 ; X, low then high.
|
||||
INIA 0d230
|
||||
OUTA 0xE9
|
||||
RSTA
|
||||
OUTA 0xE9 ; Y.
|
||||
INIA 0x11
|
||||
OUTA 0xE9 ; One tile by one.
|
||||
RSTA
|
||||
OUTA 0xE9 ; No flags.
|
||||
INIA 0d48
|
||||
OUTA 0xE9
|
||||
RSTA
|
||||
OUTA 0xE9
|
||||
INIA 0d48
|
||||
OUTA 0xE9
|
||||
RSTA
|
||||
OUTA 0xE9 ; Forty eight by forty eight, from eight by eight of art.
|
||||
INIA 0d45
|
||||
OUTA 0xE9 ; And forty five away, which is between the pillars.
|
||||
RET
|
||||
|
||||
waitFrame:
|
||||
INA 0x30
|
||||
INIB 0x01
|
||||
AND
|
||||
BRQ waitFrame
|
||||
RET
|
||||
|
||||
; Two pixels to the right, wrapping past the far side. Sixteen bits in two bytes, so the high
|
||||
; one steps only when the low one came round to nought.
|
||||
stepBall:
|
||||
SETD.0 BallX
|
||||
LDA.0
|
||||
INCA
|
||||
INCA
|
||||
STA.0
|
||||
BNA stepCheck
|
||||
SETD.0 BallXHigh
|
||||
LDA.0
|
||||
INCA
|
||||
STA.0
|
||||
stepCheck:
|
||||
SETD.0 BallXHigh
|
||||
LDA.0
|
||||
INIB 0x02
|
||||
CCF
|
||||
SUB
|
||||
BNQ stepDone ; Not past 0x0200, so nowhere near the end.
|
||||
RSTA
|
||||
SETD.0 BallX
|
||||
STA.0
|
||||
SETD.0 BallXHigh
|
||||
STA.0
|
||||
stepDone:
|
||||
RET
|
||||
|
||||
moveBall:
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0xC0
|
||||
OUTA 0xE4
|
||||
INIA 0x02
|
||||
OUTA 0xE5 ; X is bytes two and three of the entry.
|
||||
SETD.0 BallX
|
||||
LDA.0
|
||||
OUTA 0xE9
|
||||
SETD.0 BallXHigh
|
||||
LDA.0
|
||||
OUTA 0xE9
|
||||
RET
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x3000
|
||||
|
||||
Message:
|
||||
"A ball, behind the near pillars and in front of the far ones. Press a key."
|
||||
|
||||
; x, y, height, depth. Nearer pillars are taller, which is the only thing making this look
|
||||
; like distance rather than like four bars.
|
||||
Pillars:
|
||||
0x50 0x00 0x64 0x00 0xF0 0x00 0d20 ; x 80, y 100, 240 tall, near
|
||||
0xC8 0x00 0xDC 0x00 0x78 0x00 0d60 ; x 200, y 220, 120 tall, far
|
||||
0x68 0x01 0xA0 0x00 0xB4 0x00 0d30 ; x 360, y 160, 180 tall, nearish
|
||||
0xF4 0x01 0x04 0x01 0x50 0x00 0d80 ; x 500, y 260, 80 tall, furthest
|
||||
|
||||
; Where putPillarDepth reads the column from. DP3 is on the record's depth byte by then, and
|
||||
; a Data Pointer cannot be read backwards, so the two halves are kept here as they go past.
|
||||
PillarXLow:
|
||||
0x00
|
||||
PillarXHigh:
|
||||
0x00
|
||||
PillarLeft:
|
||||
0x00
|
||||
EntryLow:
|
||||
0x00
|
||||
|
||||
BallArt:
|
||||
0x00 0x00 0x01 0x01 0x01 0x01 0x00 0x00
|
||||
0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x00
|
||||
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
|
||||
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
|
||||
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
|
||||
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
|
||||
0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x00
|
||||
0x00 0x00 0x01 0x01 0x01 0x01 0x00 0x00
|
||||
|
||||
BallArtAt:
|
||||
#Reserve 0d2
|
||||
|
||||
BallX:
|
||||
0x00
|
||||
BallXHigh:
|
||||
0x00
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
; ---- Room for a PATH, not a name ----
|
||||
@@ -869,7 +869,7 @@ addByteToWord:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
Break:
|
||||
0x0A 0x00
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
; Swap ValueB and ValueA.
|
||||
@@ -72,7 +72,7 @@ start:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
ValueA:
|
||||
; Low byte, high byte.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
; Swap ValueB and ValueA.
|
||||
@@ -128,7 +128,7 @@ start:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
ValueA:
|
||||
; Lowest byte ... Highest byte.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
; Load our initial values into A and B.
|
||||
@@ -38,6 +38,6 @@ start:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
#Include print.asm
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
; ---- Write it ----
|
||||
@@ -133,7 +133,7 @@ noDelete:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
Name:
|
||||
"kept.txt"
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
; The screen nobody is looking at.
|
||||
;
|
||||
; The screen brings two map banks and shows one of them. Everything this program draws goes
|
||||
; into the other, so the picture does not change at all while it is being built - and then
|
||||
; one byte out of one port shows the whole of it at once.
|
||||
;
|
||||
; ---- Why that is worth a port ----
|
||||
;
|
||||
; A screen drawn where it can be seen is seen half drawn. A game that moves forty things and
|
||||
; rewrites the map underneath them is WRONG for as long as it takes to put them all right,
|
||||
; and at a megahertz that is long enough to look at. The machines this one is pretending to
|
||||
; be had the same problem and solved it the same way, except that they had to catch the few
|
||||
; lines between one frame and the next to do the swap in. Here a frame is drawn from one bank
|
||||
; in one go, so a flip cannot land halfway through one and there is nothing to race.
|
||||
;
|
||||
; ---- What it costs ----
|
||||
;
|
||||
; A whole bank, which is 64K of somebody's memory - and nothing, which is the point. The
|
||||
; second screen is memory the device brought, the same as the first, so a program that wants
|
||||
; it registers it and a program that does not never pays for it.
|
||||
;
|
||||
; Written by Anachronaut
|
||||
|
||||
#Include services.asm
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
; ---- The one bank this touches ----
|
||||
;
|
||||
; Six: three is the disk's and four and five are the ones the system registers to save a
|
||||
; screen with, so six is the first number free. Nothing hands these out - see the table in
|
||||
; the CosmOS README.
|
||||
INIA 0d6
|
||||
OUTA 0xE3
|
||||
INIA 0x3B
|
||||
OUTA 0xE2
|
||||
INIA 0x03
|
||||
OUTA 0xE8
|
||||
|
||||
; ---- And it does NOT take the screen ----
|
||||
;
|
||||
; osTakeScreen saves what is on the screen and puts it back at exit, and this program
|
||||
; called it at first. That was wrong in a way worth keeping written down: it saves the
|
||||
; screen AS IT WAS BEFORE, so the line printed below - the line whose whole job is to still
|
||||
; be there afterwards - was wiped out by the restore that was meant to be tidy.
|
||||
;
|
||||
; It was not needed either. NOTHING THIS TOUCHES IS THE SHELL'S: the only memory it writes
|
||||
; is the map of the screen nobody is looking at, and the only register it leaves changed is
|
||||
; which screen that is - which the system puts back itself.
|
||||
;
|
||||
; A program that wants a screen saved should ask. A program that damages nothing should
|
||||
; not, and asking anyway is not free - it costs the screen it was standing on.
|
||||
|
||||
SETD.0 Message
|
||||
SWI osPrintString
|
||||
INIA 0x0A
|
||||
OUTA 0x00 ; The assembler has no escapes; a newline is a byte.
|
||||
|
||||
; Key mode, so a key arrives when it is pressed rather than when Return is.
|
||||
INIA 0x01
|
||||
OUTA 0x02
|
||||
|
||||
; ---- Every cell of the other screen, in one command ----
|
||||
;
|
||||
; TEN, and the reason is worth the paragraph. Fill writes one byte, and a cell is two, so
|
||||
; whatever is filled with is BOTH the tile and the attribute - there is no filling a map
|
||||
; with a tile and a colour that are different numbers.
|
||||
;
|
||||
; Ten as a tile is an asterisk, because the font begins at the space and glyph n is
|
||||
; character n plus thirty two. Ten as an attribute is one of the eight REVERSED schemes,
|
||||
; whose paper is a colour and whose ink is black. So the screen comes out green, covered in
|
||||
; black asterisks, without a single tile being redefined.
|
||||
;
|
||||
; The high nibble stays at nought, which the manual asks for: it is reserved. That is the
|
||||
; real constraint on which byte this can be - it has to be under sixteen to leave the
|
||||
; nibble alone, and eight or over to land on a reversed scheme whose PAPER is the colour.
|
||||
;
|
||||
; Which means THIS PROGRAM WRITES NOTHING BUT THE SCREEN NOBODY IS LOOKING AT. It was
|
||||
; drawn with a tile of its own first, and that tile was one the system copies the font back
|
||||
; over at every exit - so the screen it had filled turned blank the moment it left, and a
|
||||
; check that it had been put back could not tell a restored screen from an abandoned one.
|
||||
; A picture that depends on the atlas does not survive leaving.
|
||||
INIA 0d6
|
||||
OUTA 0xE3
|
||||
INIA 0x40
|
||||
OUTA 0xE4
|
||||
RSTA
|
||||
OUTA 0xE5
|
||||
INIA 0x0A
|
||||
OUTA 0xE2 ; Fill takes the byte it writes from SourceLow.
|
||||
INIA 0x80
|
||||
OUTA 0xE6
|
||||
RSTA
|
||||
OUTA 0xE7 ; The whole map, which is 0x8000 bytes.
|
||||
INIA 0x02
|
||||
OUTA 0xE8
|
||||
|
||||
; NOTHING HAS CHANGED ON THE SCREEN. Every byte of that went where nobody can see it, and
|
||||
; the line printed above is still sitting there to prove it - which is the reason this
|
||||
; waits here rather than flipping straight away. What a back buffer is for is not visible
|
||||
; in the flip; it is visible in the time before one.
|
||||
CALL waitKey
|
||||
|
||||
; And this is the whole of showing it.
|
||||
INIA 0x01
|
||||
OUTA 0x3C
|
||||
|
||||
CALL waitKey
|
||||
|
||||
; ---- Which screen is showing is NOT put back here ----
|
||||
;
|
||||
; On purpose, and it is the one thing in this program worth arguing about. The system
|
||||
; restores it at exit, the same way it restores the cursor and the ink, and for the same
|
||||
; reason: a program that FAULTED while flipped could not have put it back, and a shell that
|
||||
; only came out right for programs which remembered would be a shell that came out wrong
|
||||
; the day one crashed. What the person is looking at belongs to the system.
|
||||
;
|
||||
; The console mode below IS put back, because that is this program's own borrowing rather
|
||||
; than something the system hands out.
|
||||
RSTA
|
||||
OUTA 0x02 ; Line mode again.
|
||||
SWI osExit
|
||||
|
||||
; ---- A key, asked for rather than waited on ----
|
||||
;
|
||||
; The console holds one until somebody wants it, so nothing pressed while the map was being
|
||||
; filled is lost - it is sitting there and this returns immediately, which is right. A key
|
||||
; pressed is a key meant for this program.
|
||||
waitKey:
|
||||
INA 0x01
|
||||
INIB 0x01 ; READY
|
||||
AND
|
||||
BRQ waitKey
|
||||
INA 0x00 ; Taken, so the shell is not handed a key meant for this.
|
||||
RET
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x3000
|
||||
|
||||
; One line and no newline in it. A string literal is terminated where it ends, so a second
|
||||
; one after a 0x0A byte would never be reached - printing stops at the first terminator. The
|
||||
; newline is written to the console directly instead.
|
||||
Message:
|
||||
"A screen is drawn where you cannot see it. A key shows it, another comes back."
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
; ---- Reaching video memory ----
|
||||
@@ -37,12 +37,23 @@ start:
|
||||
; 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.
|
||||
; TWO BANKS, because the screen has two. The atlas holds the tiles and the palette and is
|
||||
; written when a program starts; the screen holds the map and is written as things move.
|
||||
; They are separate memories, so a bank number reaches one or the other and never both -
|
||||
; which is the whole of what this program had to learn when they were split apart.
|
||||
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.
|
||||
OUTA 0xE8 ; RegisterBank. Four is the atlas.
|
||||
|
||||
INIA 0d5
|
||||
OUTA 0xE3
|
||||
INIA 0x3A
|
||||
OUTA 0xE2 ; And the port that owns the screen.
|
||||
INIA 0x03
|
||||
OUTA 0xE8 ; RegisterBank. Five is the map.
|
||||
|
||||
; ---- Asking for the screen back afterwards ----
|
||||
;
|
||||
@@ -153,7 +164,7 @@ finished:
|
||||
OUTA 0x34
|
||||
OUTA 0x36 ; The origins, or the shell looks at a corner of the map.
|
||||
|
||||
INIA 0d4
|
||||
INIA 0d5
|
||||
OUTA 0xE3
|
||||
INIA 0x40
|
||||
OUTA 0xE4
|
||||
@@ -350,7 +361,7 @@ putMap:
|
||||
STA.0
|
||||
|
||||
everyRow:
|
||||
INIA 0d4
|
||||
INIA 0d5
|
||||
OUTA 0xE3
|
||||
SETD.0 MapRow
|
||||
LDA.0
|
||||
@@ -411,7 +422,7 @@ everyCell:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
ScreenKept:
|
||||
0x00
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
CIF ; Nothing arrives until there is something to catch it.
|
||||
@@ -85,7 +85,7 @@ keyStop:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
Banner:
|
||||
"keys, by interrupt. q stops."
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
CALL seedGlider
|
||||
@@ -350,7 +350,7 @@ delayDone:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
RowCount:
|
||||
0x00
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
; Forty columns or eighty, whichever the screen is not in.
|
||||
;
|
||||
; A machine wakes up in the forty column mode and CosmOS asks for eighty, because that is
|
||||
; what its own output was written for. A game is the other way round: Snake on a forty column
|
||||
; screen is the same board drawn twice the size, which is what a person sitting in front of
|
||||
; it actually wants.
|
||||
;
|
||||
; ---- The smallest program this system can load ----
|
||||
;
|
||||
; Ten instructions and NO DATA AT ALL, which is not a curiosity: it is the shape that found
|
||||
; a bug in the loader. Every program written for CosmOS until this one had something in its
|
||||
; Data Segment, so the loader had never been asked to move a segment of no bytes - and a
|
||||
; length of zero asks the memory controller for the whole 64K, which does not fit, which it
|
||||
; refused, which stopped the machine in the middle of loading. On a terminal that printed a
|
||||
; fault. Behind a window it looked exactly like a hang.
|
||||
;
|
||||
; So this is kept dataless on purpose. The two digits it prints are put in a register one at
|
||||
; a time rather than being a string, which is the only reason it can say anything at all.
|
||||
;
|
||||
; Written by Anachronaut
|
||||
|
||||
#Include services.asm
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
INA 0x31 ; Which mode the screen is in now.
|
||||
BRA modeWide ; Nought is the forty column one, so go the other way.
|
||||
|
||||
; Anything else becomes forty, and that deliberately includes bitmap mode. A program that
|
||||
; left the screen with no text on it left nowhere to print, so coming back to a mode that
|
||||
; has characters in it is more use than refusing.
|
||||
RSTA
|
||||
OUTA 0x31
|
||||
INIA 0x34 ; '4'
|
||||
BRI modeSay
|
||||
modeWide:
|
||||
INIA 0x01
|
||||
OUTA 0x31
|
||||
INIA 0x38 ; '8'
|
||||
modeSay:
|
||||
OUTA 0x00
|
||||
INIA 0x30 ; '0'
|
||||
OUTA 0x00
|
||||
INIA 0x0A
|
||||
OUTA 0x00
|
||||
RSTA
|
||||
SWI osExit
|
||||
@@ -1,14 +1,15 @@
|
||||
; Read a text file one screen at a time.
|
||||
;
|
||||
; Twenty two lines are shown before a prompt. Space advances another screen, Return one
|
||||
; line, and q gives the machine back to CosmOS. This is forward-only on purpose: the file
|
||||
; A screenful of lines is shown before a prompt - as many as the screen has, asked for rather
|
||||
; than assumed, so that the eighty column mode is not read three fifths empty. Space advances
|
||||
; another screen, Return one line, and q gives the machine back to CosmOS. This is forward-only on purpose: the file
|
||||
; stream holds one block and never asks the whole document to fit in memory.
|
||||
;
|
||||
; Written by ChatGPT for Anachronaut's SplitBit
|
||||
|
||||
#Include services.asm
|
||||
#Program
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SETD.0 Name
|
||||
@@ -100,11 +101,38 @@ pauseQuit:
|
||||
INIB 0x01
|
||||
OR
|
||||
RET
|
||||
; ---- As many lines as the screen has, less the prompt ----
|
||||
;
|
||||
; Twenty two was written when there was one screen size. It is still right on the forty
|
||||
; column screen and wastes half of the eighty column one, so this ASKS: the rows register
|
||||
; says how tall the screen is, and it is readable for exactly this sort of reason.
|
||||
;
|
||||
; Rows minus three is twenty two on a twenty five row screen, so nothing changed underneath
|
||||
; anyone who was already reading files this way - it fills a bigger screen and leaves a
|
||||
; smaller one alone.
|
||||
;
|
||||
; A BITMAP SCREEN HAS NO ROWS AT ALL and says so with a nought, which would come out as 253
|
||||
; lines through an eight bit subtraction. Anything under five falls back, because a page of
|
||||
; two lines is not a page and a program should not be the thing that discovers this.
|
||||
fullPage:
|
||||
INA 0x33
|
||||
SETD.3 LinesLeft
|
||||
INIA 0d22
|
||||
STA.3 ; Kept, because the sums below want A for themselves.
|
||||
INIB 0d5
|
||||
CCF
|
||||
SUB
|
||||
BRC fullPageFallback ; Borrowed, so there are fewer than five rows.
|
||||
LDA.3
|
||||
INIB 0d3
|
||||
CCF
|
||||
SUB
|
||||
MVQA
|
||||
STA.3
|
||||
RET
|
||||
fullPageFallback:
|
||||
INIA 0d22
|
||||
STA.3 ; DP3 is still LinesLeft, from above.
|
||||
RET
|
||||
|
||||
noName:
|
||||
SETD.0 Usage
|
||||
@@ -139,7 +167,7 @@ finished:
|
||||
SWI osExit
|
||||
|
||||
#Data
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
Usage:
|
||||
"more: give me a file name
|
||||
"
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SETD.0 Wanted
|
||||
@@ -114,7 +114,7 @@ measureDone:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
Prefix:
|
||||
"system "
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
; What the controllers are doing, as they do it.
|
||||
;
|
||||
; Prints a line whenever any pad changes: which pad, and the byte it is now reporting. That is
|
||||
; the whole of the diagnostic, and it is enough to tell apart the three things that look
|
||||
; identical from inside a game which is not responding.
|
||||
;
|
||||
; Nothing printed at all, and nought pads present: the front end never saw a controller.
|
||||
; A pad present but no lines: it is seen, and the buttons are mapped to nothing.
|
||||
; Lines that do not match the buttons pressed: the mapping is wrong rather than missing.
|
||||
;
|
||||
; ---- Why a program rather than a print in the front end ----
|
||||
;
|
||||
; Because the question is what the MACHINE can see. A front end that printed what it thought
|
||||
; it was sending would answer a different question, and the gap between those two is exactly
|
||||
; where a controller that was detected, mapped and reported still did nothing: the port that
|
||||
; says which pads exist counted only recorded ones, so a game asked whether there was a
|
||||
; controller and was told no while the buttons underneath worked perfectly.
|
||||
;
|
||||
; Written by Anachronaut
|
||||
|
||||
#Include services.asm
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SETD.0 Intro
|
||||
SWI osPrintString
|
||||
|
||||
; How many are there, before anybody presses anything. One bit a pad.
|
||||
SETD.0 PresentText
|
||||
SWI osPrintString
|
||||
INA 0x64
|
||||
CALL sayByte
|
||||
INIA 0x0A
|
||||
OUTA 0x00
|
||||
|
||||
INIA 0x01
|
||||
OUTA 0x02 ; Key mode, so q arrives without a Return.
|
||||
|
||||
everyFrame:
|
||||
INA 0x30
|
||||
INIB 0x01
|
||||
AND
|
||||
BRQ everyFrame ; A frame, which is when a pad's recording steps.
|
||||
|
||||
RSTA
|
||||
SETD.2 Which
|
||||
STA.2
|
||||
eachPad:
|
||||
CALL onePad
|
||||
SETD.2 Which
|
||||
LDA.2
|
||||
INCA
|
||||
STA.2
|
||||
INIB 0d4
|
||||
CCF
|
||||
SUB
|
||||
BNQ eachPad
|
||||
|
||||
; q gives the machine back. Anything else typed is ignored, because a pad test that
|
||||
; stopped on a stray keypress would be a poor thing to lean on.
|
||||
INA 0x01
|
||||
INIB 0x01
|
||||
AND
|
||||
BRQ everyFrame
|
||||
INA 0x00
|
||||
INIB 0x71 ; q
|
||||
CCF
|
||||
SUB
|
||||
BNQ everyFrame
|
||||
|
||||
RSTA
|
||||
OUTA 0x02
|
||||
SWI osExit
|
||||
|
||||
; ---- One pad, printed only when it changes ----
|
||||
;
|
||||
; A line a frame for four pads would be two hundred and forty lines a second and unreadable.
|
||||
; What is worth seeing is the moment something goes down or comes up.
|
||||
onePad:
|
||||
; The port is 0x60 plus the pad number, and a port is an immediate byte inside the
|
||||
; instruction that names it - so it cannot be computed, and the four are written out.
|
||||
SETD.2 Which
|
||||
LDA.2
|
||||
BRA padZero
|
||||
DECA
|
||||
BRA padOne
|
||||
DECA
|
||||
BRA padTwo
|
||||
INA 0x63
|
||||
BRI padGot
|
||||
padZero:
|
||||
INA 0x60
|
||||
BRI padGot
|
||||
padOne:
|
||||
INA 0x61
|
||||
BRI padGot
|
||||
padTwo:
|
||||
INA 0x62
|
||||
|
||||
padGot:
|
||||
; Against what it was last time. Last is four bytes, one a pad, so DP1 walks to this one.
|
||||
SETD.1 Last
|
||||
PSHA
|
||||
SETD.2 Which
|
||||
LDA.2
|
||||
DPUA.1
|
||||
POPA
|
||||
LDB.1
|
||||
CCF
|
||||
SUB
|
||||
BRQ padSame ; The same as last frame, so there is nothing to say.
|
||||
|
||||
STA.1 ; Remembered, so the next frame has something to compare with.
|
||||
PSHA
|
||||
LDA.2 ; DP2 is still Which, from working out where in Last to look.
|
||||
INIB 0x30 ; '0'
|
||||
CCF
|
||||
ADD
|
||||
OUTQ 0x00
|
||||
INIA 0x3A ; ':'
|
||||
OUTA 0x00
|
||||
INIA 0x20
|
||||
OUTA 0x00
|
||||
POPA
|
||||
CALL sayByte
|
||||
INIA 0x0A
|
||||
OUTA 0x00
|
||||
padSame:
|
||||
RET
|
||||
|
||||
; ---- One byte, as a number ----
|
||||
;
|
||||
; osPrintNumber takes A and B TOGETHER, A being the high half - which is the same way round as
|
||||
; the shift register and every other pair on this machine, and is not what a byte in A wants.
|
||||
; Passed as it stood, every value came out two hundred and fifty six times too big.
|
||||
sayByte:
|
||||
RSTB
|
||||
CCF
|
||||
ADD
|
||||
MVQB ; The byte, in the low half where it belongs.
|
||||
RSTA ; And nothing in the high one.
|
||||
SWI osPrintNumber
|
||||
RET
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x3000
|
||||
|
||||
Intro:
|
||||
"Press buttons on a controller. q quits.
|
||||
"
|
||||
PresentText:
|
||||
"Pads present, as a bit each: "
|
||||
|
||||
; One byte a pad, holding what it said last frame.
|
||||
Last:
|
||||
0x00 0x00 0x00 0x00
|
||||
Which:
|
||||
0x00
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
#Include services.asm
|
||||
#Program
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SETD.0 Argument
|
||||
@@ -213,7 +213,7 @@ doneFailed:
|
||||
SWI osExit
|
||||
|
||||
#Data
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
Name:
|
||||
"poured.dat"
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SETD.0 LineText
|
||||
@@ -141,7 +141,7 @@ keySay:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
LineText:
|
||||
"a line, then keys. q stops."
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SETD.0 Saying
|
||||
@@ -45,7 +45,7 @@ start:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
Saying:
|
||||
"starting again
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SETD.0 Given
|
||||
@@ -45,7 +45,7 @@ sayEnd:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
SaidText:
|
||||
"it says: "
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SWI osBootState
|
||||
@@ -71,7 +71,7 @@ alreadySettled:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
WasTrying:
|
||||
"the disk says a start is still in progress
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
RSTA
|
||||
@@ -140,7 +140,7 @@ printCandidateHex:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
; Segment has to begin on a page boundary, and now says so itself rather than relying on
|
||||
; whatever happens to have been assembled before it. The marking loop adds the prime to
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
; Search the list until we find a prime.
|
||||
@@ -46,7 +46,7 @@ start:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
; The table of our prime candidates. It has to begin on a page boundary: marking walks
|
||||
; the pointer's low byte and treats the carry out as running off the end of the table,
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
; The two pointers whose low byte is a square number. Both regions are page aligned, so
|
||||
@@ -615,7 +615,7 @@ pauseInner:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
HeadCell:
|
||||
0x00
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
; A thing that moves without the screen moving.
|
||||
;
|
||||
; Everything drawn on this machine before sprites was in a CELL. Something between two cells
|
||||
; meant rewriting both of them, and something moving a pixel at a time meant rewriting them
|
||||
; sixty times a second - which is affordable for one thing and not for twenty.
|
||||
;
|
||||
; A sprite is put at a PIXEL, and the device draws it over whatever is already there. Moving
|
||||
; it costs two bytes: the low and high halves of where it now is. That is the whole of the
|
||||
; loop below, and it is why this program can run over the shell's own text without disturbing
|
||||
; a single character of it - nothing underneath is written to at all.
|
||||
;
|
||||
; ---- What it leaves behind ----
|
||||
;
|
||||
; The sprite, still in the table. On purpose, and for the same reason Flip leaves the screen
|
||||
; it flipped to: a program that FAULTED would have left it too, and a system that only tidied
|
||||
; up after programs which remembered would be one that left a ball sitting over the prompt
|
||||
; the first time somebody's game crashed. The table is the system's to clear.
|
||||
;
|
||||
; Written by Anachronaut
|
||||
|
||||
#Include services.asm
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
; The atlas, which is where both the tiles and the sprite table live. Four is what CosmOS
|
||||
; uses for it; see the table in the CosmOS README for who owns which number.
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0x30
|
||||
OUTA 0xE2
|
||||
INIA 0x03
|
||||
OUTA 0xE8
|
||||
|
||||
SETD.0 Message
|
||||
SWI osPrintString
|
||||
INIA 0x0A
|
||||
OUTA 0x00
|
||||
|
||||
CALL putBall
|
||||
CALL screenWidth
|
||||
CALL putSprite
|
||||
|
||||
; Key mode, so a key arrives when it is pressed rather than when Return is.
|
||||
INIA 0x01
|
||||
OUTA 0x02
|
||||
|
||||
everyFrame:
|
||||
CALL waitFrame
|
||||
CALL stepBall
|
||||
CALL moveSprite
|
||||
|
||||
; Anything typed ends it. Asked for and never waited on, so a key pressed between frames
|
||||
; is still there when this looks.
|
||||
INA 0x01
|
||||
INIB 0x01 ; READY
|
||||
AND
|
||||
BRQ everyFrame
|
||||
INA 0x00 ; Taken, so the shell is not handed a key meant for this.
|
||||
|
||||
RSTA
|
||||
OUTA 0x02 ; Line mode again. The sprite is left where it is.
|
||||
SWI osExit
|
||||
|
||||
; ---- The art, into a tile above the font ----
|
||||
;
|
||||
; Two hundred, which is well clear of the 135 glyphs the character generator copies back, so
|
||||
; nothing here costs the shell a letter. Blitted rather than poked: it is already sixty four
|
||||
; bytes of Data Segment and the controller moves it in one command.
|
||||
putBall:
|
||||
INIA 0x01
|
||||
OUTA 0xE0 ; SourceBank: Data Memory.
|
||||
; The pointer written down before it is read out of memory a byte at a time, because a Data
|
||||
; Pointer's two halves cannot be got at any other way.
|
||||
SETD.1 BallArtAt
|
||||
SETD.0 BallArt
|
||||
STD.0.1
|
||||
LDA.1
|
||||
OUTA 0xE1
|
||||
INCD.1
|
||||
LDA.1
|
||||
OUTA 0xE2
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0x32
|
||||
OUTA 0xE4 ; Tile 200 begins at 200 times 64, which is 0x3200.
|
||||
RSTA
|
||||
OUTA 0xE5
|
||||
OUTA 0xE6
|
||||
INIA 0x40
|
||||
OUTA 0xE7 ; Sixty four bytes.
|
||||
INIA 0x01
|
||||
OUTA 0xE8 ; Blit.
|
||||
RET
|
||||
|
||||
; ---- How wide the screen is, in pixels ----
|
||||
;
|
||||
; Columns times eight, and this machine cannot multiply. A and B are one sixteen bit shift
|
||||
; register though: with the column count in A and nothing in B, A:B holds columns times 256,
|
||||
; and five shifts right divide that by thirty two - which is columns times eight, high byte
|
||||
; left in A and low byte in B.
|
||||
;
|
||||
; Asked rather than assumed, because the shell runs eighty columns and a game may well have
|
||||
; asked for forty before starting this.
|
||||
screenWidth:
|
||||
INA 0x32
|
||||
RSTB
|
||||
SHR
|
||||
SHR
|
||||
SHR
|
||||
SHR
|
||||
SHR
|
||||
SETD.0 WidthHigh
|
||||
STA.0
|
||||
; B cannot be stored, and there is no move from it. Adding nothing to it puts it in Q,
|
||||
; which can be copied to A, which can.
|
||||
RSTA
|
||||
CCF
|
||||
ADD
|
||||
MVQA
|
||||
SETD.0 WidthLow
|
||||
STA.0
|
||||
RET
|
||||
|
||||
; ---- The entry, written straight through ----
|
||||
;
|
||||
; The controller's Data port steps its address on after every byte, so all eight go out of
|
||||
; one port with the address named once.
|
||||
putSprite:
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0xC0
|
||||
OUTA 0xE4
|
||||
RSTA
|
||||
OUTA 0xE5 ; Sprite nought is at 0xC000.
|
||||
INIA 0xC8
|
||||
OUTA 0xE9 ; Tile 200.
|
||||
INIA 0x01
|
||||
OUTA 0xE9 ; Attribute one, so the ball comes out in scheme one's ink.
|
||||
RSTA
|
||||
OUTA 0xE9
|
||||
OUTA 0xE9 ; X, low then high.
|
||||
INIA 0d96
|
||||
OUTA 0xE9
|
||||
RSTA
|
||||
OUTA 0xE9 ; Y, ninety six pixels down.
|
||||
INIA 0x11
|
||||
OUTA 0xE9 ; One tile across by one down.
|
||||
RSTA
|
||||
OUTA 0xE9 ; Not mirrored, not turned over, not behind.
|
||||
RET
|
||||
|
||||
; A frame, which is the only regular beat this machine has.
|
||||
waitFrame:
|
||||
INA 0x30
|
||||
INIB 0x01
|
||||
AND
|
||||
BRQ waitFrame
|
||||
RET
|
||||
|
||||
; ---- One pixel to the right ----
|
||||
;
|
||||
; Sixteen bits in two bytes, so the high one is stepped only when the low one came back round
|
||||
; to nought - which is what a carry is, done by hand.
|
||||
stepBall:
|
||||
SETD.0 BallX
|
||||
LDA.0
|
||||
INCA
|
||||
STA.0
|
||||
BNA stepCheck
|
||||
SETD.0 BallXHigh
|
||||
LDA.0
|
||||
INCA
|
||||
STA.0
|
||||
|
||||
; Round to the left edge at the far side. Both halves have to match, and the high one is
|
||||
; tested first because it is the one that is usually wrong.
|
||||
stepCheck:
|
||||
SETD.0 BallXHigh
|
||||
LDA.0
|
||||
SETD.1 WidthHigh
|
||||
LDB.1
|
||||
CCF
|
||||
SUB
|
||||
BNQ stepDone
|
||||
SETD.0 BallX
|
||||
LDA.0
|
||||
SETD.1 WidthLow
|
||||
LDB.1
|
||||
CCF
|
||||
SUB
|
||||
BNQ stepDone
|
||||
RSTA
|
||||
STA.0 ; DP0 is still BallX, from the comparison just above.
|
||||
SETD.0 BallXHigh
|
||||
STA.0
|
||||
stepDone:
|
||||
RET
|
||||
|
||||
; Two bytes out of one port, which is the whole cost of moving a sprite.
|
||||
moveSprite:
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0xC0
|
||||
OUTA 0xE4
|
||||
INIA 0x02
|
||||
OUTA 0xE5 ; X is bytes two and three of the entry.
|
||||
SETD.0 BallX
|
||||
LDA.0
|
||||
OUTA 0xE9
|
||||
SETD.0 BallXHigh
|
||||
LDA.0
|
||||
OUTA 0xE9
|
||||
RET
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x3000
|
||||
|
||||
Message:
|
||||
"A ball, over the shell's own words. Nothing underneath is written to. Press a key."
|
||||
|
||||
; Index nought is not a colour, it is a hole - so the corners of the tile are what the ball
|
||||
; is not, and whatever is behind shows through them.
|
||||
BallArt:
|
||||
0x00 0x00 0x01 0x01 0x01 0x01 0x00 0x00
|
||||
0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x00
|
||||
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
|
||||
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
|
||||
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
|
||||
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
|
||||
0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x00
|
||||
0x00 0x00 0x01 0x01 0x01 0x01 0x00 0x00
|
||||
|
||||
BallArtAt:
|
||||
#Reserve 0d2
|
||||
|
||||
BallX:
|
||||
0x00
|
||||
BallXHigh:
|
||||
0x00
|
||||
WidthLow:
|
||||
0x00
|
||||
WidthHigh:
|
||||
0x00
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SWI osLastStatus
|
||||
@@ -70,7 +70,7 @@ done:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
Prefix:
|
||||
"the last program left "
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
; ---- 1. How big is something that will not fit ----
|
||||
@@ -408,7 +408,7 @@ printWhy:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
BigName:
|
||||
"big.txt"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
#Include services.asm
|
||||
#Program
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SETD.0 Name
|
||||
@@ -70,7 +70,7 @@ finished:
|
||||
SWI osExit
|
||||
|
||||
#Data
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
Usage:
|
||||
"type: give me a file name
|
||||
"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
#Include services.asm
|
||||
#Program
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
; Where to go is the argument. Nothing else about this program says a directory name, so
|
||||
@@ -85,7 +85,7 @@ noFile:
|
||||
SWI osExit
|
||||
|
||||
#Data
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
Where:
|
||||
#Reserve 0d64
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000 ; Above the system, which keeps below here.
|
||||
#Base 0x5000 ; Above the system, which keeps below here.
|
||||
|
||||
greet:
|
||||
SETD.0 Opening
|
||||
@@ -44,7 +44,7 @@ greet:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000 ; And its data above the system's.
|
||||
#Base 0x3000 ; And its data above the system's.
|
||||
|
||||
Opening:
|
||||
"a program, loaded off a disk, running on the system that loaded it
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000 ; Change two:
|
||||
#Base 0x5000 ; Change two:
|
||||
|
||||
SETD hello ; Change three
|
||||
Start:
|
||||
@@ -26,6 +26,6 @@ End:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000 ; Five, adjust the base of the data segment.
|
||||
#Base 0x3000 ; Five, adjust the base of the data segment.
|
||||
hello: ; Throw a label here so we can explicitly point at this data. Six, actually.
|
||||
"Hello, World!"
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SETD.0 Argument
|
||||
@@ -2180,7 +2180,7 @@ report:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
Argument:
|
||||
#Reserve 0d23
|
||||
@@ -2258,7 +2258,7 @@ ReservedLeft:
|
||||
ReservedWalk:
|
||||
0x00 0x00
|
||||
ReservedCount:
|
||||
0d5
|
||||
0d7
|
||||
VecHandlerName:
|
||||
#Reserve 0d23
|
||||
|
||||
@@ -2285,6 +2285,12 @@ ReservedNames:
|
||||
"BankFault"
|
||||
#Reserve 0d5
|
||||
0d4
|
||||
"NoHandler"
|
||||
#Reserve 0d5
|
||||
0d5
|
||||
"NoDevice"
|
||||
#Reserve 0d6
|
||||
0d6
|
||||
|
||||
ProgPut:
|
||||
0x00 0x00
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SETD.0 Wanted
|
||||
@@ -60,7 +60,7 @@ noFile:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
Wanted:
|
||||
#Reserve 0d23
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
;
|
||||
; None of this is initialised data. It is scratch, wanted only while the assembler is
|
||||
; running, and while it is running everything above its own data is free: the system keeps
|
||||
; below 0x1FFF, the staging area is only in use during a load, and the Stack comes down
|
||||
; below 0x2FFF, the staging area is only in use during a load, and the Stack comes down
|
||||
; from the top. So the addresses are written down here and the file carries none of it.
|
||||
;
|
||||
; That sentence said 0x1000 for a while after the system's half of Data Memory was
|
||||
@@ -20,10 +20,10 @@
|
||||
; bad enough; a stale number sitting next to the correction is worse, because whichever
|
||||
; one a reader takes is a coin toss.
|
||||
;
|
||||
; 0x4000 6144 the label index, 1536 entries of four
|
||||
; 0x5800 16384 the label names, packed end to end
|
||||
; 0x9800 256 one block of the output file, on its way to the disk
|
||||
; 0x9900 18176 free
|
||||
; 0x5000 6144 the label index, 1536 entries of four
|
||||
; 0x6800 16384 the label names, packed end to end
|
||||
; 0xA800 256 one block of the output file, on its way to the disk
|
||||
; 0xA900 17152 free
|
||||
; 0xE000 1792 the vector names, 64 entries of twenty eight
|
||||
; 0xE700 2048 the reader's stack, six levels of 301
|
||||
; 0xEF00 368 which files have been included, sixteen names of 23
|
||||
@@ -35,9 +35,15 @@
|
||||
; and the map stayed where it was, leaving sixteen kilobytes between the two that nothing
|
||||
; touched.
|
||||
;
|
||||
; Starting at 0x4000 takes that back. The assembler's data is a little over four kilobytes
|
||||
; from 0x2000, so there is still nearly four kilobytes of slack in front of this - and room
|
||||
; for its data to double before the two would meet. `make test` measures that gap now
|
||||
; Starting above the assembler's own data takes that back. It began at 0x4000 with the data
|
||||
; from 0x2000, and moved to 0x5000 when the system was given another page and every
|
||||
; application's data moved to 0x3000 with it - THE FLOOR CAME UP A SECOND TIME, exactly as
|
||||
; the paragraph above says it did the first, and this time the check below said so before
|
||||
; anything ran: the assembler's data reached 0x40D6 and the index began at 0x4000, so the
|
||||
; buffers were sitting on the variables.
|
||||
;
|
||||
; There is still nearly four kilobytes of slack in front of this, and room for the data to
|
||||
; double before the two would meet. `make test` measures that gap now
|
||||
; rather than trusting this paragraph, and measures the floor above as well, because both
|
||||
; of those numbers describe the machine AROUND this file and neither is enforced by a line
|
||||
; of code anywhere.
|
||||
@@ -77,11 +83,11 @@
|
||||
; second is bigger than the first, which is worth knowing: the hardest thing this assembles
|
||||
; is not the operating system, it is itself.
|
||||
ScratchLabIndex:
|
||||
0x40 0x00
|
||||
0x50 0x00
|
||||
ScratchLabArena:
|
||||
0x58 0x00
|
||||
0x68 0x00
|
||||
ScratchWindow:
|
||||
0x98 0x00
|
||||
0xA8 0x00
|
||||
ScratchVecNames:
|
||||
0xE0 0x00
|
||||
ScratchSrcStack:
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
SETD.0 Wanted
|
||||
@@ -128,7 +128,7 @@ noFile:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
#Base 0x3000
|
||||
|
||||
Wanted:
|
||||
#Reserve 0d23
|
||||
|
||||
+279
-11
File diff suppressed because one or more lines are too long
+2953
-46
File diff suppressed because it is too large
Load Diff
@@ -243,6 +243,21 @@ scriptLine:
|
||||
STD.0.1
|
||||
|
||||
scriptLineAgain:
|
||||
; ---- Where this line begins ----
|
||||
;
|
||||
; Kept before it is read, because a loop has to be able to go back to the line that opened
|
||||
; it and by the time that line has been read the reader is past it. Three words, and the
|
||||
; block itself is read again on the way back, which is what scriptReread is for.
|
||||
SETD.0 ScriptIndex
|
||||
SETD.1 ScriptLineIndex
|
||||
CALL sbfsCopyWord
|
||||
SETD.0 ScriptAt
|
||||
SETD.1 ScriptLineAt
|
||||
CALL sbfsCopyWord
|
||||
SETD.0 ScriptBlocks
|
||||
SETD.1 ScriptLineBlocks
|
||||
CALL sbfsCopyWord
|
||||
|
||||
SETD.1 ScriptLength
|
||||
RSTA
|
||||
STA.1
|
||||
@@ -539,6 +554,19 @@ ScriptIndex:
|
||||
0x00 0x00
|
||||
ScriptAt:
|
||||
0x00 0x00
|
||||
|
||||
; ---- And where the line being read began ----
|
||||
;
|
||||
; A loop goes back to the line that opened it, and by the time that line has been read the
|
||||
; reader is past it. So the position is kept before every line rather than worked out
|
||||
; afterwards, which cannot be done: a line is not a fixed size and there is nothing to
|
||||
; subtract.
|
||||
ScriptLineIndex:
|
||||
0x00 0x00
|
||||
ScriptLineAt:
|
||||
0x00 0x00
|
||||
ScriptLineBlocks:
|
||||
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
|
||||
|
||||
@@ -41,6 +41,13 @@
|
||||
; going out it is A and B together, and neither direction needs a record in memory that
|
||||
; both sides have to agree on the shape of.
|
||||
osFileRead 0d20 ; DP0 names it, DP1 says where. Q is zero if it read, DP3 is how many bytes.
|
||||
; ---- AND IT WRITES WHOLE BLOCKS ----
|
||||
;
|
||||
; A disk is read a block at a time, so a sixteen byte file still puts
|
||||
; 256 bytes where it is told to. The room given has to be the file's
|
||||
; length ROUNDED UP to the next 256, and a caller that gives exactly
|
||||
; the length writes over whatever follows it. DP3 still says how many
|
||||
; bytes are the file's; the rest is whatever was on the block.
|
||||
osFileSave 0d21 ; DP0 names it, DP1 is the bytes, A and B are how many. Q is zero if it saved.
|
||||
osFileDelete 0d22 ; DP0 names it. Q is zero if it went.
|
||||
osFileRename 0d23 ; DP0 is the name it has, DP1 the name it should have. Q is zero if it moved.
|
||||
|
||||
@@ -80,8 +80,10 @@ nextBank:
|
||||
; 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.
|
||||
; Give the screen's memory a bank number. The screen brings TWO banks and this only wants
|
||||
; one of them: port 0x30 owns the atlas, where the tiles and the palette are, and the
|
||||
; palette is all this touches. 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
|
||||
@@ -89,7 +91,7 @@ nextBank:
|
||||
INIA 0x03
|
||||
OUTA 0xE8 ; Command: RegisterBank
|
||||
|
||||
; The palette sits at the top of video memory, at 0xFC00, and entry n is at n times
|
||||
; The palette sits at the top of the atlas, 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
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
; 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.
|
||||
; It lives over the top of the map, in the SCREEN bank, because that is the bank whose
|
||||
; contents change: a picture and a map are the same memory called two different things in two
|
||||
; different modes. Going to bitmap mode does not clear the text screen, it stops calling it
|
||||
; one - and coming back finds the map holding whatever the picture put there.
|
||||
;
|
||||
; 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.
|
||||
; WHAT IT DOES NOT COST IS THE FONT. The tiles and the palette are in the other bank and a
|
||||
; picture cannot reach them, which is what lets this program draw and then say something.
|
||||
|
||||
#Program
|
||||
|
||||
@@ -27,17 +27,31 @@ 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.
|
||||
; TWO OF THEM, because the screen brings two banks and this program writes to both: the
|
||||
; palette is in the atlas and the picture is in the screen.
|
||||
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
|
||||
OUTA 0xE8 ; Command: RegisterBank. Three is the atlas.
|
||||
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0x3A
|
||||
OUTA 0xE2
|
||||
INIA 0x03
|
||||
OUTA 0xE8 ; And four is the screen.
|
||||
|
||||
; ---- 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.
|
||||
; A already holds 3, because RegisterBank is command 3 and the bank wanted is bank 3.
|
||||
; Leaving the line out would turn a program that says which bank it means into one that
|
||||
; works by coincidence, and the coincidence breaks the day either number changes.
|
||||
INIA 0d3 ; splitlint[redundant-assignment]: said rather than coincided
|
||||
OUTA 0xE3 ; The ATLAS, which registering the second bank moved off
|
||||
INIA 0xFC
|
||||
OUTA 0xE4
|
||||
RSTA
|
||||
@@ -67,6 +81,8 @@ palette:
|
||||
; 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.
|
||||
INIA 0d4
|
||||
OUTA 0xE3 ; Into the SCREEN bank now, which is where a picture is
|
||||
RSTA
|
||||
OUTA 0xE4
|
||||
OUTA 0xE5 ; Dest 0x0000, the top left corner
|
||||
|
||||
@@ -35,8 +35,12 @@
|
||||
; 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.
|
||||
|
||||
#Include services.asm
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
|
||||
; ---- The instrument ----
|
||||
@@ -182,7 +186,7 @@ lastRing:
|
||||
CIF
|
||||
RSTA
|
||||
OUTA 0x35
|
||||
HALT
|
||||
SWI osExit
|
||||
|
||||
; 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
|
||||
@@ -192,6 +196,8 @@ frame:
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x3000
|
||||
|
||||
; ---- 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
|
||||
|
||||
@@ -54,8 +54,243 @@ int16sub:
|
||||
STQ ; Store the result.
|
||||
RET ; Return to the caller.
|
||||
|
||||
; ---- Multiplying, which this machine cannot do ----
|
||||
;
|
||||
; There is no MUL. What there is instead is an identity:
|
||||
;
|
||||
; a * b = qs[a + b] - qs[|a - b|] where qs[n] is n squared over four
|
||||
;
|
||||
; because (a+b)^2/4 - (a-b)^2/4 is exactly a*b, and the halves that the flooring throws away
|
||||
; cancel between the two terms. So a multiply is TWO LOOKUPS AND A SUBTRACT.
|
||||
;
|
||||
; ---- And the table is built by adding ----
|
||||
;
|
||||
; Which is the part that makes this fit a machine with no multiplier at all. A table of
|
||||
; squares would need squaring to fill, and this one does not:
|
||||
;
|
||||
; qs[n] = qs[n-1] + n/2
|
||||
;
|
||||
; and n/2 goes 0, 1, 1, 2, 2, 3, 3 - a number that steps up on every even n. So the whole
|
||||
; table is one running total and a counter, and nothing harder than an add appears anywhere in
|
||||
; building the thing that does the multiplying.
|
||||
;
|
||||
; 511 entries of two bytes, because a and b are bytes and a+b reaches 510. That is 1,022 bytes
|
||||
; of Data Memory, which is the price: this is a routine that trades a kilobyte for an
|
||||
; operation the hardware has not got.
|
||||
|
||||
; Fills the table. Call once, before the first multiply, and never again.
|
||||
mulReady:
|
||||
SETD.0 MulTable
|
||||
RSTA
|
||||
STA.0
|
||||
INCD.0
|
||||
STA.0 ; qs[0] is nought.
|
||||
INCD.0
|
||||
|
||||
SETD.1 MulTotalLow
|
||||
STA.1
|
||||
INCD.1
|
||||
STA.1 ; And so is the running total.
|
||||
SETD.1 MulStep
|
||||
STA.1
|
||||
SETD.1 MulToggle
|
||||
STA.1
|
||||
|
||||
; Five hundred and ten more entries to write.
|
||||
INIA 0xFE
|
||||
SETD.1 MulLeftLow
|
||||
STA.1
|
||||
INIA 0x01
|
||||
SETD.1 MulLeftHigh
|
||||
STA.1
|
||||
|
||||
mulEntry:
|
||||
; ---- The step goes up on every even index ----
|
||||
;
|
||||
; n/2 for n = 1, 2, 3, 4 is 0, 1, 1, 2: it rises at the even ones. A toggle says which this
|
||||
; is, which is cheaper than halving a sixteen bit counter every time round.
|
||||
SETD.1 MulToggle
|
||||
LDA.1
|
||||
INIB 0x01
|
||||
XOR
|
||||
STQ.1
|
||||
BNQ mulNoStep ; The toggle is one, so this index is odd and the step stands.
|
||||
SETD.1 MulStep
|
||||
LDA.1
|
||||
INCA
|
||||
STA.1
|
||||
mulNoStep:
|
||||
|
||||
; The running total, plus the step, sixteen bits.
|
||||
CCF
|
||||
SETD.1 MulTotalLow
|
||||
LDA.1
|
||||
SETD.2 MulStep
|
||||
LDB.2
|
||||
ADD
|
||||
STQ.1
|
||||
SETD.1 MulTotalHigh
|
||||
LDA.1
|
||||
RSTB
|
||||
ADD ; Nothing but the carry out of the half below.
|
||||
STQ.1
|
||||
|
||||
; And into the table, where DP0 has been walking all along.
|
||||
SETD.1 MulTotalLow
|
||||
LDA.1
|
||||
STA.0
|
||||
INCD.0
|
||||
SETD.1 MulTotalHigh
|
||||
LDA.1
|
||||
STA.0
|
||||
INCD.0
|
||||
|
||||
; One fewer to do, counted down sixteen bits.
|
||||
SETD.1 MulLeftLow
|
||||
LDA.1
|
||||
BNA mulCountLow
|
||||
SETD.1 MulLeftHigh
|
||||
LDA.1
|
||||
DECA
|
||||
STA.1
|
||||
INIA 0xFF
|
||||
SETD.1 MulLeftLow
|
||||
STA.1
|
||||
BRI mulCounted
|
||||
mulCountLow:
|
||||
DECA
|
||||
STA.1
|
||||
mulCounted:
|
||||
SETD.1 MulLeftLow
|
||||
LDA.1
|
||||
SETD.2 MulLeftHigh
|
||||
LDB.2
|
||||
OR
|
||||
BNQ mulEntry
|
||||
RET
|
||||
|
||||
; ---- One multiply ----
|
||||
;
|
||||
; The operands go in MulA and MulB and the sixteen bit product comes back in MulLow and
|
||||
; MulHigh. IN MEMORY RATHER THAN IN REGISTERS, because B cannot be stored and a product does
|
||||
; not fit in one byte anyway - two registers in and two out would spend more instructions
|
||||
; shuffling than the multiply costs.
|
||||
mul8:
|
||||
; The sum, which is nine bits: two bytes can reach 510 between them.
|
||||
SETD.0 MulA
|
||||
LDA.0
|
||||
SETD.1 MulB
|
||||
LDB.1
|
||||
CCF
|
||||
ADD
|
||||
BRC mulSumOver
|
||||
RSTA
|
||||
BRI mulSumHigh
|
||||
mulSumOver:
|
||||
INIA 0x01
|
||||
mulSumHigh:
|
||||
SETD.0 MulSumHigh
|
||||
STA.0
|
||||
MVQA ; Q is still the sum's low half; nothing above touched it.
|
||||
SETD.0 MulSumLow
|
||||
STA.0
|
||||
|
||||
; The difference, without its sign, which is what the identity wants.
|
||||
SETD.0 MulA
|
||||
LDA.0
|
||||
SETD.1 MulB
|
||||
LDB.1
|
||||
CCF
|
||||
SUB
|
||||
BRC mulOtherWay ; Borrowed, so B is the larger and the sum goes the other way.
|
||||
MVQA
|
||||
BRI mulDiffGot
|
||||
mulOtherWay:
|
||||
SETD.0 MulB
|
||||
LDA.0
|
||||
SETD.1 MulA
|
||||
LDB.1
|
||||
CCF
|
||||
SUB
|
||||
MVQA
|
||||
mulDiffGot:
|
||||
SETD.0 MulDiff
|
||||
STA.0
|
||||
|
||||
; qs[sum]. An entry is two bytes, so the index is doubled - one turn of the shift register,
|
||||
; where A is the high half and B the low, which is the way DPUW wants them too.
|
||||
SETD.0 MulSumHigh
|
||||
LDA.0
|
||||
SETD.0 MulSumLow
|
||||
LDB.0
|
||||
SHL
|
||||
SETD.0 MulTable
|
||||
DPUW.0
|
||||
LDA.0
|
||||
SETD.1 MulLow
|
||||
STA.1
|
||||
INCD.0
|
||||
LDA.0
|
||||
SETD.1 MulHigh
|
||||
STA.1
|
||||
|
||||
; Less qs[difference], sixteen bits, the borrow running from one half to the other.
|
||||
RSTA
|
||||
SETD.0 MulDiff
|
||||
LDB.0
|
||||
SHL
|
||||
SETD.0 MulTable
|
||||
DPUW.0
|
||||
CCF
|
||||
SETD.1 MulLow
|
||||
LDA.1
|
||||
LDB.0
|
||||
SUB
|
||||
STQ.1
|
||||
INCD.0
|
||||
SETD.1 MulHigh
|
||||
LDA.1
|
||||
LDB.0
|
||||
SUB
|
||||
STQ.1
|
||||
RET
|
||||
|
||||
#Data
|
||||
|
||||
; ---- The multiply's working ----
|
||||
;
|
||||
; MulLow and MulHigh are next to each other on purpose: the product is read as a pair.
|
||||
MulA:
|
||||
0x00
|
||||
MulB:
|
||||
0x00
|
||||
MulLow:
|
||||
0x00
|
||||
MulHigh:
|
||||
0x00
|
||||
MulSumLow:
|
||||
0x00
|
||||
MulSumHigh:
|
||||
0x00
|
||||
MulDiff:
|
||||
0x00
|
||||
MulStep:
|
||||
0x00
|
||||
MulToggle:
|
||||
0x00
|
||||
MulTotalLow:
|
||||
0x00
|
||||
MulTotalHigh:
|
||||
0x00
|
||||
MulLeftLow:
|
||||
0x00
|
||||
MulLeftHigh:
|
||||
0x00
|
||||
|
||||
; 511 entries of two bytes: n from nought to 510, which is as far as a byte plus a byte goes.
|
||||
MulTable:
|
||||
#Reserve 0d1022
|
||||
|
||||
Int16operands:
|
||||
0x00 ; Operand A high byte.
|
||||
0x00 ; Operand A low byte.
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"osc0_waveform": 5,
|
||||
"osc0_dutyCycle": 0.500000,
|
||||
"osc0_detune": 0.000000,
|
||||
"osc0_gain": 4.000000,
|
||||
"osc0_active": 1,
|
||||
"osc0_octave": 2,
|
||||
"osc0_modRouting0": 0,
|
||||
"osc0_modRouting1": 2,
|
||||
"osc0_modRouting2": 0,
|
||||
"osc0_modDepth0": 0.000000,
|
||||
"osc0_modDepth1": 180.000000,
|
||||
"osc0_modDepth2": 4.000000,
|
||||
"osc1_waveform": 5,
|
||||
"osc1_dutyCycle": 0.545000,
|
||||
"osc1_detune": -15.000000,
|
||||
"osc1_gain": 4.000000,
|
||||
"osc1_active": 1,
|
||||
"osc1_octave": 1,
|
||||
"osc1_modRouting0": 0,
|
||||
"osc1_modRouting1": 2,
|
||||
"osc1_modRouting2": 0,
|
||||
"osc1_modDepth0": -0.750000,
|
||||
"osc1_modDepth1": 1104.000000,
|
||||
"osc1_modDepth2": 4.000000,
|
||||
"ampEnv_attack": 2.000000,
|
||||
"ampEnv_decay": 0.100000,
|
||||
"ampEnv_sustain": 0.000000,
|
||||
"ampEnv_release": 0.001000,
|
||||
"modEnv_attack": 0.001000,
|
||||
"modEnv_decay": 0.603263,
|
||||
"modEnv_sustain": 0.000000,
|
||||
"modEnv_release": 2.000000,
|
||||
"lfo0_rate": 8.796101,
|
||||
"lfo0_waveform": 0,
|
||||
"lfo0_active": 0,
|
||||
"lfo0_mode": 0,
|
||||
"lfo1_rate": 1.000000,
|
||||
"lfo1_waveform": 0,
|
||||
"lfo1_active": 0,
|
||||
"lfo1_mode": 0,
|
||||
"filter_cutoff": 13294.700195,
|
||||
"filter_resonance": 0.574200,
|
||||
"filter_type": 0,
|
||||
"filter_active": 1,
|
||||
"filter_modRouting": 2,
|
||||
"filter_modDepth": 11600.000000,
|
||||
"filter_resModRouting": 0,
|
||||
"filter_resModDepth": 0.000000,
|
||||
"voice_levelSource": 2,
|
||||
"voice_gate": 1,
|
||||
"master_volume": 1.000000,
|
||||
"master_pitchBendRange": 2.000000
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"osc0_waveform": 5,
|
||||
"osc0_dutyCycle": 0.500000,
|
||||
"osc0_detune": 0.000000,
|
||||
"osc0_gain": 4.000000,
|
||||
"osc0_active": 1,
|
||||
"osc0_octave": 2,
|
||||
"osc0_modRouting0": 0,
|
||||
"osc0_modRouting1": 2,
|
||||
"osc0_modRouting2": 0,
|
||||
"osc0_modDepth0": 0.000000,
|
||||
"osc0_modDepth1": 180.000000,
|
||||
"osc0_modDepth2": 4.000000,
|
||||
"osc1_waveform": 5,
|
||||
"osc1_dutyCycle": 0.545000,
|
||||
"osc1_detune": -15.000000,
|
||||
"osc1_gain": 4.000000,
|
||||
"osc1_active": 1,
|
||||
"osc1_octave": 1,
|
||||
"osc1_modRouting0": 0,
|
||||
"osc1_modRouting1": 2,
|
||||
"osc1_modRouting2": 0,
|
||||
"osc1_modDepth0": -0.750000,
|
||||
"osc1_modDepth1": 1104.000000,
|
||||
"osc1_modDepth2": 4.000000,
|
||||
"ampEnv_attack": 0.001000,
|
||||
"ampEnv_decay": 0.100000,
|
||||
"ampEnv_sustain": 0.000000,
|
||||
"ampEnv_release": 0.001000,
|
||||
"modEnv_attack": 0.001000,
|
||||
"modEnv_decay": 0.288601,
|
||||
"modEnv_sustain": 0.000000,
|
||||
"modEnv_release": 2.000000,
|
||||
"lfo0_rate": 8.796101,
|
||||
"lfo0_waveform": 0,
|
||||
"lfo0_active": 0,
|
||||
"lfo0_mode": 0,
|
||||
"lfo1_rate": 1.000000,
|
||||
"lfo1_waveform": 0,
|
||||
"lfo1_active": 0,
|
||||
"lfo1_mode": 0,
|
||||
"filter_cutoff": 389.089539,
|
||||
"filter_resonance": 0.164624,
|
||||
"filter_type": 0,
|
||||
"filter_active": 1,
|
||||
"filter_modRouting": 0,
|
||||
"filter_modDepth": 11600.000000,
|
||||
"filter_resModRouting": 0,
|
||||
"filter_resModDepth": 0.000000,
|
||||
"voice_levelSource": 2,
|
||||
"voice_gate": 1,
|
||||
"master_volume": 1.000000,
|
||||
"master_pitchBendRange": 2.000000
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"osc0_waveform": 1,
|
||||
"osc0_dutyCycle": 0.500000,
|
||||
"osc0_detune": 0.000000,
|
||||
"osc0_gain": 4.000000,
|
||||
"osc0_active": 1,
|
||||
"osc0_octave": 2,
|
||||
"osc0_modRouting0": 0,
|
||||
"osc0_modRouting1": 3,
|
||||
"osc0_modRouting2": 1,
|
||||
"osc0_modDepth0": 0.000000,
|
||||
"osc0_modDepth1": -420.000000,
|
||||
"osc0_modDepth2": 4.000000,
|
||||
"osc1_waveform": 1,
|
||||
"osc1_dutyCycle": 0.500000,
|
||||
"osc1_detune": 0.000000,
|
||||
"osc1_gain": 4.000000,
|
||||
"osc1_active": 1,
|
||||
"osc1_octave": 0,
|
||||
"osc1_modRouting0": 0,
|
||||
"osc1_modRouting1": 3,
|
||||
"osc1_modRouting2": 1,
|
||||
"osc1_modDepth0": 0.000000,
|
||||
"osc1_modDepth1": -420.000000,
|
||||
"osc1_modDepth2": 4.000000,
|
||||
"ampEnv_attack": 0.001000,
|
||||
"ampEnv_decay": 0.577635,
|
||||
"ampEnv_sustain": 1.000000,
|
||||
"ampEnv_release": 0.065071,
|
||||
"modEnv_attack": 0.005000,
|
||||
"modEnv_decay": 0.500000,
|
||||
"modEnv_sustain": 0.000000,
|
||||
"modEnv_release": 0.100000,
|
||||
"lfo0_rate": 3.398800,
|
||||
"lfo0_waveform": 2,
|
||||
"lfo0_active": 1,
|
||||
"lfo0_mode": 1,
|
||||
"lfo1_rate": 1.000000,
|
||||
"lfo1_waveform": 0,
|
||||
"lfo1_active": 0,
|
||||
"lfo1_mode": 0,
|
||||
"filter_cutoff": 8000.000000,
|
||||
"filter_resonance": 0.000000,
|
||||
"filter_type": 0,
|
||||
"filter_active": 0,
|
||||
"filter_modRouting": 0,
|
||||
"filter_modDepth": 0.000000,
|
||||
"filter_resModRouting": 0,
|
||||
"filter_resModDepth": 0.000000,
|
||||
"voice_levelSource": 1,
|
||||
"voice_gate": 1,
|
||||
"master_volume": 1.000000,
|
||||
"master_pitchBendRange": 2.000000
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"osc0_waveform": 5,
|
||||
"osc0_dutyCycle": 0.500000,
|
||||
"osc0_detune": 0.000000,
|
||||
"osc0_gain": 4.000000,
|
||||
"osc0_active": 1,
|
||||
"osc0_octave": 0,
|
||||
"osc0_modRouting0": 0,
|
||||
"osc0_modRouting1": 0,
|
||||
"osc0_modRouting2": 0,
|
||||
"osc0_modDepth0": 0.000000,
|
||||
"osc0_modDepth1": 0.000000,
|
||||
"osc0_modDepth2": 0.000000,
|
||||
"osc1_waveform": 0,
|
||||
"osc1_dutyCycle": 0.500000,
|
||||
"osc1_detune": -29.360291,
|
||||
"osc1_gain": 2.868294,
|
||||
"osc1_active": 1,
|
||||
"osc1_octave": -1,
|
||||
"osc1_modRouting0": 0,
|
||||
"osc1_modRouting1": 3,
|
||||
"osc1_modRouting2": 0,
|
||||
"osc1_modDepth0": 0.000000,
|
||||
"osc1_modDepth1": 307.482422,
|
||||
"osc1_modDepth2": 0.000000,
|
||||
"ampEnv_attack": 0.076245,
|
||||
"ampEnv_decay": 0.182086,
|
||||
"ampEnv_sustain": 1.000000,
|
||||
"ampEnv_release": 0.001000,
|
||||
"modEnv_attack": 0.001000,
|
||||
"modEnv_decay": 0.213054,
|
||||
"modEnv_sustain": 0.000000,
|
||||
"modEnv_release": 0.001000,
|
||||
"lfo0_rate": 19.253021,
|
||||
"lfo0_waveform": 0,
|
||||
"lfo0_active": 1,
|
||||
"lfo0_mode": 1,
|
||||
"lfo1_rate": 1.000000,
|
||||
"lfo1_waveform": 0,
|
||||
"lfo1_active": 0,
|
||||
"lfo1_mode": 0,
|
||||
"filter_cutoff": 2081.465332,
|
||||
"filter_resonance": 0.000000,
|
||||
"filter_type": 0,
|
||||
"filter_active": 1,
|
||||
"filter_modRouting": 2,
|
||||
"filter_modDepth": -12846.765625,
|
||||
"filter_resModRouting": 0,
|
||||
"filter_resModDepth": 0.000000,
|
||||
"voice_levelSource": 1,
|
||||
"voice_gate": 0,
|
||||
"master_volume": 1.000000,
|
||||
"master_pitchBendRange": 2.000000
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"osc0_waveform": 5,
|
||||
"osc0_dutyCycle": 0.500000,
|
||||
"osc0_detune": 0.000000,
|
||||
"osc0_gain": 4.000000,
|
||||
"osc0_active": 1,
|
||||
"osc0_octave": 2,
|
||||
"osc0_modRouting0": 0,
|
||||
"osc0_modRouting1": 2,
|
||||
"osc0_modRouting2": 0,
|
||||
"osc0_modDepth0": 0.000000,
|
||||
"osc0_modDepth1": 180.000000,
|
||||
"osc0_modDepth2": 4.000000,
|
||||
"osc1_waveform": 5,
|
||||
"osc1_dutyCycle": 0.545000,
|
||||
"osc1_detune": -15.000000,
|
||||
"osc1_gain": 4.000000,
|
||||
"osc1_active": 1,
|
||||
"osc1_octave": 1,
|
||||
"osc1_modRouting0": 0,
|
||||
"osc1_modRouting1": 2,
|
||||
"osc1_modRouting2": 0,
|
||||
"osc1_modDepth0": -0.750000,
|
||||
"osc1_modDepth1": 1104.000000,
|
||||
"osc1_modDepth2": 4.000000,
|
||||
"ampEnv_attack": 2.000000,
|
||||
"ampEnv_decay": 0.100000,
|
||||
"ampEnv_sustain": 0.000000,
|
||||
"ampEnv_release": 0.001000,
|
||||
"modEnv_attack": 0.001000,
|
||||
"modEnv_decay": 0.247558,
|
||||
"modEnv_sustain": 0.000000,
|
||||
"modEnv_release": 2.000000,
|
||||
"lfo0_rate": 8.796101,
|
||||
"lfo0_waveform": 0,
|
||||
"lfo0_active": 0,
|
||||
"lfo0_mode": 0,
|
||||
"lfo1_rate": 1.000000,
|
||||
"lfo1_waveform": 0,
|
||||
"lfo1_active": 0,
|
||||
"lfo1_mode": 0,
|
||||
"filter_cutoff": 1242.353027,
|
||||
"filter_resonance": 0.164624,
|
||||
"filter_type": 0,
|
||||
"filter_active": 1,
|
||||
"filter_modRouting": 0,
|
||||
"filter_modDepth": 11600.000000,
|
||||
"filter_resModRouting": 0,
|
||||
"filter_resModDepth": 0.000000,
|
||||
"voice_levelSource": 2,
|
||||
"voice_gate": 1,
|
||||
"master_volume": 1.000000,
|
||||
"master_pitchBendRange": 2.000000
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
; AlarmPatch, converted from Programs/Sounds/LowFuel.json by SoundPatch.
|
||||
; Designed in soundThing, where it can be heard. Do not edit the numbers
|
||||
; here: change the patch and convert it again.
|
||||
|
||||
#Data
|
||||
|
||||
AlarmPatch:
|
||||
0d50 ; how many pairs follow
|
||||
0x00 0d1 ; oscillator 0, waveform
|
||||
0x01 0d255 ; gain
|
||||
0x02 0d128 ; duty
|
||||
0x03 0d128 ; detune, in cents
|
||||
0x04 0d130 ; octave
|
||||
0x05 0d1 ; on
|
||||
0x06 0d0 ; what moves its width
|
||||
0x07 0d128 ; and how far
|
||||
0x08 0d3 ; what moves its pitch
|
||||
0x09 0d83 ; and how far
|
||||
0x0A 0d1 ; what moves its gain
|
||||
0x0B 0d255 ; and how far
|
||||
0x10 0d1 ; oscillator 1, waveform
|
||||
0x11 0d255 ; gain
|
||||
0x12 0d128 ; duty
|
||||
0x13 0d128 ; detune, in cents
|
||||
0x14 0d128 ; octave
|
||||
0x15 0d1 ; on
|
||||
0x16 0d0 ; what moves its width
|
||||
0x17 0d128 ; and how far
|
||||
0x18 0d3 ; what moves its pitch
|
||||
0x19 0d83 ; and how far
|
||||
0x1A 0d1 ; what moves its gain
|
||||
0x1B 0d255 ; and how far
|
||||
0x20 0d4 ; amplitude envelope, attack
|
||||
0x21 0d97 ; decay
|
||||
0x22 0d255 ; sustain
|
||||
0x23 0d33 ; release
|
||||
0x30 0d9 ; modulation envelope, attack
|
||||
0x31 0d90 ; decay
|
||||
0x32 0d0 ; sustain
|
||||
0x33 0d40 ; release
|
||||
0x40 0d0 ; filter, on
|
||||
0x41 0d0 ; type: 0 low, 1 high, 2 band
|
||||
0x42 0d221 ; cutoff, in hertz
|
||||
0x43 0d0 ; resonance
|
||||
0x44 0d0 ; what moves the cutoff
|
||||
0x45 0d128 ; and how far, in hertz
|
||||
0x46 0d0 ; what moves the resonance
|
||||
0x47 0d128 ; and how far
|
||||
0x60 0d1 ; LFO 0, on
|
||||
0x61 0d2 ; waveform
|
||||
0x62 0d180 ; rate, in hertz
|
||||
0x63 0d1 ; 0 free, 1 starts with a voice
|
||||
0x70 0d0 ; LFO 1, on
|
||||
0x71 0d0 ; waveform
|
||||
0x72 0d127 ; rate, in hertz
|
||||
0x73 0d0 ; 0 free, 1 starts with a voice
|
||||
0x50 0d1 ; what shapes the level
|
||||
0x51 0d1 ; 0 gated, 1 triggered
|
||||
@@ -0,0 +1,58 @@
|
||||
; BoomPatch, converted from Programs/Sounds/Crash.json by SoundPatch.
|
||||
; Designed in soundThing, where it can be heard. Do not edit the numbers
|
||||
; here: change the patch and convert it again.
|
||||
|
||||
#Data
|
||||
|
||||
BoomPatch:
|
||||
0d50 ; how many pairs follow
|
||||
0x00 0d5 ; oscillator 0, waveform
|
||||
0x01 0d255 ; gain
|
||||
0x02 0d128 ; duty
|
||||
0x03 0d128 ; detune, in cents
|
||||
0x04 0d130 ; octave
|
||||
0x05 0d1 ; on
|
||||
0x06 0d0 ; what moves its width
|
||||
0x07 0d128 ; and how far
|
||||
0x08 0d2 ; what moves its pitch
|
||||
0x09 0d147 ; and how far
|
||||
0x0A 0d0 ; what moves its gain
|
||||
0x0B 0d255 ; and how far
|
||||
0x10 0d5 ; oscillator 1, waveform
|
||||
0x11 0d255 ; gain
|
||||
0x12 0d140 ; duty
|
||||
0x13 0d126 ; detune, in cents
|
||||
0x14 0d129 ; octave
|
||||
0x15 0d1 ; on
|
||||
0x16 0d0 ; what moves its width
|
||||
0x17 0d0 ; and how far
|
||||
0x18 0d2 ; what moves its pitch
|
||||
0x19 0d246 ; and how far
|
||||
0x1A 0d0 ; what moves its gain
|
||||
0x1B 0d255 ; and how far
|
||||
0x20 0d180 ; amplitude envelope, attack
|
||||
0x21 0d40 ; decay
|
||||
0x22 0d0 ; sustain
|
||||
0x23 0d4 ; release
|
||||
0x30 0d4 ; modulation envelope, attack
|
||||
0x31 0d99 ; decay
|
||||
0x32 0d0 ; sustain
|
||||
0x33 0d180 ; release
|
||||
0x40 0d1 ; filter, on
|
||||
0x41 0d0 ; type: 0 low, 1 high, 2 band
|
||||
0x42 0d240 ; cutoff, in hertz
|
||||
0x43 0d148 ; resonance
|
||||
0x44 0d2 ; what moves the cutoff
|
||||
0x45 0d255 ; and how far, in hertz
|
||||
0x46 0d0 ; what moves the resonance
|
||||
0x47 0d128 ; and how far
|
||||
0x60 0d0 ; LFO 0, on
|
||||
0x61 0d0 ; waveform
|
||||
0x62 0d220 ; rate, in hertz
|
||||
0x63 0d0 ; 0 free, 1 starts with a voice
|
||||
0x70 0d0 ; LFO 1, on
|
||||
0x71 0d0 ; waveform
|
||||
0x72 0d127 ; rate, in hertz
|
||||
0x73 0d0 ; 0 free, 1 starts with a voice
|
||||
0x50 0d2 ; what shapes the level
|
||||
0x51 0d1 ; 0 gated, 1 triggered
|
||||
@@ -0,0 +1,58 @@
|
||||
; LatchPatch, converted from Programs/Sounds/DockLatch.json by SoundPatch.
|
||||
; Designed in soundThing, where it can be heard. Do not edit the numbers
|
||||
; here: change the patch and convert it again.
|
||||
|
||||
#Data
|
||||
|
||||
LatchPatch:
|
||||
0d50 ; how many pairs follow
|
||||
0x00 0d5 ; oscillator 0, waveform
|
||||
0x01 0d255 ; gain
|
||||
0x02 0d128 ; duty
|
||||
0x03 0d128 ; detune, in cents
|
||||
0x04 0d130 ; octave
|
||||
0x05 0d1 ; on
|
||||
0x06 0d0 ; what moves its width
|
||||
0x07 0d128 ; and how far
|
||||
0x08 0d2 ; what moves its pitch
|
||||
0x09 0d147 ; and how far
|
||||
0x0A 0d0 ; what moves its gain
|
||||
0x0B 0d255 ; and how far
|
||||
0x10 0d5 ; oscillator 1, waveform
|
||||
0x11 0d255 ; gain
|
||||
0x12 0d140 ; duty
|
||||
0x13 0d126 ; detune, in cents
|
||||
0x14 0d129 ; octave
|
||||
0x15 0d1 ; on
|
||||
0x16 0d0 ; what moves its width
|
||||
0x17 0d0 ; and how far
|
||||
0x18 0d2 ; what moves its pitch
|
||||
0x19 0d246 ; and how far
|
||||
0x1A 0d0 ; what moves its gain
|
||||
0x1B 0d255 ; and how far
|
||||
0x20 0d4 ; amplitude envelope, attack
|
||||
0x21 0d40 ; decay
|
||||
0x22 0d0 ; sustain
|
||||
0x23 0d4 ; release
|
||||
0x30 0d4 ; modulation envelope, attack
|
||||
0x31 0d68 ; decay
|
||||
0x32 0d0 ; sustain
|
||||
0x33 0d180 ; release
|
||||
0x40 0d1 ; filter, on
|
||||
0x41 0d0 ; type: 0 low, 1 high, 2 band
|
||||
0x42 0d110 ; cutoff, in hertz
|
||||
0x43 0d42 ; resonance
|
||||
0x44 0d0 ; what moves the cutoff
|
||||
0x45 0d255 ; and how far, in hertz
|
||||
0x46 0d0 ; what moves the resonance
|
||||
0x47 0d128 ; and how far
|
||||
0x60 0d0 ; LFO 0, on
|
||||
0x61 0d0 ; waveform
|
||||
0x62 0d220 ; rate, in hertz
|
||||
0x63 0d0 ; 0 free, 1 starts with a voice
|
||||
0x70 0d0 ; LFO 1, on
|
||||
0x71 0d0 ; waveform
|
||||
0x72 0d127 ; rate, in hertz
|
||||
0x73 0d0 ; 0 free, 1 starts with a voice
|
||||
0x50 0d2 ; what shapes the level
|
||||
0x51 0d1 ; 0 gated, 1 triggered
|
||||
@@ -0,0 +1,58 @@
|
||||
; ThrustHoldPatch, converted from Programs/Sounds/ThrustHold.json by SoundPatch.
|
||||
; Designed in soundThing, where it can be heard. Do not edit the numbers
|
||||
; here: change the patch and convert it again.
|
||||
|
||||
#Data
|
||||
|
||||
ThrustHoldPatch:
|
||||
0d50 ; how many pairs follow
|
||||
0x00 0d5 ; oscillator 0, waveform
|
||||
0x01 0d255 ; gain
|
||||
0x02 0d128 ; duty
|
||||
0x03 0d128 ; detune, in cents
|
||||
0x04 0d128 ; octave
|
||||
0x05 0d1 ; on
|
||||
0x06 0d0 ; what moves its width
|
||||
0x07 0d128 ; and how far
|
||||
0x08 0d0 ; what moves its pitch
|
||||
0x09 0d128 ; and how far
|
||||
0x0A 0d0 ; what moves its gain
|
||||
0x0B 0d128 ; and how far
|
||||
0x10 0d0 ; oscillator 1, waveform
|
||||
0x11 0d183 ; gain
|
||||
0x12 0d128 ; duty
|
||||
0x13 0d125 ; detune, in cents
|
||||
0x14 0d127 ; octave
|
||||
0x15 0d1 ; on
|
||||
0x16 0d0 ; what moves its width
|
||||
0x17 0d128 ; and how far
|
||||
0x18 0d3 ; what moves its pitch
|
||||
0x19 0d161 ; and how far
|
||||
0x1A 0d0 ; what moves its gain
|
||||
0x1B 0d128 ; and how far
|
||||
0x20 0d35 ; amplitude envelope, attack
|
||||
0x21 0d54 ; decay
|
||||
0x22 0d255 ; sustain
|
||||
0x23 0d4 ; release
|
||||
0x30 0d4 ; modulation envelope, attack
|
||||
0x31 0d59 ; decay
|
||||
0x32 0d0 ; sustain
|
||||
0x33 0d4 ; release
|
||||
0x40 0d1 ; filter, on
|
||||
0x41 0d0 ; type: 0 low, 1 high, 2 band
|
||||
0x42 0d171 ; cutoff, in hertz
|
||||
0x43 0d0 ; resonance
|
||||
0x44 0d2 ; what moves the cutoff
|
||||
0x45 0d0 ; and how far, in hertz
|
||||
0x46 0d0 ; what moves the resonance
|
||||
0x47 0d128 ; and how far
|
||||
0x60 0d1 ; LFO 0, on
|
||||
0x61 0d0 ; waveform
|
||||
0x62 0d253 ; rate, in hertz
|
||||
0x63 0d1 ; 0 free, 1 starts with a voice
|
||||
0x70 0d0 ; LFO 1, on
|
||||
0x71 0d0 ; waveform
|
||||
0x72 0d127 ; rate, in hertz
|
||||
0x73 0d0 ; 0 free, 1 starts with a voice
|
||||
0x50 0d1 ; what shapes the level
|
||||
0x51 0d0 ; 0 gated, 1 triggered
|
||||
@@ -0,0 +1,58 @@
|
||||
; ThrustOnPatch, converted from Programs/Sounds/ThrustOn.json by SoundPatch.
|
||||
; Designed in soundThing, where it can be heard. Do not edit the numbers
|
||||
; here: change the patch and convert it again.
|
||||
|
||||
#Data
|
||||
|
||||
ThrustOnPatch:
|
||||
0d50 ; how many pairs follow
|
||||
0x00 0d5 ; oscillator 0, waveform
|
||||
0x01 0d255 ; gain
|
||||
0x02 0d128 ; duty
|
||||
0x03 0d128 ; detune, in cents
|
||||
0x04 0d130 ; octave
|
||||
0x05 0d1 ; on
|
||||
0x06 0d0 ; what moves its width
|
||||
0x07 0d128 ; and how far
|
||||
0x08 0d2 ; what moves its pitch
|
||||
0x09 0d147 ; and how far
|
||||
0x0A 0d0 ; what moves its gain
|
||||
0x0B 0d255 ; and how far
|
||||
0x10 0d5 ; oscillator 1, waveform
|
||||
0x11 0d255 ; gain
|
||||
0x12 0d140 ; duty
|
||||
0x13 0d126 ; detune, in cents
|
||||
0x14 0d129 ; octave
|
||||
0x15 0d1 ; on
|
||||
0x16 0d0 ; what moves its width
|
||||
0x17 0d0 ; and how far
|
||||
0x18 0d2 ; what moves its pitch
|
||||
0x19 0d246 ; and how far
|
||||
0x1A 0d0 ; what moves its gain
|
||||
0x1B 0d255 ; and how far
|
||||
0x20 0d180 ; amplitude envelope, attack
|
||||
0x21 0d40 ; decay
|
||||
0x22 0d0 ; sustain
|
||||
0x23 0d4 ; release
|
||||
0x30 0d4 ; modulation envelope, attack
|
||||
0x31 0d63 ; decay
|
||||
0x32 0d0 ; sustain
|
||||
0x33 0d180 ; release
|
||||
0x40 0d1 ; filter, on
|
||||
0x41 0d0 ; type: 0 low, 1 high, 2 band
|
||||
0x42 0d152 ; cutoff, in hertz
|
||||
0x43 0d42 ; resonance
|
||||
0x44 0d0 ; what moves the cutoff
|
||||
0x45 0d255 ; and how far, in hertz
|
||||
0x46 0d0 ; what moves the resonance
|
||||
0x47 0d128 ; and how far
|
||||
0x60 0d0 ; LFO 0, on
|
||||
0x61 0d0 ; waveform
|
||||
0x62 0d220 ; rate, in hertz
|
||||
0x63 0d0 ; 0 free, 1 starts with a voice
|
||||
0x70 0d0 ; LFO 1, on
|
||||
0x71 0d0 ; waveform
|
||||
0x72 0d127 ; rate, in hertz
|
||||
0x73 0d0 ; 0 free, 1 starts with a voice
|
||||
0x50 0d2 ; what shapes the level
|
||||
0x51 0d1 ; 0 gated, 1 triggered
|
||||
@@ -0,0 +1,83 @@
|
||||
; The quarter square multiply, against numbers whose products are known.
|
||||
;
|
||||
; a * b = qs[a+b] - qs[|a-b|], and the table of quarter squares is built by adding, so nothing
|
||||
; in the whole arrangement needs a multiply to exist. What this checks is that the arrangement
|
||||
; actually multiplies: the cases below cover nought, one, the commutation, a square, and the
|
||||
; largest product two bytes can hold.
|
||||
;
|
||||
; Written by Anachronaut
|
||||
|
||||
#Include print.asm
|
||||
#Include math.asm
|
||||
|
||||
#Program
|
||||
|
||||
start:
|
||||
CALL mulReady ; The table, once, before anything asks for a product.
|
||||
|
||||
RSTA
|
||||
CALL times ; 0 x 0
|
||||
INIA 0d7
|
||||
SETD.0 MulA
|
||||
STA.0
|
||||
RSTA
|
||||
SETD.0 MulB
|
||||
STA.0
|
||||
CALL show ; 7 x 0, which is the other way round from the last one.
|
||||
|
||||
INIA 0d1
|
||||
CALL both
|
||||
CALL show ; 1 x 1
|
||||
|
||||
INIA 0d12
|
||||
CALL both
|
||||
CALL show ; 12 x 12, a square, which is the case the identity leans on:
|
||||
; the difference term is nought and the whole answer is one entry.
|
||||
|
||||
INIA 0d3
|
||||
SETD.0 MulA
|
||||
STA.0
|
||||
INIA 0d200
|
||||
SETD.0 MulB
|
||||
STA.0
|
||||
CALL show ; 3 x 200
|
||||
|
||||
INIA 0d200
|
||||
SETD.0 MulA
|
||||
STA.0
|
||||
INIA 0d3
|
||||
SETD.0 MulB
|
||||
STA.0
|
||||
CALL show ; and 200 x 3, which had better agree.
|
||||
|
||||
INIA 0xFF
|
||||
CALL both
|
||||
CALL show ; 255 x 255, the largest a byte times a byte can be.
|
||||
|
||||
HALT
|
||||
|
||||
; A in both operands, for the square cases.
|
||||
both:
|
||||
SETD.0 MulA
|
||||
STA.0
|
||||
SETD.0 MulB
|
||||
STA.0
|
||||
RET
|
||||
|
||||
; A in both, then show it. The nought case wants this and nothing else does.
|
||||
times:
|
||||
CALL both
|
||||
CALL show
|
||||
RET
|
||||
|
||||
; The product, high byte then low, which is how a sixteen bit number reads.
|
||||
show:
|
||||
CALL mul8
|
||||
SETD.0 MulHigh
|
||||
LDA.0
|
||||
CALL printByteHex
|
||||
SETD.0 MulLow
|
||||
LDA.0
|
||||
CALL printByteHex
|
||||
CALL lineFeed
|
||||
RET
|
||||
@@ -0,0 +1,51 @@
|
||||
; A device raising its line with nothing installed to answer it, CAUGHT.
|
||||
;
|
||||
; The other half of the fault that could not be handed over. A program that asks a device to
|
||||
; interrupt it and then forgets the handler used to stop the machine dead; now it goes to a
|
||||
; vector of its own with the port in Q, so the program can say which device it was that
|
||||
; nobody was listening to.
|
||||
;
|
||||
; NOTHING IS TYPED AT THIS. The console raises its line once at the end of input as well as
|
||||
; for an arriving byte - which is exactly so that a program driven by interrupts is told when
|
||||
; nothing more is coming - and the test is run with no input at all, so that end is what
|
||||
; arrives.
|
||||
;
|
||||
; Correct output is:
|
||||
; nobody listening on port 00
|
||||
; and a clean halt.
|
||||
|
||||
#Include console.asm
|
||||
|
||||
#Program
|
||||
|
||||
start:
|
||||
INIA 0x02 ; Interrupt when the console has something to say.
|
||||
OUTA 0x02
|
||||
SIF
|
||||
spin:
|
||||
; Never touches the console. Whatever happens below is something that interrupted this.
|
||||
BRI spin
|
||||
|
||||
; Q holds the port whose entry was empty. The console is port zero.
|
||||
nobody:
|
||||
SETD.0 NobodyText
|
||||
CALL printString
|
||||
MVQA
|
||||
CALL printByteHex
|
||||
CALL newLine
|
||||
|
||||
; The console back to how it was found, so nothing else is asked for, and stop. There is
|
||||
; nothing to resume to: the loop above only exists to be interrupted.
|
||||
RSTA
|
||||
OUTA 0x02
|
||||
HALT
|
||||
|
||||
#Data
|
||||
|
||||
NobodyText:
|
||||
"nobody listening on port "
|
||||
|
||||
#Vectors
|
||||
|
||||
Boot start
|
||||
NoDevice nobody
|
||||
@@ -0,0 +1,56 @@
|
||||
; A software interrupt that names a vector with nothing in it, CAUGHT.
|
||||
;
|
||||
; That fault used to be the one the machine had no way of handing over, because the thing
|
||||
; that would hand it over is the thing that has just found nothing to hand it to. It stopped
|
||||
; the machine, and no program could do anything about it - which made calling a service the
|
||||
; system does not implement fatal, and that is an ordinary mistake to make.
|
||||
;
|
||||
; It goes to a vector of its own now, with the number of the empty entry in Q. This catches
|
||||
; it, says which one it was, steps over the SWI and its operand, and carries on - so the
|
||||
; output says both that the fault arrived and that a program can survive it.
|
||||
;
|
||||
; Correct output is:
|
||||
; empty vector 28
|
||||
; carried on
|
||||
; and a clean halt.
|
||||
|
||||
#Include console.asm
|
||||
|
||||
#Program
|
||||
|
||||
start:
|
||||
SWI 0d40 ; Forty is 0x28, and nothing is installed there.
|
||||
|
||||
SETD.0 Carried
|
||||
CALL printString
|
||||
CALL newLine
|
||||
HALT
|
||||
|
||||
; Entered because there was nowhere to go. Q holds which entry was empty, and it is the only
|
||||
; thing on this machine a handler is handed in a register.
|
||||
missing:
|
||||
SETD.0 EmptyText
|
||||
CALL printString
|
||||
MVQA
|
||||
CALL printByteHex
|
||||
CALL newLine
|
||||
|
||||
; ---- Carrying on past it ----
|
||||
;
|
||||
; The frame holds the address after the SWI and the byte naming its vector, so a bare RETI
|
||||
; already lands past the instruction that faulted. That is not true of every fault here -
|
||||
; a refused port or a byte that does not decode both resume ON the thing that failed - so
|
||||
; it is worth saying out loud which kind this one is.
|
||||
RETI
|
||||
|
||||
#Data
|
||||
|
||||
EmptyText:
|
||||
"empty vector "
|
||||
Carried:
|
||||
"carried on"
|
||||
|
||||
#Vectors
|
||||
|
||||
Boot start
|
||||
NoHandler missing
|
||||
@@ -0,0 +1,51 @@
|
||||
; Asks the machine what its controllers are doing.
|
||||
;
|
||||
; A pad reports a LEVEL and not an event: one read gives every button at once, holding is the
|
||||
; natural thing to say, and reading does not consume anything - so asking twice in a frame
|
||||
; gives the same answer twice, which this checks by doing exactly that.
|
||||
;
|
||||
; The recording behind it is one byte a frame. It presses up, holds it a second frame, adds
|
||||
; right, lets go of up, lets go of everything, then presses A and B together - which is the
|
||||
; case a console's key-at-a-time stream cannot express at all.
|
||||
;
|
||||
; Written by Anachronaut
|
||||
|
||||
#Program
|
||||
|
||||
start:
|
||||
INA 0x64
|
||||
OUTA 0x00 ; Which pads are there: one bit each, so pad nought alone is 1.
|
||||
RSTA
|
||||
SETD.0 Count
|
||||
STA.0
|
||||
|
||||
everyFrame:
|
||||
INA 0x30
|
||||
INIB 0x01
|
||||
AND
|
||||
BRQ everyFrame ; The frame the recording steps on.
|
||||
|
||||
INA 0x60
|
||||
OUTA 0x00
|
||||
; And again, without a frame in between. A level does not go away when it is looked at.
|
||||
INA 0x60
|
||||
OUTA 0x00
|
||||
|
||||
SETD.0 Count
|
||||
LDA.0
|
||||
INCA
|
||||
STA.0
|
||||
INIB 0d6
|
||||
CCF
|
||||
SUB
|
||||
BNQ everyFrame
|
||||
|
||||
; A pad that is not there reads as nothing held, which is honest rather than an error.
|
||||
INA 0x63
|
||||
OUTA 0x00
|
||||
HALT
|
||||
|
||||
#Data
|
||||
|
||||
Count:
|
||||
0x00
|
||||
@@ -24,8 +24,10 @@ wrote Asm.sbx: program 7533, data 4099, labels 555
|
||||
| [`Source/Assembler`](Source/Assembler) | The assembler that runs on a host |
|
||||
| [`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/Patch`](Source/Patch) | SoundPatch, which turns a soundThing patch into a table the sound device takes |
|
||||
| [`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/Sounds`](Programs/Sounds) | Patches as soundThing saved them, and the tables SoundPatch made from them, both checked in so the build never needs soundThing |
|
||||
| [`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/testPrograms`](Programs/testPrograms) | What the test suite drives |
|
||||
@@ -114,7 +116,7 @@ Manual.
|
||||
|
||||
## Getting Started:
|
||||
|
||||
Clone it and build the four tools. You need gcc and make, or similar:
|
||||
Clone it and build the five tools. You need gcc and make, or similar:
|
||||
|
||||
```
|
||||
git clone https://github.com/RealBusinessAccount/SplitBit-Emulator.git
|
||||
@@ -146,6 +148,11 @@ Both put a second disk in drive 1, at `Disks/personal.img`. It is made once and
|
||||
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.
|
||||
|
||||
Starting the machine copies it first, three starts back, as `personal.img.1` and so on. Not
|
||||
one copy: the way a disk is lost is that something goes wrong and the very next thing anybody
|
||||
does is start the machine again to see how bad it is, which is exactly when a single backup
|
||||
would be overwritten by the wreckage.
|
||||
|
||||
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
|
||||
@@ -299,6 +306,33 @@ this is what turns such a count into a list of routine names.
|
||||
|
||||
Without `-o` the output takes the source file's name, in the directory you called the assembler from, with the extension the format asks for: `.bin` for a boot image and `.sbx` for a loadable program. Included files are looked for beside the file that includes them, and then along the directories given with `-I`.
|
||||
|
||||
## Making A Patch: SoundPatch
|
||||
|
||||
```
|
||||
./SoundPatch <patch.json> <label> [output.asm]
|
||||
```
|
||||
|
||||
The sound device is soundThing's voice engine with the editor taken off, so a patch designed
|
||||
in soundThing makes the same sound here. What differs is how it arrives: soundThing writes
|
||||
seconds and hertz, and the device takes bytes through a selector. SoundPatch converts one into
|
||||
the other and writes a table a program can hand to the device.
|
||||
|
||||
A byte is not a number of seconds, and that is the whole reason this exists. Times are squared
|
||||
into four seconds, because the difference between five and fifty milliseconds is the character
|
||||
of a percussive sound and the difference between three and four seconds is nothing anybody can
|
||||
hear. Cutoff and LFO rate are exponential, because hearing is logarithmic. Depths and detune
|
||||
are centred on 128, so half of nothing is no change. The first sound written for a game here
|
||||
was guessed at directly in bytes: a cutoff of 40 looks small and is 57 Hz, and the bang came
|
||||
out as a low gurgle.
|
||||
|
||||
A field the tool does not recognise stops it rather than being skipped, because a patch format
|
||||
that has moved on would otherwise produce a table that quietly means something else.
|
||||
|
||||
**The build does not depend on it.** soundThing lives in its own repository and is not
|
||||
required to build anything here. `Programs/Sounds` holds both halves - the patch as soundThing
|
||||
saved it and the table made from it - so a sound can be regenerated, or read, without going
|
||||
and finding the editor first. The tool is for when a sound is being changed.
|
||||
|
||||
## Checking Assembly: SplitLint
|
||||
|
||||
```
|
||||
@@ -606,7 +640,7 @@ $(BUILD)/%.bin: %.asm
|
||||
make test
|
||||
```
|
||||
|
||||
Builds the four tools, checks they build clean under strict ISO C, and runs nine scripts.
|
||||
Builds the five 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
|
||||
@@ -625,7 +659,7 @@ correct.
|
||||
make sanitize
|
||||
```
|
||||
|
||||
Rebuilds all four tools with the address and undefined behaviour sanitizers and runs the
|
||||
Rebuilds all five 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.
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -66,7 +66,31 @@
|
||||
#define VECTOR_GUARD_VIOLATION 3
|
||||
// A bank was named that has nothing registered in it, or an access ran past its end.
|
||||
#define VECTOR_BANK_FAULT 4
|
||||
// Vectors 5 to 15 are held back for faults that do not exist yet, so that each cause
|
||||
|
||||
// ---- The two faults that used to be uncatchable ----
|
||||
//
|
||||
// A vector was dispatched through and had nothing in it. That is discovered by the very
|
||||
// thing that would dispatch, so for a long time it could only stop the machine: there was
|
||||
// no way to hand a program a fault about there being nowhere to hand it.
|
||||
//
|
||||
// These two are where it goes instead, and they are separate for the same reason every
|
||||
// other cause is - a missing software vector and a device nobody is listening to are
|
||||
// different mistakes with different fixes, and a handler should not have to work out which
|
||||
// it was.
|
||||
//
|
||||
// WHICH ENTRY WAS EMPTY ARRIVES IN Q, and that is the only thing on this machine a handler
|
||||
// is given in a register. It is not a cause register by the back door: the vector still
|
||||
// says what happened, and Q says which of the 256 entries it happened about, which is a
|
||||
// parameter rather than a cause. It costs no new state, because the frame already saved the
|
||||
// Q the interrupted program had and RETI puts it back.
|
||||
//
|
||||
// A vector THESE dispatch through and find empty stops the machine, and has to: a machine
|
||||
// that cannot report a fault about a missing handler by any route has run out of places to
|
||||
// go, and looping there would be worse than stopping.
|
||||
#define VECTOR_NO_HANDLER 5
|
||||
#define VECTOR_NO_DEVICE 6
|
||||
|
||||
// Vectors 7 to 15 are held back for faults that do not exist yet, so that each cause
|
||||
// can have an entry of its own rather than sharing one and needing a cause register to
|
||||
// tell them apart. Everything from 16 up belongs to programs, in two halves.
|
||||
//
|
||||
|
||||
@@ -226,6 +226,8 @@ static const struct {
|
||||
{ "BadOpcode", VECTOR_INVALID_OPCODE },
|
||||
{ "GuardViolation", VECTOR_GUARD_VIOLATION },
|
||||
{ "BankFault", VECTOR_BANK_FAULT },
|
||||
{ "NoHandler", VECTOR_NO_HANDLER },
|
||||
{ "NoDevice", VECTOR_NO_DEVICE },
|
||||
};
|
||||
static const int reservedVectorCount = (int)(sizeof(reservedVectors) / sizeof(reservedVectors[0]));
|
||||
|
||||
|
||||
@@ -15,7 +15,12 @@
|
||||
// share one namespace: a name may only be defined once across the whole assembly. So this
|
||||
// is not the size of one file but the size of a program and its libraries together, and
|
||||
// CosmOS with its four libraries went past 256 while still being a small system.
|
||||
#define MAX_LABELS 1024
|
||||
//
|
||||
// AND PAST 1024 the day the shell learned to finish a word somebody had started. Doubled
|
||||
// rather than nudged: a ceiling reached once is a ceiling that will be reached again, and
|
||||
// the table is pointers into source that is already in memory - 2048 of them is sixteen
|
||||
// kilobytes on a host with gigabytes of it.
|
||||
#define MAX_LABELS 2048
|
||||
#define MAX_VECTORS 256
|
||||
|
||||
typedef struct {
|
||||
|
||||
+30
-1
@@ -69,9 +69,31 @@ static uint16_t readVector(const uint8_t *programMemory, uint16_t base, uint8_t
|
||||
// Note that a zero entry means "no handler" to everything that dispatches, including the
|
||||
// two entries the CPU treats as start addresses when it reads them at reset. The
|
||||
// exemption belongs to that one read, not to the entries themselves.
|
||||
static uint8_t enterInterrupt(CPURegisters *cpu, uint16_t base, uint8_t index, uint16_t resumeAddress) {
|
||||
static uint8_t dispatch(CPURegisters *cpu, uint16_t base, uint8_t index,
|
||||
uint16_t resumeAddress, int mayEscalate) {
|
||||
uint16_t handler = readVector(cpu->Program, base, index);
|
||||
if (handler == 0x0000) {
|
||||
// ---- Nowhere to go is itself something to report ----
|
||||
//
|
||||
// This is the one fault the machine used to have no way of handing over, because
|
||||
// the thing that would hand it over is the thing that has just found nothing to
|
||||
// hand it to. So it goes to a vector of its own instead, with the number of the
|
||||
// empty entry in Q - and a missing software vector and a device nobody is
|
||||
// listening to are separate entries, because they are separate mistakes.
|
||||
//
|
||||
// NOT WHEN ALREADY ESCALATING. If the fault vector for this is itself empty then
|
||||
// the machine really has run out of places to go, and stopping is the only honest
|
||||
// answer left.
|
||||
if (mayEscalate) {
|
||||
const uint8_t escalation = (base == HARDWARE_VECTOR_BASE)
|
||||
? VECTOR_NO_DEVICE : VECTOR_NO_HANDLER;
|
||||
if (!dispatch(cpu, SOFTWARE_VECTOR_BASE, escalation, resumeAddress, 0)) {
|
||||
// After the frame, so the Q the interrupted program had is safely in it and
|
||||
// RETI will put it back. What the handler sees is which entry was empty.
|
||||
cpu->Q = index;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
cpu->Fault = FAULT_NO_HANDLER;
|
||||
cpu->FaultVector = index;
|
||||
cpu->Status |= STATUS_FAULT;
|
||||
@@ -109,6 +131,13 @@ static uint8_t enterInterrupt(CPURegisters *cpu, uint16_t base, uint8_t index, u
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Dispatching the ordinary way: through the vector asked for, and through the fault vector
|
||||
// for a missing one if that is what it turns out to be.
|
||||
static uint8_t enterInterrupt(CPURegisters *cpu, uint16_t base, uint8_t index,
|
||||
uint16_t resumeAddress) {
|
||||
return dispatch(cpu, base, index, resumeAddress, 1);
|
||||
}
|
||||
|
||||
// A device that refused what it was asked stops the machine where it stands, rather than
|
||||
// raising a line and letting execution carry on past the mistake. The frame carries the
|
||||
// address of the instruction that asked, so a handler can see which one it was, and so a
|
||||
|
||||
+53
-8
@@ -7,6 +7,7 @@
|
||||
#include "../Assembler/assembly.h" // For the fault vector numbers.
|
||||
#include "controller.h"
|
||||
#include "video.h"
|
||||
#include "pad.h"
|
||||
#include "sound.h"
|
||||
#include "font.h"
|
||||
#include <stdio.h>
|
||||
@@ -602,8 +603,25 @@ static int consoleKeyFromInput(int mayWait) {
|
||||
// Nothing to hand over: either that sequence meant nothing here, or it meant a key
|
||||
// and line mode does not deliver keys. Both are the same answer to a caller - there
|
||||
// is still no byte - so a blocking read asks again and a poll says so and leaves.
|
||||
if (got < 0 || (!consoleKeyMode && got >= CONSOLE_KEY_FIRST && got <= CONSOLE_KEY_LAST)) {
|
||||
const int undeliverable =
|
||||
!consoleKeyMode && got >= CONSOLE_KEY_FIRST && got <= CONSOLE_KEY_LAST;
|
||||
if (got < 0 || undeliverable) {
|
||||
if (!mayWait) {
|
||||
// ---- A LOOK MUST NOT CONSUME WHAT IT CANNOT REPORT ----
|
||||
//
|
||||
// This is the status port asking, and either way it has no byte to report.
|
||||
// But a key that line mode will not deliver is not the same as a key that is
|
||||
// gone: THE MODE CAN CHANGE. A program that polls and then asks for key mode
|
||||
// - which is exactly what the shell does before it reads a line - would find
|
||||
// that the first key it was reaching for had been swallowed by the looking.
|
||||
//
|
||||
// So it is held rather than dropped, and delivered as soon as something is
|
||||
// willing to take it. The blocking read below drops it instead, and must:
|
||||
// that read IS the delivery, line mode genuinely has no use for the key, and
|
||||
// a byte held there would be met again forever.
|
||||
if (undeliverable) {
|
||||
consoleHeldByte = got;
|
||||
}
|
||||
return CONSOLE_NOTHING_YET;
|
||||
}
|
||||
continue;
|
||||
@@ -1151,6 +1169,9 @@ void deviceTick(unsigned long now) {
|
||||
soundTick(now);
|
||||
// And the timer, which is the only beat a program can choose for itself.
|
||||
timerTick(now);
|
||||
// And the pads, whose recordings step on a frame so that a level read twice in one is
|
||||
// the same level both times.
|
||||
padTick(now);
|
||||
if (diskPending && now >= diskReadyAt) {
|
||||
diskSettle();
|
||||
}
|
||||
@@ -1287,11 +1308,12 @@ uint8_t *deviceMemory(uint8_t port, uint32_t *capacity) {
|
||||
*capacity = DISK_BLOCK_BYTES;
|
||||
return diskBuffer;
|
||||
}
|
||||
if (port == PORT_VIDEO) {
|
||||
// Tiles, the map and the palette, in one bank. A program blits the part that
|
||||
// changed and the rest stays as it was, which is the whole reason the screen is a
|
||||
// bank rather than a window onto a port.
|
||||
return videoMemory(capacity);
|
||||
if (port == PORT_VIDEO || port == VIDEO_SCREEN0 || port == VIDEO_SCREEN1) {
|
||||
// Two banks: the atlas of tiles and colours on the base port, and the map or the
|
||||
// bitmap on its own. A program blits the part that changed and the rest stays as it
|
||||
// was, which is the whole reason the screen is memory rather than a window onto a
|
||||
// port - and having two means a picture costs the map and not the font.
|
||||
return videoMemory(port, capacity);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -1323,6 +1345,7 @@ static const DeviceRecord deviceTable[] = {
|
||||
{ PORT_VIDEO, DEVICE_VIDEO, DEVICE_FLAG_HAS_MEMORY },
|
||||
{ PORT_SOUND, DEVICE_SOUND, 0 },
|
||||
{ PORT_TIMER, DEVICE_TIMER, 0 },
|
||||
{ PORT_PAD, DEVICE_PAD, 0 },
|
||||
{ PORT_REGISTRY, DEVICE_REGISTRY, 0 },
|
||||
};
|
||||
static const int deviceCount = (int)(sizeof(deviceTable) / sizeof(deviceTable[0]));
|
||||
@@ -1352,8 +1375,22 @@ static const DeviceRecord *deviceOnPort(uint8_t port) {
|
||||
return deviceOnPort(PORT_DISK);
|
||||
}
|
||||
if (port > PORT_VIDEO && port <= PORT_VIDEO_TOP) {
|
||||
// Sixteen ports, one device, and the same rule again.
|
||||
return deviceOnPort(PORT_VIDEO);
|
||||
// Sixteen ports, one device, and the same rule again - with one difference, because
|
||||
// this device owns TWO banks. Forwarding the whole block to the base record used to
|
||||
// say that all sixteen ports brought memory, which was harmless only while nobody
|
||||
// believed it: a program that enumerated the block and registered everything
|
||||
// claiming memory would have faulted on the fourteen that have none.
|
||||
//
|
||||
// So the block answers honestly. The screen port says it brings memory because it
|
||||
// does, and the rest of the block says it does not.
|
||||
static const DeviceRecord videoScreen0Record =
|
||||
{ VIDEO_SCREEN0, DEVICE_VIDEO, DEVICE_FLAG_HAS_MEMORY };
|
||||
static const DeviceRecord videoScreen1Record =
|
||||
{ VIDEO_SCREEN1, DEVICE_VIDEO, DEVICE_FLAG_HAS_MEMORY };
|
||||
static const DeviceRecord videoPlainRecord = { PORT_VIDEO, DEVICE_VIDEO, 0 };
|
||||
if (port == VIDEO_SCREEN0) { return &videoScreen0Record; }
|
||||
if (port == VIDEO_SCREEN1) { return &videoScreen1Record; }
|
||||
return &videoPlainRecord;
|
||||
}
|
||||
if (port > PORT_SOUND && port <= PORT_SOUND_TOP) {
|
||||
return deviceOnPort(PORT_SOUND);
|
||||
@@ -1361,6 +1398,11 @@ static const DeviceRecord *deviceOnPort(uint8_t port) {
|
||||
if (port > PORT_TIMER && port <= PORT_TIMER_TOP) {
|
||||
return deviceOnPort(PORT_TIMER);
|
||||
}
|
||||
// Sixteen ports, one device: four pads, a port saying which are there, and eleven kept
|
||||
// for the analogue axes that are not built.
|
||||
if (port > PORT_PAD && port <= PORT_PAD_TOP) {
|
||||
return deviceOnPort(PORT_PAD);
|
||||
}
|
||||
for (int i = 0; i < deviceCount; i++) {
|
||||
if (deviceTable[i].port == port) {
|
||||
return &deviceTable[i];
|
||||
@@ -1538,6 +1580,9 @@ uint8_t InputHandler(uint8_t Address) {
|
||||
if (Address >= PORT_SOUND && Address <= PORT_SOUND_TOP) {
|
||||
return soundRead(Address);
|
||||
}
|
||||
if (Address >= PORT_PAD && Address <= PORT_PAD_TOP) {
|
||||
return padRead(Address);
|
||||
}
|
||||
if (Address >= PORT_TIMER && Address <= PORT_TIMER_TOP) {
|
||||
return timerRead(Address);
|
||||
}
|
||||
|
||||
@@ -314,6 +314,9 @@ void consoleSetInputHook(int (*hook)(int mayWait));
|
||||
#define DEVICE_VIDEO 0x14
|
||||
#define DEVICE_SOUND 0x15
|
||||
#define DEVICE_TIMER 0x16
|
||||
// Game controllers. Polled, never interrupting: a game asks once a frame because that is when
|
||||
// it draws, and an interrupt for every button would be the event model a pad exists to avoid.
|
||||
#define DEVICE_PAD 0x17
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "cpu.h"
|
||||
#include "controller.h"
|
||||
#include "io.h"
|
||||
#include "pad.h"
|
||||
#include "video.h"
|
||||
#include "sound.h"
|
||||
#include "utility.h"
|
||||
@@ -134,6 +135,7 @@ static int machineRestart(Machine *m) {
|
||||
return 0;
|
||||
}
|
||||
videoReset();
|
||||
padReset();
|
||||
soundReset();
|
||||
timerReset();
|
||||
consoleHome();
|
||||
@@ -207,6 +209,7 @@ uint8_t machineStart(Machine *m, const EmulatorOptions *options, const char *pro
|
||||
// the device's, and a reset that left last program's screen up would be a reset that
|
||||
// did not happen.
|
||||
videoReset();
|
||||
padReset();
|
||||
soundReset();
|
||||
timerReset();
|
||||
if (options->sound != NULL) {
|
||||
@@ -220,6 +223,39 @@ uint8_t machineStart(Machine *m, const EmulatorOptions *options, const char *pro
|
||||
if (m->options.debug) {
|
||||
printRegisters(&m->cpu, Program, Data);
|
||||
}
|
||||
// ---- The pads, from files ----
|
||||
//
|
||||
// MACHINE_ERROR and not nought. Nought is MACHINE_OK here, so returning it after printing
|
||||
// an error said the machine had started - and the front end then ran a machine whose
|
||||
// clock had never been set up and divided by it. A missing file came out as a floating
|
||||
// point exception and a core dump.
|
||||
//
|
||||
// The trap is that machineRestart, thirty lines up, returns 1 for worked and 0 for did
|
||||
// not. Two functions in one file with opposite conventions, and this copied the nearer
|
||||
// one.
|
||||
//
|
||||
// Opened here beside the keyboard because they are the same kind of thing: a recording
|
||||
// standing in for a person, so that what a person would exercise is reachable from a
|
||||
// suite. They are never closed, for the same reason the keyboard is not - the machine
|
||||
// outlives the call and the host reclaims them when it stops.
|
||||
for (int n = 0; n < options->padCount; n++) {
|
||||
FILE *pad = fopen(options->pads[n], "rb");
|
||||
if (pad == NULL) {
|
||||
fprintf(stderr, "Error: Couldn't open pad file: %s\n", options->pads[n]);
|
||||
return MACHINE_ERROR;
|
||||
}
|
||||
padFromFile(n, pad);
|
||||
}
|
||||
|
||||
if (options->padRecord != NULL) {
|
||||
FILE *pad = fopen(options->padRecord, "wb");
|
||||
if (pad == NULL) {
|
||||
fprintf(stderr, "Error: Couldn't write pad file: %s\n", options->padRecord);
|
||||
return MACHINE_ERROR;
|
||||
}
|
||||
padRecordTo(pad);
|
||||
}
|
||||
|
||||
if (options->keyboard != NULL) {
|
||||
keyboardFile = fopen(options->keyboard, "rb");
|
||||
if (keyboardFile == NULL) {
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// pad.c
|
||||
// Game controllers for the Voyager.
|
||||
// Written by Anachronaut
|
||||
|
||||
#include "pad.h"
|
||||
#include "video.h"
|
||||
|
||||
// What each pad is holding, and where each one gets it from.
|
||||
static uint8_t held[PAD_COUNT];
|
||||
static FILE *recorded[PAD_COUNT];
|
||||
static uint8_t live[PAD_COUNT];
|
||||
static int connected[PAD_COUNT];
|
||||
|
||||
// ---- What the machine is told, which changes only on a frame ----
|
||||
//
|
||||
// The live state is written by whatever is watching real hardware, on ITS clock - a window
|
||||
// polls its keyboard once a host frame, and a host frame is not a machine frame. Read
|
||||
// straight through, that made a pad whose value could change in the middle of a machine
|
||||
// frame, which breaks the one promise the device makes: that asking twice in a frame gives
|
||||
// the same answer both times.
|
||||
//
|
||||
// It also made recordings that were not of the flight. The recorder samples on a frame
|
||||
// boundary and the program reads whenever it reads, so the two saw different bytes - and a
|
||||
// replay of that is a DIFFERENT FLIGHT, faithfully reproduced. It flew a lander off the top
|
||||
// of the screen that had never gone there.
|
||||
//
|
||||
// So the live state is latched here once a frame, and what the machine reads and what the
|
||||
// recorder writes are the same thing by construction. Real hardware does this too, and for
|
||||
// the same reason: a controller is sampled once a frame, not continuously.
|
||||
static uint8_t reported[PAD_COUNT];
|
||||
static FILE *recording;
|
||||
|
||||
// ---- The frame the recordings advance on ----
|
||||
//
|
||||
// The screen's frame, and it is the same one on purpose: a game reads its pad once a frame
|
||||
// because that is when it draws, so a byte a frame is a byte a poll for anything written the
|
||||
// ordinary way - without making it a byte a READ, which would answer a game that asked twice
|
||||
// differently from one that asked once.
|
||||
//
|
||||
// On the machine's clock, so a recording plays back the same over the same cycles however
|
||||
// fast the host ran.
|
||||
static unsigned long lastFrame;
|
||||
static int started;
|
||||
|
||||
void padReset(void) {
|
||||
for (int n = 0; n < PAD_COUNT; n++) {
|
||||
held[n] = 0;
|
||||
live[n] = 0;
|
||||
reported[n] = 0;
|
||||
connected[n] = 0;
|
||||
// The files are NOT closed or forgotten. They were named on the command line and
|
||||
// outlive a reset, the same as a disk image does: a machine that restarted itself
|
||||
// and lost its controllers would be a strange thing to debug.
|
||||
}
|
||||
lastFrame = 0;
|
||||
started = 0;
|
||||
}
|
||||
|
||||
// What the device would report for this pad, which is what a recording has to hold: a
|
||||
// recording of a playback that wrote the LIVE state would be a file of noughts.
|
||||
static uint8_t effective(int which) {
|
||||
return (recorded[which] != NULL) ? held[which] : reported[which];
|
||||
}
|
||||
|
||||
void padRecordTo(FILE *file) {
|
||||
recording = file;
|
||||
}
|
||||
|
||||
void padFromFile(int which, FILE *file) {
|
||||
if (which < 0 || which >= PAD_COUNT) {
|
||||
return;
|
||||
}
|
||||
recorded[which] = file;
|
||||
}
|
||||
|
||||
void padSet(int which, int isConnected, uint8_t heldNow) {
|
||||
if (which < 0 || which >= PAD_COUNT) {
|
||||
return;
|
||||
}
|
||||
connected[which] = isConnected;
|
||||
live[which] = heldNow;
|
||||
}
|
||||
|
||||
void padTick(unsigned long now) {
|
||||
// The first tick sets the clock rather than counting a frame from nought, or a machine
|
||||
// that started late would take a run of bytes all at once.
|
||||
if (!started) {
|
||||
lastFrame = now;
|
||||
started = 1;
|
||||
}
|
||||
while (now - lastFrame >= VIDEO_FRAME_CYCLES) {
|
||||
lastFrame += VIDEO_FRAME_CYCLES;
|
||||
for (int n = 0; n < PAD_COUNT; n++) {
|
||||
// The live state is taken as it stands at the frame boundary and held there
|
||||
// until the next one, so nothing the machine reads was never recorded.
|
||||
reported[n] = live[n];
|
||||
if (recorded[n] == NULL) {
|
||||
continue;
|
||||
}
|
||||
const int byte = fgetc(recorded[n]);
|
||||
// ---- The end of a recording is nothing held ----
|
||||
//
|
||||
// Not a pad that vanishes and not the last frame repeating for ever. A recording
|
||||
// that ran out and left a direction pressed would send whatever it was driving
|
||||
// off the edge of the world long after the test meant to stop.
|
||||
held[n] = (byte == EOF) ? 0 : (uint8_t)byte;
|
||||
}
|
||||
// ---- And a byte written for every frame that went by ----
|
||||
//
|
||||
// Inside the loop rather than after it, so a machine that jumped several frames at
|
||||
// once still writes one byte for each of them. A recording is a TIMELINE, and one
|
||||
// that skipped the frames nobody was looking at would play back faster than it was
|
||||
// flown.
|
||||
//
|
||||
// Flushed as it goes, because a recording is usually stopped by whoever is playing
|
||||
// rather than by the program ending, and a demo lost to a buffer would be a demo
|
||||
// flown twice.
|
||||
if (recording != NULL) {
|
||||
uint8_t all = 0;
|
||||
for (int n = 0; n < PAD_COUNT; n++) {
|
||||
all |= effective(n);
|
||||
}
|
||||
fputc(all, recording);
|
||||
fflush(recording);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t padRead(uint8_t port) {
|
||||
if (port == PAD_PRESENT) {
|
||||
uint8_t there = 0;
|
||||
for (int n = 0; n < PAD_COUNT; n++) {
|
||||
// A recording is a pad, and so is anything the front end says is plugged in.
|
||||
// Counting only the recordings meant this said nought on the one machine that
|
||||
// has real controllers, which is the only machine where the answer matters.
|
||||
if (recorded[n] != NULL || connected[n]) {
|
||||
there |= (uint8_t)(1u << n);
|
||||
}
|
||||
}
|
||||
return there;
|
||||
}
|
||||
const int which = port - PORT_PAD;
|
||||
if (which < 0 || which >= PAD_COUNT) {
|
||||
// Everything else in the block is reserved and reads as nothing, which is what a
|
||||
// port block being kept for later should do.
|
||||
return 0;
|
||||
}
|
||||
// A recording wins over a live pad, so a test is not at the mercy of whatever somebody
|
||||
// is leaning on while it runs.
|
||||
return effective(which);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// pad.h
|
||||
// Game controllers for the Voyager.
|
||||
// Written by Anachronaut
|
||||
|
||||
#ifndef PAD_H
|
||||
#define PAD_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
// ---- What a pad is, and why it is not the console ----
|
||||
//
|
||||
// The console says WHICH KEY WENT DOWN. That is the right shape for typing and the wrong one
|
||||
// for playing: a game wants to know what is being held, this frame, possibly several things
|
||||
// at once, and a stream of presses cannot say that. A key that is down and staying down sends
|
||||
// nothing at all.
|
||||
//
|
||||
// So a pad reports a LEVEL rather than an event. One read gives the state of every button at
|
||||
// once, holding is the natural thing to express, and two directions at the same time costs
|
||||
// nothing. Reading it does not consume it: a game may ask twice in a frame and get the same
|
||||
// answer both times, which an event queue cannot promise.
|
||||
//
|
||||
// IT IS NOT AN EXTENSION OF THE CONSOLE, and that is deliberate. A terminal hands over
|
||||
// characters and cannot report a key coming up however it is asked, so key-up on the console
|
||||
// would have been a thing that worked behind a window and silently did not down a wire - and
|
||||
// "the same program behaves the same everywhere" is worth more than the convenience. A
|
||||
// separate device can honestly say it is not there.
|
||||
|
||||
#define PORT_PAD 0x60
|
||||
#define PORT_PAD_TOP 0x6F
|
||||
|
||||
// Four, because a party is four. They cost a port each and nothing at all when unused.
|
||||
#define PAD_COUNT 4
|
||||
|
||||
// ---- The buttons, in one byte ----
|
||||
//
|
||||
// The four directions in the low nibble, so "which way" is an AND with 0x0F and needs no
|
||||
// shifting. The four buttons in the high nibble for the same reason.
|
||||
#define PAD_RIGHT 0x01
|
||||
#define PAD_LEFT 0x02
|
||||
#define PAD_DOWN 0x04
|
||||
#define PAD_UP 0x08
|
||||
#define PAD_A 0x10
|
||||
#define PAD_B 0x20
|
||||
#define PAD_START 0x40
|
||||
#define PAD_SELECT 0x80
|
||||
|
||||
// ---- Which of them are there ----
|
||||
//
|
||||
// One bit a pad, so a game can say "this wants a controller" rather than sitting silent while
|
||||
// somebody presses things at it. A pad that is not there reads as nothing held, which is the
|
||||
// same as a pad nobody is touching - the difference matters only to whoever wants to explain
|
||||
// it, and that is exactly who this port is for.
|
||||
#define PAD_PRESENT 0x64
|
||||
|
||||
// Ports 0x65 to 0x6F are reserved. Analogue axes are the thing they are being kept for: a
|
||||
// paddle is two more bytes a pad and an argument about deadzones, and nothing wants one yet.
|
||||
|
||||
void padReset(void);
|
||||
|
||||
// ---- Recorded input, which is what makes this testable ----
|
||||
//
|
||||
// Voyager reads a real pad, and its tests run headless with no window and no hands. Without a
|
||||
// recorded path this whole device would be exercised only by somebody playing, which is the
|
||||
// state the console's line editing was in when it broke twice in two days.
|
||||
//
|
||||
// A file is one byte a FRAME, not one byte a read. A pad is a level: a game that asks twice
|
||||
// in one frame has to be told the same thing both times, and a file that advanced per read
|
||||
// would answer differently depending on how the game was written.
|
||||
void padFromFile(int which, FILE *file);
|
||||
|
||||
// A live pad, set by whatever is watching real hardware. Ignored for a pad that has a file,
|
||||
// so a recording always wins over whatever somebody happens to be holding.
|
||||
//
|
||||
// CONNECTED IS SAID SEPARATELY FROM HELD, and it has to be: a front end calls this every
|
||||
// frame for every pad, so "held nothing" is the commonest thing it says and cannot also mean
|
||||
// "there is no pad here". Getting that wrong made 0x64 answer nought with a controller
|
||||
// plugged in, and a game that asked whether there was one was told no while the device
|
||||
// underneath was reporting its buttons perfectly.
|
||||
void padSet(int which, int connected, uint8_t held);
|
||||
|
||||
// ---- Writing one down ----
|
||||
//
|
||||
// A demo recorder. What it writes is EXACTLY WHAT --pad READS, one byte a frame, so playing a
|
||||
// recording back needs no conversion and no second format - and the round trip is a property
|
||||
// worth testing: play a file while recording it and the same bytes come out.
|
||||
//
|
||||
// It exists because some inputs cannot sensibly be written by hand. Flying a lander from one
|
||||
// base to another is a few hundred frames of steering that has to arrive somewhere eight cells
|
||||
// wide, and hand-authoring one is a piloting exercise rather than a test. Playing it once and
|
||||
// keeping what happened is the whole answer.
|
||||
//
|
||||
// EVERY PAD AT ONCE, or-ed into one byte, and not one pad chosen by number. Which pad a
|
||||
// controller lands on is an accident of the host - the first recording made with this came
|
||||
// back 1,766 frames of nothing, because it recorded pad nought and the controller was
|
||||
// somewhere else. A demo is a record of what somebody DID, and on a machine one person is
|
||||
// playing the number it arrived on is not part of that.
|
||||
//
|
||||
// A recording therefore plays back on pad nought, which is where --pad puts the first file
|
||||
// given. Any program that reads more than one pad reads them or-ed anyway, for exactly the
|
||||
// same reason.
|
||||
void padRecordTo(FILE *file);
|
||||
|
||||
void padTick(unsigned long now);
|
||||
uint8_t padRead(uint8_t port);
|
||||
|
||||
#endif // PAD_H
|
||||
@@ -213,6 +213,7 @@ static void setLfo(LFO *l, uint8_t which, uint8_t value) {
|
||||
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;
|
||||
case SP_LFO_MODE: l->mode = value ? LFO_RETRIGGER : LFO_FREE; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -228,8 +229,12 @@ static void soundParameter(uint8_t value) {
|
||||
case SP_MODENV: setEnvelope(&v->modEnv, which, value); break;
|
||||
case SP_FILTER: setFilter(&v->filter, parameter, value); break;
|
||||
case SP_LEVEL_SOURCE:
|
||||
// Two settings share a group, because both are about the voice as a whole rather
|
||||
// than about one of its parts.
|
||||
if (parameter == SP_LEVEL_SOURCE) {
|
||||
v->levelSource = sourceFor(value);
|
||||
} else if (parameter == SP_VOICE_GATE) {
|
||||
v->gate = value ? VOICE_TRIGGER : VOICE_GATE;
|
||||
}
|
||||
break;
|
||||
// The LFOs belong to the device rather than to a channel, so whichever channel is
|
||||
|
||||
@@ -82,6 +82,17 @@
|
||||
// LFOs. Nought is the one that could not be said before - see synth.h.
|
||||
#define SP_LEVEL_SOURCE 0x50
|
||||
|
||||
// ---- Whether a note waits to be let go of ----
|
||||
//
|
||||
// 0 is gated and 1 is triggered. Gated is what a keyboard wants: the sound lasts as long as
|
||||
// the finger does, and dropping the gate starts the release. Triggered is what a GAME wants -
|
||||
// a bang, a pickup, a door - where the note is struck and then plays its own length, and
|
||||
// nothing has to remember to end it. Sustain and release have no meaning in a triggered
|
||||
// voice, because both are answers to a question about a key that is not being asked.
|
||||
//
|
||||
// A program driving one-shots does not need port 0x45 at all once this is set.
|
||||
#define SP_VOICE_GATE 0x51
|
||||
|
||||
// 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
|
||||
@@ -89,6 +100,16 @@
|
||||
#define SP_LFO_ACTIVE 0x00
|
||||
#define SP_LFO_WAVE 0x01
|
||||
#define SP_LFO_RATE 0x02
|
||||
// ---- Whether it starts over when a voice does ----
|
||||
//
|
||||
// 0 free and 1 retriggered. Free is one cycle running under everything, which is what vibrato
|
||||
// across a held chord wants. Retriggered starts at the beginning of its shape every time a
|
||||
// voice begins, which is the only way a one-shot sounds the SAME twice: a free LFO is wherever
|
||||
// the wall clock left it, so the same drum caught at a different moment is a different drum.
|
||||
//
|
||||
// The mode belongs to the LFO and the cycle belongs to the voice, so retriggering costs
|
||||
// nothing to a channel that is not using it.
|
||||
#define SP_LFO_MODE 0x03
|
||||
|
||||
// ---- Samples come from the machine's clock ----
|
||||
//
|
||||
|
||||
+150
-56
@@ -10,25 +10,22 @@
|
||||
//
|
||||
// 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.
|
||||
// carried across on purpose - in BOTH directions, which has now happened once each way.
|
||||
//
|
||||
// ---- 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.
|
||||
// Nothing. This is soundThing's engine at b73e5c0, character for character, except that
|
||||
// em-dashes and arrows in its comments are written as ASCII here because this tree is ASCII
|
||||
// only. That rule is local and is not an improvement, so it was not sent upstream.
|
||||
//
|
||||
// 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.
|
||||
// It did not start that way. Three changes were made here first - a routed voice level, a
|
||||
// seeded noise generator, and channels asked for by number - and all three went up. What came
|
||||
// back was those three plus what they made possible: a voice that can end itself rather than
|
||||
// waiting for a key, and a triggered voice that re-arms its oscillators so a one-shot is the
|
||||
// same one-shot twice. A game is nearly all one-shots, which is why the traffic went that 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.
|
||||
// So the thing to keep current is no longer a list. It is this: if either copy changes, the
|
||||
// other one has to be told.
|
||||
//
|
||||
// Written by Anachronaut
|
||||
|
||||
@@ -42,6 +39,30 @@
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
// ---- The seeds a retrigger goes back to ----
|
||||
//
|
||||
// Deliberately NOT keyed on the voice, unlike the seeds synthInit hands out. A retriggered
|
||||
// one-shot has to sound the same whichever voice happens to be free for it, and a seed that
|
||||
// varied per voice would make the same drum a different drum eight ways. Keyed on the
|
||||
// oscillator, though, because osc 0 and osc 1 drawing one stream are one noise heard twice.
|
||||
static uint32_t oscTriggerSeed(int o)
|
||||
{
|
||||
return (o == 0) ? 0x9E3779B9u : 0x7F4A7C15u;
|
||||
}
|
||||
|
||||
static uint32_t lfoTriggerSeed(int l)
|
||||
{
|
||||
return 0x2545F491u + (uint32_t)l * 3266489917u;
|
||||
}
|
||||
|
||||
static void lfoStateInit(LfoState *st, uint32_t seed)
|
||||
{
|
||||
st->phase = 0.0f;
|
||||
st->noiseHeld = 0.0f;
|
||||
st->noisePhase = 0.0f;
|
||||
st->noiseState = seed;
|
||||
}
|
||||
|
||||
void synthInit(Synth *s, float sampleRate)
|
||||
{
|
||||
s->sampleRate = sampleRate;
|
||||
@@ -58,14 +79,16 @@ void synthInit(Synth *s, float sampleRate)
|
||||
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.
|
||||
// than one heard twice. The constants are arbitrary and non-zero.
|
||||
// Envelope 0 shapes the level, which is what it always did - the difference is that
|
||||
// it is now said rather than assumed, and can be said differently.
|
||||
s->voices[i].levelSource = MOD_SOURCE_AMP_ENV;
|
||||
s->voices[i].gate = VOICE_GATE;
|
||||
for (int l = 0; l < LFO_COUNT; l++)
|
||||
lfoStateInit(&s->voices[i].lfoRun[l], lfoTriggerSeed(l));
|
||||
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
|
||||
@@ -92,13 +115,13 @@ void synthInit(Synth *s, float sampleRate)
|
||||
s->voices[0].oscillators[0].active = 1;
|
||||
|
||||
for (int l = 0; l < LFO_COUNT; l++) {
|
||||
s->lfos[l].phase = 0.0f;
|
||||
lfoStateInit(&s->lfos[l].run, lfoTriggerSeed(l));
|
||||
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;
|
||||
// Free unless a patch says otherwise, which is what every patch that exists was
|
||||
// made against.
|
||||
s->lfos[l].mode = LFO_FREE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,9 +130,14 @@ 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.
|
||||
// always was - now said out loud, so that a patch which routed it elsewhere does not
|
||||
// leave the next one silent.
|
||||
v->levelSource = MOD_SOURCE_AMP_ENV;
|
||||
|
||||
// Held up by the key unless a patch says otherwise, which is what every patch that
|
||||
// exists was made against.
|
||||
v->gate = VOICE_GATE;
|
||||
|
||||
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++) {
|
||||
@@ -139,13 +167,13 @@ void synthResetPatch(Synth *s)
|
||||
v->filter.resModDepth = 0.0f;
|
||||
|
||||
for (int l = 0; l < LFO_COUNT; l++) {
|
||||
s->lfos[l].phase = 0.0f;
|
||||
lfoStateInit(&s->lfos[l].run, lfoTriggerSeed(l));
|
||||
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;
|
||||
// Free unless a patch says otherwise, which is what every patch that exists was
|
||||
// made against.
|
||||
s->lfos[l].mode = LFO_FREE;
|
||||
}
|
||||
|
||||
s->volume = 0.8f;
|
||||
@@ -191,19 +219,25 @@ static float getModValue(float ampEnv, float modEnv, float lfo0, float lfo1, Mod
|
||||
}
|
||||
}
|
||||
|
||||
float lfoTick(LFO *l, float sampleRate)
|
||||
float lfoTickState(const LFO *l, LfoState *st, float sampleRate)
|
||||
{
|
||||
if (!l->active) return 0.0f;
|
||||
l->phase += l->rate / sampleRate;
|
||||
if (l->phase >= 1.0f) l->phase -= 1.0f;
|
||||
st->phase += l->rate / sampleRate;
|
||||
if (st->phase >= 1.0f) st->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);
|
||||
st->noisePhase += l->rate / sampleRate;
|
||||
if (st->noisePhase >= 1.0f) {
|
||||
st->noisePhase -= 1.0f;
|
||||
st->noiseHeld = nextNoise(&st->noiseState);
|
||||
}
|
||||
}
|
||||
return waveformSample(l->waveform, l->phase, 0.5f, l->noiseHeld);
|
||||
return waveformSample(l->waveform, st->phase, 0.5f, st->noiseHeld);
|
||||
}
|
||||
|
||||
// The LFO advancing its own cycle: the free-running one, the same for every voice.
|
||||
float lfoTick(LFO *l, float sampleRate)
|
||||
{
|
||||
return lfoTickState(l, &l->run, sampleRate);
|
||||
}
|
||||
|
||||
float filterTick(Filter *f, float input, float cutoff, float resonance, float sampleRate)
|
||||
@@ -330,6 +364,43 @@ const char *waveformName(Waveform w)
|
||||
}
|
||||
}
|
||||
|
||||
// Everything about a voice that a note begins rather than inherits.
|
||||
//
|
||||
// The envelopes are told, once, whether this note is waiting on a key - asked here rather
|
||||
// than read live in the mixer so a note already sounding keeps the shape it began with.
|
||||
//
|
||||
// A TRIGGERED voice also starts its oscillators over. They are the larger half of why the
|
||||
// same one-shot came out different every time: the envelopes restarted and the filter was
|
||||
// cleared, but the oscillator phase carried on from wherever the last note left it, so a
|
||||
// kick began a third of the way into its own cycle depending on what played before it. A
|
||||
// gated voice is left alone, because a key being held is not a claim about phase and every
|
||||
// patch that exists was made against the old behaviour.
|
||||
//
|
||||
// Retriggered LFOs restart for THIS voice only, whatever the gate - an LFO starting fresh
|
||||
// per note is wanted under held notes too, and it is the shared cycle that must not move.
|
||||
static void voiceArm(Synth *s, Voice *v)
|
||||
{
|
||||
int oneShot = (v->gate == VOICE_TRIGGER);
|
||||
v->ampEnv.oneShot = oneShot;
|
||||
v->modEnv.oneShot = oneShot;
|
||||
|
||||
if (v->gate == VOICE_TRIGGER) {
|
||||
for (int o = 0; o < OSC_COUNT; o++) {
|
||||
v->oscillators[o].phase = 0.0f;
|
||||
v->oscillators[o].noiseHeld = 0.0f;
|
||||
v->oscillators[o].noisePhase = 0.0f;
|
||||
v->oscillators[o].noiseState = oscTriggerSeed(o);
|
||||
}
|
||||
}
|
||||
|
||||
for (int l = 0; l < LFO_COUNT; l++)
|
||||
if (s->lfos[l].mode == LFO_RETRIGGER)
|
||||
lfoStateInit(&v->lfoRun[l], lfoTriggerSeed(l));
|
||||
|
||||
envelopeNoteOn(&v->ampEnv);
|
||||
envelopeNoteOn(&v->modEnv);
|
||||
}
|
||||
|
||||
void synthNoteOn(Synth *s, int midiNote)
|
||||
{
|
||||
float hz = 440.0f * powf(2.0f, (midiNote - 69) / 12.0f);
|
||||
@@ -341,8 +412,7 @@ void synthNoteOn(Synth *s, int 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);
|
||||
voiceArm(s, &s->voices[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -355,29 +425,28 @@ void synthNoteOn(Synth *s, int 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);
|
||||
voiceArm(s, &s->voices[i]);
|
||||
}
|
||||
|
||||
|
||||
void synthNoteOff(Synth *s, int midiNote) {
|
||||
for (int i = 0; i < VOICE_COUNT; i++) {
|
||||
if (s->voices[i].active && s->voices[i].midiNote == midiNote) {
|
||||
// A triggered voice plays its own length; the key coming up is not its business.
|
||||
// Asked of the envelope rather than the voice, because that is what the note was
|
||||
// started with and the switch may have moved since.
|
||||
if (s->voices[i].ampEnv.oneShot) continue;
|
||||
envelopeNoteOff(&s->voices[i].ampEnv);
|
||||
envelopeNoteOff(&s->voices[i].modEnv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- A channel is the channel you asked for ----
|
||||
// ---- Asked for by number, rather than allocated ----
|
||||
//
|
||||
// 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.
|
||||
// Channel two is channel two, it holds its patch between notes, and a program driving this as
|
||||
// hardware can rely on both. synthNoteOn above steals a voice round-robin, which is right for
|
||||
// a keyboard and wrong for anything addressing a fixed set of parts.
|
||||
void synthChannelOn(Synth *s, int channel, int midiNote)
|
||||
{
|
||||
if (channel < 0 || channel >= VOICE_COUNT) {
|
||||
@@ -391,8 +460,7 @@ void synthChannelOn(Synth *s, int channel, int midiNote)
|
||||
// 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);
|
||||
voiceArm(s, v);
|
||||
}
|
||||
|
||||
void synthChannelOff(Synth *s, int channel)
|
||||
@@ -413,8 +481,10 @@ void synthFillBuffer(Synth *s, int16_t *out, int frames) {
|
||||
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);
|
||||
// The shared cycle advances once per sample whatever is listening, so a free LFO is
|
||||
// one sweep under everything and does not stall when nothing is sounding.
|
||||
float freeLfo0 = lfoTick(&s->lfos[0], sr);
|
||||
float freeLfo1 = lfoTick(&s->lfos[1], sr);
|
||||
|
||||
for (int v = 0; v < VOICE_COUNT; v++) {
|
||||
Voice *vv = &s->voices[v];
|
||||
@@ -423,11 +493,24 @@ void synthFillBuffer(Synth *s, int16_t *out, int frames) {
|
||||
float amp = envelopeTick(&vv->ampEnv, sr);
|
||||
float mod = envelopeTick(&vv->modEnv, sr);
|
||||
|
||||
if (vv->ampEnv.stage == ENV_IDLE) {
|
||||
// A gated voice is over when envelope 0 is, which is after the key came up.
|
||||
// A triggered one has no key to wait for, so it is over only when BOTH envelopes
|
||||
// are - envelope 0 alone would cut a level that envelope 1 is still shaping.
|
||||
int finished = (vv->ampEnv.stage == ENV_IDLE);
|
||||
if (vv->ampEnv.oneShot)
|
||||
finished = finished && (vv->modEnv.stage == ENV_IDLE);
|
||||
if (finished) {
|
||||
vv->active = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// A retriggered LFO reads this voice's own cycle, started when it was struck.
|
||||
// A free one reads the shared cycle above, exactly as it always did.
|
||||
float lfo0 = (s->lfos[0].mode == LFO_RETRIGGER)
|
||||
? lfoTickState(&s->lfos[0], &vv->lfoRun[0], sr) : freeLfo0;
|
||||
float lfo1 = (s->lfos[1].mode == LFO_RETRIGGER)
|
||||
? lfoTickState(&s->lfos[1], &vv->lfoRun[1], sr) : freeLfo1;
|
||||
|
||||
float oscMix = 0.0f;
|
||||
int activeOscs = 0;
|
||||
for (int o = 0; o < OSC_COUNT; o++) {
|
||||
@@ -491,6 +574,7 @@ void envelopeInit(Envelope *e, float attackSec, float decaySec, float sustainLev
|
||||
e->decaySec = decaySec;
|
||||
e->sustainLevel = sustainLevel;
|
||||
e->releaseSec = releaseSec;
|
||||
e->oneShot = 0;
|
||||
}
|
||||
|
||||
void envelopeNoteOn(Envelope *e)
|
||||
@@ -521,9 +605,13 @@ float envelopeTick(Envelope *e, float sampleRate)
|
||||
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;
|
||||
// Sustain is where the decay stops and waits for the key. A one-shot has no key
|
||||
// to wait for, so it decays the whole way and is finished - and MUST, or a patch
|
||||
// with a sustain above nothing would hold a triggered voice open forever.
|
||||
float floorLevel = e->oneShot ? 0.0f : e->sustainLevel;
|
||||
if (e->value <= floorLevel) {
|
||||
e->value = floorLevel;
|
||||
e->stage = e->oneShot ? ENV_IDLE : ENV_SUSTAIN;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -549,6 +637,12 @@ float envelopeTick(Envelope *e, float sampleRate)
|
||||
void synthSyncVoices(Synth *s)
|
||||
{
|
||||
for (int v = 1; v < VOICE_COUNT; v++) {
|
||||
// Sync what shapes the level. Without this the voices below hold whatever synthInit
|
||||
// gave them, so a patch that routes its level elsewhere is honoured by voice 0 and by
|
||||
// nothing else - which sounds like it works until a second note is playing.
|
||||
s->voices[v].levelSource = s->voices[0].levelSource;
|
||||
s->voices[v].gate = s->voices[0].gate;
|
||||
|
||||
// Sync oscillator settings
|
||||
for (int o = 0; o < OSC_COUNT; o++) {
|
||||
s->voices[v].oscillators[o].waveform = s->voices[0].oscillators[o].waveform;
|
||||
|
||||
+70
-24
@@ -10,25 +10,22 @@
|
||||
//
|
||||
// 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.
|
||||
// carried across on purpose - in BOTH directions, which has now happened once each way.
|
||||
//
|
||||
// ---- 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.
|
||||
// Nothing. This is soundThing's engine at b73e5c0, character for character, except that
|
||||
// em-dashes and arrows in its comments are written as ASCII here because this tree is ASCII
|
||||
// only. That rule is local and is not an improvement, so it was not sent upstream.
|
||||
//
|
||||
// 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.
|
||||
// It did not start that way. Three changes were made here first - a routed voice level, a
|
||||
// seeded noise generator, and channels asked for by number - and all three went up. What came
|
||||
// back was those three plus what they made possible: a voice that can end itself rather than
|
||||
// waiting for a key, and a triggered voice that re-arms its oscillators so a one-shot is the
|
||||
// same one-shot twice. A game is nearly all one-shots, which is why the traffic went that 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.
|
||||
// So the thing to keep current is no longer a list. It is this: if either copy changes, the
|
||||
// other one has to be told.
|
||||
//
|
||||
// Written by Anachronaut
|
||||
|
||||
@@ -69,6 +66,17 @@ typedef enum {
|
||||
MOD_SOURCE_LFO2 = 4
|
||||
} ModSource;
|
||||
|
||||
// ---- Whether the key holds the note up ----
|
||||
//
|
||||
// Gated is what a keyboard wants: the sound lasts as long as the finger does, and lifting it
|
||||
// starts the release. Triggered is what a drum wants - the note is struck and then plays its
|
||||
// own length, and the key coming up is not its business. Sustain and release have no meaning
|
||||
// in a triggered voice, because both of them are answers to a question about the key.
|
||||
typedef enum {
|
||||
VOICE_GATE = 0,
|
||||
VOICE_TRIGGER = 1
|
||||
} VoiceGate;
|
||||
|
||||
typedef struct {
|
||||
EnvStage stage;
|
||||
float value; // current output value, 0.0 to 1.0
|
||||
@@ -76,20 +84,46 @@ typedef struct {
|
||||
float decaySec;
|
||||
float sustainLevel;
|
||||
float releaseSec;
|
||||
// Set from the voice's gate at note-on, not read from it live, so that flipping the
|
||||
// switch under a sounding note cannot strand it half-way through a shape it was not
|
||||
// started in. One-shot runs the decay to nothing and finishes there.
|
||||
int oneShot;
|
||||
} Envelope;
|
||||
|
||||
// ---- Where an LFO is in its cycle ----
|
||||
//
|
||||
// Split out of the LFO itself because there is now more than one answer at a time. A free
|
||||
// LFO has one cycle that every voice reads, which is what makes it a single sweep across a
|
||||
// chord. A retriggered one has a cycle PER VOICE, restarted when that voice is struck - and
|
||||
// a voice resetting the shared one would drag every note already sounding along with it.
|
||||
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
|
||||
// varies between C libraries, so the same patch 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;
|
||||
} LfoState;
|
||||
|
||||
// ---- Whether an LFO keeps its own time or starts when struck ----
|
||||
//
|
||||
// Free is one cycle running under everything, which is what vibrato across a held chord
|
||||
// wants. Retriggered starts at the beginning of its shape every time a voice begins, which
|
||||
// is the only way a one-shot sounds the same twice - a free LFO is wherever the wall clock
|
||||
// left it, so the same drum caught at a different moment is a different drum.
|
||||
typedef enum {
|
||||
LFO_FREE = 0,
|
||||
LFO_RETRIGGER = 1
|
||||
} LfoMode;
|
||||
|
||||
typedef struct {
|
||||
LfoState run; // the free-running cycle, read by every voice in LFO_FREE
|
||||
float rate; // Hz
|
||||
Waveform waveform;
|
||||
int active;
|
||||
LfoMode mode;
|
||||
} LFO;
|
||||
|
||||
typedef enum {
|
||||
@@ -119,11 +153,10 @@ typedef struct {
|
||||
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
|
||||
// varies between C libraries, so the same patch 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
|
||||
@@ -155,6 +188,16 @@ typedef struct {
|
||||
// It also makes two things possible that were not: envelope 1 shaping the volume, and an
|
||||
// LFO doing it, which is tremolo.
|
||||
ModSource levelSource;
|
||||
// ---- Whether the key holds this voice up ----
|
||||
//
|
||||
// The other half of what envelope 0 used to decide on its own. Naming the level's source
|
||||
// said what shapes the sound; this says who ends it. Without it a voice can only ever
|
||||
// finish because a key came up, which is no use to a drum.
|
||||
VoiceGate gate;
|
||||
// One cycle per LFO per voice, used only by the LFOs set to LFO_RETRIGGER. Runtime state
|
||||
// like the envelope stages and the filter's integrators, so synthSyncVoices leaves it
|
||||
// alone - it is where this voice is, not what the patch says.
|
||||
LfoState lfoRun[LFO_COUNT];
|
||||
} Voice;
|
||||
|
||||
typedef struct {
|
||||
@@ -184,11 +227,12 @@ void synthResetPatch(Synth *s);
|
||||
void synthNoteOn(Synth *s, int midiNote);
|
||||
void synthNoteOff(Synth *s, int midiNote);
|
||||
|
||||
// ---- 3. A channel is asked for by number ----
|
||||
// ---- A channel 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.
|
||||
// The two above hunt for a free voice and steal round-robin, which is what a keyboard wants:
|
||||
// a player presses keys and does not care which voice sounds them. Something driving this as
|
||||
// HARDWARE does care - channel two is channel two, it keeps its patch between notes, and
|
||||
// nothing may take it away. Both ways of asking are here and neither changes the other.
|
||||
void synthChannelOn(Synth *s, int channel, int midiNote);
|
||||
void synthChannelOff(Synth *s, int channel);
|
||||
void synthFillBuffer(Synth *s, int16_t *out, int frames);
|
||||
@@ -196,6 +240,8 @@ void synthSyncVoices(Synth *s);
|
||||
|
||||
// LFO functions:
|
||||
float lfoTick(LFO *l, float sampleRate);
|
||||
// The same advance against a cycle that is not the LFO's own, so a voice can run its own.
|
||||
float lfoTickState(const LFO *l, LfoState *st, float sampleRate);
|
||||
|
||||
// Filter functions:
|
||||
float filterTick(Filter *f, float input, float cutoff, float resonance, float sampleRate);
|
||||
|
||||
@@ -34,6 +34,15 @@ void printHelp(const char *programName) {
|
||||
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(" -P, --pad FILE Hold a controller from a file, one byte a frame. Given\n");
|
||||
printf(" again for the next pad. A byte is the buttons held:\n");
|
||||
printf(" 1 right, 2 left, 4 down, 8 up, 16 A, 32 B, 64 start,\n");
|
||||
printf(" 128 select.\n");
|
||||
printf(" -Y, --record-pad FILE\n");
|
||||
printf(" Write what the controllers held, one byte a frame, in the\n");
|
||||
printf(" format --pad reads. Every pad at once, because which one a\n");
|
||||
printf(" controller lands on is the host's business. Play a thing\n");
|
||||
printf(" once and keep what happened.\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");
|
||||
@@ -52,6 +61,8 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
||||
{"screen", required_argument, 0, 'S'},
|
||||
{"keyboard", required_argument, 0, 'K'},
|
||||
{"sound", required_argument, 0, 'N'},
|
||||
{"pad", required_argument, 0, 'P'},
|
||||
{"record-pad", required_argument, 0, 'Y'},
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{0, 0, 0, 0 }
|
||||
};
|
||||
@@ -71,7 +82,7 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
||||
*options = (EmulatorOptions){0};
|
||||
|
||||
// Parse options
|
||||
while ((opt = getopt_long(argc, argv, "dc:fhD:WL:S:K:N:R:", long_options, &option_index)) != -1) {
|
||||
while ((opt = getopt_long(argc, argv, "dc:fhD:WL:S:K:N:R:P:Y:", long_options, &option_index)) != -1) {
|
||||
switch (opt) {
|
||||
case 'd':
|
||||
options->debug = 1;
|
||||
@@ -131,6 +142,16 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
||||
case 'N':
|
||||
options->sound = optarg;
|
||||
break;
|
||||
case 'Y':
|
||||
options->padRecord = optarg;
|
||||
break;
|
||||
case 'P':
|
||||
// Fills the pads in turn, the same way --disk fills the drives, so the first
|
||||
// one named is pad nought and the machine has as many as were asked for.
|
||||
if (options->padCount < PAD_DRIVE_COUNT) {
|
||||
options->pads[options->padCount++] = optarg;
|
||||
}
|
||||
break;
|
||||
case 'h':
|
||||
printHelp(argv[0]);
|
||||
return OPTIONS_HELP;
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
#define OPTIONS_HELP 1 // The user asked for help, so stop, but not because of an error.
|
||||
#define OPTIONS_ERROR 2 // The command line was no good, stop and complain.
|
||||
|
||||
// As many as the machine has pads.
|
||||
#define PAD_DRIVE_COUNT 4
|
||||
|
||||
typedef struct {
|
||||
uint8_t debug; // Step one instruction at a time, printing the registers.
|
||||
uint8_t fast; // Ignore the cycle rate and run as fast as the host allows.
|
||||
@@ -36,6 +39,18 @@ typedef struct {
|
||||
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.
|
||||
// ---- A controller made of a file ----
|
||||
//
|
||||
// One byte a frame, each byte the buttons held during it. Voyager reads a real pad, and
|
||||
// its own tests run headless with nobody holding anything - so without this the device
|
||||
// would be exercised only by somebody playing, which is exactly the state the console's
|
||||
// line editing was in when it broke twice in two days.
|
||||
const char *pads[PAD_DRIVE_COUNT];
|
||||
int padCount; // --pad given more than once fills them in turn, like --disk.
|
||||
// Where to write what the controllers held, one byte a frame, in the format --pad reads.
|
||||
// Every pad or-ed together, because which one a controller lands on is the host's business
|
||||
// and a demo is a record of what somebody did.
|
||||
const char *padRecord;
|
||||
} EmulatorOptions;
|
||||
|
||||
uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options);
|
||||
|
||||
+297
-22
@@ -11,7 +11,44 @@
|
||||
// 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];
|
||||
// The two banks. Which one an address is in is a property of the address and never of the
|
||||
// mode: tiles and the palette are always in the atlas, the map and a bitmap always in the
|
||||
// screen. That is what makes the split cost nothing to think about at a call site.
|
||||
static uint8_t videoAtlas[VIDEO_MEMORY_BYTES];
|
||||
static uint8_t videoScreen[VIDEO_SCREEN_COUNT][VIDEO_MEMORY_BYTES];
|
||||
|
||||
// ---- Where the background was empty ----
|
||||
//
|
||||
// One byte a pixel, set while the map or the bitmap is drawn and read while the sprites are.
|
||||
// A sprite marked "behind" needs to know whether the thing already at a pixel was a picture
|
||||
// or a gap, and by the time it is drawn the pixel holds a colour rather than the index it
|
||||
// came from - the palette is not one to one, so two different indices can be the same
|
||||
// colour and asking the picture would get it wrong.
|
||||
//
|
||||
// Host memory, and it costs the machine nothing: it is scratch the device uses inside one
|
||||
// frame, exactly like the pixel buffer beside it.
|
||||
static uint8_t backgroundEmpty[VIDEO_MAX_WIDTH * VIDEO_MAX_HEIGHT];
|
||||
|
||||
// ---- Where a tile's art is ----
|
||||
//
|
||||
// The scheme nibble and the page bits live in the same byte and are asked of it in the same
|
||||
// breath, in the two places a tile is drawn from: a map cell and a sprite. One function, so
|
||||
// that the two cannot drift apart - which they would, because the sprite pass was written
|
||||
// three days after the map pass and neither is where the other is looked at.
|
||||
static const uint8_t *tileArt(uint8_t attribute, uint8_t tile) {
|
||||
const int page = (attribute & VIDEO_ATTRIBUTE_PAGE) >> VIDEO_ATTRIBUTE_SHIFT;
|
||||
return videoAtlas + VIDEO_TILE_BASE + page * VIDEO_TILE_PAGE_BYTES
|
||||
+ tile * VIDEO_TILE_BYTES;
|
||||
}
|
||||
|
||||
// Which screen is being shown. The console draws into THIS one rather than into a screen of
|
||||
// its own, so text goes where whoever is looking is looking - which matters most when the
|
||||
// text is a fault message printed over a game that had flipped.
|
||||
static uint8_t displayed = 0;
|
||||
|
||||
// How tall the window is and where it starts. Nought tall is no window.
|
||||
static uint8_t windowHeight = 0;
|
||||
static uint8_t windowAt = 0;
|
||||
|
||||
static uint8_t mode;
|
||||
// Which map row is drawn at the top. THE MAP IS A RING: rendering row r reads map row
|
||||
@@ -119,13 +156,17 @@ void videoTick(unsigned long now) {
|
||||
}
|
||||
|
||||
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);
|
||||
// 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 of a scheme mean ink and
|
||||
// paper.
|
||||
//
|
||||
// ONLY THE GLYPHS THE ROM HAS. Tile memory used to be cleared first, on the grounds that
|
||||
// a glyph the font does not have should be blank rather than whatever was there - which
|
||||
// was fine while this happened at reset and nothing else, and is wrong now that a program
|
||||
// can ask for it. A program that defined a tile of its own above the font and then wanted
|
||||
// its text back would have lost the tile to get it.
|
||||
for (int glyph = 0; glyph < CONSOLE_FONT_GLYPHS && glyph < VIDEO_TILE_COUNT; glyph++) {
|
||||
uint8_t *tile = videoRAM + VIDEO_TILE_BASE + glyph * VIDEO_TILE_BYTES;
|
||||
uint8_t *tile = videoAtlas + 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++) {
|
||||
@@ -133,7 +174,10 @@ void videoLoadFont(void) {
|
||||
}
|
||||
}
|
||||
}
|
||||
uint8_t *palette = videoRAM + VIDEO_PALETTE_BASE;
|
||||
}
|
||||
|
||||
void videoLoadPalette(void) {
|
||||
uint8_t *palette = videoAtlas + 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.
|
||||
@@ -158,7 +202,7 @@ void videoPutCell(int screenRow, int column, uint8_t tile, uint8_t attribute) {
|
||||
// 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
|
||||
uint8_t *cell = videoScreen[displayed] + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE
|
||||
+ mapColumn * VIDEO_CELL_BYTES;
|
||||
cell[0] = tile;
|
||||
cell[1] = attribute;
|
||||
@@ -171,11 +215,16 @@ void videoScrollUp(void) {
|
||||
// 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);
|
||||
memset(videoScreen[displayed] + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE, 0,
|
||||
VIDEO_MAP_STRIDE);
|
||||
}
|
||||
|
||||
void videoReset(void) {
|
||||
memset(videoRAM, 0, sizeof(videoRAM));
|
||||
memset(videoAtlas, 0, sizeof(videoAtlas));
|
||||
memset(videoScreen, 0, sizeof(videoScreen));
|
||||
displayed = 0;
|
||||
windowHeight = 0;
|
||||
windowAt = 0;
|
||||
mode = VIDEO_MODE_40x25;
|
||||
scroll = 0;
|
||||
scrollColumn = 0;
|
||||
@@ -187,14 +236,27 @@ void videoReset(void) {
|
||||
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.
|
||||
// A machine wakes up able to show text, and it does so by COPYING from the character
|
||||
// generator into ordinary video memory - which a program may overwrite the moment it
|
||||
// wants the screen for something else, and can ask back afterwards.
|
||||
videoLoadFont();
|
||||
videoLoadPalette();
|
||||
}
|
||||
|
||||
uint8_t *videoMemory(uint32_t *capacity) {
|
||||
uint8_t *videoMemory(uint8_t port, uint32_t *capacity) {
|
||||
*capacity = VIDEO_MEMORY_BYTES;
|
||||
return videoRAM;
|
||||
if (port == VIDEO_STATUS) {
|
||||
return videoAtlas;
|
||||
}
|
||||
if (port == VIDEO_SCREEN0) {
|
||||
return videoScreen[0];
|
||||
}
|
||||
if (port == VIDEO_SCREEN1) {
|
||||
return videoScreen[1];
|
||||
}
|
||||
// Every other port in the block owns no memory. Saying so is what stops a bank being
|
||||
// registered onto one of them and pointing at nothing.
|
||||
return NULL;
|
||||
}
|
||||
|
||||
uint8_t videoWrite(uint8_t value, uint8_t port) {
|
||||
@@ -207,6 +269,20 @@ uint8_t videoWrite(uint8_t value, uint8_t port) {
|
||||
mode = value;
|
||||
}
|
||||
break;
|
||||
case VIDEO_WINDOW_HEIGHT:
|
||||
windowHeight = value;
|
||||
break;
|
||||
case VIDEO_WINDOW_AT:
|
||||
windowAt = value;
|
||||
break;
|
||||
case VIDEO_DISPLAY:
|
||||
// A screen that does not exist is not taken, for the same reason a mode that
|
||||
// does not exist is not: whoever asked still has the screen they had, and
|
||||
// stopping the machine over it would be a poor trade.
|
||||
if (value < VIDEO_SCREEN_COUNT) {
|
||||
displayed = value;
|
||||
}
|
||||
break;
|
||||
case VIDEO_CONTROL:
|
||||
frameInterrupts = (value & VIDEO_CONTROL_FRAME) != 0;
|
||||
if (!frameInterrupts) {
|
||||
@@ -235,6 +311,25 @@ uint8_t videoWrite(uint8_t value, uint8_t port) {
|
||||
case VIDEO_FINE_Y:
|
||||
fineY = (uint8_t)(value & VIDEO_FINE_MASK);
|
||||
break;
|
||||
case VIDEO_COMMAND:
|
||||
// ---- Asking the character generator for its contents ----
|
||||
//
|
||||
// Written, and it happens at once - the same shape as the console's Command port
|
||||
// and the controller's, rather than a bit in a register that otherwise holds
|
||||
// state. There is nothing to read back: what a copy did is visible in the memory
|
||||
// it copied into.
|
||||
//
|
||||
// IN BITMAP MODE THE TILES ARE THE PICTURE, so asking for the font there draws
|
||||
// glyphs across the top of it. That is not a special case being ignored; it is
|
||||
// what the memory means in that mode, and a caller that wants text has to be in
|
||||
// a mode that has some.
|
||||
if (value & VIDEO_COMMAND_FONT) {
|
||||
videoLoadFont();
|
||||
}
|
||||
if (value & VIDEO_COMMAND_PALETTE) {
|
||||
videoLoadPalette();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// 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.
|
||||
@@ -245,6 +340,10 @@ uint8_t videoWrite(uint8_t value, uint8_t port) {
|
||||
|
||||
uint8_t videoRead(uint8_t port) {
|
||||
switch (port) {
|
||||
case VIDEO_DISPLAY:
|
||||
return displayed;
|
||||
case VIDEO_WINDOW_HEIGHT: return windowHeight;
|
||||
case VIDEO_WINDOW_AT: return windowAt;
|
||||
case VIDEO_STATUS: {
|
||||
uint8_t status = 0;
|
||||
if (frameWaiting) {
|
||||
@@ -272,6 +371,10 @@ uint8_t videoRead(uint8_t port) {
|
||||
case VIDEO_SCROLL_COLUMN: return scrollColumn;
|
||||
case VIDEO_FINE_X: return fineX;
|
||||
case VIDEO_FINE_Y: return fineY;
|
||||
case VIDEO_COMMAND:
|
||||
// Write only, like the console's. A device that does something when told does
|
||||
// not take instructions and hand out state through the same hole.
|
||||
return 0;
|
||||
// Asked rather than assumed. A program that wants to know how wide the screen is
|
||||
// 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);
|
||||
@@ -281,6 +384,159 @@ uint8_t videoRead(uint8_t port) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- The sprites, over whatever is already there ----
|
||||
//
|
||||
// BACKWARDS THROUGH THE TABLE, so that where two overlap the lower number comes out on top:
|
||||
// it is drawn last and writes over. Every entry is looked at, because the ones that draw
|
||||
// nothing say so in a byte and skipping them costs one test.
|
||||
static void drawSprites(uint8_t *pixels, int width, int height) {
|
||||
const uint8_t *palette = videoAtlas + VIDEO_PALETTE_BASE;
|
||||
const uint8_t *depths = videoAtlas + VIDEO_DEPTH_BASE;
|
||||
for (int n = VIDEO_SPRITE_COUNT - 1; n >= 0; n--) {
|
||||
const uint8_t *entry = videoAtlas + VIDEO_SPRITE_BASE + n * VIDEO_SPRITE_BYTES;
|
||||
const int wide = (entry[VIDEO_SPRITE_SIZE] >> 4) & 0x0F;
|
||||
const int tall = entry[VIDEO_SPRITE_SIZE] & 0x0F;
|
||||
if (wide == 0 || tall == 0) {
|
||||
continue;
|
||||
}
|
||||
// How big the art is, and how big it is being asked to look. Nought means the one
|
||||
// is the other, which is what every sprite written before scaling existed says.
|
||||
const int naturalWide = wide * VIDEO_CELL_PIXELS;
|
||||
const int naturalTall = tall * VIDEO_CELL_PIXELS;
|
||||
int drawWide = entry[VIDEO_SPRITE_WIDTH] | (entry[VIDEO_SPRITE_WIDTH + 1] << 8);
|
||||
int drawTall = entry[VIDEO_SPRITE_HEIGHT] | (entry[VIDEO_SPRITE_HEIGHT + 1] << 8);
|
||||
if (drawWide == 0) { drawWide = naturalWide; }
|
||||
if (drawTall == 0) { drawTall = naturalTall; }
|
||||
|
||||
// Signed, and low byte first like everything else this machine writes to a device.
|
||||
const int left = (int16_t)(uint16_t)(entry[VIDEO_SPRITE_X]
|
||||
| (entry[VIDEO_SPRITE_X + 1] << 8));
|
||||
const int top = (int16_t)(uint16_t)(entry[VIDEO_SPRITE_Y]
|
||||
| (entry[VIDEO_SPRITE_Y + 1] << 8));
|
||||
const uint8_t bank =
|
||||
(uint8_t)((entry[VIDEO_SPRITE_ATTRIBUTE] & VIDEO_ATTRIBUTE_SCHEME) << 4);
|
||||
const uint8_t flags = entry[VIDEO_SPRITE_FLAGS];
|
||||
const int mirrored = (flags & VIDEO_SPRITE_HFLIP) != 0;
|
||||
const int inverted = (flags & VIDEO_SPRITE_VFLIP) != 0;
|
||||
const int behind = (flags & VIDEO_SPRITE_BEHIND) != 0;
|
||||
const uint8_t depth = entry[VIDEO_SPRITE_DEPTH];
|
||||
|
||||
// ---- Only the part that lands on the screen is walked ----
|
||||
//
|
||||
// Clipped BEFORE the loop rather than inside it, which used to be enough and is not
|
||||
// any more: a target size is sixteen bits, so a sprite asked to be 60,000 pixels
|
||||
// tall would otherwise be sixty thousand turns of a loop that drew eight rows.
|
||||
const int fromDown = (top < 0) ? -top : 0;
|
||||
const int toDown = (drawTall < height - top) ? drawTall : height - top;
|
||||
const int fromAcross = (left < 0) ? -left : 0;
|
||||
const int toAcross = (drawWide < width - left) ? drawWide : width - left;
|
||||
|
||||
// ---- Walked over where it is GOING, not over where it came from ----
|
||||
//
|
||||
// Every destination pixel asks which source pixel it is showing, which is what makes
|
||||
// a stretch and a squash the same operation and needs no accumulator carried between
|
||||
// rows. It also makes flipping fall out: turning the source coordinate round mirrors
|
||||
// the tile ORDER and the pixels inside each tile in one step, where drawing tile by
|
||||
// tile had to be told to do both.
|
||||
for (int down = fromDown; down < toDown; down++) {
|
||||
const int atY = top + down;
|
||||
int sourceDown = (int)((long)down * naturalTall / drawTall);
|
||||
if (inverted) {
|
||||
sourceDown = naturalTall - 1 - sourceDown;
|
||||
}
|
||||
for (int across = fromAcross; across < toAcross; across++) {
|
||||
const int atX = left + across;
|
||||
int sourceAcross = (int)((long)across * naturalWide / drawWide);
|
||||
if (mirrored) {
|
||||
sourceAcross = naturalWide - 1 - sourceAcross;
|
||||
}
|
||||
// Which tile of the group, in reading order, and where inside it. Wrapping,
|
||||
// because a byte plus a byte is a byte and the tile number is one.
|
||||
const uint8_t tile = (uint8_t)(entry[VIDEO_SPRITE_TILE]
|
||||
+ (sourceDown / VIDEO_CELL_PIXELS) * wide
|
||||
+ (sourceAcross / VIDEO_CELL_PIXELS));
|
||||
const uint8_t *art = tileArt(entry[VIDEO_SPRITE_ATTRIBUTE], tile);
|
||||
const uint8_t pixel = art[(sourceDown % VIDEO_CELL_PIXELS) * VIDEO_CELL_PIXELS
|
||||
+ (sourceAcross % VIDEO_CELL_PIXELS)];
|
||||
// Nought is not a colour here, it is the absence of one, and it is tested
|
||||
// before the attribute is added so that it stays the same hole in all
|
||||
// sixteen schemes.
|
||||
if (pixel == 0) {
|
||||
continue;
|
||||
}
|
||||
if (behind && !backgroundEmpty[atY * width + atX]) {
|
||||
continue;
|
||||
}
|
||||
// ---- And whether anything nearer is already in this column ----
|
||||
//
|
||||
// Per column, which is the whole reason this is a buffer and not a number:
|
||||
// a billboard is in front of the wall at one end of itself and behind it at
|
||||
// the other, and no ordering of the table can say that.
|
||||
if (depth != 0) {
|
||||
const uint8_t there = depths[atX];
|
||||
if (there != 0 && depth >= there) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const uint8_t index = (uint8_t)(pixel + bank);
|
||||
const uint8_t *colour = palette + index * VIDEO_PALETTE_BYTES;
|
||||
uint8_t *out = pixels + (atY * width + atX) * 3;
|
||||
out[0] = colour[0];
|
||||
out[1] = colour[1];
|
||||
out[2] = colour[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- The window, over everything ----
|
||||
//
|
||||
// Drawn last, after the sprites, because a status bar is the thing nothing gets in front of.
|
||||
// A sprite that could cover the fuel gauge would be a bug in every game that had both.
|
||||
//
|
||||
// NO SCROLL AND NO FINE OFFSET. That is the entire feature: a window cell is at a screen
|
||||
// position, full stop, where a map cell is at a position in a world the screen is looking at
|
||||
// part of. The two coordinate systems are what the register block keeps apart.
|
||||
static void drawWindow(uint8_t *pixels, int width, int height, int columns) {
|
||||
if (windowHeight == 0) {
|
||||
return;
|
||||
}
|
||||
const uint8_t *palette = videoAtlas + VIDEO_PALETTE_BASE;
|
||||
for (int row = 0; row < windowHeight && row < VIDEO_WINDOW_ROWS; row++) {
|
||||
const int atRow = windowAt + row;
|
||||
if (atRow < 0 || atRow * VIDEO_CELL_PIXELS >= height) {
|
||||
continue;
|
||||
}
|
||||
const uint8_t *cells = videoScreen[displayed] + VIDEO_WINDOW_BASE
|
||||
+ row * VIDEO_MAP_STRIDE;
|
||||
for (int column = 0; column < columns; column++) {
|
||||
const uint8_t tile = cells[column * VIDEO_CELL_BYTES];
|
||||
const uint8_t attribute = cells[column * VIDEO_CELL_BYTES + 1];
|
||||
const uint8_t bank = (uint8_t)((attribute & VIDEO_ATTRIBUTE_SCHEME) << 4);
|
||||
const uint8_t *art = tileArt(attribute, tile);
|
||||
for (int y = 0; y < VIDEO_CELL_PIXELS; y++) {
|
||||
const int atY = atRow * VIDEO_CELL_PIXELS + y;
|
||||
if (atY >= height) {
|
||||
break;
|
||||
}
|
||||
for (int x = 0; x < VIDEO_CELL_PIXELS; x++) {
|
||||
const int atX = column * VIDEO_CELL_PIXELS + x;
|
||||
if (atX >= width) {
|
||||
break;
|
||||
}
|
||||
const uint8_t index =
|
||||
(uint8_t)(art[y * VIDEO_CELL_PIXELS + x] + bank);
|
||||
const uint8_t *entry = palette + index * VIDEO_PALETTE_BYTES;
|
||||
uint8_t *out = pixels + (atY * width + atX) * 3;
|
||||
out[0] = entry[0];
|
||||
out[1] = entry[1];
|
||||
out[2] = entry[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void videoRender(void) {
|
||||
if (mode == VIDEO_MODE_BITMAP) {
|
||||
// ---- A byte a pixel, and nothing in the way ----
|
||||
@@ -288,17 +544,22 @@ void videoRender(void) {
|
||||
// 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;
|
||||
const uint8_t *palette = videoAtlas + VIDEO_PALETTE_BASE;
|
||||
const uint8_t *from = videoScreen[displayed] + 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];
|
||||
backgroundEmpty[at] = (from[at] == 0);
|
||||
}
|
||||
renderedWidth = VIDEO_BITMAP_WIDTH;
|
||||
renderedHeight = VIDEO_BITMAP_HEIGHT;
|
||||
// Over a picture as much as over a map. A bitmap is what a program draws once and
|
||||
// leaves; sprites are what moves on top of it, and there is no reason the mode that
|
||||
// cannot afford to redraw itself should be the one that cannot have them.
|
||||
drawSprites(pixels, VIDEO_BITMAP_WIDTH, VIDEO_BITMAP_HEIGHT);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -319,7 +580,8 @@ void videoRender(void) {
|
||||
// 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;
|
||||
const uint8_t *cells = videoScreen[displayed] + 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];
|
||||
@@ -344,8 +606,8 @@ void videoRender(void) {
|
||||
// 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;
|
||||
const uint8_t bank = (uint8_t)((attribute & VIDEO_ATTRIBUTE_SCHEME) << 4);
|
||||
const uint8_t *art = tileArt(attribute, tile);
|
||||
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
|
||||
@@ -362,17 +624,30 @@ void videoRender(void) {
|
||||
// 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
|
||||
const uint8_t was = art[y * VIDEO_CELL_PIXELS + x];
|
||||
const uint8_t index = (uint8_t)(was + bank);
|
||||
const uint8_t *entry = videoAtlas + 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];
|
||||
// Before the nibble, so that a cell drawn in scheme five is empty in the
|
||||
// same places as the same cell drawn in scheme nought.
|
||||
backgroundEmpty[atY * width + atX] = (was == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ---- And then the things that move ----
|
||||
//
|
||||
// After the map and not woven into it, because a sprite is not tied to a cell: one can
|
||||
// sit across four of them, and a pass that drew each cell and then whatever overlapped it
|
||||
// would have to draw parts of the same sprite four times and get the order right between
|
||||
// them. Over the finished picture there is no order to get wrong.
|
||||
drawSprites(pixels, width, height);
|
||||
drawWindow(pixels, width, height, columns);
|
||||
|
||||
renderedWidth = width;
|
||||
renderedHeight = rows * VIDEO_CELL_PIXELS;
|
||||
}
|
||||
|
||||
+293
-10
@@ -19,20 +19,53 @@
|
||||
// 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 ----
|
||||
// ---- The device brings memory, in two banks ----
|
||||
//
|
||||
// One bank, registered the way the disk's buffer is, so it costs a program nothing in Data
|
||||
// Registered the way the disk's buffer is, so the screen 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
|
||||
// and the rest stays as it was, which is the whole reason this is memory rather than a
|
||||
// window onto a port.
|
||||
|
||||
//
|
||||
// TWO BANKS AND NOT ONE, because the two halves of a screen are written at completely
|
||||
// different rates. Tiles and colours are an ATLAS: put there when a program loads and then
|
||||
// left alone. The map is a SCREEN: rewritten as often as anything moves. Sharing one bank
|
||||
// made them compete for 64K they did not both need, and it had a worse consequence than
|
||||
// being cramped - a bitmap took the whole bank, so ENTERING BITMAP MODE DESTROYED THE FONT.
|
||||
// A program could not draw a picture and then say anything about it.
|
||||
//
|
||||
// Split, each gets a whole 64K and neither can tread on the other. A bitmap now overwrites
|
||||
// the map, which is the same memory meaning a different thing in a different mode and is
|
||||
// exactly what it should overwrite. The tiles behind the text survive it.
|
||||
//
|
||||
// Each bank is the same size, and every address below says which of the two it is in.
|
||||
#define VIDEO_MEMORY_BYTES 0x10000
|
||||
|
||||
// ---- In the ATLAS bank ----
|
||||
//
|
||||
// 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
|
||||
|
||||
// ---- Four pages of them ----
|
||||
//
|
||||
// A tile number is a byte and a byte reaches 256, which is not many once a font has taken
|
||||
// 135 of them and a game wants a character, a background and a wall. So two bits of the
|
||||
// ATTRIBUTE say which page of 256 the number is in - bits that were already there, already
|
||||
// written on every cell and every sprite, and reserved from the day the attribute was
|
||||
// defined for exactly this.
|
||||
//
|
||||
// Four pages of 16K is 64K, which is the whole atlas, so THE FOURTH PAGE IS THE MEMORY THE
|
||||
// SPRITE TABLE AND THE PALETTE ARE IN. That is not a hole in the design, it is the same
|
||||
// answer shared video memory has always given: the atlas is 1024 tiles, and what a program
|
||||
// spends on sprites and colours comes out of them. A program that wants no sprites may have
|
||||
// page three for art, and one that wants sprites has 768 tiles and knows why.
|
||||
//
|
||||
// The page is per CELL and per SPRITE rather than a mode, so a screen can show tiles from
|
||||
// all four at once and a program never has to decide which page it is "in".
|
||||
#define VIDEO_TILE_PAGE_BYTES (VIDEO_TILE_COUNT * VIDEO_TILE_BYTES)
|
||||
#define VIDEO_TILE_PAGES 4
|
||||
|
||||
// ---- 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
|
||||
@@ -43,6 +76,11 @@
|
||||
//
|
||||
// It also frees the geometry from having to be a power of two, which is what lets the
|
||||
// pixel resolution be whatever looks right.
|
||||
//
|
||||
// IN THE SCREEN BANK, and still at 0x4000 rather than at the bottom of a bank it now has to
|
||||
// itself. Moving it would have been tidier and would have meant changing which bank a
|
||||
// program registers AND which address it writes to in the same breath - so a screen that
|
||||
// came out wrong would have had two possible causes. The address costs nothing where it is.
|
||||
#define VIDEO_MAP_BASE 0x4000
|
||||
#define VIDEO_MAP_STRIDE 256
|
||||
#define VIDEO_MAP_ROWS 128
|
||||
@@ -51,6 +89,16 @@
|
||||
// Two bytes to a cell: which tile, and how to colour it.
|
||||
#define VIDEO_CELL_BYTES 2
|
||||
|
||||
// ---- What the attribute byte means, in both places it appears ----
|
||||
//
|
||||
// A map cell's second byte and a sprite's byte 1 are the same thing and are read the same
|
||||
// way, which is what lets the same art be a background in one place and a moving thing in
|
||||
// another with nothing rewritten.
|
||||
#define VIDEO_ATTRIBUTE_SCHEME 0x0F
|
||||
#define VIDEO_ATTRIBUTE_PAGE 0x30
|
||||
#define VIDEO_ATTRIBUTE_SHIFT 4
|
||||
// Bits 6 and 7 are still reserved and should be left at nought.
|
||||
|
||||
// ---- 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
|
||||
@@ -59,25 +107,190 @@
|
||||
// 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.
|
||||
// screen; it stops calling it a text screen. Coming back finds the map holding whatever the
|
||||
// picture put there, which is what taking the screen means.
|
||||
//
|
||||
// IN THE SCREEN BANK, so what a picture costs is the map and nothing else. THE TILES AND THE
|
||||
// PALETTE ARE IN THE OTHER BANK AND SURVIVE IT, which is what lets a program draw a picture
|
||||
// and then put text back on the screen without reloading the character generator first.
|
||||
#define VIDEO_BITMAP_BASE 0x0000
|
||||
#define VIDEO_BITMAP_WIDTH 320
|
||||
#define VIDEO_BITMAP_HEIGHT 200
|
||||
|
||||
// ---- Sprites ----
|
||||
//
|
||||
// Things that move without the map moving. A map cell is where it is, and a program that
|
||||
// wanted something between two cells had to redraw both of them; a sprite is put at a PIXEL
|
||||
// and the device draws it over whatever is behind.
|
||||
//
|
||||
// MADE OF TILES, which is the decision the rest follows from. A sprite is m by n tiles taken
|
||||
// in reading order from one index, so it needs no pixel format of its own, no second kind of
|
||||
// memory, and no way for its art to be anything the map could not also show. A 16 by 16
|
||||
// character is four tiles and a program that wants it in the background too just names the
|
||||
// same four.
|
||||
//
|
||||
// The table is 256 entries of 16 bytes. Sixteen so that entry n begins at n times sixteen,
|
||||
// which is a shift - the same no-multiply argument that makes a palette entry four bytes and
|
||||
// a map row a page.
|
||||
//
|
||||
// It was eight, and grew when scaling arrived: four bytes for a target size and one for a
|
||||
// depth would not fit beside what was already there. Grown NOW rather than later, because
|
||||
// the cost of moving it is a rebuild of the two programs that use it, and the cost of moving
|
||||
// it once somebody has written a game on top of it is not.
|
||||
#define VIDEO_SPRITE_BASE 0xC000
|
||||
#define VIDEO_SPRITE_COUNT 256
|
||||
#define VIDEO_SPRITE_BYTES 16
|
||||
|
||||
// Byte 0 is the top left tile, byte 1 the attribute, which means what a map cell's attribute
|
||||
// means: its low nibble times sixteen is added to every index in the art.
|
||||
#define VIDEO_SPRITE_TILE 0
|
||||
#define VIDEO_SPRITE_ATTRIBUTE 1
|
||||
|
||||
// Bytes 2 to 5, low byte first, and SIGNED - a screen is 640 by 400 in the larger mode, so
|
||||
// neither axis fits in a byte, and a sprite has to be able to sit half off the left or the
|
||||
// top rather than appearing whole at the edge.
|
||||
#define VIDEO_SPRITE_X 2
|
||||
#define VIDEO_SPRITE_Y 4
|
||||
|
||||
// Byte 6: how many tiles across in the high nibble, how many down in the low. Fifteen each
|
||||
// way, so 120 by 120 pixels.
|
||||
//
|
||||
// A SPRITE OF NO WIDTH OR NO HEIGHT DRAWS NOTHING, and that is the off switch. It saves a
|
||||
// flag, it is per sprite rather than a global the whole table shares, and it means the table
|
||||
// is already off when the machine starts, since the atlas wakes up cleared.
|
||||
//
|
||||
// Deliberately the OPPOSITE of what a length of zero means to the memory controller, where
|
||||
// it means the whole 64K. The reason is the same both times: moving no bytes is a useless
|
||||
// thing to ask for, so zero was free to mean something else there - and drawing no sprite is
|
||||
// the commonest state in this table, so zero has to mean nothing here.
|
||||
#define VIDEO_SPRITE_SIZE 6
|
||||
|
||||
// Byte 7.
|
||||
#define VIDEO_SPRITE_FLAGS 7
|
||||
#define VIDEO_SPRITE_HFLIP 0x01
|
||||
#define VIDEO_SPRITE_VFLIP 0x02
|
||||
// Drawn only where the background had nothing, so a thing can walk behind a pillar. See
|
||||
// below for what "nothing" means.
|
||||
#define VIDEO_SPRITE_BEHIND 0x04
|
||||
|
||||
// ---- How big to draw it, which is not the same as how big it is ----
|
||||
//
|
||||
// Bytes 8 to 11: a target width and a target height in PIXELS, low byte first. The device
|
||||
// stretches the m by n tiles to fill that, so a program says how big the thing should look
|
||||
// and never works out a ratio.
|
||||
//
|
||||
// A TARGET IN PIXELS RATHER THAN A MULTIPLIER, which is the whole of why this is usable on
|
||||
// a machine with no divide. A billboard at distance d wants to be k/d pixels tall, and that
|
||||
// is a number the program has anyway - out of a lookup table, most likely. A multiplier
|
||||
// would have to be a fixed point fraction, computed by dividing, which is the one thing
|
||||
// this CPU cannot do.
|
||||
//
|
||||
// NOUGHT MEANS NATURAL SIZE, eight times the tile count on that axis. So every sprite
|
||||
// written before scaling existed still means what it meant, and the common case - a thing
|
||||
// drawn at the size it was drawn at - costs nothing to say.
|
||||
#define VIDEO_SPRITE_WIDTH 8
|
||||
#define VIDEO_SPRITE_HEIGHT 10
|
||||
|
||||
// ---- And how far away it is ----
|
||||
//
|
||||
// Byte 12. Nought means no depth test at all, which is what every ordinary sprite wants and
|
||||
// what a cleared table already says.
|
||||
//
|
||||
// Otherwise it is compared against the DEPTH BUFFER below, one column at a time, and the
|
||||
// sprite draws only in the columns it is in front of. That is the thing table order cannot
|
||||
// do: a billboard can be nearer than the wall in one column and further in the next, and no
|
||||
// amount of sorting the table expresses that.
|
||||
#define VIDEO_SPRITE_DEPTH 12
|
||||
// Bytes 13 to 15 are reserved and should be left at nought.
|
||||
|
||||
// ---- The depth buffer ----
|
||||
//
|
||||
// One byte a screen column, written by the program and read by the device. A wall pass says
|
||||
// how far away the thing it drew in each column was; a sprite with a depth says how far away
|
||||
// it is; and a sprite pixel is drawn only where it is nearer.
|
||||
//
|
||||
// NOUGHT IN A COLUMN MEANS NOTHING IS THERE, so a program that never writes this has a
|
||||
// buffer of noughts and every sprite draws - which is the behaviour there was before it
|
||||
// existed. It is not cleared between frames: it belongs to the program, and a program that
|
||||
// draws walls rewrites all of it every frame anyway.
|
||||
//
|
||||
// It is in the atlas because that is where the things a program sets up live, and because
|
||||
// the alternative is a port and this is 640 bytes.
|
||||
#define VIDEO_DEPTH_BASE 0xD000
|
||||
|
||||
// ---- What a sprite does not cover ----
|
||||
//
|
||||
// A PIXEL OF ZERO IS NOT DRAWN. Without that every sprite is a rectangle, and there is no
|
||||
// other candidate: the font already uses index 0 for paper, so it is the value art in this
|
||||
// machine has always left empty.
|
||||
//
|
||||
// Tested BEFORE the attribute is added, so it is a property of the art and not of the colour
|
||||
// scheme it is being shown in. A sprite drawn in indices 1 to 15 is transparent in the same
|
||||
// places in all sixteen schemes, which is the whole point of the additive nibble.
|
||||
//
|
||||
// The same rule read the other way is what "behind" means: a sprite marked behind draws only
|
||||
// where the BACKGROUND pixel was zero. One rule, applied to whichever layer is in front.
|
||||
//
|
||||
// ---- How many at once ----
|
||||
//
|
||||
// All of them. Every entry in the table is drawn every frame, so sprites cannot flicker.
|
||||
// Real machines dropped them per scanline because they had a fixed number of shift registers
|
||||
// and a fixed time to fill them; this has a loop. The limit is how many entries there are,
|
||||
// which is a constant a program can count on rather than a property of what it happens to be
|
||||
// drawing this frame.
|
||||
//
|
||||
// Where they overlap, THE LOWER NUMBER IS IN FRONT.
|
||||
|
||||
// ---- 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.
|
||||
// At the TOP of the ATLAS bank, 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 it
|
||||
// is written when a program loads rather than per frame, which is what the atlas is for.
|
||||
//
|
||||
// Being out of the screen bank is what leaves a bitmap the WHOLE of one: 64,000 bytes of
|
||||
// picture in 65,536, with nothing it has to dodge.
|
||||
#define VIDEO_PALETTE_BASE 0xFC00
|
||||
#define VIDEO_PALETTE_BYTES 4
|
||||
#define VIDEO_PALETTE_SIZE 256
|
||||
|
||||
// ---- The window: a layer that does not scroll ----
|
||||
//
|
||||
// A status bar over a playfield. The map moves and this does not, which the map alone cannot
|
||||
// express: the scroll registers move ALL of it, so a score printed into the map is a score
|
||||
// that slides away, and one printed into the rows the view happens to be showing is one that
|
||||
// jumps a pixel at a time as the fine offset changes.
|
||||
//
|
||||
// ITS OWN MEMORY, and that is the whole argument. The first design drew the top rows of the
|
||||
// MAP without the scroll applied, which needs no new memory and costs one register - and
|
||||
// makes those rows part of the playfield's ring, so a game that scrolls vertically has to
|
||||
// route its world around its own scoreboard for ever. The whole point of a status bar is that
|
||||
// it is not somewhere in the level.
|
||||
//
|
||||
// 0xC000 to 0xFFFF in the screen bank, which the map does not reach: it ends at 0xBFFF. Sixty
|
||||
// four rows of the same 256 byte cells, so a window row is a page like a map row is, and a
|
||||
// cell in it means exactly what a cell in the map means - same tiles, same pages, same
|
||||
// colour schemes.
|
||||
//
|
||||
// PER SCREEN, because it is in the screen bank: flipping to the other buffer flips the status
|
||||
// bar with it, which is what a double buffered game wants and would be surprising the other
|
||||
// way round.
|
||||
#define VIDEO_WINDOW_BASE 0xC000
|
||||
#define VIDEO_WINDOW_ROWS 64
|
||||
|
||||
// How many screen rows tall it is, and which screen row it starts at. NOUGHT IS OFF, so a
|
||||
// machine that wakes up with a cleared screen has no window and every program written before
|
||||
// this one means what it meant.
|
||||
//
|
||||
// A start row is worth a register because a status bar at the bottom is as common as one at
|
||||
// the top, and working it out from the screen height is a thing every program would have to
|
||||
// do again.
|
||||
#define VIDEO_WINDOW_HEIGHT 0x3D
|
||||
#define VIDEO_WINDOW_AT 0x3E
|
||||
|
||||
// ---- Modes ----
|
||||
//
|
||||
// Both are 8x8 cells over the same engine; only how many of them differ. The pixel count
|
||||
@@ -124,6 +337,63 @@ int videoTextRows(void);
|
||||
#define VIDEO_FINE_X 0x37
|
||||
#define VIDEO_FINE_Y 0x38
|
||||
|
||||
// ---- The character generator, which is a chip and not a memory that remembers ----
|
||||
//
|
||||
// The font and the sixteen colour schemes used to be WRITTEN INTO VIDEO RAM at reset, and
|
||||
// that was the one piece of magic left in this device: RAM does not wake up with anything
|
||||
// in it. It looked harmless until something needed the font BACK - a program that redefines
|
||||
// a tile has overwritten a glyph, and there was nowhere to get it from, because the only
|
||||
// copy was the one that had just been drawn over.
|
||||
//
|
||||
// So the device has a ROM, the way the machines this one is pretending to be really did,
|
||||
// and the copy into RAM is a thing it DOES rather than a state it mysteriously starts in.
|
||||
// Reset performs it, and a program can ask for it again.
|
||||
//
|
||||
// THE RAM IS STILL RAM. A program may overwrite every glyph and every colour, and should be
|
||||
// able to: that is what makes a tile engine a tile engine rather than a text display. What
|
||||
// has changed is that doing so is no longer a one way door.
|
||||
//
|
||||
// A SYSTEM THAT WANTS ITS OWN FONT still loads one over the top. This is the floor, not the
|
||||
// policy - it is what makes a machine with no disk able to say so, and what lets a program
|
||||
// with no system behind it put readable text on a screen.
|
||||
#define VIDEO_COMMAND 0x39
|
||||
// Copy the 135 glyphs back into the tiles they live in, leaving every other tile alone.
|
||||
#define VIDEO_COMMAND_FONT 0x01
|
||||
// Copy the sixteen ink and paper pairs back, leaving the rest of the palette alone.
|
||||
#define VIDEO_COMMAND_PALETTE 0x02
|
||||
|
||||
// ---- The ports that own banks ----
|
||||
//
|
||||
// A bank is registered by naming THE PORT THAT OWNS IT, which the controller settled long
|
||||
// before the screen had more than one. So a device with several banks needs several ports
|
||||
// that own memory, and needs no new mechanism at all.
|
||||
//
|
||||
// The base port keeps the atlas rather than a screen because tiles have been at 0x0000 since
|
||||
// there was a screen at all, and whichever way round that went, one of the two meanings had
|
||||
// to move. Nothing is READ OR WRITTEN at any of these - they are names for banks, and the
|
||||
// registry is where a program finds out which of them bring one.
|
||||
#define VIDEO_SCREEN0 0x3A
|
||||
#define VIDEO_SCREEN1 0x3B
|
||||
|
||||
// ---- Two screens, and only one register to say which ----
|
||||
//
|
||||
// A back buffer is a whole screen's worth of map written where nobody can see it, and then
|
||||
// shown all at once. It is what stops a picture being seen half finished - a game that moves
|
||||
// forty sprites and rewrites the map underneath them is not finished being wrong until the
|
||||
// last of them has been put right.
|
||||
//
|
||||
// THE DEVICE ONLY NEEDS TO KNOW WHICH IS DISPLAYED. Real machines needed a second register
|
||||
// saying which one the CPU's window pointed at; there is no window here, because a program
|
||||
// reaches a bank through the memory controller BY ITS NUMBER. Writing to the one that is not
|
||||
// being shown is a matter of naming its bank, and the screen never has to be told.
|
||||
//
|
||||
// A FLIP CANNOT TEAR. A frame is drawn from one bank in one go, so a flip either happened
|
||||
// before that frame or it happens before the next one; there is no state of having flipped
|
||||
// halfway. That is worth saying because it is the thing the hardware this imitates had to
|
||||
// work for, with an interrupt and a register written in the few lines between frames.
|
||||
#define VIDEO_DISPLAY 0x3C
|
||||
#define VIDEO_SCREEN_COUNT 2
|
||||
|
||||
// Eight pixels to a cell, so three bits say where inside one the view begins.
|
||||
#define VIDEO_FINE_MASK 0x07
|
||||
|
||||
@@ -163,7 +433,16 @@ void videoReset(void);
|
||||
//
|
||||
// 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.
|
||||
// Copies the character generator ROM into the tiles the console draws from, and the default
|
||||
// schemes into the palette entries they live in. Reset does both; the Command port is how a
|
||||
// program asks for either afterwards.
|
||||
//
|
||||
// NEITHER CLEARS WHAT IT DOES NOT OWN. The font writes glyphs 0 to 134 and stops, so a tile
|
||||
// a program defined above them survives; the palette writes the two entries of each of the
|
||||
// sixteen schemes and stops, so a program's own colours in between survive. Asking for the
|
||||
// font back should not cost a program the tile it was drawing with.
|
||||
void videoLoadFont(void);
|
||||
void videoLoadPalette(void);
|
||||
|
||||
// ---- The cursor ----
|
||||
//
|
||||
@@ -191,7 +470,11 @@ void videoPutCell(int screenRow, int column, uint8_t tile, uint8_t attribute);
|
||||
// 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);
|
||||
// The memory behind one of the device's memory-owning ports: VIDEO_STATUS for the atlas,
|
||||
// VIDEO_SCREEN0 and VIDEO_SCREEN1 for the two screens. NULL for any other port, because the
|
||||
// rest of the block owns no memory and registering a bank onto one would put a number in the
|
||||
// table that leads nowhere.
|
||||
uint8_t *videoMemory(uint8_t port, uint32_t *capacity);
|
||||
|
||||
uint8_t videoWrite(uint8_t value, uint8_t port);
|
||||
uint8_t videoRead(uint8_t port);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include "machine.h"
|
||||
#include "video.h"
|
||||
#include "pad.h"
|
||||
#include "sound.h"
|
||||
#include "io.h"
|
||||
#include "utility.h"
|
||||
@@ -106,6 +107,73 @@ static int keyTake(void) {
|
||||
}
|
||||
|
||||
// Everything Raylib has, taken before it can throw any of it away.
|
||||
// ---- The pads, read as levels ----
|
||||
//
|
||||
// Once a frame, from whatever is actually there. THIS IS THE ONE THING THE CONSOLE CANNOT DO:
|
||||
// a window knows which keys are down, a terminal only ever learns which one was pressed, and
|
||||
// asking the console to report a key coming up would have been a promise it could keep behind
|
||||
// a window and nowhere else.
|
||||
//
|
||||
// The keyboard drives pad nought as well as any real controller, so a game written for a pad
|
||||
// is playable on a machine with none - and so is a game written for four, badly. What a game
|
||||
// reads is the pad; it never learns which one of them somebody used.
|
||||
static void readPads(void) {
|
||||
for (int n = 0; n < PAD_COUNT; n++) {
|
||||
uint8_t held = 0;
|
||||
// ---- Pad nought is always there, because the keyboard is behind it ----
|
||||
//
|
||||
// Which is the useful answer rather than the literal one: a game that asks for a
|
||||
// controller and finds none falls back to whatever the console can tell it, and
|
||||
// behind a window the console is the worse of the two ways to read the same keys.
|
||||
int there = (n == 0);
|
||||
if (IsGamepadAvailable(n)) {
|
||||
there = 1;
|
||||
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_LEFT_FACE_RIGHT)) { held |= PAD_RIGHT; }
|
||||
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_LEFT_FACE_LEFT)) { held |= PAD_LEFT; }
|
||||
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_LEFT_FACE_DOWN)) { held |= PAD_DOWN; }
|
||||
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_LEFT_FACE_UP)) { held |= PAD_UP; }
|
||||
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_RIGHT_FACE_DOWN)) { held |= PAD_A; }
|
||||
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT)){ held |= PAD_B; }
|
||||
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_MIDDLE_RIGHT)) { held |= PAD_START; }
|
||||
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_MIDDLE_LEFT)) { held |= PAD_SELECT; }
|
||||
|
||||
// ---- And the stick, which is what most people actually push ----
|
||||
//
|
||||
// The four buttons above are the D-PAD, and a lot of controllers made this
|
||||
// century have one that nobody uses: the thumb goes on the stick, which reports
|
||||
// as an axis and not as a button, so a pad that was plugged in and working did
|
||||
// nothing at all.
|
||||
//
|
||||
// Past halfway counts as held. That is a blunt line and the right kind of blunt
|
||||
// for a device that reports what is DOWN - a machine with eight bits a pad has
|
||||
// nothing to say about three-fifths of a push, and picking the threshold here
|
||||
// rather than in every program is the point of the pad being a device.
|
||||
const float across = GetGamepadAxisMovement(n, GAMEPAD_AXIS_LEFT_X);
|
||||
const float down = GetGamepadAxisMovement(n, GAMEPAD_AXIS_LEFT_Y);
|
||||
if (across > 0.5f) { held |= PAD_RIGHT; }
|
||||
if (across < -0.5f) { held |= PAD_LEFT; }
|
||||
if (down > 0.5f) { held |= PAD_DOWN; }
|
||||
if (down < -0.5f) { held |= PAD_UP; }
|
||||
}
|
||||
if (n == 0) {
|
||||
// ---- And the keyboard, on top of it ----
|
||||
//
|
||||
// Arrows or WASD for the direction, Z and X for the buttons. OR-ed with whatever
|
||||
// a real pad is doing rather than chosen between, so unplugging one mid-game does
|
||||
// not leave somebody holding nothing.
|
||||
if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) { held |= PAD_RIGHT; }
|
||||
if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) { held |= PAD_LEFT; }
|
||||
if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) { held |= PAD_DOWN; }
|
||||
if (IsKeyDown(KEY_UP) || IsKeyDown(KEY_W)) { held |= PAD_UP; }
|
||||
if (IsKeyDown(KEY_Z)) { held |= PAD_A; }
|
||||
if (IsKeyDown(KEY_X)) { held |= PAD_B; }
|
||||
if (IsKeyDown(KEY_ENTER)) { held |= PAD_START; }
|
||||
if (IsKeyDown(KEY_TAB)) { held |= PAD_SELECT; }
|
||||
}
|
||||
padSet(n, there, held);
|
||||
}
|
||||
}
|
||||
|
||||
static void drainKeyboard(void) {
|
||||
int character;
|
||||
while ((character = GetCharPressed()) > 0) {
|
||||
@@ -276,6 +344,7 @@ static void presentFrame(void) {
|
||||
EndDrawing();
|
||||
// EndDrawing has just polled, which is the one moment Raylib's queues hold anything.
|
||||
drainKeyboard();
|
||||
readPads();
|
||||
checkResetButton();
|
||||
}
|
||||
|
||||
@@ -387,6 +456,18 @@ int main(int argc, char *argv[]) {
|
||||
// to find out that a default was left as it was found.
|
||||
SetExitKey(KEY_NULL);
|
||||
|
||||
// ---- What controllers the host can see, said out loud ----
|
||||
//
|
||||
// A pad that is plugged in and does nothing is indistinguishable from a pad the front
|
||||
// end never noticed, and the difference is the whole of what to do about it: one is a
|
||||
// mapping to fix and the other is a driver. Saying which at startup costs a line and
|
||||
// answers it without anybody having to guess.
|
||||
for (int n = 0; n < PAD_COUNT; n++) {
|
||||
if (IsGamepadAvailable(n)) {
|
||||
printf("Controller %d: %s\n", n, GetGamepadName(n));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- And a speaker, if the host has one ----
|
||||
//
|
||||
// Asked for rather than assumed: a machine with no audio device is a perfectly good
|
||||
@@ -406,9 +487,20 @@ int main(int argc, char *argv[]) {
|
||||
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.
|
||||
// ---- The keyboard becomes the console's input ----
|
||||
//
|
||||
// In place of a standard input the window does not have. UNLESS A KEYBOARD FILE WAS
|
||||
// NAMED, which used to be overridden here without a word: machineStart installs the
|
||||
// file's hook and this replaced it, so --keyboard was a flag that did nothing behind
|
||||
// a window and said nothing about it.
|
||||
//
|
||||
// Both together is the combination a demo wants. Recording a flight needs the typing
|
||||
// that STARTS it to be the same every time - a human reaching the shell at a slightly
|
||||
// different moment shifts every frame of the recording after it - while the flying
|
||||
// itself comes from whatever is actually in somebody's hands.
|
||||
if (options.keyboard == NULL) {
|
||||
consoleSetInputHook(voyagerKey);
|
||||
}
|
||||
// ---- A slice a frame ----
|
||||
//
|
||||
// The machine gets its turn, then the window gets its turn. Closing the window stops
|
||||
|
||||
+112
-2
@@ -23,7 +23,7 @@
|
||||
static const char *const ruleNames[] = {
|
||||
"redundant-setd", "redundant-assignment", "pointer-offset", "redundant-ccf",
|
||||
"known-branch", "zero-load", "dead-assignment", "q-through-stack", "self-push-pop",
|
||||
"branch-to-next", "unreachable", "dead-suppression",
|
||||
"branch-to-next", "unreachable", "dead-suppression", "falls-into-subroutine",
|
||||
};
|
||||
#define RULE_COUNT ((int)(sizeof(ruleNames) / sizeof(ruleNames[0])))
|
||||
|
||||
@@ -329,7 +329,76 @@ static int stopsFallthrough(const SourceInstruction *instruction) {
|
||||
|| strcmp(instruction->mnemonic, "RET") == 0
|
||||
|| strcmp(instruction->mnemonic, "RRET") == 0
|
||||
|| strcmp(instruction->mnemonic, "RETI") == 0
|
||||
|| strcmp(instruction->mnemonic, "HALT") == 0;
|
||||
// SRET was missing here, which is a gap in every rule that asks what reaches an
|
||||
// instruction: it returns from a handler exactly as RET returns from a call.
|
||||
|| strcmp(instruction->mnemonic, "SRET") == 0
|
||||
|| strcmp(instruction->mnemonic, "HALT") == 0
|
||||
// ---- The one trap that does not come back ----
|
||||
//
|
||||
// A SWI is a call and almost every one of them returns, so a SWI does not end a run
|
||||
// of code. osExit is the exception and the exception matters: it is how a loaded
|
||||
// program gives the machine back, so nothing after it runs, and every program on
|
||||
// this machine ends with it and then writes its helper routines underneath.
|
||||
//
|
||||
// NAMED RATHER THAN NUMBERED, because that is what the source says. This is the only
|
||||
// name from the system that this tool knows, and it is here because without it the
|
||||
// rule below would report every well written program in the repository.
|
||||
|| (strcmp(instruction->mnemonic, "SWI") == 0
|
||||
&& strcmp(instruction->operand, "osExit") == 0);
|
||||
}
|
||||
|
||||
// ---- Which labels are entered by a CALL ----
|
||||
//
|
||||
// Collected in a pass of its own before anything is judged, because the rule below asks a
|
||||
// question about a label that cannot be answered until the whole file has been read: a
|
||||
// subroutine is very often called from further down than it is written.
|
||||
#define CALL_TARGET_CAPACITY 2048
|
||||
static char callTargets[CALL_TARGET_CAPACITY][TOKEN_CAPACITY];
|
||||
static int callTargetCount = 0;
|
||||
|
||||
static int isCallTarget(const char *name) {
|
||||
for (int i = 0; i < callTargetCount; i++) {
|
||||
if (strcmp(callTargets[i], name) == 0) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Reads the file once for nothing but CALL and RCAL operands. Answers 0 if there were more
|
||||
// distinct ones than there is room for, which is reported rather than quietly truncated: a
|
||||
// rule that silently forgot half its input would go quiet instead of wrong, which is worse.
|
||||
static int collectCallTargets(const char *path) {
|
||||
callTargetCount = 0;
|
||||
FILE *file = fopen(path, "r");
|
||||
if (!file) {
|
||||
return 1; // The caller opens it again and reports the failure properly.
|
||||
}
|
||||
char line[LINE_CAPACITY];
|
||||
int lineNumber = 0;
|
||||
while (fgets(line, sizeof(line), file)) {
|
||||
lineNumber++;
|
||||
SourceInstruction current;
|
||||
if (parseInstruction(line, lineNumber, ¤t) != SOURCE_INSTRUCTION) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(current.mnemonic, "CALL") != 0 && strcmp(current.mnemonic, "RCAL") != 0) {
|
||||
continue;
|
||||
}
|
||||
if (!current.operand[0] || isCallTarget(current.operand)) {
|
||||
continue;
|
||||
}
|
||||
if (callTargetCount >= CALL_TARGET_CAPACITY) {
|
||||
fclose(file);
|
||||
fprintf(stderr, "%s: error: more than %d distinct call targets\n",
|
||||
path, CALL_TARGET_CAPACITY);
|
||||
return 0;
|
||||
}
|
||||
snprintf(callTargets[callTargetCount], TOKEN_CAPACITY, "%s", current.operand);
|
||||
callTargetCount++;
|
||||
}
|
||||
fclose(file);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int isDirectBranch(const SourceInstruction *instruction) {
|
||||
@@ -699,12 +768,21 @@ static int lintFile(const char *path) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!collectCallTargets(path)) {
|
||||
fclose(file);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char line[LINE_CAPACITY];
|
||||
int lineNumber = 0;
|
||||
int warnings = 0;
|
||||
int havePrevious = 0;
|
||||
int fallthroughStopped = 0;
|
||||
int havePendingBranch = 0;
|
||||
// Whether anything at all has been read since the last #Program or #Data, and whether
|
||||
// the run of code we are in began at a label something calls. See the rule below.
|
||||
int sawInstruction = 0;
|
||||
int runHasCallTarget = 0;
|
||||
SourceInstruction pendingBranch;
|
||||
KnownRegisters known = {0};
|
||||
KnownPointer pointers[DATA_POINTERS] = {0};
|
||||
@@ -751,6 +829,33 @@ static int lintFile(const char *path) {
|
||||
"remove the branch");
|
||||
havePendingBranch = 0;
|
||||
}
|
||||
// ---- Walking into a subroutine instead of calling it ----
|
||||
//
|
||||
// The code above ends without going anywhere, and the label below is one
|
||||
// something CALLs. So execution walks into the subroutine, reaches its RET,
|
||||
// and returns to whatever the Stack happens to hold - which is not a caller,
|
||||
// because nobody called. It goes somewhere nobody named.
|
||||
//
|
||||
// THAT IS WHAT FORMATTED A DISK. CosmOS's monitor had no branch at the end
|
||||
// of its command list, so an unrecognised word walked into sayPrompt; the
|
||||
// RET at the bottom of it went to whatever was on the Stack, and one of the
|
||||
// places that turned out to be was inside the filesystem's format routine.
|
||||
// The symptom is nowhere near the cause and changes with the Stack, which is
|
||||
// exactly the kind of fault worth spending a rule on.
|
||||
//
|
||||
// UNLESS THE RUN WE ARE IN WAS ITSELF CALLED. Falling out of one subroutine
|
||||
// into another is an ordinary tail call: the RET returns to the outer
|
||||
// caller, which is real and is what the author meant. So this only fires
|
||||
// when nothing since the last RET or branch was a call target either.
|
||||
if (!fallthroughStopped && sawInstruction && !runHasCallTarget
|
||||
&& current.spelling[0] && isCallTarget(current.spelling)) {
|
||||
warnings += warning(path, "falls-into-subroutine", current.line,
|
||||
"execution walks into a subroutine nothing here called",
|
||||
"branch past it, or end the code above with a branch or a return");
|
||||
}
|
||||
if (current.spelling[0] && isCallTarget(current.spelling)) {
|
||||
runHasCallTarget = 1;
|
||||
}
|
||||
fallthroughStopped = 0;
|
||||
forgetRegisters(&known);
|
||||
forgetPointers(pointers);
|
||||
@@ -758,6 +863,8 @@ static int lintFile(const char *path) {
|
||||
} else if (sourceLine == SOURCE_BOUNDARY) {
|
||||
fallthroughStopped = 0;
|
||||
havePendingBranch = 0;
|
||||
sawInstruction = 0;
|
||||
runHasCallTarget = 0;
|
||||
forgetRegisters(&known);
|
||||
forgetPointers(pointers);
|
||||
carry = CARRY_UNKNOWN;
|
||||
@@ -778,8 +885,11 @@ static int lintFile(const char *path) {
|
||||
warnings += lintKnownPointers(path, ¤t, pointers);
|
||||
warnings += lintKnownCarry(path, ¤t, carry);
|
||||
warnings += lintInstruction(path, havePrevious ? &previous : &empty, ¤t);
|
||||
sawInstruction = 1;
|
||||
if (stopsFallthrough(¤t)) {
|
||||
fallthroughStopped = 1;
|
||||
// A new run of code starts after this, and it has not been called yet.
|
||||
runHasCallTarget = 0;
|
||||
}
|
||||
if (isDirectBranch(¤t) && current.operand[0]) {
|
||||
pendingBranch = current;
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
// SoundPatch.c
|
||||
// Turns a soundThing patch into a table the sound device can be handed.
|
||||
//
|
||||
// ---- Why this exists ----
|
||||
//
|
||||
// soundThing is where a patch gets DESIGNED, because it has a screen, a keyboard and a pair
|
||||
// of ears attached to it. Voyager's sound device is the same voice engine with the editor
|
||||
// taken off, so the sound a patch makes is the same sound - but its settings arrive as bytes
|
||||
// through a selector, and a byte is not seconds or hertz.
|
||||
//
|
||||
// The first sound written for a game here was guessed at in bytes: a cutoff of 40 looked
|
||||
// small and is 57 Hz, so the bang came out as a low gurgle. That is what this is for. Design
|
||||
// it where it can be heard, convert it, and the numbers stop being a matter of opinion.
|
||||
//
|
||||
// ---- What a byte means ----
|
||||
//
|
||||
// Every parameter the device takes is a documented function of a natural value, and all of
|
||||
// them invert. Times are SQUARED into four seconds, 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 and LFO rate are EXPONENTIAL,
|
||||
// because pitch is logarithmic and so is where a filter sounds like it is. Depths and detune
|
||||
// are CENTRED on 128, so half of nothing is no change and either side of it is a direction.
|
||||
//
|
||||
// Written by Anachronaut
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
// How a natural value becomes a byte. The inverse of soundParameter in sound.c, and the
|
||||
// reason that file and this one have to be read together if either changes.
|
||||
typedef enum {
|
||||
AS_DIRECT, // already a byte: a waveform, a routing, a filter type
|
||||
AS_BOOL, // nought or one
|
||||
AS_RANGE, // linear from low to high
|
||||
AS_SQUARED, // times, so that the short end has the resolution
|
||||
AS_EXPONENTIAL, // cutoff and rate, because hearing is logarithmic
|
||||
AS_SIGNED, // centred on 128, reaching `high` either way
|
||||
AS_OCTAVE // a small signed number, offset by 128
|
||||
} Shape;
|
||||
|
||||
typedef struct {
|
||||
const char *field; // what soundThing calls it
|
||||
unsigned char parameter; // what the device calls it
|
||||
Shape shape;
|
||||
double low, high;
|
||||
const char *note;
|
||||
} Mapping;
|
||||
|
||||
// ---- The whole of the correspondence ----
|
||||
//
|
||||
// Oscillator one is oscillator nought's parameters plus 0x10, and the modulation envelope is
|
||||
// the amplitude one plus 0x10, which is why the parameter numbers are laid out the way they
|
||||
// are. Written out in full anyway: a table that has to be understood before it can be read is
|
||||
// worse than a long one.
|
||||
static const Mapping mappings[] = {
|
||||
{ "osc0_waveform", 0x00, AS_DIRECT, 0, 0, "oscillator 0, waveform" },
|
||||
{ "osc0_gain", 0x01, AS_RANGE, 0, 4.0, "gain" },
|
||||
{ "osc0_dutyCycle", 0x02, AS_RANGE, 0.05, 0.95, "duty" },
|
||||
{ "osc0_detune", 0x03, AS_SIGNED, 0, 1200, "detune, in cents" },
|
||||
{ "osc0_octave", 0x04, AS_OCTAVE, 0, 0, "octave" },
|
||||
{ "osc0_active", 0x05, AS_BOOL, 0, 0, "on" },
|
||||
{ "osc0_modRouting0", 0x06, AS_DIRECT, 0, 0, "what moves its width" },
|
||||
{ "osc0_modDepth0", 0x07, AS_SIGNED, 0, 0.5, "and how far" },
|
||||
{ "osc0_modRouting1", 0x08, AS_DIRECT, 0, 0, "what moves its pitch" },
|
||||
{ "osc0_modDepth1", 0x09, AS_SIGNED, 0, 1200, "and how far" },
|
||||
{ "osc0_modRouting2", 0x0A, AS_DIRECT, 0, 0, "what moves its gain" },
|
||||
{ "osc0_modDepth2", 0x0B, AS_SIGNED, 0, 4.0, "and how far" },
|
||||
|
||||
{ "osc1_waveform", 0x10, AS_DIRECT, 0, 0, "oscillator 1, waveform" },
|
||||
{ "osc1_gain", 0x11, AS_RANGE, 0, 4.0, "gain" },
|
||||
{ "osc1_dutyCycle", 0x12, AS_RANGE, 0.05, 0.95, "duty" },
|
||||
{ "osc1_detune", 0x13, AS_SIGNED, 0, 1200, "detune, in cents" },
|
||||
{ "osc1_octave", 0x14, AS_OCTAVE, 0, 0, "octave" },
|
||||
{ "osc1_active", 0x15, AS_BOOL, 0, 0, "on" },
|
||||
{ "osc1_modRouting0", 0x16, AS_DIRECT, 0, 0, "what moves its width" },
|
||||
{ "osc1_modDepth0", 0x17, AS_SIGNED, 0, 0.5, "and how far" },
|
||||
{ "osc1_modRouting1", 0x18, AS_DIRECT, 0, 0, "what moves its pitch" },
|
||||
{ "osc1_modDepth1", 0x19, AS_SIGNED, 0, 1200, "and how far" },
|
||||
{ "osc1_modRouting2", 0x1A, AS_DIRECT, 0, 0, "what moves its gain" },
|
||||
{ "osc1_modDepth2", 0x1B, AS_SIGNED, 0, 4.0, "and how far" },
|
||||
|
||||
{ "ampEnv_attack", 0x20, AS_SQUARED, 0, 4.0, "amplitude envelope, attack" },
|
||||
{ "ampEnv_decay", 0x21, AS_SQUARED, 0, 4.0, "decay" },
|
||||
{ "ampEnv_sustain", 0x22, AS_RANGE, 0, 1.0, "sustain" },
|
||||
{ "ampEnv_release", 0x23, AS_SQUARED, 0, 4.0, "release" },
|
||||
|
||||
{ "modEnv_attack", 0x30, AS_SQUARED, 0, 4.0, "modulation envelope, attack" },
|
||||
{ "modEnv_decay", 0x31, AS_SQUARED, 0, 4.0, "decay" },
|
||||
{ "modEnv_sustain", 0x32, AS_RANGE, 0, 1.0, "sustain" },
|
||||
{ "modEnv_release", 0x33, AS_SQUARED, 0, 4.0, "release" },
|
||||
|
||||
{ "filter_active", 0x40, AS_BOOL, 0, 0, "filter, on" },
|
||||
{ "filter_type", 0x41, AS_DIRECT, 0, 0, "type: 0 low, 1 high, 2 band" },
|
||||
{ "filter_cutoff", 0x42, AS_EXPONENTIAL, 20.0, 20000.0, "cutoff, in hertz" },
|
||||
{ "filter_resonance", 0x43, AS_RANGE, 0, 0.99, "resonance" },
|
||||
{ "filter_modRouting", 0x44, AS_DIRECT, 0, 0, "what moves the cutoff" },
|
||||
{ "filter_modDepth", 0x45, AS_SIGNED, 0, 8000.0, "and how far, in hertz" },
|
||||
{ "filter_resModRouting", 0x46, AS_DIRECT, 0, 0, "what moves the resonance" },
|
||||
{ "filter_resModDepth", 0x47, AS_SIGNED, 0, 0.99, "and how far" },
|
||||
|
||||
{ "lfo0_active", 0x60, AS_BOOL, 0, 0, "LFO 0, on" },
|
||||
{ "lfo0_waveform", 0x61, AS_DIRECT, 0, 0, "waveform" },
|
||||
{ "lfo0_rate", 0x62, AS_EXPONENTIAL, 0.05, 20.0, "rate, in hertz" },
|
||||
{ "lfo0_mode", 0x63, AS_DIRECT, 0, 0, "0 free, 1 starts with a voice" },
|
||||
{ "lfo1_active", 0x70, AS_BOOL, 0, 0, "LFO 1, on" },
|
||||
{ "lfo1_waveform", 0x71, AS_DIRECT, 0, 0, "waveform" },
|
||||
{ "lfo1_rate", 0x72, AS_EXPONENTIAL, 0.05, 20.0, "rate, in hertz" },
|
||||
{ "lfo1_mode", 0x73, AS_DIRECT, 0, 0, "0 free, 1 starts with a voice" },
|
||||
|
||||
// ---- The two that are about the voice as a whole ----
|
||||
//
|
||||
// soundThing gained a field for each of these at the same time the engine did. A patch
|
||||
// written before that has neither, which is why the level source has a default below: it
|
||||
// was always envelope 0 when there was nothing to say otherwise.
|
||||
{ "voice_levelSource", 0x50, AS_DIRECT, 0, 0, "what shapes the level" },
|
||||
{ "voice_gate", 0x51, AS_DIRECT, 0, 0, "0 gated, 1 triggered" },
|
||||
};
|
||||
#define MAPPING_COUNT ((int)(sizeof(mappings) / sizeof(mappings[0])))
|
||||
|
||||
// ---- What soundThing has and the device does not ----
|
||||
//
|
||||
// Named rather than ignored, so a field that is simply new shows up as unknown and a field
|
||||
// that is deliberately dropped does not. Master volume is the editor's own output level and
|
||||
// the device has one of its own; pitch bend has no wheel to come from.
|
||||
static const char *ignored[] = { "master_volume", "master_pitchBendRange" };
|
||||
#define IGNORED_COUNT ((int)(sizeof(ignored) / sizeof(ignored[0])))
|
||||
|
||||
static int clampByte(double raw) {
|
||||
long v = lround(raw);
|
||||
if (v < 0) return 0;
|
||||
if (v > 255) return 255;
|
||||
return (int)v;
|
||||
}
|
||||
|
||||
static int toByte(const Mapping *m, double value) {
|
||||
switch (m->shape) {
|
||||
case AS_DIRECT: return clampByte(value);
|
||||
case AS_BOOL: return value != 0.0 ? 1 : 0;
|
||||
case AS_RANGE: return clampByte(255.0 * (value - m->low) / (m->high - m->low));
|
||||
// The inverse of part*part*high, so the root comes back out.
|
||||
case AS_SQUARED: return clampByte(255.0 * sqrt(value / m->high));
|
||||
// And of low * (high/low)^part.
|
||||
case AS_EXPONENTIAL:
|
||||
if (value < m->low) value = m->low;
|
||||
return clampByte(255.0 * log(value / m->low) / log(m->high / m->low));
|
||||
case AS_SIGNED: return clampByte(128.0 + 128.0 * value / m->high);
|
||||
case AS_OCTAVE: return clampByte(128.0 + value);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 3) {
|
||||
fprintf(stderr,
|
||||
"Usage: %s <patch.json> <label> [output.asm]\n\n"
|
||||
"Turns a soundThing patch into a table for the sound device: a count, then that\n"
|
||||
"many parameter and value pairs. Hand the label to playPatch.\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
const char *path = argv[1];
|
||||
const char *label = argv[2];
|
||||
|
||||
FILE *in = fopen(path, "r");
|
||||
if (!in) { fprintf(stderr, "SoundPatch: cannot read %s\n", path); return 1; }
|
||||
|
||||
int values[MAPPING_COUNT];
|
||||
int seen[MAPPING_COUNT];
|
||||
memset(seen, 0, sizeof(seen));
|
||||
|
||||
char line[512];
|
||||
int unknown = 0;
|
||||
while (fgets(line, sizeof(line), in)) {
|
||||
char field[128];
|
||||
double value;
|
||||
// Every line of one of these is `"name": number`, with or without a trailing comma.
|
||||
if (sscanf(line, " \"%127[^\"]\" : %lf", field, &value) != 2) continue;
|
||||
int found = 0;
|
||||
for (int i = 0; i < MAPPING_COUNT; i++) {
|
||||
if (strcmp(field, mappings[i].field) == 0) {
|
||||
values[i] = toByte(&mappings[i], value);
|
||||
seen[i] = 1;
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) continue;
|
||||
for (int i = 0; i < IGNORED_COUNT; i++) {
|
||||
if (strcmp(field, ignored[i]) == 0) { found = 1; break; }
|
||||
}
|
||||
if (!found) {
|
||||
fprintf(stderr, "SoundPatch: %s has a field this does not know: %s\n", path, field);
|
||||
unknown++;
|
||||
}
|
||||
}
|
||||
fclose(in);
|
||||
|
||||
// A field this does not know is a patch format that has moved on, and writing a table
|
||||
// that quietly leaves it out would make a sound nobody could account for.
|
||||
if (unknown) {
|
||||
fprintf(stderr, "SoundPatch: %d unknown field%s, so nothing was written.\n",
|
||||
unknown, unknown == 1 ? "" : "s");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- A patch from before the level could be routed ----
|
||||
//
|
||||
// Those files have no voice_levelSource, and what it meant there was always envelope 0.
|
||||
// Said out loud rather than left out, because a channel keeps its patch between notes and
|
||||
// would otherwise carry a previous one's routing into this one.
|
||||
int levelIndex = -1;
|
||||
for (int i = 0; i < MAPPING_COUNT; i++) {
|
||||
if (mappings[i].parameter == 0x50) { levelIndex = i; break; }
|
||||
}
|
||||
int assumedLevel = (levelIndex >= 0 && !seen[levelIndex]);
|
||||
if (assumedLevel) {
|
||||
values[levelIndex] = 1; // MOD_SOURCE_AMP_ENV
|
||||
seen[levelIndex] = 1;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (int i = 0; i < MAPPING_COUNT; i++) if (seen[i]) count++;
|
||||
|
||||
FILE *out = stdout;
|
||||
if (argc > 3) {
|
||||
out = fopen(argv[3], "w");
|
||||
if (!out) { fprintf(stderr, "SoundPatch: cannot write %s\n", argv[3]); return 1; }
|
||||
}
|
||||
|
||||
fprintf(out, "; %s, converted from %s by SoundPatch.\n", label, path);
|
||||
fprintf(out, "; Designed in soundThing, where it can be heard. Do not edit the numbers\n");
|
||||
fprintf(out, "; here: change the patch and convert it again.\n\n");
|
||||
// Data, and no base: a table of bytes belongs wherever the program including it has got
|
||||
// to, the way every other included table does.
|
||||
fprintf(out, "#Data\n\n");
|
||||
fprintf(out, "%s:\n", label);
|
||||
fprintf(out, " 0d%-24d; how many pairs follow\n", count);
|
||||
for (int i = 0; i < MAPPING_COUNT; i++) {
|
||||
if (!seen[i]) continue;
|
||||
fprintf(out, " 0x%02X 0d%-3d ; %s%s\n",
|
||||
mappings[i].parameter, values[i], mappings[i].note,
|
||||
(i == levelIndex && assumedLevel) ? ", which this patch predates" : "");
|
||||
}
|
||||
if (out != stdout) fclose(out);
|
||||
return 0;
|
||||
}
|
||||
@@ -633,7 +633,7 @@ A `#Vectors` line that names a **handler** says this program implements that vec
|
||||
|
||||
`Boot` in a loadable program fills the **entry** field rather than being installed. Vector zero is where the whole machine starts, and a program being loaded into a running system has no business saying anything about that. A boot image is the one thing that does, so there it is installed like any other.
|
||||
|
||||
`Device` is named by the port it is plugged into, because that is what decides which vector it arrives through. `Device`, `Boot`, `SoftReset`, `BadOpcode`, `GuardViolation` and `BankFault` are matched **without regard to case**, the way mnemonics are: they are part of the language rather than names the programmer chose.
|
||||
`Device` is named by the port it is plugged into, because that is what decides which vector it arrives through. `Device`, `Boot`, `SoftReset`, `BadOpcode`, `GuardViolation`, `BankFault`, `NoHandler` and `NoDevice` are matched **without regard to case**, the way mnemonics are: they are part of the language rather than names the programmer chose.
|
||||
|
||||
**A declaration and an implementation are the same entry.** `services.asm` says a service is called `osPrintString` and has number 16; `cosmos.asm` says `osPrintString` is handled by `handlePrintString`. Both sides include the first file, so the name is met twice and the second time fills in the handler. That is what lets one shared file serve both a program that calls a service and the system that implements it - and it is why the first pass declares and the second implements, a handler being an address and no address being known until every label has been placed.
|
||||
|
||||
|
||||
+264
-10
@@ -309,6 +309,8 @@ Backspace is 0x08 and always has been. It is a different key from Delete and doe
|
||||
|
||||
**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.
|
||||
|
||||
**But asking the status port in line mode does not throw one away.** A key line mode will not deliver is not the same as a key that is gone, because the mode can change: a program that looks at the status port and then asks for key mode - which is exactly what a system does before it reads a line - would otherwise find that the first key it was reaching for had been swallowed by the looking. So the console holds it and delivers it as soon as something is willing to take it. Reading the data port in line mode does discard it, and must: that read is the delivery, and a key held there would be met again forever.
|
||||
|
||||
**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:
|
||||
@@ -545,7 +547,17 @@ If a device interrupts and its vector is empty, that is a fault: the machine sto
|
||||
|
||||
If the CPU reads a byte from Program Memory that does not decode to an instruction, it dispatches through Software Vector 2.
|
||||
|
||||
Faults get a vector each rather than sharing one. Vector 2 is the only cause defined so far, and vectors 3 through 15 are held back for the ones that come later, so that a handler always knows what happened from the entry it arrived through. That is why the machine has no fault cause register to read.
|
||||
Faults get a vector each rather than sharing one, so that a handler always knows what happened from the entry it arrived through. That is why the machine has no fault cause register to read.
|
||||
|
||||
| Vector | What happened |
|
||||
| --- | --- |
|
||||
| 2 | A byte in Program Memory does not decode to an instruction. |
|
||||
| 3 | A device refused a write that landed inside a raised fence. |
|
||||
| 4 | A bank was named that has nothing in it, or an access ran past its end. |
|
||||
| 5 | A software vector was dispatched through and had no handler. |
|
||||
| 6 | A device interrupted and its hardware vector had no handler. |
|
||||
|
||||
Vectors 7 through 15 are held back for the causes that come later.
|
||||
|
||||
The address in the frame is the address of the offending byte itself, not the one after it. A handler can therefore read the byte that failed and say what it was. It also means a handler that returns with a bare RETI will meet the same byte again, because resuming past a fault means deciding where to resume, and only the handler knows that.
|
||||
|
||||
@@ -553,6 +565,18 @@ If nothing is installed at Vector 2, the CPU sets the Fault Flag and the Halt Fl
|
||||
|
||||
Stopping matters because the alternative is worse. A byte that means nothing is almost always a sign that execution has wandered into data, or that a program was built for a machine with instructions this one does not have. Stepping over it and carrying on turns a clear failure into a program that appears to run and quietly does the wrong thing.
|
||||
|
||||
### Nowhere To Go:
|
||||
|
||||
Vectors 5 and 6 are the fault of dispatching through an empty entry, and they were a long time coming, because the thing that would hand that fault over is the thing which has just found nothing to hand it to. Until they existed, a `SWI` naming a service the system does not implement stopped the machine and no program could do anything about it - and calling a service that is not there is an ordinary mistake.
|
||||
|
||||
**Which entry was empty arrives in Q**, and it is the only thing on this machine a handler is given in a register. That is not a fault cause register by another route: the vector still says *what* happened, and Q says which of the 256 entries it happened about, which is a parameter and not a cause. It costs nothing, because the frame already saved the Q the interrupted program had and RETI puts it back.
|
||||
|
||||
The two are separate entries because they are separate mistakes with separate fixes. A missing software vector is usually a program calling something that is not there; a device with nobody listening is usually a program that asked to be interrupted and forgot the handler.
|
||||
|
||||
**The frame's address is past the SWI**, unlike every other fault here, because the instruction did dispatch - it was the entry that was empty. A bare RETI therefore carries straight on, where a bad opcode or a refused port would meet the same instruction again.
|
||||
|
||||
If Vector 5 or 6 is *itself* empty, the machine stops the way it always did. It has run out of places to go, and looping there would be worse than stopping. A handler that commits the same fault it was called about recurses like any other, which is the same bargain as a Vector 2 handler containing a byte that does not decode.
|
||||
|
||||
## Refusing:
|
||||
|
||||
A device can refuse what it was asked to do. This is not the same as interrupting. An interrupt is a device asking for attention later, answered between instructions once the CPU is ready. A refusal is a device saying no to the instruction happening now, so the machine stops where it stands rather than carrying on as though the access had worked.
|
||||
@@ -578,6 +602,7 @@ If nothing is installed for the vector a device refused with, the machine stops
|
||||
| 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 |
|
||||
| 0x60 - 0x6F | The game controllers. See Controllers. Four pads, polled, reporting what is held. | 0x17 |
|
||||
| 0xE0 - 0xEF | The memory controller. See The Memory Controller. | 0x03 |
|
||||
| 0xFF | The bus registry. See Asking What Is There. | 0x01 |
|
||||
|
||||
@@ -591,30 +616,152 @@ It follows that colour depth is free. The map is the same size whatever is behin
|
||||
|
||||
### 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.
|
||||
**Two banks**, brought by the device and reached only through the memory controller, like the disk's buffer. They keep what is in them between frames, so a program writes the part that changed and the rest stays as it was.
|
||||
|
||||
| Address | Holds |
|
||||
Two rather than one because the two halves of a screen are written at completely different rates. The **atlas** is tiles and colours: put there when a program loads and then left alone. The **screen** is the map: rewritten as often as anything moves.
|
||||
|
||||
| Bank | Address | Holds |
|
||||
| --- | --- | --- |
|
||||
| Atlas | 0x0000 - 0x3FFF | Tile page 0. 256 tiles of 8 by 8, one byte a pixel, so tile n begins at n times 64. |
|
||||
| Atlas | 0x4000 - 0x7FFF | Tile page 1. |
|
||||
| Atlas | 0x8000 - 0xBFFF | Tile page 2. |
|
||||
| Atlas | 0xC000 - 0xCFFF | The sprite table. 256 entries of sixteen bytes - and tiles 0 to 63 of page 3. |
|
||||
| Atlas | 0xD000 - 0xD27F | The depth buffer. One byte a screen column - and tiles 64 to 73 of page 3. |
|
||||
| Atlas | 0xD280 - 0xFBFF | Free - and tiles 74 to 239 of page 3. |
|
||||
| Atlas | 0xFC00 - 0xFFFF | The palette. 256 entries of four bytes: red, green, blue, and one unused - and tiles 240 to 255 of page 3. |
|
||||
| Screen | 0x0000 - 0x3FFF | Free in a tile mode. |
|
||||
| Screen | 0x4000 - 0xBFFF | The map. 128 rows of 256 bytes. |
|
||||
| Screen | 0xC000 - 0xFFFF | The window. 64 rows of cells that do not scroll. |
|
||||
| Screen | 0x0000 - 0xF9FF | In bitmap mode, the picture instead: 64,000 bytes, one to a pixel. |
|
||||
|
||||
**There are two screen banks and one atlas**, laid out identically, and the device shows one screen at a time. See Two Screens below.
|
||||
|
||||
**The bitmap is the same memory as the map**, which is what shared video memory has always been. Going to bitmap mode does not clear the text screen - it stops calling it one, and coming back finds the map holding whatever the picture put there.
|
||||
|
||||
**What a picture no longer costs is the font.** The tiles and the palette are in the other bank, where a bitmap cannot reach them, so a program can draw a picture and then put readable text back on the screen without asking the character generator for its glyphs again. While the two shared a bank, drawing anything destroyed them.
|
||||
|
||||
The palette is in the atlas, at the top and out of the way, because it is written when a program loads rather than per frame - and because being out of the screen bank is what leaves a bitmap the whole of one.
|
||||
|
||||
Which bank is which is a property of the **address**, never of the mode: tiles and the palette are always in the atlas, the map and a bitmap always in the screen.
|
||||
|
||||
### Naming Them:
|
||||
|
||||
A bank is registered by naming the port that owns it, so a device with two banks needs two ports that own memory. The screen has them:
|
||||
|
||||
| Port | Owns |
|
||||
| --- | --- |
|
||||
| 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. |
|
||||
| 0x30 | The atlas. |
|
||||
| 0x3A | Screen 0. |
|
||||
| 0x3B | Screen 1. |
|
||||
|
||||
**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.
|
||||
Nothing is read or written at 0x3A or 0x3B - they are names for banks, and the bus registry is where a program finds out they bring one. Asking the registry about the block gives an honest answer: those three ports say they bring memory and the other thirteen say they do not.
|
||||
|
||||
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.
|
||||
### Two Screens:
|
||||
|
||||
There are two screen banks and the device shows one of them. **Port 0x3C says which**, 0 or 1, and reads back what it was told; a screen that does not exist is not taken, the same as a mode that does not exist.
|
||||
|
||||
That is a **back buffer**: a whole screen's worth of map written where nobody can see it, and then shown all at once. A screen drawn where it can be seen is seen half drawn, and a program that moves forty things and rewrites the map underneath them is wrong for as long as it takes to put them all right - which at a megahertz is long enough to look at.
|
||||
|
||||
**One register is enough, where real hardware needed two.** The other said which screen the CPU's window pointed at. There is no window here: a program reaches a bank through the memory controller by its number, so writing to the screen that is not being shown is a matter of naming its bank, and the device never has to be told.
|
||||
|
||||
**A flip cannot tear.** A frame is drawn from one bank in one go, so a flip either happened before that frame or it happens before the next one; there is no state of having flipped halfway. The machines this one imitates had to catch the few lines between one frame and the next to do the swap in.
|
||||
|
||||
**The console draws into whichever screen is displayed**, rather than into one of its own. A game that flipped and then faulted needs the message to land where somebody can read it, and the console has no way of knowing that happened.
|
||||
|
||||
What a second screen costs is a whole bank of somebody's memory, and nothing else: it is memory the device brought, so a program that wants it registers it and a program that does not never pays for it. `Programs/CosmOS/Apps/Flip.asm` is the shortest thing to read that uses one.
|
||||
|
||||
The base port owns the atlas because tiles have been at 0x0000 since there was a screen at all. `Programs/Examples/picture.asm` registers both, and is the shortest thing to read that does.
|
||||
|
||||
**Registering the second one moves DestBank**, which is worth saying because it is easy to be caught by: RegisterBank takes the number being handed out in DestBank, so a program that registers two banks and then writes without setting DestBank again writes into the second one.
|
||||
|
||||
**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.
|
||||
|
||||
### Sprites:
|
||||
|
||||
Things that move without the map moving. A cell is where it is; something between two cells meant rewriting both of them, and something moving a pixel at a time meant rewriting them sixty times a second - which is affordable for one thing and not for twenty. A sprite is put at a **pixel**, and the device draws it over whatever is behind.
|
||||
|
||||
A sprite's attribute is a cell's attribute, read the same way and by the same code - which is what lets one piece of art be a wall in one place and a moving thing in another with nothing rewritten.
|
||||
|
||||
**A sprite is m by n tiles**, taken in reading order from one index. That is the decision the rest follows from: it needs no pixel format of its own, no second kind of memory, and nothing its art can be that the map could not also show. A 16 by 16 character is four tiles, and a program that wants the same picture in the background just names the same four.
|
||||
|
||||
The table is 256 entries of 16 bytes at **0xC000 in the atlas**. Sixteen so that entry n begins at n times sixteen, which is a shift - the same reason a palette entry is four bytes.
|
||||
|
||||
| Byte | Holds |
|
||||
| --- | --- |
|
||||
| 0 | The top left tile, in the page its attribute names. The rest follow it in reading order, wrapping at 255 inside that page. |
|
||||
| 1 | Attribute, which means exactly what a cell's does: low nibble the colour scheme, bits 4 and 5 the tile page. |
|
||||
| 2, 3 | X, low byte first, **signed**. |
|
||||
| 4, 5 | Y, the same. |
|
||||
| 6 | Size: tiles across in the high nibble, tiles down in the low. |
|
||||
| 7 | Flags. Bit 0 mirrors it, bit 1 turns it over, bit 2 puts it behind. |
|
||||
| 8, 9 | Target width in pixels. Zero means the natural width, eight times the tiles across. |
|
||||
| 10, 11 | Target height, the same. |
|
||||
| 12 | Depth. Zero means no depth test. |
|
||||
| 13 - 15 | Reserved. Leave at zero. |
|
||||
|
||||
The position is signed and sixteen bits because the larger mode is 640 by 400, so neither axis fits in a byte - and because a sprite has to be able to sit half off the left or the top rather than appearing whole at the edge.
|
||||
|
||||
**A pixel of zero is not drawn.** Without that every sprite is a rectangle. It is tested before the attribute is added, so a hole is a property of the art rather than of the colour scheme: a sprite drawn in indices 1 to 15 is transparent in the same places in all sixteen.
|
||||
|
||||
The same rule read the other way is what **behind** means. A sprite marked behind draws only where the background pixel was zero, so a thing can walk behind a pillar and in front of the floor in the same frame. One rule, applied to whichever layer is in front.
|
||||
|
||||
**A sprite of no width or no height draws nothing**, and that is the off switch: it saves a flag, it is per sprite rather than a global the whole table shares, and it means the table is already off when the machine starts, since the atlas wakes up cleared. Note that this is deliberately the opposite of what a length of zero means to the memory controller. The reason is the same both times - moving no bytes is a useless thing to ask for, so zero was free to mean 64K there, and drawing no sprite is the commonest state in this table, so zero has to mean nothing here.
|
||||
|
||||
**All of them are drawn, every frame.** Sprites here cannot flicker. Real machines dropped them per scanline because they had a fixed number of shift registers and a fixed time to fill them; this has a loop. The limit is how many entries the table has, which is a constant a program can count on rather than something that depends on what it happens to be drawing. Where two overlap, **the lower number is in front**.
|
||||
|
||||
Sprites are drawn over a bitmap as readily as over a map. A bitmap is what a program draws once and leaves; there is no reason the mode that cannot afford to redraw itself should be the one that cannot have things moving on it.
|
||||
|
||||
### Scaling:
|
||||
|
||||
**Bytes 8 to 11 say how big to draw it**, in pixels, and the device stretches the m by n tiles to fill that. Zero on an axis means the natural size, so every sprite written before scaling existed still means what it meant, and a thing drawn at the size it was drawn at costs nothing to say.
|
||||
|
||||
**A target in pixels rather than a multiplier**, which is the whole of why this is usable here. A billboard at distance *d* wants to be *k/d* pixels tall, and that is a number a program has anyway - out of a lookup table, most likely. A multiplier would have to be a fixed-point fraction, arrived at by dividing, and this CPU cannot divide.
|
||||
|
||||
The two axes are independent, so a sprite can be stretched one way and not the other. That shape - one tile wide at its own size, stretched to whatever height a distance says - is a wall column in a pseudo-3D game, and it is the reason such a game is possible at all on this machine. Drawing 640 by 400 pixels of wall from the CPU is 256,000 writes, which is fifteen frames of cycles for one frame of screen. Writing sixteen bytes a column and letting the device do the pixels is about three thousand.
|
||||
|
||||
### Depth:
|
||||
|
||||
**Byte 12 says how far away a sprite is**, and the **depth buffer** at 0xD000 says how far away the scenery is: one byte a screen column, written by the program. A sprite pixel is drawn only in the columns it is in front of.
|
||||
|
||||
Zero in a sprite's depth means no depth test at all, which is what every ordinary sprite wants and what a cleared table already says. Zero in a column means nothing is there, so a program that never writes the buffer has one of zeroes and every sprite draws - which is exactly the behaviour there was before the buffer existed.
|
||||
|
||||
**Per column, and that is the point.** A billboard can be nearer than the wall at one end of itself and further at the other, and no ordering of the sprite table can say that. Table order settles sprites against each other; the buffer settles them against the scenery.
|
||||
|
||||
The buffer belongs to the program. It is not cleared between frames, because a program that draws scenery rewrites all of it every frame anyway.
|
||||
|
||||
`Programs/CosmOS/Apps/Sprite.asm` moves one across the shell's own text without writing a byte of the map.
|
||||
|
||||
### The Window:
|
||||
|
||||
A layer that does not scroll. The map moves and this does not, which the map alone cannot express: the scroll registers move **all** of it, so a score printed into the map is a score that slides away, and one printed into whichever rows the view happens to be showing jumps a pixel at a time as the fine offset changes.
|
||||
|
||||
**Port 0x3D** is how many screen rows tall it is and **0x3E** is which row it starts at. Zero tall is no window, so a cleared screen has none and every program written before it existed means what it meant. A start row is a register because a status bar along the bottom is as common as one along the top.
|
||||
|
||||
Its cells are at **0xC000 in the screen bank**, 64 rows of 256 bytes, and a cell means exactly what a map cell means - same tiles, same pages, same colour schemes. **A window cell is at a screen position**, where a map cell is at a position in a world the screen is looking at part of; keeping those two coordinate systems apart is the whole of the feature.
|
||||
|
||||
**It has its own memory, and that is the argument for it.** The cheaper design draws the top rows of the *map* without the scroll applied, which needs no new memory at all - and makes those rows part of the playfield's ring, so a game that scrolls vertically has to route its world around its own scoreboard for ever. The point of a status bar is that it is not somewhere in the level.
|
||||
|
||||
Being in the screen bank means it is **per screen**: flipping to the other buffer flips the status bar with it, which is what a double-buffered game wants and would be surprising the other way round.
|
||||
|
||||
**It is drawn over everything, sprites included.** A sprite that could cover the fuel gauge would be a bug in every game that had both.
|
||||
|
||||
It is a tile-mode layer. In bitmap mode there is nothing to draw it from - the picture is using that memory - so a bitmap program that wants something pinned to the screen uses sprites, which are in screen coordinates for the same reason.
|
||||
|
||||
### 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.
|
||||
**Bits 4 and 5 say which page of tiles the number is in.** A tile number is a byte and a byte reaches 256, which is not many once a font has taken 135 of them and a game wants a character, a background and a wall. Two bits that were already being written on every cell reach 1024.
|
||||
|
||||
Four pages of 16K is 64K, which is the whole atlas, so **the fourth page is the memory the sprite table and the palette are in.** That is not a hole in the design; it is the same answer shared video memory has always given. The atlas is 1024 tiles, and what a program spends on sprites and colours comes out of them. A program that wants no sprites may use page 3 for art, and one that wants sprites has 768 tiles and knows why.
|
||||
|
||||
The page is a property of the **cell**, not a mode, so one screen can show tiles from all four pages at once and a program never has to decide which page it is "in".
|
||||
|
||||
Bits 6 and 7 are still reserved and should be left at zero.
|
||||
|
||||
### Registers:
|
||||
|
||||
@@ -629,6 +776,12 @@ The high nibble is reserved and should be left at zero, so that a meaning can be
|
||||
| 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. |
|
||||
| 0x39 | Command. Bit 0 copies the font back, bit 1 the sixteen colour schemes. |
|
||||
| 0x3A | Owns screen 0. Not read or written. |
|
||||
| 0x3B | Owns screen 1. Not read or written. |
|
||||
| 0x3C | Display. Which of the two screens is being shown. |
|
||||
| 0x3D | Window height, in rows. Zero is no window. |
|
||||
| 0x3E | Window start, which screen row it begins at. |
|
||||
|
||||
| Mode | Screen | Cells |
|
||||
| --- | --- | --- |
|
||||
@@ -711,6 +864,24 @@ The alternative was to let a write of 8 step the column and set the fine part to
|
||||
|
||||
**None of the four does anything in bitmap mode**, which has no map to slide.
|
||||
|
||||
### The Character Generator:
|
||||
|
||||
The font and the sixteen colour schemes come from a **ROM in the device**. Reset copies them into video memory, and Command port `0x39` copies them again on request:
|
||||
|
||||
| Port | Register |
|
||||
| --- | --- |
|
||||
| 0x39 | Command. Bit 0 asks for the font back, bit 1 for the sixteen schemes. Write only. |
|
||||
|
||||
**This used to be magic and now is not.** The glyphs were written into video RAM at reset and existed nowhere else, which looked harmless until something wanted the font *back*: RAM does not wake up with anything in it, and a program that redefined a glyph had destroyed the only copy there was. A machine with a character generator is what the machines this one is pretending to be actually had, and the copy into RAM is now a thing the device **does** rather than a state it mysteriously starts in.
|
||||
|
||||
The RAM is still RAM. A program may overwrite every glyph and every colour and should be able to - that is what makes this a tile engine rather than a text display. What changed is that doing so is no longer a one way door.
|
||||
|
||||
**Neither command clears what it does not own.** The font writes the 135 glyphs it has and stops, so a tile a program defined above them survives; the schemes write the two entries of each of the sixteen and stop, so a program's own colours in between survive. Asking for the font back must not cost a program the tile it was drawing with.
|
||||
|
||||
In bitmap mode the tiles *are* the picture, so asking for the font there draws glyphs across the top of it. That is not a case being ignored: it is what the memory means in that mode.
|
||||
|
||||
A system that wants a different font still loads one over the top. The ROM is the floor rather than the policy - it is what lets a machine with no disk say that it has no disk, and what lets a program with no system behind it put readable text on a screen.
|
||||
|
||||
### Writing On The Screen:
|
||||
|
||||
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.
|
||||
@@ -855,6 +1026,7 @@ The high nibble says which part of the voice, the low nibble which setting of it
|
||||
| 0x30 - 0x3F | The modulation envelope. |
|
||||
| 0x40 - 0x4F | The filter. |
|
||||
| 0x50 | What shapes the channel's level. |
|
||||
| 0x51 | Whether a note waits to be let go of. |
|
||||
| 0x60 - 0x6F | LFO 0. |
|
||||
| 0x70 - 0x7F | LFO 1. |
|
||||
|
||||
@@ -891,6 +1063,7 @@ The high nibble says which part of the voice, the low nibble which setting of it
|
||||
| 0 | On, or off at zero. |
|
||||
| 1 | Waveform, from the same six. |
|
||||
| 2 | Rate. |
|
||||
| 3 | 0 free, 1 starts over with every voice. See The Same Sound Twice. |
|
||||
|
||||
Anywhere a setting asks *what modulates* something, the answer is one of these:
|
||||
|
||||
@@ -936,6 +1109,45 @@ output, so an envelope routed somewhere useful - opening the filter, bending a p
|
||||
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.
|
||||
|
||||
### Struck Or Held:
|
||||
|
||||
Setting 0x51 says whether a note waits to be let go of: **0 gated**, which is how it has always
|
||||
been, and **1 triggered**.
|
||||
|
||||
A gated voice lasts as long as something keeps hold of it. Writing the gate port ends it, and
|
||||
the release begins there. That is what a keyboard is, and it is right for anything a player
|
||||
holds down.
|
||||
|
||||
A triggered voice is struck and then plays its own length. Nothing has to remember to end it,
|
||||
and the gate port need never be written at all. Sustain and release have no meaning in one,
|
||||
because both of them are answers to a question about a key that is not being asked - so in a
|
||||
triggered voice the decay runs to nothing rather than stopping at the sustain level, or a
|
||||
patch with any sustain at all would hold the voice open for ever.
|
||||
|
||||
**A game is nearly all one-shots.** A bang, a pickup, a door: not one of them wants its length
|
||||
decided by how long a note was held, and every one of them would otherwise need a program to
|
||||
come back later and let go of it.
|
||||
|
||||
### The Same Sound Twice:
|
||||
|
||||
Setting 3 of either LFO says whether it starts over when a voice does: **0 free**, which is how
|
||||
it has always been, and **1 retriggered**.
|
||||
|
||||
A free LFO is one cycle running under everything, which is what vibrato across a held chord
|
||||
wants. A retriggered one starts at the beginning of its shape every time a voice begins, and
|
||||
the cycle belongs to the voice rather than to the device - so retriggering costs nothing to a
|
||||
channel not using it.
|
||||
|
||||
This is the other half of a repeatable sound effect, and **neither half is sufficient alone**.
|
||||
A triggered voice re-arms its oscillators, so a hit begins at the same point in its waveform
|
||||
every time and a noise source draws the same noise. But an LFO left free is wherever the wall
|
||||
clock happened to leave it, so the same drum caught at a different moment is still a different
|
||||
drum. Set both and a one-shot is the same one-shot, sample for sample.
|
||||
|
||||
The consequence is worth knowing rather than fixing: a retriggered noise source is *bit*
|
||||
repeatable, so every hit is literally the same noise, the way a sampler is. On a hi-hat that
|
||||
can read as machine-gunny. Where variation is wanted, leave that LFO or that voice free.
|
||||
|
||||
### 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
|
||||
@@ -1016,8 +1228,50 @@ One thing to be careful of: the registry remembers which port it was asked about
|
||||
| 0x14 | Screen. |
|
||||
| 0x15 | Sound. |
|
||||
| 0x16 | Timer. |
|
||||
| 0x17 | Game controllers. |
|
||||
| 0x17 - 0xFF | Peripherals. |
|
||||
|
||||
## Controllers:
|
||||
|
||||
Four pads on ports 0x60 to 0x6F. Each one is **one byte, read, saying what is held right now**.
|
||||
|
||||
| Port | Holds |
|
||||
| --- | --- |
|
||||
| 0x60 - 0x63 | Pads 0 to 3. |
|
||||
| 0x64 | Which pads are there, one bit each. |
|
||||
| 0x65 - 0x6F | Reserved. |
|
||||
|
||||
| Bit | Button |
|
||||
| --- | --- |
|
||||
| 0x01 | Right |
|
||||
| 0x02 | Left |
|
||||
| 0x04 | Down |
|
||||
| 0x08 | Up |
|
||||
| 0x10 | A |
|
||||
| 0x20 | B |
|
||||
| 0x40 | Start |
|
||||
| 0x80 | Select |
|
||||
|
||||
The four directions are the low nibble, so *which way* is an `AND` with `0x0F` and needs no shifting. The four buttons are the high nibble for the same reason.
|
||||
|
||||
### Why This Is Not The Console:
|
||||
|
||||
The console says **which key went down**. That is the right shape for typing and the wrong one for playing: a game wants to know what is being held, this frame, possibly several things at once, and a stream of presses cannot say that. A key that is down and staying down sends nothing at all.
|
||||
|
||||
A pad reports a **level** rather than an event. One read gives every button at once, holding is the natural thing to express, and two directions together cost nothing. **Reading does not consume it** - a game may ask twice in a frame and be told the same thing both times, which an event queue cannot promise.
|
||||
|
||||
**A pad is sampled once a frame**, and what it reports does not change in between. That is not an implementation detail: what is watching a real controller runs on its own clock - a window polls its keyboard once a *host* frame, which is not a machine frame - and read straight through, a pad's value could change in the middle of a frame and a program asking twice would get two answers. Real hardware latches a controller once a frame for the same reason.
|
||||
|
||||
Key-up on the console would have been the other way to do it, and it was rejected: a terminal hands over characters and can never report a key coming up however it is asked, so it would have been a thing that worked behind a window and silently did not down a wire. A separate device can honestly say it is not there.
|
||||
|
||||
**They never interrupt.** A game polls once a frame because that is when it draws, and an interrupt for every button would be exactly the event model a pad exists to avoid.
|
||||
|
||||
### When There Is No Pad:
|
||||
|
||||
A pad that is not there reads as nothing held, which is the same as a pad nobody is touching. The difference matters only to whoever wants to explain it, so 0x64 says which are really there and a game can ask for a controller rather than sitting silent while somebody presses things at it.
|
||||
|
||||
`Programs/testPrograms/padTest.asm` reads one twice a frame to show that looking does not take it away.
|
||||
|
||||
## Keeping Time:
|
||||
|
||||
A period, in cycles, and a bit that says when one has gone by.
|
||||
|
||||
+87
-8
@@ -9,7 +9,7 @@ believe them.
|
||||
|
||||
## What The Suite Claims:
|
||||
|
||||
The suite is not one thing. It is eleven scripts making five different kinds of claim, and
|
||||
The suite is not one thing. It is twelve 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.
|
||||
@@ -37,7 +37,7 @@ Everything in between is somewhere on that line.
|
||||
make test
|
||||
```
|
||||
|
||||
Builds the four tools - and Voyager, where Raylib is installed - checks they compile under
|
||||
Builds the five 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.
|
||||
|
||||
@@ -45,7 +45,7 @@ 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
|
||||
The same suite with the five tools rebuilt under AddressSanitizer and
|
||||
UndefinedBehaviorSanitizer. See [The Sanitizer Run](#the-sanitizer-run).
|
||||
|
||||
Individual scripts can be run on their own, from anywhere:
|
||||
@@ -64,12 +64,58 @@ Individual scripts can be run on their own, from anywhere:
|
||||
./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/replay.sh A recording of input, played back as itself.
|
||||
./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.
|
||||
|
||||
`Tests/replay.sh` checks that `--record-pad` and `--pad` are the same format read two ways.
|
||||
A recording is written one byte a frame and is only worth having if playing it back does what
|
||||
was recorded, so the strongest form of that claim is checked: a recording is made **of a
|
||||
playback**, and the bytes coming out have to be the bytes that went in.
|
||||
|
||||
**Every pad is recorded at once**, or-ed into one byte, rather than one pad chosen by number.
|
||||
Which pad a controller lands on is an accident of the host: the first recording ever made with
|
||||
this came back 1,766 frames of nothing, because it recorded pad 0 and the controller was
|
||||
somewhere else, and the flight had to be flown again for nothing. A demo is a record of what
|
||||
somebody *did*, and on a machine one person is playing the number it arrived on is not part of
|
||||
that.
|
||||
|
||||
`Tests/input/landerDemo.pad` is the first fixture that was **played rather than written**: a
|
||||
cyan-to-red cargo delivery in Lunar Porter, twenty five seconds of steering, recorded with
|
||||
`--record-pad`. `video.sh` replays it and checks that the base answers with a *D*, which only
|
||||
a delivery does. It is the only check that a cargo ever reaches anywhere.
|
||||
|
||||
It exists because some inputs cannot sensibly be written by hand. Flying a lander from one
|
||||
base to another is a few hundred frames of steering that has to arrive somewhere eight cells
|
||||
wide, and several attempts at authoring one by hand got within two columns and no closer. That
|
||||
is a piloting exercise rather than a test; playing it once and keeping what happened is the
|
||||
answer.
|
||||
|
||||
`Tests/break.sh` is not in it either, for the reason turned round: it checks that a check
|
||||
works. Run it by hand when a check is written, not as part of `make test`.
|
||||
|
||||
```
|
||||
./Tests/break.sh <file> <anchor> <replacement> <suite> [suite ...]
|
||||
```
|
||||
|
||||
It replaces the anchor - which must appear exactly once - rebuilds, runs the suites, restores
|
||||
the file, and reports whether anything failed. **A suite failing is the good outcome.**
|
||||
|
||||
A check that passes proves nothing until it has been seen to fail, and doing that by hand
|
||||
went wrong twice in two days. Both times it looked like a result: the suite ran, went green,
|
||||
and read exactly like "this check does not catch that". Once the edit did not compile, `make`
|
||||
failed, the exit status was not looked at, and the previous binary ran the suite. Once the
|
||||
anchor was right and the filename was wrong, so nothing was edited at all.
|
||||
|
||||
Neither had anything to do with the build system; headers have always rebuilt what depends on
|
||||
them. What was missing was a harness that refuses to report a result it did not earn, so
|
||||
every step is checked and anything unexpected is a hard error rather than a green run. **Not
|
||||
finding the break is the answer this tool exists to give, and it is worthless if it can also
|
||||
be the answer when the break never happened.**
|
||||
|
||||
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.
|
||||
@@ -79,7 +125,7 @@ from `make`, not from here.
|
||||
### 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
|
||||
everything it printed against a file in `Tests/expected`. 211 tests, of which 149 run, 35
|
||||
only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image
|
||||
given at all.
|
||||
|
||||
@@ -350,7 +396,7 @@ 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 |
|
||||
| `disks/name.img` | A fixture built by `makedisks.sh`, copied fresh for the run |
|
||||
| `...: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 |
|
||||
|
||||
@@ -358,6 +404,15 @@ The `@N` form deserves a note. Every other test runs with the disk's answer avai
|
||||
before the next instruction, which is the one condition under which failing to wait looks
|
||||
exactly like working.
|
||||
|
||||
**Both kinds of disk are fresh for every test**, and the fixture was not always. It used to
|
||||
be handed to each test where it lay, and twenty four tests name `disks/cosmos.img` while
|
||||
several of them write to one. `romBoot` was the test that found it: its recorded output
|
||||
described a directory that `selfBoot` had made earlier in the same run, so it passed in a
|
||||
full run and failed on its own. **That is the worst way round for a test to be wrong** - the
|
||||
form nobody runs is the one that tells the truth - and the shape of it is worth naming,
|
||||
because it is the same shape as a program that works only because of what ran before it.
|
||||
The whole suite was checked one test at a time afterwards, and that was the only one.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -373,6 +428,23 @@ A keyboard file installs the same hook a window does, so the same path runs. It
|
||||
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.
|
||||
|
||||
**pad** names a file in `Tests/input` to be held on a controller, one byte a frame, each
|
||||
byte the buttons held during it. **Several, comma separated**, fill the pads in turn - so a
|
||||
first fixture holding nothing and a second that does something puts the flying on pad one,
|
||||
which is how a game that reads only the first controller gets caught. It exists for the same reason as **keys** and matters more:
|
||||
a pad reports what is *held*, and a suite has no hands.
|
||||
|
||||
**A byte a frame, not a byte a read.** A pad is a level, so a game that asks twice in one
|
||||
frame has to be told the same thing both times, and a file that advanced per read would
|
||||
answer differently depending on how the program happened to be written. The frame is the
|
||||
machine's own, so a recording plays back the same over the same cycles however fast the host
|
||||
ran.
|
||||
|
||||
It is also the only way this device is reachable at all. Voyager reads a real controller and
|
||||
its own tests run `--headless`, with no window and nobody holding anything - so without a
|
||||
recorded path a pad would be exercised only by somebody playing, which is exactly the state
|
||||
the console's line editing was in when it broke twice in two days.
|
||||
|
||||
## Fixture Disks:
|
||||
|
||||
`Tests/makedisks.sh` builds 27 images with SplitDisk before anything runs, into
|
||||
@@ -434,9 +506,16 @@ than reading all of them:
|
||||
## 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
|
||||
file across the whole corpus - 33 file-and-rule pairs. It is checked on every run, and it
|
||||
is checked **in both directions.**
|
||||
|
||||
The corpus holds none of `falls-into-subroutine`, and that is worth saying rather than
|
||||
leaving as a gap in the file. It is the rule added after a bug in CosmOS's monitor walked
|
||||
into `sayPrompt`, reached a `RET` that had no caller, and returned into the filesystem's
|
||||
format routine - which formatted the disk the machine had booted from. A rule whose count is
|
||||
zero everywhere is not a rule doing nothing; it is the shape of fault that is worth never
|
||||
having again.
|
||||
|
||||
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
|
||||
@@ -461,7 +540,7 @@ which stops the markers outliving the code they were about.
|
||||
make sanitize
|
||||
```
|
||||
|
||||
Rebuilds all four tools with `-fsanitize=address,undefined` and runs **the whole suite**
|
||||
Rebuilds all five 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.
|
||||
|
||||
@@ -473,7 +552,7 @@ deliberate `calloc`. The machine's Program and Data memories are static arrays,
|
||||
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
|
||||
It runs everything because it used to not. It built all five 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
|
||||
|
||||
Executable
+124
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
# Breaks something on purpose and checks that a suite notices.
|
||||
#
|
||||
# ---- Why this is a script and not a thing to type each time ----
|
||||
#
|
||||
# A check that passes proves nothing until it has been seen to fail. Typing that by hand went
|
||||
# wrong twice in two days, and BOTH TIMES IT LOOKED LIKE A RESULT: the suite ran, went green,
|
||||
# and read exactly like "this check does not catch that".
|
||||
#
|
||||
# Once the edit produced code that would not compile. make failed, the exit status was not
|
||||
# looked at, and the PREVIOUS binary ran the suite.
|
||||
# Once the anchor was right but the filename was wrong, so nothing was edited at all.
|
||||
#
|
||||
# Neither had anything to do with the build system - headers have always rebuilt what depends
|
||||
# on them. What was missing was a harness that refuses to report a result it did not earn. So
|
||||
# every step below is checked, and anything unexpected is a hard error rather than a green
|
||||
# run: NOT FINDING THE BREAK IS THE ANSWER THIS TOOL EXISTS TO GIVE, and it is worthless if it
|
||||
# can also be the answer when the break never happened.
|
||||
#
|
||||
# Usage:
|
||||
# ./Tests/break.sh <file> <anchor> <replacement> <suite> [suite ...]
|
||||
#
|
||||
# The anchor must appear EXACTLY ONCE in the file. The file is restored afterwards whatever
|
||||
# happens, including on an interrupt.
|
||||
#
|
||||
# Written by Anachronaut
|
||||
|
||||
set -u
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
GREEN=$'\033[32m'; RED=$'\033[31m'; RESET=$'\033[0m'
|
||||
[ -t 1 ] || { GREEN=""; RED=""; RESET=""; }
|
||||
|
||||
die() { printf '%sbreak.sh: %s%s\n' "$RED" "$1" "$RESET" >&2; exit 2; }
|
||||
|
||||
[ $# -ge 4 ] || die "usage: break.sh <file> <anchor> <replacement> <suite> [suite ...]"
|
||||
|
||||
FILE="$1"; ANCHOR="$2"; REPLACEMENT="$3"; shift 3
|
||||
[ -f "$FILE" ] || die "no such file: $FILE"
|
||||
|
||||
# ---- The file comes back whatever happens ----
|
||||
#
|
||||
# Including a Ctrl-C in the middle of a suite, which is a thing that happens: a half broken
|
||||
# working tree that looks fine is worse than any failing test.
|
||||
KEEP="$(mktemp)"
|
||||
cp "$FILE" "$KEEP"
|
||||
restore() {
|
||||
cp "$KEEP" "$FILE"; rm -f "$KEEP"
|
||||
(cd "$ROOT" && make >/dev/null 2>&1 && ./Tests/makedisks.sh Tests/build >/dev/null 2>&1)
|
||||
}
|
||||
trap restore EXIT INT TERM
|
||||
|
||||
# ---- The edit, and proof it happened ----
|
||||
python3 - "$FILE" "$ANCHOR" "$REPLACEMENT" <<'PY' || die "the edit did not apply"
|
||||
import sys
|
||||
path, anchor, replacement = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
text = open(path).read()
|
||||
seen = text.count(anchor)
|
||||
if seen == 0:
|
||||
sys.stderr.write("anchor not found in %s\n" % path)
|
||||
raise SystemExit(1)
|
||||
if seen > 1:
|
||||
sys.stderr.write("anchor appears %d times in %s; it must be unique\n" % (seen, path))
|
||||
raise SystemExit(1)
|
||||
open(path, "w").write(text.replace(anchor, replacement))
|
||||
PY
|
||||
|
||||
# ---- The build, and proof of that too ----
|
||||
#
|
||||
# And the disk images after it, because half the things worth breaking are SplitBit assembly
|
||||
# rather than C, and those live on the fixture disks rather than in the binaries. Rebuilding
|
||||
# only the emulator meant an edit to a .asm file changed nothing the suite could see and the
|
||||
# tool reported that nothing caught the break - which is the exact lie it exists to prevent,
|
||||
# turning up in a new place.
|
||||
if ! (cd "$ROOT" && make) > "$KEEP.build" 2>&1; then
|
||||
printf '%sbreak.sh: the broken version does not build, so nothing was tested%s\n' \
|
||||
"$RED" "$RESET" >&2
|
||||
tail -5 "$KEEP.build" >&2
|
||||
rm -f "$KEEP.build"
|
||||
exit 2
|
||||
fi
|
||||
rm -f "$KEEP.build"
|
||||
|
||||
if ! (cd "$ROOT" && ./Tests/makedisks.sh Tests/build) > "$KEEP.disks" 2>&1; then
|
||||
printf '%sbreak.sh: the disks did not build, so nothing was tested%s\n' "$RED" "$RESET" >&2
|
||||
tail -5 "$KEEP.disks" >&2
|
||||
rm -f "$KEEP.disks"
|
||||
exit 2
|
||||
fi
|
||||
rm -f "$KEEP.disks"
|
||||
|
||||
# ---- And then the suites ----
|
||||
#
|
||||
# A suite that fails is the GOOD outcome here, so the exit status is inverted: this reports
|
||||
# success when the break was noticed.
|
||||
NOTICED=0
|
||||
for suite in "$@"; do
|
||||
name="$(basename "$suite" .sh)"
|
||||
if [ -x "$ROOT/Tests/$name.sh" ]; then
|
||||
run="$ROOT/Tests/$name.sh"
|
||||
elif [ -x "$suite" ]; then
|
||||
run="$suite"
|
||||
else
|
||||
die "no such suite: $suite"
|
||||
fi
|
||||
printf -- '---- %s ----\n' "$name"
|
||||
if "$run" 2>&1 | tee "$KEEP.out" | grep -E '^\s*\[FAIL\]' | sed 's/^ *//'; then
|
||||
:
|
||||
fi
|
||||
if grep -qE '^[0-9]+ passed, [0-9]+ failed' "$KEEP.out"; then
|
||||
NOTICED=1
|
||||
grep -E '^[0-9]+ passed, [0-9]+ failed' "$KEEP.out" | tail -1
|
||||
else
|
||||
printf '%s%s did not notice%s\n' "$RED" "$name" "$RESET"
|
||||
fi
|
||||
rm -f "$KEEP.out"
|
||||
done
|
||||
|
||||
if [ "$NOTICED" = "1" ]; then
|
||||
printf '%sThe break was caught.%s\n' "$GREEN" "$RESET"
|
||||
exit 0
|
||||
fi
|
||||
printf '%sNOTHING CAUGHT THE BREAK.%s\n' "$RED" "$RESET"
|
||||
exit 1
|
||||
Executable
+277
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env bash
|
||||
# Checks SplitDisk against the SBFS format.
|
||||
#
|
||||
# The tool and the SplitBit side are two implementations of one written specification,
|
||||
# and nothing but that document keeps them the same. This checks the host half on its
|
||||
# own: that a file put onto a disk comes back off it byte for byte, that the sizes which
|
||||
# exercise the block and tail arithmetic all survive, and that the things the format
|
||||
# says cannot happen are refused rather than half done.
|
||||
#
|
||||
# Written by Anachronaut
|
||||
|
||||
set -u
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
TOOL="$ROOT/SplitDisk"
|
||||
WORK="$ROOT/Tests/build/disk"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
FAILED_NAMES=()
|
||||
|
||||
GREEN=$'\033[32m'; RED=$'\033[31m'; RESET=$'\033[0m'
|
||||
[ -t 1 ] || { GREEN=""; RED=""; RESET=""; }
|
||||
|
||||
check() {
|
||||
local name="$1"; shift
|
||||
if "$@" >/dev/null 2>&1; then
|
||||
PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "$name"
|
||||
else
|
||||
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
|
||||
printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "$name"
|
||||
fi
|
||||
}
|
||||
|
||||
# The opposite: the command is supposed to fail, and passing would be the bug.
|
||||
refuses() {
|
||||
local name="$1"; shift
|
||||
if "$@" >/dev/null 2>&1; then
|
||||
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
|
||||
printf " [%sFAIL%s] %s (it was allowed)\n" "$RED" "$RESET" "$name"
|
||||
else
|
||||
PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "$name"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ ! -x "$TOOL" ]; then
|
||||
echo "SplitDisk is not built."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$WORK"; mkdir -p "$WORK"
|
||||
cd "$WORK" || exit 1
|
||||
|
||||
echo "Checking SplitDisk against the SBFS format."
|
||||
|
||||
check "format a disk" "$TOOL" format work.img 64 2
|
||||
refuses "refuse a disk with no room" "$TOOL" format tiny.img 2 4
|
||||
refuses "refuse an unformatted disk" "$TOOL" list /dev/null
|
||||
|
||||
# The sizes that exercise every corner of blocks-plus-tail: nothing at all, less than a
|
||||
# block, exactly a block, a part block, and an exact multiple.
|
||||
: > empty.bin
|
||||
printf 'x' > one.bin
|
||||
head -c 256 /dev/urandom > exact.bin
|
||||
head -c 700 /dev/urandom > part.bin
|
||||
head -c 768 /dev/urandom > whole.bin
|
||||
|
||||
for f in empty.bin one.bin exact.bin part.bin whole.bin; do
|
||||
check "put $f" "$TOOL" put work.img "$f"
|
||||
done
|
||||
|
||||
roundTrip() {
|
||||
"$TOOL" get work.img "$1" "got_$1" >/dev/null 2>&1 || return 1
|
||||
cmp -s "$1" "got_$1"
|
||||
}
|
||||
for f in empty.bin one.bin exact.bin part.bin whole.bin; do
|
||||
check "$f comes back byte for byte" roundTrip "$f"
|
||||
done
|
||||
|
||||
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 file that is not there" "$TOOL" get work.img nosuch.bin out.bin
|
||||
check "delete" "$TOOL" delete work.img one.bin
|
||||
refuses "the deleted file is gone" "$TOOL" get work.img one.bin out.bin
|
||||
check "the name can be used again" "$TOOL" put work.img one.bin
|
||||
|
||||
# Contiguous files mean a disk can have room without having room in one piece. That is a
|
||||
# consequence of the format rather than a bug, so it is checked rather than worked around.
|
||||
"$TOOL" format frag.img 16 1 >/dev/null 2>&1
|
||||
head -c 1024 /dev/urandom > a.bin; cp a.bin b.bin; cp a.bin c.bin
|
||||
"$TOOL" put frag.img a.bin >/dev/null 2>&1
|
||||
"$TOOL" put frag.img b.bin >/dev/null 2>&1
|
||||
"$TOOL" put frag.img c.bin >/dev/null 2>&1
|
||||
"$TOOL" delete frag.img a.bin >/dev/null 2>&1
|
||||
"$TOOL" delete frag.img c.bin >/dev/null 2>&1
|
||||
head -c 2048 /dev/urandom > big.bin
|
||||
refuses "refuse a file with no run long enough" "$TOOL" put frag.img big.bin
|
||||
head -c 512 /dev/urandom > fits.bin
|
||||
check "but one that fits the gap goes on" "$TOOL" put frag.img fits.bin
|
||||
|
||||
# ---- Directories ----
|
||||
#
|
||||
# Version two, which adds a parent to each entry and a flag bit saying an entry is a
|
||||
# directory. Both come out of bytes the entry had already set aside, so nothing moved and
|
||||
# a version one disk needs no converting: zero in those bytes means the root, which is
|
||||
# exactly where every file on a flat disk is.
|
||||
#
|
||||
# The version is therefore a statement about what is ON a disk rather than about what made
|
||||
# it, and these check that it is only raised when it becomes true.
|
||||
"$TOOL" format tree.img 64 2 >/dev/null 2>&1
|
||||
printf 'a file in the root' > root.txt
|
||||
check "a fresh disk is flat" "$TOOL" put tree.img root.txt
|
||||
version() { "$TOOL" list "$1" 2>/dev/null | head -1 | grep -q "version $2"; }
|
||||
check "and says it is version 1" version tree.img 1
|
||||
check "make a directory" "$TOOL" mkdir tree.img /Apps
|
||||
check "which raises it to version 2" version tree.img 2
|
||||
check "make one inside it" "$TOOL" mkdir tree.img /Apps/Source
|
||||
check "put a file down a path" "$TOOL" put tree.img root.txt /Apps/Source/deep.txt
|
||||
|
||||
# The point of the whole exercise: a name means something different in each place, so the
|
||||
# same one can be used twice without either being in the other's way.
|
||||
check "the same name in two places" "$TOOL" put tree.img root.txt /Apps/root.txt
|
||||
roundTripAt() {
|
||||
"$TOOL" get tree.img "$1" got_deep.txt >/dev/null 2>&1 || return 1
|
||||
cmp -s root.txt got_deep.txt
|
||||
}
|
||||
check "it comes back byte for byte" roundTripAt /Apps/Source/deep.txt
|
||||
check ". and .. walk the path" roundTripAt /Apps/./Source/../root.txt
|
||||
check ".. from the root is the root" roundTripAt /Apps/../../root.txt
|
||||
|
||||
# Each of these is a way the tree could be made to contradict itself, and each is refused
|
||||
# rather than half done.
|
||||
refuses "no file where a directory goes" "$TOOL" put tree.img root.txt /root.txt/x.txt
|
||||
refuses "no putting into thin air" "$TOOL" put tree.img root.txt /Nowhere/x.txt
|
||||
refuses "no duplicate in one directory" "$TOOL" mkdir tree.img /Apps
|
||||
refuses "no getting a directory" "$TOOL" get tree.img /Apps out.bin
|
||||
refuses "delete will not take a directory" "$TOOL" delete tree.img /Apps
|
||||
refuses "rmdir will not take a file" "$TOOL" rmdir tree.img /root.txt
|
||||
refuses "nor the root" "$TOOL" rmdir tree.img /
|
||||
|
||||
# THE REFUSAL THAT MATTERS MOST. Parents are entry indices and a freed index is handed out
|
||||
# again, so removing a directory with things still in it would let the next file created
|
||||
# adopt them. Emptying it first is the only safe order.
|
||||
refuses "no removing an occupied one" "$TOOL" rmdir tree.img /Apps/Source
|
||||
check "empty it first" "$TOOL" delete tree.img /Apps/Source/deep.txt
|
||||
check "then it goes" "$TOOL" rmdir tree.img /Apps/Source
|
||||
|
||||
# A path is names with separators between them, and a name is still twenty two characters.
|
||||
refuses "refuse a 23 character component" "$TOOL" mkdir tree.img /Apps/abcdefghijklmnopqrstuvw
|
||||
refuses "refuse a path naming nothing" "$TOOL" mkdir tree.img /Apps/
|
||||
|
||||
# A directory costs an entry and no blocks at all, which is what keeps the flat array of
|
||||
# entries the whole allocation map. If a directory ever took a block, this would drop.
|
||||
blocksFree() { "$TOOL" list "$1" 2>/dev/null | tail -1 | sed 's/.*used, //; s/ blocks free.*//'; }
|
||||
before=$(blocksFree tree.img)
|
||||
"$TOOL" mkdir tree.img /Empty >/dev/null 2>&1
|
||||
check "a directory costs no blocks" [ "$before" = "$(blocksFree tree.img)" ]
|
||||
|
||||
# ---- A directory no bigger than the parent field can name ----
|
||||
#
|
||||
# Eight entries to a block and the parent is an index plus one in two bytes, so entry
|
||||
# 65535 has no parent number: adding one wraps to zero, and zero is the root. Such an
|
||||
# entry does not refuse what is put inside it. It writes the thing into the ROOT while
|
||||
# reporting the path that was asked for, and then cannot find it again - so the same
|
||||
# create succeeds over and over, piling up entries of one name in one directory, which is
|
||||
# the exact corruption rename exists to refuse.
|
||||
refuses "no directory past the wrap" "$TOOL" format huge.img 65535 8192
|
||||
check "the largest that fits" "$TOOL" format huge.img 65535 8191
|
||||
|
||||
# And a disk claiming one, which is what something that never checked would have written.
|
||||
# The claim is in the superblock, so it does not need a disk that size to be made.
|
||||
"$TOOL" format lying.img 64 2 >/dev/null
|
||||
printf '\x20\x00' | dd of=lying.img bs=1 seek=10 conv=notrunc status=none
|
||||
refuses "nor reading one that claims it" "$TOOL" list lying.img
|
||||
|
||||
# ---- A boot area, and the two halves of the superblock that describe it ----
|
||||
#
|
||||
# bootBlocks and directoryStart say the same thing from two sides, so a disk where they
|
||||
# disagree is one where there is no way to tell which is wrong. Both are refused.
|
||||
check "format with a boot area" "$TOOL" format boot.img 512 4 32
|
||||
check "and it reads back" "$TOOL" list boot.img
|
||||
refuses "no boot area bigger than a disk" "$TOOL" format small.img 32 2 64
|
||||
check "and none at all is still fine" "$TOOL" format plain.img 64 2
|
||||
|
||||
bootField() { python3 -c "
|
||||
import sys
|
||||
f = open(sys.argv[1], 'r+b'); f.seek(int(sys.argv[2])); f.write(bytes.fromhex(sys.argv[3]))
|
||||
" "$@"; }
|
||||
cp boot.img lying.boot.img
|
||||
bootField lying.boot.img 14 0010 # Claims 16 blocks a slot, directory says 32.
|
||||
refuses "nor a boot area that disagrees" "$TOOL" list lying.boot.img
|
||||
cp boot.img badslot.img
|
||||
bootField badslot.img 16 07 # Names slot 7, and there are two.
|
||||
refuses "nor a slot that does not exist" "$TOOL" list badslot.img
|
||||
|
||||
# ---- Writing a boot slot, and choosing between them ----
|
||||
#
|
||||
# Two commands rather than one, deliberately: writing a slot and starting from it are
|
||||
# different decisions, and joining them would make every write a commitment.
|
||||
printf 'not really a bootloader' > stage.bin
|
||||
check "write a boot slot" "$TOOL" boot boot.img stage.bin 0
|
||||
check "and the other one" "$TOOL" boot boot.img stage.bin 1
|
||||
check "choose which one starts" "$TOOL" bootslot boot.img 1
|
||||
refuses "no third slot to write" "$TOOL" boot boot.img stage.bin 2
|
||||
refuses "nor a third to choose" "$TOOL" bootslot boot.img 2
|
||||
refuses "no boot slot without an area" "$TOOL" boot plain.img stage.bin 0
|
||||
|
||||
# A slot holds what it holds. Something too big for one is refused rather than cut off,
|
||||
# because half a bootloader is the failure with no way back.
|
||||
head -c 9000 /dev/zero > toobig.bin # A slot on boot.img is 32 blocks, so 8192.
|
||||
refuses "nor more than a slot holds" "$TOOL" boot boot.img toobig.bin 0
|
||||
|
||||
# THE WHOLE SLOT IS WRITTEN, not just the part the file fills. A slot still holding the
|
||||
# tail of whatever was there before is one whose contents depend on its history.
|
||||
printf 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' > long.bin
|
||||
"$TOOL" boot boot.img long.bin 0 >/dev/null
|
||||
"$TOOL" boot boot.img stage.bin 0 >/dev/null
|
||||
check "and it is written whole" python3 -c "
|
||||
import sys
|
||||
d = open('boot.img','rb').read()
|
||||
slot = d[256:256 + 32 * 256]
|
||||
sys.exit(1 if b'aaaa' in slot else 0)"
|
||||
|
||||
# ---- How the last start went ----
|
||||
check "the boot state reads" "$TOOL" bootstate boot.img
|
||||
check "and can be set" "$TOOL" bootstate boot.img 2
|
||||
refuses "but only to a state there is" "$TOOL" bootstate boot.img 7
|
||||
check "a fresh disk is settled" python3 -c "
|
||||
import sys
|
||||
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
|
||||
if [ "$FAIL" -eq 0 ]; then
|
||||
echo "All $PASS disk tool checks passed."
|
||||
exit 0
|
||||
fi
|
||||
echo "$PASS passed, $FAIL failed: ${FAILED_NAMES[*]}"
|
||||
exit 1
|
||||
+62
-2
@@ -456,6 +456,50 @@ for name, text in (("the Programming Manual", pm), ("the Assembler Manual", am),
|
||||
problems.append("%s still says \"%s\", and it has not been true since"
|
||||
" directories arrived" % (name, claim))
|
||||
|
||||
# ---- The shell's words are a table as well as a chain of comparisons ----
|
||||
#
|
||||
# The dispatch is a run of "is the line this name" tests, which is fine to execute and
|
||||
# impossible to WALK - so completing a half typed command needs the names as data too, and
|
||||
# they are: fifteen strings packed end to end from ShellNames, each ending in the zero that
|
||||
# says where the next begins.
|
||||
#
|
||||
# THE TWO CAN DISAGREE AND THE WAY THEY DO IS QUIET. A command added to the dispatch and not
|
||||
# to the run simply never completes, which nobody would think to test by hand; something put
|
||||
# BETWEEN the strings ends the walk early and takes the rest of the commands with it. So this
|
||||
# reads both and compares them, and reads the count as well, because a run of strings does
|
||||
# not say where it stops.
|
||||
cosmosSource = open("Programs/CosmOS/Source/cosmos.asm").read()
|
||||
|
||||
# The dispatch, up to the point where the monitor's single letters begin - those are one
|
||||
# character each and there is nothing to complete about them.
|
||||
dispatchEnd = cosmosSource.find("SETD.0 Mode")
|
||||
dispatched = re.findall(r"SETD\.1 (\w+Name)\b", cosmosSource[:dispatchEnd])
|
||||
|
||||
# The run, which ends at the first thing that is not a label and a string.
|
||||
runAt = cosmosSource.find("ShellNames:")
|
||||
packed = []
|
||||
if runAt < 0:
|
||||
problems.append("cosmos.asm has no ShellNames run for the shell's own words")
|
||||
else:
|
||||
lines = cosmosSource[runAt:].split("\n")[1:]
|
||||
while len(lines) >= 2 and re.fullmatch(r"(\w+Name):", lines[0]) \
|
||||
and re.fullmatch(r'"[^"]*"', lines[1]):
|
||||
packed.append(lines[0][:-1])
|
||||
lines = lines[2:]
|
||||
|
||||
stated = re.search(r"ShellNameCount:\s*\n\s*0d(\d+)", cosmosSource)
|
||||
if not stated:
|
||||
problems.append("cosmos.asm no longer says how many shell names there are")
|
||||
elif int(stated.group(1)) != len(packed):
|
||||
problems.append("cosmos.asm says there are %s shell names and the run holds %d"
|
||||
% (stated.group(1), len(packed)))
|
||||
if runAt >= 0 and sorted(packed) != sorted(dispatched):
|
||||
missing = sorted(set(dispatched) - set(packed))
|
||||
extra = sorted(set(packed) - set(dispatched))
|
||||
problems.append("the shell's dispatch and its packed names disagree:%s%s"
|
||||
% ("".join(" %s is dispatched and not in the run;" % n for n in missing),
|
||||
"".join(" %s is in the run and not dispatched;" % n for n in extra)))
|
||||
|
||||
# ---- CosmOS fits in the half of the machine it says it does ----
|
||||
#
|
||||
# The memory map in the CosmOS README is a CONVENTION. Nothing in the assembler, the
|
||||
@@ -599,7 +643,11 @@ else:
|
||||
# file.
|
||||
#
|
||||
# The bullets now live in the Test Manual rather than the README, so that is what is read.
|
||||
# makedisks.sh is not counted, because it builds the images rather than checking anything;
|
||||
# makedisks.sh is not counted, because it builds the images rather than checking anything,
|
||||
# and break.sh is not counted for the same reason turned round: it checks that a check works,
|
||||
# is run by hand at the moment a check is written, and is not part of what "make test" means.
|
||||
# Both are still described in the manual - what they are excluded from is the COUNT of the
|
||||
# suite, not from being documented, and the check below enforces that.
|
||||
# run.sh is counted, because the manual describes it alongside the rest.
|
||||
rootReadme = open("README.md").read()
|
||||
manual = open("SplitBit Test Manual.md").read()
|
||||
@@ -607,8 +655,9 @@ manual = open("SplitBit Test Manual.md").read()
|
||||
# lines. Every pattern below runs against a copy with its whitespace flattened.
|
||||
flat = re.sub(r"\s+", " ", manual)
|
||||
|
||||
notSuite = ("makedisks.sh", "break.sh")
|
||||
scripts = sorted(os.path.basename(p) for p in glob.glob("Tests/*.sh")
|
||||
if os.path.basename(p) != "makedisks.sh")
|
||||
if os.path.basename(p) not in notSuite)
|
||||
# Spelled out, because that is how the documents say them. Kept a few ahead of the count so
|
||||
# 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.
|
||||
@@ -625,6 +674,17 @@ for name in scripts:
|
||||
problems.append("Tests/%s runs in the suite and the Test Manual does not say what"
|
||||
" it is for" % name)
|
||||
|
||||
# ---- And the two that are not in the suite are still described ----
|
||||
#
|
||||
# Being left out of the COUNT is not the same as being left out of the manual, and the gap
|
||||
# between those two is exactly where a script goes undocumented for months. A tool nobody has
|
||||
# written down is a tool nobody uses, which for break.sh would be a particular waste: it
|
||||
# exists because the technique it automates was got wrong by hand twice.
|
||||
for name in notSuite:
|
||||
if ("`Tests/%s`" % name) not in manual:
|
||||
problems.append("Tests/%s is a tool the suite does not count, and the Test Manual"
|
||||
" does not say what 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
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
CosmOS
|
||||
> set a hello
|
||||
> if same $a hello
|
||||
> echo taken
|
||||
taken
|
||||
> else
|
||||
> echo not taken
|
||||
> end
|
||||
> if load nosuch.sbx
|
||||
no such file
|
||||
> echo loaded
|
||||
> else
|
||||
> echo did not
|
||||
did not
|
||||
> end
|
||||
> else
|
||||
else with no if above it
|
||||
> end
|
||||
end with no if above it
|
||||
> if
|
||||
if wants a command to decide by
|
||||
> do blocks.script
|
||||
> set colour red
|
||||
> if same $colour red
|
||||
> echo it is red
|
||||
it is red
|
||||
> if same $colour blue
|
||||
> echo and blue
|
||||
> else
|
||||
> echo but not blue
|
||||
but not blue
|
||||
> end
|
||||
> else
|
||||
> echo $neverSetAnywhere
|
||||
> end
|
||||
> if same $colour blue
|
||||
> echo wrong
|
||||
> else
|
||||
> echo right
|
||||
right
|
||||
> end
|
||||
> if same $colour blue
|
||||
> if same $colour red
|
||||
> echo deep wrong
|
||||
> else
|
||||
> echo deep also wrong
|
||||
> end
|
||||
> end
|
||||
> echo done
|
||||
done
|
||||
> exit
|
||||
halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -1,15 +1,15 @@
|
||||
CosmOS
|
||||
> load Break.sbx
|
||||
loaded, starting at 4000
|
||||
loaded, starting at 5000
|
||||
> run
|
||||
two stops, and what the registers were at each
|
||||
break at 4016
|
||||
break at 5016
|
||||
A 11 B 22 Q 00 status 00
|
||||
DP0 2030 DP1 2000 DP2 2037 DP3 4000 SP FFFF
|
||||
DP0 3030 DP1 3000 DP2 3037 DP3 5000 SP FFFF
|
||||
press a key
|
||||
break at 4034
|
||||
break at 5034
|
||||
A 44 B 55 Q 00 status 00
|
||||
DP0 2000 DP1 2037 DP2 2030 DP3 4000 SP FFF5
|
||||
DP0 3000 DP1 3037 DP2 3030 DP3 5000 SP FFF5
|
||||
press a key
|
||||
carried on to the end
|
||||
finished
|
||||
|
||||
@@ -19,8 +19,19 @@ Snake.sbx 2164
|
||||
Keys.sbx 664
|
||||
Say.sbx 156
|
||||
Break.sbx 149
|
||||
Grid.sbx 559
|
||||
Grid.sbx 571
|
||||
Press.sbx 872
|
||||
Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 9220
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
blocks.script 343
|
||||
loops.script 272
|
||||
tune.sbx 306
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
@@ -32,7 +43,7 @@ outer.script 376
|
||||
inner.script 44
|
||||
loop.script 35
|
||||
crossed.txt 560
|
||||
19 files, 1 directory
|
||||
30 files, 1 directory
|
||||
> exit
|
||||
halted
|
||||
Execution halted.
|
||||
|
||||
@@ -9,8 +9,19 @@ Snake.sbx 2164
|
||||
Keys.sbx 664
|
||||
Say.sbx 156
|
||||
Break.sbx 149
|
||||
Grid.sbx 559
|
||||
Grid.sbx 571
|
||||
Press.sbx 872
|
||||
Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 9220
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
blocks.script 343
|
||||
loops.script 272
|
||||
tune.sbx 306
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
@@ -21,7 +32,7 @@ nonl.script 38
|
||||
outer.script 376
|
||||
inner.script 44
|
||||
loop.script 35
|
||||
18 files, 1 directory
|
||||
29 files, 1 directory
|
||||
> drive 1
|
||||
> dir
|
||||
other.txt 28
|
||||
|
||||
@@ -1,24 +1,42 @@
|
||||
CosmOS
|
||||
> load Edit.sbx
|
||||
loaded, starting at 4000
|
||||
loaded, starting at 5000
|
||||
> run poem.txt
|
||||
poem.txt, new file
|
||||
> : : : : > : : > 1: alpha
|
||||
> a
|
||||
: alpha
|
||||
: beta
|
||||
: gamma
|
||||
: .
|
||||
> i 2
|
||||
: INSERTED
|
||||
: .
|
||||
> l
|
||||
1: alpha
|
||||
2: INSERTED
|
||||
3: beta
|
||||
4: gamma
|
||||
> : > > 1: CHANGED
|
||||
> c 1
|
||||
: CHANGED
|
||||
> d 4
|
||||
> l
|
||||
1: CHANGED
|
||||
2: INSERTED
|
||||
3: beta
|
||||
> written, 22 bytes
|
||||
> finished
|
||||
> w
|
||||
written, 22 bytes
|
||||
> q
|
||||
finished
|
||||
> run poem.txt
|
||||
poem.txt, 3 lines
|
||||
> 1: CHANGED
|
||||
> l
|
||||
1: CHANGED
|
||||
2: INSERTED
|
||||
3: beta
|
||||
> there is no such line
|
||||
> finished
|
||||
> d 99
|
||||
there is no such line
|
||||
> q
|
||||
finished
|
||||
> exit
|
||||
halted
|
||||
Execution halted.
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
CosmOS
|
||||
> Edit hello.asm
|
||||
hello.asm, 31 lines
|
||||
> finished
|
||||
> q
|
||||
finished
|
||||
> Edit hello.asm
|
||||
hello.asm, 31 lines
|
||||
> finished
|
||||
> q
|
||||
finished
|
||||
> Status
|
||||
the last program left 0, which is: it did what it was asked
|
||||
finished
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
CosmOS
|
||||
> echo remembered by the shell
|
||||
remembered by the shell
|
||||
> load Edit.sbx
|
||||
loaded, starting at 5000
|
||||
> run typed.txt
|
||||
typed.txt, new file
|
||||
> a
|
||||
: alpah[9;3Halpa [9;3Halpha[9;8H
|
||||
: second
|
||||
: .
|
||||
> l
|
||||
1: alpha
|
||||
2: second
|
||||
> w
|
||||
written, 13 bytes
|
||||
> q
|
||||
finished
|
||||
> [19;3Hrun typed.txt[19;16H
|
||||
typed.txt, 2 lines
|
||||
> q
|
||||
finished
|
||||
> exit
|
||||
halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -0,0 +1,56 @@
|
||||
CosmOS
|
||||
> Crash opcode
|
||||
that byte is not an instruction, at 5081
|
||||
A 00 B 0F Q 00
|
||||
the program was stopped
|
||||
> Crash service
|
||||
nothing is installed at service 28, at 5084
|
||||
A 00 B 0F Q 00
|
||||
the program was stopped
|
||||
> Crash bank
|
||||
a bank that is not there, or an address past its end, at 509E
|
||||
A 01 B 0F Q 00
|
||||
the program was stopped
|
||||
> Crash device
|
||||
nothing is installed for the device on port 00, at 50A5
|
||||
A 02 B 0F Q 00
|
||||
the program was stopped
|
||||
> Crash sideways
|
||||
Crash opcode | service | bank | device | blind
|
||||
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 571
|
||||
Press.sbx 872
|
||||
Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 9220
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
blocks.script 343
|
||||
loops.script 272
|
||||
tune.sbx 306
|
||||
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
|
||||
29 files, 1 directory
|
||||
> exit
|
||||
halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -0,0 +1,12 @@
|
||||
CosmOS
|
||||
> monitor
|
||||
x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves
|
||||
* b program
|
||||
bank 00
|
||||
* s 4F00 00
|
||||
* g 4F00
|
||||
that byte is not an instruction, at 4F00
|
||||
A 01 B 7F Q 00
|
||||
that was the system itself, so there is nowhere to carry on from. Start the machine again.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -0,0 +1,46 @@
|
||||
CosmOS
|
||||
> Say before Flip
|
||||
it says: before Flip
|
||||
finished
|
||||
> Flip
|
||||
A screen is drawn where you cannot see it. A key shows it, another comes back.
|
||||
finished
|
||||
> Say after Flip
|
||||
it says: after Flip
|
||||
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 571
|
||||
Press.sbx 872
|
||||
Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 9220
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
blocks.script 343
|
||||
loops.script 272
|
||||
tune.sbx 306
|
||||
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
|
||||
29 files, 1 directory
|
||||
> exit
|
||||
halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -16,8 +16,19 @@ Snake.sbx 2164
|
||||
Keys.sbx 664
|
||||
Say.sbx 156
|
||||
Break.sbx 149
|
||||
Grid.sbx 559
|
||||
Grid.sbx 571
|
||||
Press.sbx 872
|
||||
Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 9220
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
blocks.script 343
|
||||
loops.script 272
|
||||
tune.sbx 306
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
@@ -28,7 +39,7 @@ nonl.script 38
|
||||
outer.script 376
|
||||
inner.script 44
|
||||
loop.script 35
|
||||
18 files, 1 directory
|
||||
29 files, 1 directory
|
||||
> exit
|
||||
halted
|
||||
Execution halted.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user