diff --git a/Programs/Examples/colours.asm b/Programs/Examples/colours.asm new file mode 100644 index 0000000..1c39bfb --- /dev/null +++ b/Programs/Examples/colours.asm @@ -0,0 +1,142 @@ +; colours.asm +; Every colour the machine wakes up with, and how to change one. +; Written by Anachronaut +; +; ---- What a colour is on this machine ---- +; +; The screen draws CELLS, and a cell is two bytes: which tile, and an attribute. A tile is +; eight by eight pixels and every pixel is a byte - a number, not a colour. What colour that +; number means is looked up in the PALETTE, which is 256 entries of four bytes: red, green, +; blue, and one spare. Four rather than three so that entry n begins at n times four, which +; is a shift; three would need a multiply and this machine has none. +; +; A cell's attribute nibble is ADDED to every number in its tile, sixteen at a time. So the +; same tile drawn with attribute 0 reads palette entries 0 and 1, with attribute 1 it reads +; 16 and 17, and so on. Sixteen banks of sixteen. +; +; The console's glyphs are drawn in numbers 0 and 1 - paper and ink - so for text those +; sixteen banks are sixteen INK AND PAPER PAIRS. Writing the attribute register at port 0x06 +; says which one to use. +; +; The palette a machine wakes up with is laid out so one bit inverts a pair: +; +; banks 0 to 7 a colour on black +; banks 8 to 15 the same colour AS the background, with black text on it +; +; So attribute XOR 8 highlights, which is all a cursor is. +; +; ---- Reaching the palette ---- +; +; Video memory belongs to the screen, not to the program, so the CPU cannot write it with a +; store. It is reached the way every device's memory is reached: registered as a bank, and +; written through the memory controller. That is what the last part of this program does. + +#Program + +start: + ; The console wants sixteen columns for the name and a bit more, so the wide screen is + ; not needed. This is the mode the machine wakes up in and is here to be seen. + RSTA + OUTA 0x31 + + ; ---- Sixteen pairs, one line each ---- + ; + ; Counting in memory rather than in a register, because the loop below uses A and B for + ; the arithmetic and there is nowhere else to keep it. + SETD.0 Bank + STA.0 ; Still the zero from the mode write above: SETD does not touch A + +nextBank: + LDA.0 + OUTA 0x06 ; Draw in this pair from now on + SETD.1 SampleText + RCAL say + + ; The same bank with bit 3 set, which is the same colour inside out. + LDA.0 + INIB 0x08 + XOR + MVQA + OUTA 0x06 + SETD.1 HighlightText + RCAL say + + RSTA + OUTA 0x06 ; Back to plain for the newline + INIA 0x0A + OUTA 0x00 + + LDA.0 + INCA + STA.0 + INIB 0d8 ; Eight banks; the other eight are their reverses + CCF + SUB + BNQ nextBank + + ; ---- And one written by hand ---- + ; + ; Bank 2 is green when the machine starts. This makes its ink orange instead, by writing + ; three bytes into the palette - which means reaching video memory, which means the + ; controller. + + ; Give the screen's memory a bank number. The screen answers on port 0x30, and bank 3 is + ; the first number software is allowed to hand out: 0, 1 and 2 belong to the machine. + INIA 0d3 + OUTA 0xE3 ; DestBank: the number being given + INIA 0x30 + OUTA 0xE2 ; SourceLow: the port that owns the memory + INIA 0x03 + OUTA 0xE8 ; Command: RegisterBank + + ; The palette starts at 0xC000, 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 0xC084. + INIA 0xC0 + OUTA 0xE4 ; DestHigh + INIA 0x84 + OUTA 0xE5 ; DestLow + + ; Writing the controller's Data port puts a byte at the destination and steps it on, so + ; three writes are red, green and blue in order. + INIA 0xF0 + OUTA 0xE9 ; red + INIA 0x80 + OUTA 0xE9 ; green + INIA 0x20 + OUTA 0xE9 ; blue + + INIA 0x02 + OUTA 0x06 ; That pair again, now that it has been changed + SETD.1 ChangedText + RCAL say + RSTA + OUTA 0x06 + HALT + +; DP1 names a string. Printing is one byte at a time out of port 0x00, which is the oldest +; thing on this machine and has never changed. +say: + LDA.1 + BRA sayDone + OUTA 0x00 + INCD.1 + BRI say +sayDone: + RRET + +#Data + +Bank: + 0x00 + +SampleText: +" ordinary " +HighlightText: +" highlighted " +ChangedText: +" + bank 2's ink is orange now, because this program said so +" + +#Vectors + Boot start diff --git a/README.md b/README.md index da64816..5c92ff3 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ 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 | -| [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life | +| [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life, the colours | | [`Programs/Libraries`](Programs/Libraries) | Code included by name rather than linked, since there is no linker | | [`Programs/Loader`](Programs/Loader) | The standalone loader CosmOS grew out of | | [`Programs/CosmOS`](Programs/CosmOS) | The operating system, its applications, and the native assembler | diff --git a/Source/Emulator/cpu.c b/Source/Emulator/cpu.c index dbdd251..20934a1 100644 --- a/Source/Emulator/cpu.c +++ b/Source/Emulator/cpu.c @@ -48,6 +48,10 @@ static inline uint8_t portIn(CPURegisters *cpu, uint8_t port) { cpu->busCycles++; uint8_t value = InputHandler(port); cpu->busCycles += controllerTakeCycles(); + // And whatever time went by while the device kept the machine waiting. Idle rather than + // bus, because a machine stopped on a port is not using memory - the same distinction + // WAIT makes, arrived at from the other direction. + cpu->idleCycles += takeIdleCycles(); return value; } diff --git a/Source/Emulator/io.c b/Source/Emulator/io.c index 1520204..582b5b8 100644 --- a/Source/Emulator/io.c +++ b/Source/Emulator/io.c @@ -341,6 +341,34 @@ static void consoleDraw(uint8_t byte) { // when one is typed, or -1 to say the window has gone. static int (*inputHook)(int mayWait) = NULL; +// About one frame, which is how long a hook that presents takes to come back. It does not +// have to be exact - nothing is being measured, and the only thing downstream of it is a +// cursor blinking at somebody who is thinking about what to type. +#define CONSOLE_WAIT_CYCLES 16667 + +static unsigned long idleCycles = 0; + +// The machine's clock as the devices last heard it. Kept here rather than passed about, +// because a device that has to know how long it has been waiting has to know what time it +// is now. +static unsigned long deviceNow = 0; + +// A frame has gone by with nobody typing. TWO THINGS FOLLOW FROM THAT, and only doing the +// first is what left the cursor frozen: the CPU is told afterwards that it was stopped for a +// while, which is what idle cycles are - and the DEVICES are told now, because they are +// still running. A display controller blinking a cursor does not stop because the processor +// is waiting on a key, and neither does a disk finishing a read. +static void consoleWaited(void) { + idleCycles += CONSOLE_WAIT_CYCLES; + deviceTick(deviceNow + CONSOLE_WAIT_CYCLES); +} + +unsigned long takeIdleCycles(void) { + const unsigned long taken = idleCycles; + idleCycles = 0; + return taken; +} + // ---- Line editing, which the terminal used to do ---- // // A TERMINAL IN LINE MODE DOES NOT HAND A PROGRAM EVERY KEYSTROKE. It collects a line, @@ -369,6 +397,8 @@ static int consoleGatherLine(void) { return 0; } if (got < 0) { + // Nobody has typed yet, and the hook spent a frame keeping the window alive. + consoleWaited(); continue; } if (got == 0x08) { @@ -436,7 +466,9 @@ uint8_t consoleReadByte(void) { consoleEnded = 1; return 0xFF; } - // Nothing typed yet. The hook kept the window alive; ask it again. + // Nothing typed yet. The hook kept the window alive, which took a frame, and a + // frame of waiting is a frame of time passing. + consoleWaited(); } } unsigned char byte; @@ -660,7 +692,6 @@ static uint8_t diskBuffer[DISK_BLOCK_BYTES]; // what a program that ignores the busy bit deserves to read. // The machine's clock as devices see it, which the emulator advances as the CPU spends // cycles. A device says when it will be finished in these, and is believed. -static unsigned long deviceNow = 0; static void diskTransfer(uint8_t command); static unsigned long diskLatency = 0; diff --git a/Source/Emulator/io.h b/Source/Emulator/io.h index eb84170..ebcfd4d 100644 --- a/Source/Emulator/io.h +++ b/Source/Emulator/io.h @@ -250,6 +250,18 @@ void consoleSetInputHook(int (*hook)(int mayWait)); // display that refreshes, a port that waits on the host - wants exactly this shape. void deviceTick(unsigned long now); +// ---- Time that passed while the machine was stopped ---- +// +// A console waiting on a key it has not been given has stopped the machine, and time is +// still going by: the cursor still blinks, a disk still turns. That is exactly what idle +// cycles are for, and without them the machine's clock froze the moment somebody was asked +// a question - so the cursor stopped blinking precisely when there was a person looking at +// it and waiting to type. +// +// Returned and cleared, the way the controller's cycles are, and picked up in the same +// place: after a port access, by the CPU that was stopped. +unsigned long takeIdleCycles(void); + // How many cycles a block read or write takes. Zero means the answer is there before the // next instruction is, which is what this machine has always done and what every recorded // test assumes. diff --git a/Source/Emulator/machine.c b/Source/Emulator/machine.c index 67b04fa..08575c4 100644 --- a/Source/Emulator/machine.c +++ b/Source/Emulator/machine.c @@ -95,6 +95,16 @@ static int keyboardHook(int mayWait) { if (byte == EOF) { return CONSOLE_GONE; } + // ---- A zero is a moment of nobody typing ---- + // + // The commonest thing that happens behind a window is NOTHING: sixty times a second the + // console asks and is told to come back later, and everything that goes on while that is + // true - the clock advancing, a cursor blinking, a disk finishing - was unreachable from + // here, because a file always has another byte. A zero is a byte no keyboard sends, so it + // is free to mean the one thing a file otherwise cannot say. + if (byte == 0x00) { + return CONSOLE_NOTHING_YET; + } return byte & 0xFF; } diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index e071038..69a0d7c 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -607,6 +607,10 @@ So `attribute XOR 8` turns any pair inside out, which is what a highlighted line **That arrangement is a convention rather than a rule of the machine.** A program that wants different colours writes its own palette, and one that wants thirty-two of something rather than sixteen pairs can have that too - the device only ever adds the nibble and looks the answer up. +The palette lives at 0xC000 in video memory, four bytes an entry - red, green, blue, and one spare - so entry *n* begins at 0xC000 plus *n* times four. Video memory belongs to the screen rather than to the program, so it is written the way every device's memory is written: registered as a bank, and reached through the memory controller. + +`Programs/Examples/colours.asm` does all of that in eighty lines and prints the result. It shows the sixteen pairs, shows what XOR 8 does to each, and then changes one of them by writing three bytes into the palette, so that the difference between using the colours a machine wakes up with and choosing your own is visible in one program. + ### Moving The Cursor: Three more registers, because that is how this machine talks to everything else. diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index 4db09c0..4b1191c 100644 --- a/SplitBit Test Manual.md +++ b/SplitBit Test Manual.md @@ -78,7 +78,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`. 166 tests, of which 104 run, 35 +everything it printed against a file in `Tests/expected`. 167 tests, of which 105 run, 35 only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image given at all. diff --git a/Tests/expected/colours.out b/Tests/expected/colours.out new file mode 100644 index 0000000..bf8c81b --- /dev/null +++ b/Tests/expected/colours.out @@ -0,0 +1,12 @@ + ordinary highlighted + ordinary highlighted + ordinary highlighted + ordinary highlighted + ordinary highlighted + ordinary highlighted + ordinary highlighted + ordinary highlighted + + bank 2's ink is orange now, because this program said so +Execution halted. +[exit 0] diff --git a/Tests/manifest b/Tests/manifest index 6367fe4..4cc7e14 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -44,6 +44,7 @@ printHello | Examples/printHello.asm | run | - 8bitFibonacci | Examples/Fibonacci/8bitFibonacci.asm | run | - | - 16bitFibonacci | Examples/Fibonacci/16bitFibonacci.asm | run | - | - 32bitFibonacci | Examples/Fibonacci/32bitFibonacci.asm | run | - | - +colours | Examples/colours.asm | run | - | - 8bitSieve | Examples/primeSieve/8bitSieve.asm | run | - | - 16bitSegmentedSieve | Examples/primeSieve/16bitSegmentedSieve.asm | run | - | - # The four pointer rewrite. It emits exactly the same primes as the line above, which diff --git a/Tests/video.sh b/Tests/video.sh index ed1f178..4127f91 100755 --- a/Tests/video.sh +++ b/Tests/video.sh @@ -109,13 +109,27 @@ say() { done } +# Waits, as a keyboard file: a zero is a moment of nobody typing, which is the commonest +# thing that happens behind a window and the only thing a file otherwise cannot say. +waiting() { + python3 -c "import sys; sys.stdout.buffer.write(b'\\x00' * int(sys.argv[1]) + b'x')" "$1" \ + > "$BUILD/waits.keys" + echo "$BUILD/waits.keys" +} + # Assembles what is on standard input, runs it, and leaves the picture in $BUILD/.ppm. +# A second argument names a keyboard file to feed it. run() { local name="$1" cat > "$BUILD/$name.asm" "$ASM" "$BUILD/$name.asm" -o "$BUILD/$name.bin" >"$BUILD/$name.log" 2>&1 || { echo "could not assemble $name"; sed 's/^/ /' "$BUILD/$name.log"; return 1; } - "$EMU" --fast --screen "$BUILD/$name.ppm" "$BUILD/$name.bin" > "$BUILD/$name.out" 2>&1 + if [ -n "${2:-}" ]; then + "$EMU" --fast --keyboard "$2" --screen "$BUILD/$name.ppm" "$BUILD/$name.bin" \ + > "$BUILD/$name.out" 2>&1 + else + "$EMU" --fast --screen "$BUILD/$name.ppm" "$BUILD/$name.bin" > "$BUILD/$name.out" 2>&1 + fi } # One pixel out of a PPM, as "r,g,b". @@ -478,6 +492,39 @@ coloured cursorblink 0 0 "0,0,0" \ && result ok "the cursor blinks off again" "half a second later, dark" \ || result no "the cursor blinks off again" "got $(pixel cursorblink 0 0)" +# ---- Blinking while the machine is stopped ---- +# +# THE MACHINE IS NOT RUNNING while it waits for a key, and that is exactly when somebody is +# looking at the cursor. Time still has to reach the devices: a display controller does not +# stop blinking because the processor is waiting on a keyboard, any more than a disk stops +# turning. Waiting is charged as idle cycles and the devices are told as it happens, so the +# phase below is a pure function of how long nobody typed for. +# +# Key mode, so nothing is echoed and the cursor stays in the corner where it can be seen. +BLINKER='#Program +start: + INIA 0x05 + OUTA 0x02 + INA 0x00 + HALT +#Vectors + Boot start' + +echo "$BLINKER" | run blinkon "$(waiting 4)" || exit 1 +coloured blinkon 0 0 "216,216,216" \ + && result ok "the cursor is lit while waiting" "sixty thousand cycles in" \ + || result no "the cursor is lit while waiting" "got $(pixel blinkon 0 0)" + +echo "$BLINKER" | run blinkoff "$(waiting 40)" || exit 1 +coloured blinkoff 0 0 "0,0,0" \ + && result ok "and dark half a second later" "the machine's clock, not the host's" \ + || result no "and dark half a second later" "got $(pixel blinkoff 0 0)" + +echo "$BLINKER" | run blinkagain "$(waiting 70)" || exit 1 +coloured blinkagain 0 0 "216,216,216" \ + && result ok "and lit again after that" "which is what blinking is" \ + || result no "and lit again after that" "got $(pixel blinkagain 0 0)" + # And what scrolled off the top is still in the map, which is scrollback nothing had to keep. { printf '#Program\nstart:\n' say "A"