71f6e215f9b2e41440fa5adf4d6b46053ac54a9f
241
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
71f6e215f9 |
The shell remembers what was typed before it
Up walks back through the last eight lines and Down forward again. It exists only because the keys reach the system now: until A1 and A2 there was nothing to press Up at, and the line was assembled somewhere the shell could not see. A RING RATHER THAN A LIST. A ninth line pushes the oldest out by moving where the ring starts, not by moving any of the lines - so keeping a line costs a copy of that line and nothing else, however full the history is. Eight is a power of two, so which slot an entry lives in is an AND. The ISA had the awkward part already: A and B are a sixteen bit shift register, so one SHR with B empty turns a slot number into the offset of a 128 byte slot, high byte and low, ready for DPUW. A NINTH SLOT HOLDS WHAT WAS BEING TYPED when Up left it, and Down brings it back. Losing a half written line to a keypress is the sort of small rudeness that makes a thing unpleasant to use, and it costs one slot to avoid. An empty line is not kept, and neither is one the same as the line already at the top. The test proves the second by looking one further back: if a repeated command were kept twice, the line behind the newest would be the same line again. The redraw had to learn to rub out. One space was enough while the only thing that shortened a line was taking one character out of it; a recalled line replaces the whole of it, and a short line over a long one left the tail of the long one on screen looking like part of what you were typing. It now covers exactly what was lost - which turned out to be one space fewer than before in the cases that GREW, so two lines of cosmosEditKeys lost a trailing space that was never doing anything. Costs 1157 bytes of Data Memory, taking CosmOS to 6220 of the 8192 it has before a loaded program's data begins. Worth writing down: that is the budget, and this is the largest single thing in it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
81e544eb3d |
Load a program that has no data
A five instruction program that writes one port and exits has no Data Segment at all, and the loader stopped the machine dead on it. It asked the memory controller to move a segment of no bytes, and a length of zero asks for the whole 64K - which is the machine's rule, and a reasonable one, since two bytes cannot say 65536 and a transfer of nothing is not usually what anybody meant. It is exactly what was meant here. 64K did not fit, the controller refused, and the load stopped half done. ON A TERMINAL THAT PRINTS A FAULT WITH AN ADDRESS. Behind a window it is a frozen picture and no reason at all, which is how it was found and is a separate problem from this one. The header says how long each segment is, so the loader knows before it asks. Both bytes are already in hand, so the test costs one OR. Nothing is lost by skipping the transfer: a blit leaves the controller's addresses past whatever it touched, and a blit of nothing would have left them where they already are, which is where the vectors are read from next. Guarded for the code segment too. A program with no code is equally assemblable and would have stopped in exactly the same place. Mode.sbx is the fix's test and a program worth having on its own: forty columns or eighty, whichever the screen is not in, which is what a person wanting Snake drawn twice the size actually needs. Ten instructions and no data, deliberately - it prints its two digits a register at a time rather than from a string, so it stays the smallest shape a loadable program can take. Nothing else on that disk had ever been that shape, which is why nothing had ever tried it. Reported by the user, who wrote the program. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
373454ec00 |
A fresh disk for every test, fixtures included
A fixture built by makedisks.sh was handed to each test where it lay. Twenty four tests name disks/cosmos.img and several of them write to one, so a test could hand the next one a disk with its leavings on. romBoot is what 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 - which is the worst way round for a test to be wrong, because the form nobody runs is the one telling the truth. Its recording now says "made" like selfBoot's, which is what running the same input on the same disk should always have said. Fixed as a class rather than as an instance: run.sh copies a fixture before attaching it, the same way it already removed a scratch image. Then every one of the 138 run and rom tests was run on its own to see whether anything else was leaning on what ran before it. Nothing was, before or after. Also, cosmosEditKeys.in was written by Python's write_text, which encodes as UTF-8, so every key byte was 0xC2 and then the key. The test passed anyway, because the shell ignores a byte it has no use for - a fixture working for a reason it was not built on, which is exactly the thing that stops working without anybody touching it. Written as bytes now; the recording is unchanged, which is the proof the stray bytes were being ignored. docs.sh is what caught that, and it turns out to draw the line in the right place by construction: a deliberately binary fixture does not decode as UTF-8 and is skipped, while one that is accidentally UTF-8 decodes and is reported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
736037462e |
The shell edits the line it is given
Three different things used to do this job, and which one you got depended on where the machine was running. On a terminal the host held the line and did the echoing and the backspacing; behind a window the console's own gatherer did it; from a file nothing did it at all. One job, three implementations, none of them in the system - which is why there was no way to move about in a line and nowhere for a history to live. So editLine does it. Key mode while a line is being read and line mode straight after, so nothing else in the system and no program calling osReadLine notices anything changed. Left and Right, Home and End, Backspace for the character before the cursor and Delete for the one under it, and anything typed goes in where the cursor is with the rest of the line moving along. Ctrl-D means the end of input again, on an empty line, because that was a thing the terminal did while it was holding the line and it is not holding it now. Same trade as the echoing. MOST KEYSTROKES DRAW NOTHING BUT THEMSELVES. A character typed at the end of a line needs no cursor moved: printing it is the whole change, and a backspace there is three ordinary bytes. That matters beyond speed - moving the cursor by hand is what a terminal is TOLD about, in an escape sequence, so redrawing on every keypress would fill every recorded transcript in this suite with them. The line is only reprinted when something happened in the middle of it. Where the line STARTS is worked out backwards from where printing ended, rather than trusted from what was remembered. That is what makes it survive the screen scrolling: a line printed on the bottom row moves everything up by one, and a remembered row would be one too low from then on. The command line holds 127 characters, up from 63. The limit started to be felt the moment a line could be moved about in. 58 recordings changed, and every one of them by the echo. THE PROOF IS NOT A HEURISTIC: a CosmOS built with the echo silenced reproduces 187 of the 188 recordings byte for byte. The one exception is cosmosTyped, the backspace test, where the rub-out marks now come from the shell instead of from the console's gatherer - same marks, different author. cosmosEditKeys is the new test, and every line in it is typed wrong and then corrected with a different key. Its last line is eighty six characters at a prompt in column two on an eighty column screen, so the line runs onto the row below and the shell has to find the start of something it can no longer see; breaking either half of that arithmetic fails it. Also: agree.sh looked for "> the same", anchored to a prompt that no longer precedes what a command prints. 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 |
||
|
|
2a29cebc6b |
Make the screen come back on the machine people actually run
Found by running it: Grid exits and the shell prints its prompt into the grid, with the view up to seven pixels out of alignment. Three faults, and the first is the one that made the other two visible. MAKE RUN-VOYAGER HAD NO SCRATCH DRIVE. It gives drive 1 to Disks/personal.img, which is a file and not volatile, so there was nowhere to keep a screen - osTakeScreen answered no and the whole feature silently did not happen. It was tested with --ram-disk and shipped without one, which is as good a description of testing the wrong machine as I can write. There is now a RAM disk in drive 2, after the personal disk so that drive 1 stays the one that is yours. A PROGRAM TOLD NO MUST COPE. A refusal is not a fault, it means doing what the program did before there was anywhere to save a screen. Grid deleted its own tidying up when it started asking, so being refused left the grid on screen with a prompt written into it. It clears up again when refused, and only then. AND THE SYSTEM ALWAYS LEAVES THE SCREEN USABLE. The fine scroll registers go back to zero at every program exit, whether or not the picture could be saved: the console draws in whole cells, so a view three pixels into one puts every character three pixels out for ever. That is not part of saving a screen and should never have depended on it. Both paths are checked now. With a scratch drive the screen comes back cell for cell; without one, no grid is left behind. Breaking either fails one of them and not the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
ab72443b99 |
Give the screen back: osTakeScreen, and the RAM disk earns its keep
A program that takes the whole screen leaves the shell a blank one, and
whatever was on it is gone. There was nowhere to put 48K of video memory on
a machine with 64K of Data Memory that CosmOS already lives in.
A DRIVE MADE OF MEMORY IS SOMEWHERE. The screen goes to a file on the
scratch drive - the first volatile drive found at boot - like any other
file, and comes back from handleExit alongside the vectors and console mode
already put back there. The filesystem does the allocating, so this
invented nothing: it is 196 pages of tiles, map and palette, with a block
on the front holding the cursor, the four scroll registers and the mode.
NOT AUTOMATIC, and that is the whole design. Saving on every program start
would be cheap enough; restoring on every exit would be wrong, because dir
and Files and Say print and stop and their output is the reason you ran
them. A program says it took the screen, and one that says nothing behaves
exactly as every program did before this existed.
It deleted thirty lines of Grid, and they were all wrong anyway: four
scroll registers put back by hand, the map filled with spaces, the cursor
sent home, palette bank 0 written out - and the other fifteen banks kept
Grid's colours, because there was nowhere to have kept the real ones. Grid
is 64 bytes smaller and gives back what was actually there.
The check compares the screen before against the screen after, CELL BY
CELL, and allows only the rows around the cursor to differ - found from
where the text ends rather than guessed at, because the first version
assumed the cursor was near the bottom of the screen and let three real
differences through.
Two things cost time and neither was the feature:
- An edit adding "SWI osTakeScreen" to Grid was in the same script as a
failing s.index, so the file was never written - and the COMMENT
describing the call did land, from a later edit. Grid documented a call
it did not make, and read as though it should have worked.
- docs.sh caught osTakeScreen having no row in the services table, which
is the check the service layer added for exactly this and the second
time it has earned itself.
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 |
||
|
|
3e48e9690d |
Put a disk of your own in drive 1
make run-voyager and make run-cosmos now attach Disks/personal.img as the second drive. It is made the first time it is wanted and then left alone: a rule with no prerequisites, so make never looks at it again. NOT UNDER build/, and that is the whole point. Everything else in this repository is made from source and can be thrown away without losing anything, so 'clean' empties build/ without a thought - and a disk of your own that a clean deletes is not a disk of your own. It is the one place here where something MADE ON THE MACHINE can live, which starts to matter the moment there are tools on it that make things. Disks/ is in .gitignore for the same reason: what you make on the machine is yours and not the repository's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
e4f4bae762 |
Work across two disks: copy between them, and run a program from one on
files from the other Two things anybody expects of a second disk, and each needed something different. COPYING NEEDED TWO THINGS TO REMEMBER A DRIVE. The write stream is the only thing here that lives across service calls, so it is the only thing whose drive can change underneath it: every osFileBlock names its source path again and goes back to the source drive, and then osFileWrite has to come home. It records the drive it was opened on and returns there. And the file lookup CACHE. It keeps the last path resolved so a reader walking a file does not re-walk the directory for every block - and skipping the walk skipped the drive the path named, so block one of a cross-drive copy read the source's block numbers off the DESTINATION disk. It only showed on files of more than one block, because a file of one is never looked up twice. One block worked and two did not, which is a suspicious enough shape to have suspected sooner. RUNNING A PROGRAM FROM ELSEWHERE NEEDED A THIRD PLACE TO LOOK, and two restorations. The shell tried where you are and /Apps on the disk you are on. It now tries /Apps on drive 0 as well, which is what makes the system's programs work from a disk of your own - one with your files on it and no system, which is most of the point of having a second disk. The drive goes back after the load, because by then the program is in memory and the blocks it came from mean nothing; and again when it exits, because a program that copies between disks moves the drive as its own paths need to and being left wherever it finished is not what was asked for. Copy 1:/a 0:/b now leaves you exactly where you were. The fixture disk grew an /Apps, because it kept its programs at the root and so could not exercise the third place at all. Two hours of the debugging above were spent on a stale disk image. The machine boots the system that is ON the image, so a rebuilt cosmos.bin means nothing until the image is rebuilt too - and the trace said my new code never ran, which was true. Third time this project has been misled by one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
4cc6393f5b |
Name a drive in a path
"1:/notes", or "1:" on its own for wherever that drive already was. Done in sbfsWalk, which is where every path in the system arrives - eight callers between the shell, the config reader and the filesystem - so it works for anything that takes a path rather than for whichever commands somebody remembered to change. NAMING A DRIVE GOES THERE AND STAYS THERE. Switching for the length of one command and switching back reads better and cannot work: a path resolves to a start block and a length, and those mean nothing without the drive they were read from. A load that resolved on drive 1 and then read its blocks on drive 0 would read the right blocks of the wrong disk. A name beginning with a digit is still a name. The colon is the whole of what tells them apart, and /2things is on the fixture disk to keep it that way. Two bugs, and the second is the interesting one. SUB sets carry on a BORROW, so a character below '0' leaves it set - and the test for "not a digit" branched on clear. Every prefix was ignored. Then the leading-separator test reads the first character through DP0, which sbfsPathDrive could not move because RET puts DP0 back the way it found it. It advanced SbfsPathAt and DP0 still pointed at the digit, so every prefixed path was judged relative and walked from the named drive's working directory. IT ONLY SHOWED WHEN THAT DRIVE WAS STANDING SOMEWHERE OTHER THAN ITS ROOT, because a relative walk from the root is an absolute one - so "cd 1:/2things" worked from a fresh boot and failed after "cd 1:/notes". The test does it in that order for that reason. Copying between two disks is still not one command: each path resolves on its own drive and the drive stays where the last path left it. That wants Copy to change drives between blocks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
5644c24113 |
CosmOS knows about all four drives
A mounted disk is EIGHT BYTES - where its directory starts, how many
blocks it is, how big the disk is, and where you are on it. They now sit
together in the data segment, and changing drives is one copy out and one
copy in. The other three thousand lines of filesystem go on reading the
same four names they always have and never learn there is more than one
disk, which is the whole reason this was affordable.
The version is not in the record. It is checked at mount and thrown away,
because a version one disk's zero parent already reads as "in the root".
Every drive is mounted at boot: the controller says how many are plugged
in and each is tried in turn. One with nothing in it, or a disk this
cannot read, is left unmounted rather than stopping the others, so a
machine with a good disk in drive 0 and a blank in drive 1 starts.
'drive' says which one, 'drive 1' goes to another, and the working
directory goes with it - where you are on a disk is part of which disk you
are on. A drive the machine has not got is refused, and refused
differently from one that is there with nothing readable in it.
Three things the assembly caught me on, all the same misunderstanding of
what survives a call:
- OR reads A and B, and the bit came back from sbfsDriveBit in Q, which
RET does not disturb - but RET does put A back. The mounted mask never
got set and drive 0 was reported unmountable.
- MVQA then RSTA throws away the copy it just made, so doubling a bit
doubled nothing. SHL does it in one instruction, because A and B are
one register to it.
- There is no move from A to B. INB reads a port straight into B, which
is what the drive count comparison wanted.
run.sh takes more than one image now, separated by a plus, since the
machine has four drives and a test that could only name one could not
check any of this.
The buffer note is forgotten on a drive change and that is DELIBERATELY
kept although nothing can currently reach it: only the file read-ahead
consults it, a directory scan does not, and finding a file requires a
scan which overwrites the note on the way past. Two disks were built with
the same file at the same block to try to catch it and the answer was
right either way. Three instructions to hold an invariant rather than a
story about a bug - and the comment says so instead of claiming a fix.
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 |
||
|
|
3b650cabcd |
Grid took the disk's bank number, and gave the screen back untidy
Found by playing with it: after running Grid, the shell could not start anything by name and dir said the disk was empty. Several commands after the program that did it had exited, and nothing had said a word. BANK NUMBERS ARE ONE NAMESPACE FOR THE WHOLE MACHINE. Grid registered video memory as bank 3, which is the number CosmOS gives the disk's buffer when it mounts - and that does not fail, it succeeds. Every read the filesystem made afterwards came out of video memory. Grid uses 4 now, and the CosmOS README has a table of who owns what, because the one place this was written down was a line in a service description about sbfsMount. Nothing hands bank numbers out and nothing refuses one that is taken. If programs start wanting banks routinely, a service that allocates them is what should exist rather than a longer table - noted there rather than built, since one program wanting one bank is not yet a system. Also puts the cursor home on the way out. The map was emptied and the console was not told, so the shell carried on writing from wherever the cursor had been standing when Grid started - twelve rows down a screen with nothing on it. Clearing is what homes a cursor and it costs one write. The regression test runs a program by name, then Grid, then the same program again; the second one is the check. Putting Grid back on bank 3 fails it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
1aa45fcfc4 |
Grid: fill the map, not the window
Scrolling sideways walked off the end of what the program had filled, so the grid went blank for six seconds and then came round again. A map row holds 128 cells - 256 bytes at two a cell, whatever mode the screen is in - and an eighty column screen shows eighty of them, so 48 were empty. This is the third thing this loop has counted and the first right one. It said forty, which filled half the screen. Then it asked the screen how wide it was, which fixed what could be seen and was still wrong. ASKING THE SCREEN IS RIGHT FOR FILLING A SCREEN AND WRONG FOR FILLING A MAP: a program writing one screenful wants the window, and a program that scrolls wants everything the window can be moved over. There is no register for that because it is a property of video memory rather than of the display. The check that should have caught it did not, and that is the more useful half. periodic.py looked at 32 pixels - four cells at the left edge - so it could not see a gap that was on the right, and at the cycle count it samples the origin had moved to column 22 and the gap was off in the middle distance. It reads the whole scanline now and says which column the picture stops repeating at, which is how the two failures above were told apart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
bcd42e75ca |
Scroll the screen sideways, and by less than a cell
The screen could move one way, a cell at a time. Three registers were missing and this adds them: a column origin so the map can be wider than the screen as well as taller, and a pixel remainder for each axis so the step can be one pixel rather than eight. 0x36 Scroll column, in cells, wrapping at 128 0x37 Fine X, 0 to 7 pixels 0x38 Fine Y, 0 to 7 pixels FINE DOES NOT CARRY INTO COARSE. Writing 8 to a fine register writes 0, because only its low three bits mean anything. The alternative was for a write of 8 to step the coarse register, and it was rejected for one reason: a program that scrolls has to know where it has got to, and if the hardware carries then the only way to find out is to read the register back. Keeping them apart means the program already knows, because it did the arithmetic itself. It is also what the machines this one is pretending to be did. The renderer now draws one more row and one more column than fit and clips them, because with a fine offset the screen no longer begins on a cell boundary and the cells at two edges are partly off it. videoPutCell follows the column origin as it has always followed the row - a caller means a cell of the SCREEN, and the screen is a window onto the map. The fine offsets are deliberately not applied there: they move the finished picture by less than a cell, and there is no such thing as less than a cell to write into. So a program may scroll to any pixel without the console's idea of where row three, column five is moving underneath it. Grid now scrolls diagonally, a pixel a frame, in four port writes and two carries. It moved eight pixels every fourth frame before, which reads as the picture jumping rather than travelling. Seven checks, each one the same program with one register changed, so what is compared is where the picture stopped. Breaking fine X, fine Y, the column origin, the three-bit mask, or the console's use of the origin each fails exactly one of them. Grid's own two checks had to be rewritten, and the reason is worth keeping: they asked whether pixel 4 was a grid line, which was really a check that the scroll happened to be at a cell boundary. A picture that moves a pixel a frame can only be asked things that are true at every offset - that it repeats every eight pixels, and that one band of eight rows holds different colours from the next. Also repairs docs.sh, which found the minimal CosmOS application by taking the first asm block in the README. Documenting a program with an example above it made that a different block, and the check complained that the minimal application had no #Base about something that never claimed to be one. It looks under System Services now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
848103f5e4 |
Grid: ask the screen how wide it is
It said forty and filled exactly half of an eighty column screen. CosmOS asks for the wide mode when it starts, because its own help text is seventy-four characters across. So a program that assumes the shape the MACHINE wakes up in is wrong about the shape the SYSTEM is running in - and the Programming Manual says as much where it describes the columns and rows registers: how big the screen is, is asked for rather than assumed. Port 0x32 says. One instruction, and the program now fills whatever it is given. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
1a8a5efe03 |
Grid: the first program to use the screen as a screen
Everything drawn on this machine so far has been text or a bitmap. The tile
engine has been there since the screen was built and only the console had
touched it, and only ever to put a letter in a cell - the one thing it can
do that a plain character display could do too.
Grid redefines a tile, fills all 128 map rows with it, and scrolls by
writing ONE BYTE A FRAME. Nothing moves. The rows above and below the
screen are already drawn, so a screenful of movement costs one OUTA and the
rows that leave the top are still there.
Its tile goes at 200 because the machine wakes with the font in tile memory
- glyph n at tile n, for 135 of the 256 - so a program starting at zero
paints over the alphabet and the shell it is about to hand the machine back
to. Its sixteen colour bands are one tile and not sixteen: the attribute
nibble is added to every index in a cell, so the same 64 bytes come out in
sixteen colourings.
Three things it cost, all of them the same lesson about this machine:
- "SETD.0 X" then "STD.0.1" stores through DP1, which had not been set
yet. It assembles, and the blit then reads its 64 bytes from wherever
DP1 was last left, so the tile came out as noise.
- The palette entry for scheme n is at 0xFC00 + 64n, which reaches
0xFFC0 - four pages, not one. And doubling A by adding B needs B to
hold A, which RSTB is the opposite of. Both went away by writing all
256 entries in order and letting the controller step the address, so
nothing computes an address at all.
- The screen it hands back had the right cells and the wrong colours,
because restoring the map is not restoring the palette.
That last one is a gap in the machine rather than in this program, and is
written up in the CosmOS README. The console's colours live at exactly the
entries the attribute nibble lands on, so any program using the nibble
overwrites them and has nowhere else to write. Grid puts bank 0 back - grey
on black - and leaves the other fifteen. The real answer is a command to
the screen meaning "give me back what you woke up with", the way the
console has one for clearing. There is not one, and this is the first
program that ever wanted it.
Two checks in video.sh, which boots the whole system and reads the pixels
the renderer produced rather than trusting what the program believed.
Breaking the tile fails one and breaking the attribute fails the other.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
0e0731e2b1 |
Put the cursor home when the screen is cleared
A screen with nothing on it and a cursor half way down it is not a cleared screen: the next thing written lands where the last thing happened to leave off, at a position whose meaning was just erased. The shell's clear did exactly that, and left the next line one row below wherever it had been. Both halves were missing. consoleClearScreen blanks the cells and does not touch cursorRow or cursorColumn, and 2J on a terminal empties the screen without moving anything - H is what puts the cursor at the top. Life and Snake never showed this because they follow their clear with an explicit 1;1H of their own. They were working around it, which is why the bug survived until a command cleared the screen and then let somebody type. The attribute is deliberately not reset. Clearing is about what is on the screen rather than how the next thing will be drawn, and a program that chose a colour and then cleared still wants that colour - which is what a terminal does too. Two checks in video.sh, and they took two goes to make independent. The first pointed at the row where the letter lands when the cursor is NOT homed, so removing the clear and removing the homing failed the same pair and neither said which. The second now looks at a row nothing writes to either way - and at a letter whose ink actually reaches the pixel it reads, which "two" did not, so it passed on a screen that had never been cleared. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
553882d28d |
Start CosmOS with a script, and let a script hold its tongue
Three things scripts wanted, and they are one thing: a machine that can have a face. /System/Boot/startup.sh runs before anybody can type. Every way of reaching the prompt for the first time goes through it, including the one where there is no disk - in which case there is nothing to find and nothing is said. A MISSING one is ordinary and silent, because a clean install has none and a machine that complained every boot about a file nobody wrote would be teaching its owner to ignore it. One that is THERE and does not begin with #! is the other case entirely: somebody meant that to run. #quiet stops each line being echoed, #loud puts it back. The prompt and the echo go together, because together they are what makes a script look like typing, so a quiet script gets neither and what it prints is all that appears. A nested script inherits quiet - a build that asked for it meant its helpers too - and gets its own setting back when the helper returns. Anything else beginning with # is handed to the shell, which does not know it and stops the script, because a script that asked for something this shell cannot do should not carry on as though it had been given it. clear empties the screen, which the console has been able to do since before there was a screen to do it on. THE PROMPT IS NOW SAID BY WHOEVER SUPPLIES THE LINE. It used to be said at the top of the loop, which is a decision made before the line is read and an answer not known until after - and it was wrong at both ends. #quiet is itself a line, so its prompt went out before anything knew to stay silent; and the line after a quiet script's last one comes from the console, having already been denied one. Off by exactly one line in opposite directions. A first attempt at this remembered whether the prompt had been skipped, which worked and was a flag standing in for a structure. The monitor's assembler prints a prompt of its own, so it reads through shellReadRaw, which is the same source without one. One admission. Handing the console its prompt back when a quiet script ended was a real fix when I wrote it and stopped being one an hour later, because the restructure above means the console's own path prompts whatever the flag holds. The comment claimed it fixed something. Breaking it on purpose changed nothing, which is how that was found, and it is now a comment saying so instead of a line pretending to work. The startup fixture ends QUIET on purpose: nothing puts the flag back when the outermost script finishes, so a script ending #loud would have tested the easy half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
c28826df77 |
Let a script run a script, four deep
A build script calling a setup script is the first thing anybody tries. What is saved when one script starts another is A POSITION AND NOT A BUFFER: the name, which block comes next, how many are left, and where in the block it had got to. Seventy bytes, and they sit next to each other in the data segment on purpose so that saving them is one copy. The block itself is read again on the way back, which costs one disk read per return and saves 257 bytes a level - the inner script reads its own block into the single buffer there is, so coming back means fetching the outer one's block again and landing on the byte it left. The slot is reached by stepping rather than by multiplying, because this machine has no multiply and the depth is never more than three steps. Four levels. Deep enough for a script calling a script that calls a helper, shallow enough that a script running itself says so rather than filling memory. A line that fails now stops every level and not just the innermost, because a build whose helper failed should not carry on in its caller. The caller's place is saved BEFORE the new file is looked at, and put back on every way out that is not success. Opening writes the name into the live state in order to ask the disk about it, so by the time "there is no such file" is known, the caller's place has already been overwritten - a failed 'do' inside a script would otherwise leave the script that ran it reading from a name it never chose. The test resumes in the outer script's SECOND block, which is the case the whole design turns on and the one an ordinary nesting test would miss. Breaking the re-read, the save, or the limit each fails it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a12d61fb80 |
Give the shell an echo, so a script can say what it is doing
Say.sbx has printed words since long before there were scripts, and is the wrong shape for one. It is a program: it has to be found on the disk, loaded and started, it prefixes what it was told with "it says:", and the system prints "finished" after it. Three lines of noise around one line of narration, and a load off the disk to produce them. echo is a command, so it costs a comparison. With nothing after it, a blank line - which is what anybody expects and is worth having to space a long script out. Not called "say", although that is the better word. Built-in commands are tried before the disk and always win, so a built-in say would shadow Say.sbx and quietly change what every existing script and test meant by it. Also puts "do" in the help, which the commit that added it forgot, and splits the help text: the new lines pushed it over the assembler's 255 character limit for one string. That failure was hidden for a few minutes by a 'make' whose errors were going to /dev/null - the build kept the disk it already had, and the machine cheerfully reported "I do not know: echo" from a system assembled before echo existed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
2466d79d9c |
Let the shell run a file of commands
'do <file>' runs the lines in a file as though they had been typed. The
only thing a script changes is where the next line comes from: everything
below shellReadLine - splitting the line, matching it, loading a program -
cannot tell the difference and does not have to.
What makes a file a script is '#!' on the front of it, not its name and not
a flag in its entry. The rule this filesystem keeps is that an entry holds
only what the content cannot say about itself, and a script can say what it
is; the loader already refuses anything that is not SBEX, so the two kinds
of runnable file turn each other away without either knowing about the
other. It is also the deferred half of the file-typing design, which said
to wait for a second kind of runnable thing before building any of it. This
is that second kind.
'#' is a directive and ';' is a comment, as in SplitBit assembly - one rule
across the machine rather than two dialects. Not Unix's convention: there
'#!' really is a comment that only the kernel reads, while here the shell
requires it and refuses the file without it, so calling it a comment would
be a lie about what it does.
A script stops at the first line that does not work, which is what the
LineFailed groundwork was for. Comments and blank lines are dropped by the
reader rather than by the dispatch, so they are not echoed either. A script
running out hands back to the console rather than ending the shell, because
running out of file and running out of typing are not the same thing. The
interactive assembler reads through the same path, so a script can contain
a block of assembly.
Three things this cost that were not obvious:
- RET puts A and B back, so a routine cannot answer in them. scriptByte
returning the character in A assembled, ran, and handed the caller its
own A back every time. It answers in memory now.
- A last line with no newline is still a line. Text files do not reliably
end with one and an editor eating it is a bad way to find out a command
did not run.
- Not LastStatus. See the commit before this one.
Six checks in three tests, two of which are about byte positions rather
than behaviour - a command lying across the boundary between two blocks,
and that missing newline - so their fixtures are generated rather than
committed, where an editor cannot helpfully repair them.
Nesting is not in yet: a script cannot run a script. That wants a stack of
positions rather than the one the reader keeps.
Also derives native.sh's self-hosting source list from cosmos.asm's own
#Include lines. It was a hand written list and went stale the moment
script.asm existed - the fourth time a list beside a thing has drifted from
the thing - so it now asks the thing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
fc56e815fc |
Have the shell remember whether a line worked
Groundwork for scripts, and invisible until there is something to read it: the suite passes unchanged, which is the point of doing it on its own. A script has to decide whether to run the next line, and nothing in the shell knew whether the last one worked. LineFailed is cleared as each line is read and set by the fourteen paths that fail. Cleared at the start rather than set at the end, because there are thirty seven ways back to the prompt and only fourteen are failures - and the twenty three successes would have to be found again every time a command grew a new way to finish. A command that says nothing worked. Twelve of the fourteen already funnelled through fileComplain, so this is fourteen lines rather than the refactor it looked like. It is deliberately NOT LastStatus, which was the obvious place and is wrong. That one is a program's own answer, reported by the status command and recorded by two tests; clearing it as each line began wiped the answer before the command that reports it could read its own line. The tests said so immediately. Two questions, two bytes - and a program exiting non-zero now sets both, because a program answering "no" is one of the ways a line can fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
e52d8d84f8 |
Collapse the two makefiles into one
Programs/makefile is gone and everything it did is here. The split was not paying for itself: nothing at this level ever ran that file, so it rotted, and all three bugs found in it this week share that one cause. It named two source files that had been renamed months earlier and failed outright. The disk did not depend on the tree it mirrors, so a new file silently was not on it. And the disk was in no default target, so 'make clean' threw it away and 'make' did not bring it back - which is what prompted this. The platform and the system stay separate, as TARGETS rather than as files: 'make SplitBit Assembler' builds the machine and its tools, 'make cosmos' and 'make disk' build the system, and somebody who wants to write their own system can ignore the second group entirely. That boundary is now one make enforces, which the directory boundary never did - Tests/makedisks.sh has always reached across it to build its own fixtures. 'make' now builds a bootable disk as well as the tools, because a machine with nothing in the drive does not do anything and the first thing anybody wants after building this is to watch CosmOS come up. It costs half a second: the whole system, twenty six apps and the native assembler assemble in less time than the emulator links. Two things the merge needed that the split did not. Assembling now takes an order-only dependency on the assembler, which came free when you had already built the tools before changing directory. And the mirror is told which directory to walk: it was ".", meaning Programs/, and left alone it would have meant the whole repository - the C sources, the tests and the manuals mirrored onto a disk for an 8-bit machine. The disk comes out at 188 files where it was 189. The one that went is Programs/makefile, which was itself being mirrored onto it as /Source/makefile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
2f22807458 |
Give the demo a real tune: the Anachronaut Labs theme
The composition is the user's - a leitmotif they already have variants of, written into the note table by hand. It replaces the eight note scale that was there to prove a note could be played at all, and it is a better demo for the obvious reason and one less obvious one: it is long enough to hear whether the machine keeps time, which a run of eight notes is not. Forty five notes, 567 frames, 9.45 seconds, peaking at 19,461 of 32,767. The test budget goes to twelve million cycles, which is a duration rather than a guess: at 16,667 cycles a frame the music is 9.45 million, so there is room to add bars before anybody has to come back here. Also records what borrowing the screen's frame actually costs, which came out of writing music rather than out of theory. The frame is not too slow, it is FIXED: every duration is a whole number of 16.67 ms, so a note worth a third of a beat cannot be written, and the way round it is choosing a tempo whose subdivisions land on whole frames - making the tune fit the machine. That is the argument for the timer peripheral wanting an arbitrary tick rather than a faster fixed one, and for the screen not being the clock: a display refresh and a music routine have no reason to share a rate. 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 |
||
|
|
62a657f1a7 |
Give Voyager a speaker
The device already made the samples; this takes them out of its ring and hands them to Raylib, a sub-buffer at a time. Nothing here decides what a sound is, the same as nothing in presentFrame decides what the screen looks like - which is why the headless binary and this one make the same sound, and why the suite can check a device with no speaker in it. Asked for rather than assumed: a host with no audio device still gets a window, because a machine worth looking at should not need one to start. When the ring runs short the missing samples are filled by holding the last one rather than by zeroes. It is still a glitch; a jump to silence and back is a click, and a held level is not. Also removes a comment that had been left in twice above the frame loop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
8366bf7721 |
Fix the Programs build, and make the disk depend on what it mirrors
Two faults, both introduced by
|
||
|
|
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
|
||
|
|
b0d06aa6e5 |
Make the vendored synth plain ASCII
soundThing's comments use em dashes and an arrow, and this repository is plain ASCII throughout because the tooling around it does not do Unicode. Tests/docs.sh caught it the first time it ran against the new file, which is what that check is for. Comments only; nothing the compiler sees has changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
f58b0f93af |
Vendor the voice engine, with the level it always had and could not say
soundThing's synth.c and synth.h, and nothing else of it: the voice engine pulls in maths, stdlib, stdint and stdio and knows nothing about Raylib, MIDI, patches or the interface, which is what made it liftable. Copied rather than submoduled - two files against tying this build to another repository's history is not a close call - so the header carries the whole of the difference and is meant to stay current. A VOICE'S LEVEL IS A ROUTING NOW. Envelope 0 was multiplied into the output unconditionally, 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. There was no flag to turn that off and there never had been: Envelope is the only one of the four modulating things without an active flag, where LFO, Filter and Oscillator all have one. It could not take the filter's shape either. filterTick returning its input unchanged is the right nothing-happens answer, but an envelope returning a constant would be useless as a mod source - and being a mod source while NOT being the volume is exactly the thing wanted. So the voice names the source that shapes its level, the way every other destination names its source, and MOD_SOURCE_NONE means nothing shapes it. Two things fall out that were not possible before: envelope 1 shaping the volume, and an LFO doing it, which is tremolo. Envelope 0 still decides how long a voice lasts, because it still ticks. A voice with its level unrouted will therefore cut rather than fade when the envelope reaches idle - fine or wanted for percussion, a click if the oscillators are loud at that moment, and a one line change to end on note-off instead if it turns out to matter. NOISE COMES FROM A SEEDED GENERATOR, one to each thing that makes it. rand() is global state shared with the whole process and varies between C libraries, so the same program sounded different on different machines and no recorded result could have meant anything. One generator each rather than one shared, because two noise sources drawing the same stream are not two noise sources. Checked rather than assumed: two synths from the same start produce identical samples, and 4,617 of 4,800 of them are non-zero, so it is noise and not silence. AND A CHANNEL IS THE CHANNEL YOU ASKED FOR. synthNoteOn hunts for a free voice and steals round-robin, which is what a keyboard wants and what the standalone synthesizer still does. Channel two is channel two. Both old calls are untouched. Nothing links it yet. It compiles clean and standalone under -std=c11 -pedantic, which is what make strict already checks, and the 169 tests are unmoved because nothing calls it. 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 |
||
|
|
4e3258e3f7 |
Put the libraries on the disk, and say which include is missing
Sieve-16.asm, Life.asm and Fib-16.asm could be read on the machine and not assembled on it. All three #Include print.asm, and print.asm was not on the disk. /LIB WAS STILL A HAND-WRITTEN LIST. Programs/Libraries is what an #Include means when it is not a CosmOS source - print, the integer helpers, the maths - and nobody had thought to name them, which is the exact failure mirroring the sources was meant to end. It is mirrored now, so the next one nobody thinks of is there anyway. AND THE ASSEMBLER SAID NOTHING USEFUL. A missing include returned a failure and printed no message, so the catch-all at the top said "nothing was written", which is true and useless - finding out why meant reading the assembler rather than the message. It names the file now, and where it looked: cannot find print.asm, not beside the file that asked and not as /Lib/print.asm Both halves are worth having, because a name that is in neither place is usually one or the other spelt wrong. The same shape as More's "error 2": a program that knows exactly what happened, reporting a number or nothing at all. The disk also depends on the recipe that lays it out now. It did not, so adding the libraries left an image built without them and the next run reported the same thing missing - which sends you looking at the change you just made rather than at the stale thing in front of you. It cost twenty minutes to notice. 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 |
||
|
|
adefce975b |
Let the reset button reach a machine that has stopped
The gesture rebooted CosmOS and could not reboot picture.bin, which is the case it was added for. picture.asm ends in HALT, and a halted machine runs no instructions - so nothing ever reached the code that notices a reset, because a reset is noticed BETWEEN INSTRUCTIONS and there are none. It only ever worked because CosmOS was still going. Which is backwards: a machine that is not going anywhere is exactly the one worth restarting, and it is the one that cannot hear a request by itself. The restart is lifted out of the run loop into machineTakeReset, and the window asks every frame whether the machine is running or not. NAMED AS EMULATOR MAGIC, because it is. There is no reset line on this machine and no keyboard controller to assert one; the window reaches in and sets the same flag the machine port sets. When those are designed, a keyboard controller will have to see the gesture and pull reset regardless of what the CPU is doing - which is the property that matters and the one a port write can never have, since a port write needs a program willing and able to make it. The shape of that is already visible here: asking every frame rather than leaving it to the machine to notice is what a line does. A restart now clears the cycle limit as well, since a machine stopped for reaching one is another thing somebody would press the button over. The three existing reset tests still pass, and they are the ones that matter: they exercise the same restart through the machine port. What no test reaches is the gesture itself, which exists only when there is a window. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
8c6ed62044 |
Reset on a gesture the host has no opinion about
Control, Shift and R. It was Control, Alt and Delete, which brings up a shutdown prompt rather than reaching the machine - and no amount of asking will change that. IT IS A SECURE ATTENTION KEY. Every serious operating system reserves it so that it always reaches the system and never an application, precisely so a program cannot imitate a login screen. On Windows an application cannot see it without a kernel driver; on Linux the desktop takes it. That is not an oversight to work around, it is the same guarantee a reset button wants being enforced one layer further down, and there is no call this program could make that would win the argument. It is unavailable for exactly the reason it seemed right. So the requirement list grew a third item that was not obvious until it bit: the gesture has to be one the host has no opinion about. Control and Shift with a letter is about as free as a combination gets - not window management, not a virtual terminal switch, and not a shortcut any desktop claims by default. It keeps the other two properties: plain F12 and plain Delete stay free for software, and three keys are not pressed by accident. If some platform does send a character for it, nothing comes of that either. Whatever arrives is in memory that is about to be thrown away. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
0afead00de |
Make the reset a gesture rather than a key
Control, Alt and Delete, which was F12. Two reasons, and the first one is about hardware rather than about the emulator. ON REAL HARDWARE THIS IS NOT A KEY AT ALL. A Voyager has a button on the case, and what a window has instead of a case is a gesture standing in for one. So it must not be a key software might want - a machine with a keyboard has function keys on it and something will eventually have a use for F12, which would have meant taking it away again later, from programs already written to expect it. And it must not be reachable by accident. Restarting throws away everything in memory, which is the same data loss Escape was one keypress from causing, arriving by a different door. Three keys together are not pressed by mistake. It has meant this since 1981, so it is also the one gesture nobody has to be told the meaning of - and Delete stays free for software, since the gesture is all three or nothing. 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 |
||
|
|
13b20c8834 |
Give the screen a bitmap mode
V4. Mode 2 is 320 by 200 with a byte a pixel: no tile to look up and no attribute to add, the byte IS the palette index. Programs/Examples/picture.asm fills a whole one in 127 bytes of program and 47,498 cycles. IT 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 holding whatever the picture put there. Taking the screen means taking it. The palette moves to 0xFC00, the top of video memory, because it is the one thing that has to mean the same in every mode and 64,000 bytes of picture leaves nowhere in the middle for it to hide. That is a documented address, so the example, the tests and the manual move with it. A BITMAP HAS NO COLUMNS AND NO ROWS, and both registers read zero rather than a leftover from the last mode. The console asks: told there is no character screen, it has nowhere to put a glyph and draws nothing, while still saying everything down the serial line. The honest alternative is what a machine with shared video memory really does, which is scribble marks nobody can read across somebody's picture - honest and useless, since a program that has taken the screen has not stopped wanting to print. Six checks in Tests/video.sh, to 55: that the mode is 320 by 200, that a byte is one pixel's colour and only that pixel, that printing leaves a picture alone while the letter still goes out, and that the columns register says nought and then forty again. The example is worth reading for one thing beyond the mode: Fill leaves its destination past what it touched, so two hundred rows are drawn from one address set once. Working out where row n begins would be n times 320, and this machine has no multiply. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
1174bd9af5 |
Give the machine a frame to wait for
V3. The screen interrupts at each frame on hardware vector 0x30, and WAIT finally has something worth sleeping on. THERE WAS NO CLOCK. Every program that wanted to happen at a certain speed counted instructions and hoped, which is why Snake's pause silently halved the day a cycle stopped being an instruction and became a memory access - the program was right and the thing it was counting changed underneath it. A screen finishing sixty times a second is a real beat, and it is counted in the MACHINE'S cycles rather than the host's, so the same program sees the same number of frames in the same number of cycles however fast anything really ran. That is what makes a frame something a test can count and a recorded result can hold. Status bit 0 goes up when a frame has gone by and reading the status port puts it down, so a program with no handler can watch for it instead. Control bit 0 asks to be interrupted, and is OFF when the machine starts: an interrupt with nothing installed to catch it is a fault, so a screen that began interrupting the moment it was switched on would take down every program written before frames existed. More than one frame can pass between two looks, and the flag and the line are each one thing, so several still mean one of each. A missed frame is missed. Programs/Examples/frames.asm prints a dot a frame for a second: 1,000,324 cycles, and 996,460 of them spent asleep. That split is the thing worth seeing - a program that polled instead would print the same sixty dots, take the same second, and spend every cycle of it on the bus. Its header explains why waiting is not spinning and why a machine with a beat can stop guessing at one. Six checks in Tests/video.sh, and two of them are about the clock rather than the output, because the output cannot tell the difference. That the machine slept through nearly all of ten frames, and that polling three frames actually took three frames - a status flag that stayed up once set would print exactly the same character and look perfectly correct. Breaking the frame interrupt on purpose left a machine asleep for ever and hung the whole suite, which is a worse way to be told than a failing check. Tests/video.sh bounds its runs at ten seconds now, the way Tests/run.sh always has. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
3da020898c |
Write down what the machine costs, before it has to be worked out again
Three measurements that would otherwise be re-derived from scratch in three months, put where a reader will meet them rather than where somebody has to know to look. The README's cost model section says what a nominal 1 MHz actually means: about 270,000 instructions a second, since the average instruction costs 3.72 cycles. The same program therefore takes 3.72 times the wall clock it did when a cycle was an instruction - nothing got slower, the number got honest, and the number it replaced described no machine anybody could build. Beside it, the two figures that put that in proportion: the emulator runs 195 million cycles a second in --fast, and CosmOS assembling CosmOS is 654 million of them, which is eleven minutes at a megahertz, 3.3 seconds at --fast, and six and a half seconds at a hypothetical 100 MHz. That last is pessimistic rather than hopeful: it assumes hardware overlaps nothing, which is the same conservative reading the controller's cost model takes. The CosmOS README gets what that means for its assembler, which is superlinear in what it reads: 1,383 cycles a byte for a four kilobyte source, 6,290 for a hundred kilobyte one. IT IS NOT THE DISK, and the number that says so is worth keeping - 654 million on a disk carrying the whole source tree against 653 million on a flat one with a sixth as many files. The suspected cause is walking a table of some nine hundred labels once per reference, suspected rather than measured. Nothing is being done about it, and the reason is written down too: development happens with the host assembler. But faster hardware buys a constant factor and does not change the shape of the curve, so the program that forces this is not CosmOS - it is the first one twice its size. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
5732a31b2e |
Stop the allocator starting the directory again for every file in its way
Placing one file on a disk with the source tree on it cost 9.4 million cycles. It costs 1.4 million now, and assembling colours.asm went from 13.9 to 6.0 seconds. sbfsAllocate gave up the moment it found anything in the candidate's way: it moved the candidate past that one entry and STARTED THE DIRECTORY AGAIN FROM THE FIRST BLOCK. With files laid down one after another that is a restart per file, and every restart reads directory blocks off the disk until it reaches the next thing in the way - which is further in each time. Placing one file among 183 of them cost thousands of block reads. The candidate moves along DURING the pass now, and the pass carries on from where it is, so entries later in the directory are tested against where the candidate has got to. On a disk that has been appended to - which is what a disk mostly is - one pass walks it past everything and a second confirms nothing is left. Two passes rather than one per file. IT IS STILL FIRST FIT, and Tests/agree.sh is what says so: the machine and SplitDisk build the same tree and the images still match byte for byte, which they could not if allocation had started choosing differently. The argument is that the candidate only ever moves past something that genuinely overlaps it, and when it does there is nowhere below to go - the entry in the way covers everything up to its end and begins before the candidate ends. The first attempt at this was slower than what it replaced, by three times. It finished the pass and jumped to the FURTHEST overlap, which sounds better and is worse: with files laid contiguously only one entry ever overlaps, so the old early exit was the fast path and reading the whole directory to find the one thing was pure loss. The number of passes was never the thing to fix - restarting them was. The boot slot in the test fixtures goes from 32 blocks to 40, which is what a shipped disk has. Stage two is 8,231 bytes and 32 blocks is 8,192: a fixture tighter than the thing it stands in for fails on a change the real disk would have taken, and says "the boot slot is too small" rather than what actually grew. WHAT THIS DOES NOT FIX is assembling CosmOS, and that is worth saying plainly. It takes 654 million cycles on the mirrored disk and 653 million on a flat test disk with a sixth as many files, so it is not a filesystem problem at all. Cycles per byte of source climb with the size of it - 1,383 for colours.asm, about 3,000 for Edit.asm, 6,290 for cosmos.asm - which says the native assembler is superlinear in what it reads. That is a separate thing to go and look at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
7073b972e6 |
Say what went wrong, and give the file tools room for a path
The makefile on the disk was fine. "Makefile" is not "makefile", and SBFS names are case sensitive - but neither tool said so, and both failed in ways that pointed somewhere else. MORE PRINTED A NUMBER THAT MEANT NOTHING. "cannot find the file, error 2" invents a vocabulary the system does not have: the filesystem library documents its answer as zero or not zero, never as a code, so 2 could not be looked up anywhere. It says "there is no file by that name" now, which is the only way opening fails that a person can do anything about, and is nearly always a name typed slightly wrong. EDIT SAID "0 LINES", which is also what an empty file that IS on the disk says. A name typed slightly wrong therefore looked exactly like the document you meant to open, right up until you saved it somewhere new. It says "new file" instead. Two bugs came out of writing that, and both are worth more than the feature. The first is mine and the label lied to me: loadNothing is not where a load FAILS, it is where every load FINISHES, reached at the end of splitLast on files that opened perfectly well. A flag set there on the strength of the name was set on everything. It is called loadDone now, and the failure has its own name. The second is older and general: a program is loaded once and may be run many times, so "load Edit.sbx" then "run" twice is two sessions over one copy of the Data Segment. Anything a session changes has to be put back by the session. A zero written in the Data Segment is the state a program starts in the first time and never again - and cosmosEdit runs Edit twice from one load, which is why it caught it immediately. AND THE FILE TOOLS COULD NOT ADDRESS THE TREE THEY NOW HAVE. Edit took 23 characters of name and More and Type took 29, which were right when everything lived in the root. With the sources mirrored onto the disk, "/Source/CosmOS/Assembler/classify.asm" is an ordinary thing to type - thirty-seven characters, cut down to a name meaning something else, or nothing. All of them take sixty-three now, which is what the shell reads of a command line, so nothing longer can arrive. Wander with it, since a directory is a path too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
0852666e73 |
Mirror the source tree onto the system disk
A list of files in a makefile goes stale the moment somebody adds a program and forgets to name it, and what they forgot is invisible until they go looking for it on the machine. So SplitDisk gained a mirror command and the disk rule is one line: putting a file where the others live is now the whole of putting it on the disk. EVERY FILE GOES THROUGH put AND EVERY DIRECTORY THROUGH mkdir. That is the point of it - mirror adds a walk and no filesystem code at all, so anything the format refuses here it refuses everywhere, in the same words. What is new is the walk, and the walk is what the six checks in Tests/disk.sh are about: that it goes all the way down, that it leaves dotfiles and named directories behind, and that a name too long stops it. REFUSED RATHER THAN SKIPPED, because a disk quietly missing a file is the exact failure a mirror exists to prevent. Which meant four sources had to be renamed - a directory entry holds 22 characters and they were 23, 23, 24 and 29: 16bitSegmentedSieve.asm -> 16bitSieve.asm 16bitSegmentedSieveModern.asm -> 16bitSieveModern.asm consoleInterruptTest.asm -> consoleInterrupt.asm controllerWriteTest.asm -> controllerWrite.asm The test names in the manifest are unchanged, since those are identifiers and every recorded result is filed under them. Only where the source lives has moved. The entries are sorted before anything is written. readdir hands them back in whatever order the host filesystem feels like, and a disk image that comes out different from one run to the next is an image no test could compare against another. The disk grew from one megabyte to four and from 192 directory entries to 1,024. The sources are 2,850 blocks and the mirror filled the old directory on its first run, which is a thing that should not need thinking about again. The Tests fixture disk is deliberately NOT mirrored. It is a controlled fixture with known contents, and the shipped disk is the one meant to be useful; they want different things. 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 |