0264b19a3dc6f0efcdd7d0dcb7e3fbad785a4ffc
28
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fed1453e6e |
Controllers: four pads that say what is held
The console says WHICH KEY WENT DOWN, which 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. Lunar Porter's thrust is a burn per press for exactly that reason. So a pad is its own device on ports 0x60 to 0x6F, reporting a LEVEL. One read gives every button at once, holding is the natural thing to express, two directions together cost nothing, and reading does not consume it - a game may ask twice in a frame and be told the same thing both times. Four of them, because a party is four. They cost a port each and nothing at all when unused. The directions are the low nibble so "which way" is an AND with 0x0F; the buttons are the high nibble for the same reason. 0x64 says which are really there, so a game can ask for a controller rather than sitting silent while somebody presses things at it. They never interrupt: a game polls once a frame because that is when it draws. KEY-UP ON THE CONSOLE WAS THE OTHER WAY TO DO THIS AND 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. Voyager drives pad nought from the keyboard as well as from any real controller, OR-ed rather than chosen between, so a game written for a pad is playable on a machine with none and unplugging one mid-game does not leave somebody holding nothing. And a recorded path, which is what makes any of it testable: --pad names a file of one byte a frame, and the manifest has an eighth column for it. A BYTE A FRAME AND NOT A BYTE A READ - a level asked twice in one frame has to answer the same both times, and a file that advanced per read would depend on how the program happened to be written. Voyager's own tests run headless with nobody holding anything, so without this the device would be exercised only by somebody playing: the state the console's line editing was in when it broke twice in two days. 0x50 is the timer, not free. The block this went in was chosen after looking rather than before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
b3726c950a |
Deliver the keys that are not characters
An arrow key has never reached this machine. Voyager threw it away for want of anywhere to put it, and a terminal sent ESC [ A, which arrived in the middle of whatever was being read and made it unrecognisable - typing Up at the CosmOS prompt put three bytes in the command line and got "I do not know". So the console names them: one byte each, 0x80 upward, above ASCII so nothing written before them can collide. Up, Down, Left, Right, Home, End and forward Delete, with room above for the paging and function keys. The console normalises, which is what it already does. Behind a window it turns the key somebody pressed into a byte; on a terminal it turns the sequence into the same byte. That is the act it has always performed on Return and Backspace, one layer further along, and it is why a program need not know which of the two it is talking to. What a key MEANS is not the console's business - that belongs to whoever is reading, the same way what is on a disk belongs to the system and what a drive is belongs to the machine. Translated only when standard input really is a terminal. Nothing else sends these sequences, a pipe holds exactly the bytes somebody put in it, and it keeps the Escape-or-Up timing problem out of every test here: a test writes the key values themselves. Line mode drops them, in both front ends, because line mode delivers characters and a line somebody else has finished editing cannot be moved about in. Press.sbx says what it was handed, in hexadecimal and by name, and reads a line before it reads keys so both halves of that rule are checked. Two recordings, one fed as standard input and one as a keyboard, agreeing byte for byte; each break fails exactly one of them. Three checks in terminal.sh type real escape sequences at a pseudo-terminal, which is the only place they are ever read as sequences: that they arrive as keys, that Escape alone is still Escape, and that a character typed straight after an escape is held rather than swallowed. Five recordings re-blessed for Press.sbx appearing on the shared disk, and the whole of that diff is the file's own line and the counts above it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
04f1ffabd4 |
A disk made of memory, brought up by whoever owns it
THE MACHINE SUPPLIES BLOCKS AND SAYS WHAT A DRIVE IS. It says nothing about filesystems, which is what leaves room for a system that would rather have its own - and is why the volatile bit is a fact about the hardware rather than a promise about SBFS. 0x26 what the selected drive is: bit 0, contents do not survive 0x27, 0x28 how many blocks it has --ram-disk N a drive of N blocks with memory behind it A drive of memory selects, reads, writes and has a size like any other, and a program cannot tell the difference except by how fast it was. The one thing it cannot work out for itself is that the contents are volatile, because an empty disk and a volatile disk look identical from outside. THAT BIT IS THE DIFFERENCE BETWEEN A DRIVE A SYSTEM MAY FORMAT ON SIGHT AND ONE IT MUST NOT. CosmOS formats a volatile drive it cannot read, because there was never anything on it to lose, and leaves every other unreadable drive alone - an unformatted floppy is not an invitation, it is a blank floppy. Removing that check formats somebody's blank disk, which is checked rather than asserted: cosmosBlankDisk boots with one and requires it to be refused. So CosmOS grew a format. The size comes from the drive rather than from a superblock, since a superblock states a size too and that is no use on a disk which has not got one yet. Sixteen directory blocks, 128 names, chosen rather than worked out: a scratch disk runs out of names long before room, and this machine cannot divide. The RAM disk is no faster on this emulator by default, and that is honest rather than disappointing: the emulated disk has no seek time unless asked for one. With --disk-cycles 10000 the same copy is 7.94M cycles against 8.70M, the difference being every write. run.sh takes "ram:2048" where an image name goes, which needs no removing between runs because there is nothing to remove. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
6b51d6391f |
A beat a program sets for itself
The only regular thing on this machine was the screen finishing a frame, sixty times a second and not negotiable - a clock a program BORROWS rather than one it sets. Every duration became a multiple of 16,667 cycles, so a sixteenth note at 120 beats a minute, which is 125,000, is seven and a half frames and cannot be asked for at all. The way round it was to choose a tempo whose subdivisions happen to land on whole frames, which is making the music fit the machine. Examples/tune.asm says so in its own header. 0x50 Status: a period went by, it is running, it will interrupt 0x51 Control: run, repeat, interrupt 0x52-0x54 The period, in cycles, most significant first THE PERIOD IS IN CYCLES because that is what everything else here is counted in - the cost model counts them and a frame is measured in them - so a timer counting anything else would be a second unit to remember. Twenty four bits reaches from one cycle to sixteen and a half seconds, with 120 beats a minute at 500,000 in the middle, and there is no range left for a prescaler to buy. Starting loads the period; asking it to run while it already is does not, so turning interrupts on half way through a period does not silently move the beat being kept. What is left over carries into the next period, so a period of 1,000 ticks every 1,000 and not every 1,000 plus however late anybody looked. Reading the status takes the tick down and the line with it, which is the rule this machine settled two days ago about every status port. The timing check is in terminal.sh and not the manifest, and the reason is worth keeping: settle() strips cycle counts from recordings, which is right for every other program and useless for a clock. "It printed eight dots" would pass on a timer that fired them all at once. terminal.sh measures that eight periods of 125,000 come to a million within a couple of hundred cycles, and that 99.97% of them were spent asleep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
b1538e0618 |
Give the disk four drives, behind one controller
SEVERAL DISKS ARE ONE CONTROLLER AND NOT SEVERAL DEVICES, and the instruction set decided that rather than taste. A port is an immediate byte inside the OUT that names it - portOut takes it from Program Memory - so a program cannot compute one. "The disk on port 0x20 plus drive times four" is not something this machine can say, and two disks as two devices would mean a branch on the drive number in all eleven places sbfs.asm names a disk port. A drive register is what a floppy controller has always been. 0x24 Drive, which the block, command and status registers refer to 0x25 Drives, read only: how many are plugged in --disk given more than once fills them in order. What is per drive is the image, its size and its write protection; the block register, the status and the one buffer belong to the controller, which is the same division real hardware makes. A drive that is not there is refused rather than wrapped, because wrapping means a program asking for a drive this machine has not got quietly reading the one it has - the same shape of fault as taking a bank number somebody else was using. An EMPTY drive is a different thing and is selectable: a controller has its drives whether or not there are disks in them, and reading one fails with the error bit the way an empty drive should. Changing drives finishes whatever the one being left was in the middle of. A transfer waits for the clock, so one may be owed at any moment, and running it against the disk that is arriving would be a fault with no owner. Also stops parseOptions setting its defaults field by field. It was nine assignments beside a struct, and a list beside a thing drifts from the thing: adding two fields left them holding whatever was on the stack, so a machine given one disk was told it already had four drives. It is one zeroing now, and a default that is not nought can be written under it where it reads as the exception. That struct growing a field once before left Voyager linked against an object that disagreed about its size. Nothing in CosmOS uses any of this yet. The mount record is next. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
85329f13c3 |
Take a device's line down when its status port is read
A device raises a line and something has to take it down. Two things did: being interrupted, and being woken from WAIT with the Interrupt Flag down - the second because a masked program has nowhere to dispatch to, so nobody else would. There was a third way to learn a device had finished and nothing answered it. The documented idiom reads the status, branches out if the device is already done, and only WAITs otherwise; on a disk quick enough to finish before the first look, which is every disk here, the WAIT is unreachable. The line then stood for the rest of the machine's life. The program that leaves it standing never pays for it - it was masked throughout. The bill arrives at whoever next sets the Interrupt Flag. The boot chain reads the disk to load a program, leaves the line up, and hands over; the loaded program is then interrupted on behalf of a read that finished before it existed, through a vector table with no entry for a device it never touched, and faults on the instruction after its SIF. Found by running Examples/tune.asm through Once. It set up its whole sound and died four bytes before its first note, which is why it was silent rather than wrong - and why it looked like a sound bug for a while. So reading the port that answers a device takes its line down, the same way taking the byte already took the console's down. Disk and screen do it on their status port. And a reset now clears every line, which is the sentence the manual already makes about the vector table: a handler left behind aims an interrupt into a program that is no longer running, and so does a line. testPrograms/diskLineTest.asm pins it - the racy idiom, then SIF with no handler installed anywhere. It faults without the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
d388cd3122 |
Give the machine a sound device
Four channels on ports 0x40 to 0x4F, each one a whole soundThing voice:
two oscillators, two envelopes, a filter and the routing between them. A
channel keeps its patch between notes, so a program sets an instrument up
once and then plays it.
Six ports rather than forty, because a voice has around forty settings and
four of them would spend more than half the port space on one device.
There is a selector and a value instead: say which channel, say which
setting, write it. That is three writes to change a setting and two to
play a note, which is the right way round - patches are loaded, notes are
played in an inner loop.
Samples come from the machine's clock and not the host's: 48,000 a second
of emulated time, worked out in whole numbers so it never drifts. A
million cycles is exactly 48,000 samples on any host at any speed, which
is what makes a sound something a test can compare. --sound writes them
out, the way --screen writes a picture, for the same reason: the suite has
no speaker.
Tests/sound.sh is 22 checks and found three real defects the first time it
ran, all the same shape - a synthesizer written for a patch editor, wired
up as hardware and inheriting the editor's assumptions:
- Only one voice had an oscillator switched on, so three of the four
channels could not make a sound whatever was written to them.
- That voice's oscillator arrived at full gain and every other one
arrived at nothing, an asymmetry with no reason behind it.
- A note with no sustain is silent but not over, so the obvious way to
wait for a sound to finish waits for ever.
The first two are fixed by the device defining its own power-on state
rather than inheriting synthInit's: every channel arrives able to make a
sound, so writing a note number is the whole of playing a note. The third
was already written into the manual as advice, an hour before the check
existed. The check disagreed with the documentation and the check was
right; the manual now says the one rule, which is that a note sounds until
the gate is dropped.
Programs/Examples/tune.asm plays eight notes, taking its tempo from the
screen's frame interrupt because that is the only regular beat this
machine has. It spends 99.8% of its cycles asleep in WAIT.
Voyager has no speaker yet - this is the device and its tests. Playing the
samples out of the window is the next commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
33afc20abc |
Give the memory controller to a bus rather than to the machine
The third and largest piece of the peripheral core, and like the two before it nothing changes: 169 tests, and the two that would notice a misroute pass loudest of all. A CONTROLLER IS THE MOST STATEFUL THING ON THIS MACHINE - a source bank and address, a destination, a length, five guard registers and a table describing every bank it can reach. Two processors sharing one would interleave into nonsense: one sets a source, the other sets a destination, the first issues a blit and moves the wrong bytes somewhere else again. No arbitration fixes that, because there is nothing to arbitrate - both writes were legal and the result belongs to neither of them. So it is a struct threaded through all seventeen functions that touch it, rather than a pointer to a current one set on the way in. The smaller change was tempting and is the shape that produced the stale reset flag and the editor's IsNew surviving a second run, both this week: state that has to be set on the right path before anything reads it. A controller is where that goes wrong quietly rather than loudly. THE STATICS WERE DELETED RATHER THAN LEFT, which is what made this safe to do mechanically. A missed reference is a compile error rather than a variable that still exists and serves the wrong core, so "did I catch all 165?" stopped being a judgement and became a question the compiler answered. Two things the transformation nearly got wrong, both caught by reading rather than by building. guardStart and guardEnd are fields of Bank as well as registers of the controller, so banks[n].guardStart had to keep its name while a bare guardStart changed - the difference between a fence and the register about to be written into one. And a definition and a call look alike enough that the first attempt turned publishBank(number) into publishBank(Controller *c, number); definitions start at column zero here and calls never do. Tests/agree.sh is what says this is right, more than the count does. It builds the same disk with SplitDisk and with CosmOS and compares byte for byte, and every filesystem operation on the machine goes through the controller - so a blit that went to the wrong place would corrupt a disk that is checked against one built by different code entirely. Tests/cycles.sh covers the other half, since what a transfer costs depends on pendingCycles landing in the right one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
5be5bea994 |
Make the interrupt lines belong to a bus rather than to the machine
The second piece of the peripheral core, and like the first it changes nothing: one array of bits became a struct, and the machine's own devices reach it through wrappers because every one of them really is on this bus. IT WAS ONE ARRAY INDEXED BY PORT FOR THE WHOLE MACHINE. With two processors that is not a tidiness problem, it is each of them seeing the other's lines: a disk finishing would interrupt a sound core, a sound core's tick would interrupt the shell, and both would arrive at a handler installed for something else entirely. Hardware vectors are per port, so the numbers would even look plausible on the way in. Unlike the shift register, which was harmless until there was a second core to share it with, this one was always going to be wrong the moment there was one. Still to come on this rung: a controller each, since it holds the source, the destination, the length and five guard registers, and two cores setting those between each other's instructions would interleave into nonsense. Then the interleaving rule, which belongs in the manual as machine behaviour rather than as something the emulator happens to do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
dae3455da0 |
Give the CPU a bus, so that there can be more than one
The first piece of the peripheral core, and it changes no behaviour: the machine still has exactly one processor, and every one of the 169 tests still passes. What changes is that the code has stopped assuming so. FIVE THINGS A CPU ASKS OF THE WORLD OUTSIDE ITSELF, and every one of them was a call to a function there was exactly one of: the port handlers, what the controller spent moving memory, what was spent stopped waiting on a device, and the two that work the interrupt lines. Fine for a machine with one processor and wrong for a machine with two, because A PERIPHERAL CORE'S BUS IS ITS OWN - it sees the devices its own device gives it, raises its own lines, and stalls on its own controller, none of which are the host's. They are gathered into a Bus that the CPU holds a pointer to, rather than threaded through as a bus number, because a bus is something a device PROVIDES. A device that provides one should hand over the answers rather than be looked up by an index somebody else has to keep right. initializeCPU puts the machine's own there, which is what every processor was on when there could only be one, so nothing at any call site moved. And shiftRegister is a local now. It always was one in effect - written and read inside a single instruction and never carried to the next - but it sat at file scope, where a second processor would have shared it. Two cores taking each other's shift halfway through an SHL is a poor thing to discover later, and it cost two braces to make impossible. Still to come on this rung: the interrupt bitmap, which is one file-scope array indexed by port for the whole machine; a controller each; and the interleaving rule, which has to be written into the manual as machine behaviour rather than left as something the emulator does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
f2e26c1852 |
Stop a pending reset outliving the reset it belonged to
Reset out of picture.bin and CosmOS booted and then halted at once, having been told there was nobody at the keyboard. The button set two things: the machine's reset request, and a flag of the window's own that said "end the next console read, so a machine blocked on a key can get to the point where it notices". The second was only cleared when the console actually asked - and picture.bin never asks. It draws and halts. So the flag survived the restart and answered the NEXT machine's first read with the end of input, which for CosmOS means stop. There is one fact and it now lives in one place. The window asks whether a reset is still waiting rather than remembering that it asked for one, so the read ends only while a restart is genuinely on its way and goes back to normal the moment it has happened. The local flag is gone. Two pieces of state meaning one thing, one of them cleared on a path the other did not need - which is the same shape as the console's line editing flag surviving a second run, a fortnight ago. Worth noticing twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
f7657081be |
Put a reset button on the case, and stop Escape closing the window
ESCAPE WAS A BUG I LEFT. This machine sends Escape to the console like any other key, and Raylib closes a window on Escape unless it is told not to - so a program reading keys could be ended by one of them, taking whatever was in memory with it. SetExitKey(KEY_NULL), and it is a byte again. F12 is the reset button. A button on the case rather than a key the machine can see: nothing sends a function key to the console, so nothing can be surprised by one. It does what writing MACHINE_RESET does, which is that the machine starts the way it started - the boot chain runs again and finds whatever the disk now says to run. Which is what makes a bare metal program escapable. Once puts a demo in front of the next start and deletes the request before jumping, so a demo that has taken the whole machine is one keypress from the system coming back, instead of closing the window and opening it again. IT HAD TO REACH A MACHINE THAT IS WAITING, and that took two more things. A reset is acted on between instructions, and a machine blocked on a key is part way through one - so the button would have set a flag that nothing ever came along to notice, in exactly the situation a reset button is for. The wait ends now: the console is told its input is over, which it is for a machine about to stop existing. And the reset puts the console's input back - nothing pushed back, no line half gathered, and not at the end of input. That was already wrong before the button existed: a reset after the input ran out left a console that had run out afterwards, so a machine could be restarted once and then never typed at again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
ff4b025058 |
Make the cursor blink while the machine is waiting, and show how the palette works
THE CURSOR DID NOT BLINK, and the reason is worth stating: it blinks on the machine's clock, and the machine's clock had stopped. A console waiting on a key stops the CPU, so no cycles passed, so the phase never moved - and the one moment somebody is looking at a cursor is the moment they are being asked to type. Waiting is now charged as IDLE CYCLES, which is what they were built for: a machine stopped on a device is not using memory, the same distinction WAIT makes, arrived at from the other direction. And the devices are told as it happens rather than when the instruction finally finishes, because a display controller does not stop blinking because the processor is waiting on a keyboard, any more than a disk stops turning. A keyboard file can now say NOTHING happened. A zero is a byte no keyboard sends, so it is free to mean "a moment went by with nobody typing" - which is the commonest thing behind a window and the only thing a file otherwise could not express. That unlocked the whole waiting path: three checks that the cursor is lit, then dark half a second later, then lit again, which is what blinking is. And Programs/Examples/colours.asm, because the palette had nowhere a newcomer could read it. It prints the sixteen pairs, prints each one again turned inside out, and then CHANGES ONE by writing three bytes into the palette - so the difference between using the colours a machine wakes up with and choosing your own is visible in one program. Its header explains what a cell is, what the attribute nibble does, why palette entries are four bytes rather than three, and why video memory has to be reached through the controller. The manual now says where the palette lives and points at it. SplitLint found a redundant RSTA in the example, which was worth acting on rather than suppressing: the zero was already in A from the mode write two lines up, and saying so in a comment teaches that SETD does not touch A, which is a thing worth knowing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
d6feddd1b6 |
Give the console colour and a cursor
COLOUR COSTS A NIBBLE AND NO HARDWARE. A glyph is drawn in palette indices 0 and 1, paper and ink, and a cell's attribute nibble adds sixteen to both - so sixteen banks is already sixteen ink and paper pairs, and all that was missing was a register saying which one the console draws in. That is port 0x06, read as well as written like the rest. The palette a machine wakes up with is arranged so that HIGHLIGHTING IS ONE BIT: banks 0 to 7 are colours on black, banks 8 to 15 are the same colours as paper with black ink. So attribute XOR 8 turns any pair inside out. That is a convention rather than a rule of the machine - the device only ever adds the nibble and looks the answer up - but it is the convention that makes a highlighted line and a cursor free. Bank 0 is still grey on black, so nothing that was written before this has changed colour. THE CURSOR IS THE SAME BIT AGAIN. It is drawn by turning its cell inside out rather than by putting a block over it, so the character underneath stays readable, which matters to somebody editing a line. The device draws it rather than the window, because on a machine with a screen a cursor is a hardware feature - one drawn by the presenter would not be in a picture the machine saved. It blinks on the machine's own clock, half a second each way, so the phase is a pure function of the cycle count and a screen saved at a given cycle is the same screen every time. A blink on the host's clock would have made every saved picture a matter of luck. Off unless asked for, with bit 2 of the control port. That is right for a machine - a program painting its own screen does not want something blinking in the middle of it - and CosmOS asks for one at boot. It also asks again when it takes the console back from a program that has stopped, because a program handing key mode back the way it was told to writes zero, which turns the cursor off. The shell owns the prompt, so the shell is what makes sure there is something blinking at it. Nine more checks in Tests/video.sh, to 41: that the attribute colours the ink and not the paper, that XOR 8 turns both, that it reads back, that a cursor appears where the registers put it and only when asked for, and that it goes dark again half a million cycles later. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
43a05b3df1 |
Replace the escape parser with cursor registers
The console had grown an ANSI parser, and that was the wrong shape. ANSI exists because a screen used to be on the other end of a serial line and a byte stream was the only channel there was. This screen is memory the program can already address, so reaching it by sending characters for a state machine to take apart is a middleman for something the machine does better - and it meant accepting an open protocol somebody else defines, in hardware, with no natural end to it. Everything else on this machine is registers. So the console gets three: cursor row at 0x03, cursor column at 0x04, and a command port at 0x05 where 1 clears the screen. Both cursor registers are READ as well as written, which is the thing an escape cannot do without sending a query and parsing a reply - a routine that wants to put the cursor back where it found it can now ask. Clearing is one command against a thousand cells walked one at a time. Snake and Life are smaller for it: 2,168 bytes to 2,163 and 1,410 to 1,396. A HOST TERMINAL STILL SPEAKS ANSI, and bridging to the host is the emulator's job, the same job it does reading standard input. So the escapes are now GENERATED, outbound, for the set this device chooses, rather than parsed inbound as though the machine were a terminal. The set cannot grow behind our backs because we are the ones saying it. The cursor is announced lazily, at the next character rather than at the register write, so setting a row and a column costs one sequence rather than two. The console's block widens from three ports to six, which registryTest noticed: it had been asking about port 0x05 precisely BECAUSE nothing was there, and the console had just moved in. Re-blessing it would have left it checking nothing, so it asks about 0x80 instead - clear of the console, the disk, the screen, the controller, and the sound device coming to 0x40. Six checks in Tests/video.sh swapped from the sequences to the registers, including that the cursor reads back and that one sent past the edge is clamped rather than refusing. Those checks also stopped counting bytes from the ends of a file, which had quietly started measuring an escape the moment the console began announcing the cursor. SplitLint caught the one thing worth catching in the port: the clear command leaves A at 1 and key mode is also 1, so the second load looks redundant. Acting on it would tie a console command to a console mode by coincidence, and break silently if either ever moved, so it is suppressed with that reason rather than removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
556a14b288 |
Teach the console the sequences the corpus already speaks, and let the status port see the window
Three things Snake found the moment somebody ran it in a window, and all three are the same kind of mistake: the console grew a screen and kept asking the terminal. IT COULD NOT CLEAR THE SCREEN. Every program here that moves a cursor does it with ANSI escapes, because until there was a screen the thing on the other end was somebody's terminal. The controller drew "[2J" as three letters and left the board underneath. It now parses them, which is what a video terminal did - a VT100 is exactly this. The whole corpus uses two, ESC[2J and ESC[H, and the general shape is recognised so anything else is swallowed rather than drawn: a sequence nobody implemented should leave no marks. Cursor positioning is in too, since it is the same parse and one line more. IT DID NOT SEE KEYS FROM THE WINDOW, but did when the terminal behind it was focused, which is the whole diagnosis in one sentence. Snake polls the READY bit and never blocks, and consoleFetch - what the status port asks - was polling standard input regardless of whether a front end had installed a hook. So a window's keys were invisible to every program that looks before it reads, and a keystroke aimed at the terminal would be picked up instead. The hook now takes a question. Zero is the status port looking, and must not present or sleep: a program polling in a loop would otherwise be charged a frame for every glance. One is the data port blocking, where presenting is exactly right, because a machine waiting for a key is still a machine somebody is looking at. One value for both would have made either polling ruinous or waiting dead. AND IT RAN SLOWLY, which was the same bug wearing a hat: a game that never receives a steering key is a game that only ever goes one way. Six more checks in Tests/video.sh, to 32: that ESC[2J clears, that ESC[H goes to the corner without disturbing what is drawn, that ESC[3;5H counts rows and columns from one, and that an unknown sequence is swallowed and leaves nothing behind. The hook itself is still the one thing here the suite cannot reach - it exists only when there is a window, and this host has no display. It was found by a person playing Snake, which is where the Test Manual says these go on being found. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
773b0f8add |
Put CosmOS on the screen without changing a line of it
The console is now a display controller as well as a port: it owns a font, keeps a cursor, handles newline, carriage return, backspace and wrapping, and scrolls. That is an ordinary kind of chip - it is what a video terminal's character generator did - and it is the reason this rung needed no changes to CosmOS at all. CosmOS already writes bytes to port 0x00. It writes to BOTH the screen and standard output, which is deliberate. A machine with a screen and a serial line is an ordinary machine, the emulator's standard output is that serial line, and one console drives both. It is also what keeps all 165 recorded results passing under Voyager, and what makes --screen work on the plain SplitBit: there is one console and it drives everything it has. Scrolling moves the video device's origin and no memory. The row arriving at the bottom is cleared because the map is a ring and it holds what was there 128 rows ago; the rows going off the top are not, and that is a hundred rows of scrollback nothing had to keep. The test reads the register back rather than looking at the screen, because a console blitting rows instead would look identical and cost twelve percent of a frame for every line printed. The font is vendored from Hatchet-GPU with a note saying where it came from, since that repository is not part of this one. 135 glyphs in ASCII order, which is the thing that makes it worth keeping - PETSCII's whole inconvenience was that its order was not ASCII's, so a machine using it needed a translation table in front of every string. Here the machine subtracts 32. It is stored one bit a pixel and expanded into tile memory at reset: 1,088 bytes against 16 kilobytes. Voyager gets a keyboard. A window has no standard input, and a machine blocking on it inside a frame would stop drawing and stop answering, so a front end with a window installs a hook that the console calls while it has nothing: it keeps the window alive and hands back a key. The hook has to tell "nobody has typed yet", which happens sixty times a second, apart from "the window has gone", which is the end of input - one value for both would have made the first keystroke look like a closed machine. In line mode the console echoes what it is given, because there is no terminal behind a window to do it and that was always the terminal's job. Tests/video.sh grew from 14 checks to 26, half of them about the console rather than the device: those programs ask the video device for nothing and write bytes to port 0x00 like every SplitBit program always has. Verified by breaking two things - removing the scroll failed exactly the two checks about scrolling, and removing the cursor advance failed exactly the three that depend on it. Two video checks had quietly depended on palette entry 0 being black, which stopped being true the moment a machine woke up able to show text. They now set what they are about to look at, and a new check pins the waking state itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
83623a3df3 |
Give the Voyager a screen
A tile engine on ports 0x30 to 0x3F, bringing one bank of video memory registered the way the disk's buffer is. The CPU writes cell indices and the device turns them into pixels, which is the whole reason a screen is affordable at a megahertz: a frame is 16,667 cycles, a full 320 by 200 picture is 64,000 bytes, and a 40 by 25 map is 2,000. A program that changes two cells writes four bytes. The cost of a screen becomes the number of cells that changed rather than the number of pixels on it. Which makes colour depth free, so the tiles are eight bits: an 8 by 8 cell is 64 pixels and each picks independently out of 256 colours, with no per-cell limit of the kind that made a Spectrum two and C64 multicolour four. The low nibble of a cell's attribute is ADDED to every index in its tile, sixteen at a time, so a tile drawn in 0 to 15 appears in any of sixteen schemes without a second copy in tile memory - and a tile wanting all 256 leaves the nibble at zero and gets them. Neither use costs the other anything. Two decisions are arithmetic rather than taste, and both come from the machine having no multiply. A map row is a page whether the mode fills it or not, so a cell address is the row number as the high byte and the doubled column as the low byte with no arithmetic at all; otherwise every cursor move on a 40 column screen would cost a row-times-40 in software. And a palette entry is four bytes rather than three, so entry n is at n times four, a shift. THE MAP IS A RING and the Scroll register says which of its 128 rows is on top. Scrolling moves a register and no memory: blitting a 40 by 25 screen up one line is 1,920 bytes inside one bank, which is twelve percent of a frame even with the controller widened, and a program printing one page would spend six frames shuffling memory. It is now one port write - and the rows that scrolled off are still there, which is where a terminal gets scrollback it never had. The device is part of the machine rather than part of the window. It renders into a buffer that is a pure function of video memory, so the same program draws the same picture with nobody watching; Voyager puts that buffer on the glass and decides nothing. Both binaries take --screen, which saves a PPM when the machine stops, and that is what makes a screen checkable on a host with no display at all. Tests/video.sh checks fourteen named behaviours rather than comparing a recorded image, because a recorded image would say "something changed" and leave which of the palette, the tile, the attribute, the map or the scroll register broke to be found by hand. Verified by breaking three things in turn: the additive nibble failed exactly one check, the scroll origin exactly two, and moving every cell one pixel sideways exactly the four about placement. Tests/docs.sh could not count past nine, which is how a suite of ten scripts reported itself as wrong for the wrong reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
79727044b7 |
Reboot, and the machine device that makes it possible
Until now the only way to restart was to stop the emulator and run it again, which meant the one thing the machine could not do was the thing Once was written for. The loop now closes without leaving it: > Once /System/Boot/bare.bin next start: /System/Boot/bare.bin, once > Reboot starting again stage two just this once: /System/Boot/bare.bin bare metal: no system, just this Writing 1 to port 0x13 asks the machine to start over. A PORT RATHER THAN A SERVICE, because a reset has to work when the system does not: something only askable through SWI would be unavailable in exactly the case that wants it most, and a program that owns the whole machine has no system to ask. It is device class 0x04, in the range kept for the machine rather than among the peripherals, because it is not one - it is not attached to anything and cannot be unplugged. WHAT A RESET REPEATS IS HOW THE MACHINE STARTED. Named an image, the emulator places it again; named none, the ROM is shadowed again and reads the disk. Anything else would mean a reset changed what the machine IS, which is the one thing a reset must not do. Both are tested. Taken between instructions, because a device cannot restart the machine from inside the instruction that asked: the CPU is part way through a step and its state is not yet anything a reset could leave behind consistently. The disk stays attached and keeps everything written to it - that is what warm means. The vector table is cleared, which is the one deliberate departure from leaving memory alone: a vector points into whatever installed it, and after a reset that program is not running, so a handler left behind would aim an interrupt at an address belonging to something gone. It is the argument CosmOS already makes at exit, applied to the machine. Reboot is 45 bytes, most of them the word it prints. |
||
|
|
d4cba36c5e |
Devices that take time, and a filesystem that waits for one
The disk's status has always had a bit meaning "still going", and the header beside it has always said to honour it. Nothing did, because nothing could: the host finished the transfer inside the instruction that asked for it, so the bit could never be seen up and asking about it was asking about something that cannot happen. --disk-cycles gives it a latency. The command is still checked at once, because a refusal is not work - a block that is not there fails before any head moves - but the transfer is remembered and done when the machine has run that far. Until then the buffer holds the block BEFORE this one. That last part is the point. A program that does not wait gets the wrong bytes rather than an error, which is the failure the bit exists to prevent and the one that would never have shown up. With a latency of two thousand, CosmOS could not even mount: sbfsMount reads block zero and looks straight at the buffer. deviceTick is the general shape rather than a disk feature. Called once per instruction with the machine's clock, it lets anything whose moment has come finish - which is what a display that refreshes, or a port that waits on the host, would want in exactly the same way. The filesystem watches the bit now, in one small routine reached with RCAL. That is not decoration: what it hands back is the settled status in A, and CALL puts A back the way it found it, so an ordinary call cannot carry the one thing this exists to carry. Two bytes of Stack rather than ten, in a routine that runs on every block the machine ever touches - the first place in the system where the new call is the right one rather than merely a cheaper one. The manifest takes a @N after a disk, the way it already takes :ro, so a test can ask for a slow one. cosmosSlowDisk lists a directory at two thousand cycles a block and gets the same listing as everything else, which is the whole assertion: a filesystem that did not wait would print nonsense rather than fail. Zero is the default and every other test runs at it. What waiting costs, on a directory heavy run: 229k cycles at zero, 275k at five hundred, 415k at two thousand, 1.16M at ten thousand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
9e3425d34b | Programs can now pin specific routines to specific vectors in SplitBit assembly. Added snake game. | ||
|
|
08624925fe | Interrupt on keypress mode | ||
|
|
91c9d49d1b | CosmOS pre-alpha and launchable application versions of old programs. | ||
|
|
eff6902bcf | Block device peripheral and SBFS file system implemented. | ||
|
|
04dfcd707b | Memory controller implemented. | ||
|
|
c4b2c27a2d | Device table added to IO Port 0xFF. | ||
|
|
6d1966d500 | Interrupt system implemented, some new programs. | ||
|
|
dfa5ad2638 |
Assembler Completed
Added the Assembler. Added instructions for the assembler to README.md Added Assembler Manual Modified Makefile to build the Assembler. |