4b1c3d8e3ffa7af27f8b3f69aa69d91316357a84
182
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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
|
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
978aec4809 |
Let the console edit a line, and let a file be a keyboard
BACKSPACE REACHED THE SHELL. A terminal in line mode does not hand a program every keystroke: it collects a line, rubs out a backspace, and delivers the finished thing at Return. CosmOS has always relied on that, and behind a window there is no terminal to do it, so the raw 0x08 went into the command buffer. Correcting a typo produced a line that looked perfectly right on the screen and matched no command at all - "I do not know: help". So the console does it, because behind a window the console IS the terminal. In key mode it does not, and must not: a program in key mode asked for every keystroke as it happens. CosmOS now asks for eighty columns at boot. Its own help text is seventy-four characters wide, and dir, the monitor and the assembler's messages all assume room. The machine still wakes up in the smaller mode, which is right for a machine - it is the system that knows what shape of screen its own output needs, and a game that wants forty columns says so. AND A FILE CAN BE A KEYBOARD, which is the part that matters beyond today. The console behind a window is not the console behind a terminal, and until now the difference was unreachable: it broke twice in two days and a person typing found it both times. --keyboard installs the same hook a window does, so the same path runs, and the manifest has a column for it. cosmosTyped types "halp", backs over it, arrives at "help", and requires the help to come out. Verified by removing the rub-out, which loses the whole help text. It does not test the window. Voyager's key queue and everything about presenting frames are still out of reach. It tests the console, which is where the logic is. Along the way: VOY_OBJS was missing from the dependency include, so voyager.o never rebuilt when a header changed. EmulatorOptions grew a field, Voyager kept an object that disagreed about the size of the struct, and smashed its stack on every run. A clean build hides it and 'make sanitize' cleans first, so that would never have found it either. Tests/voyager.sh did, by failing all 115 tests that start the machine - which is the differential test earning its keep on a bug that has nothing to do with what it was built to check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
761c11a66b |
Add run-voyager, and make running depend on the disk
The jitter was a stale disk. An image built before the escape sequences came out still had the old Snake on it, which sends ESC [ H every frame: the console swallows the escape, because it is below the font's first character, and then draws "[H" as two ordinary letters. So every frame began two characters to the right and one line further down than the last, and the board walked down the screen. Not timing at all. WHAT IS ON A DISK IS WHATEVER WAS BUILT WHEN THE DISK WAS MADE, and a machine whose console has changed will start that image quite happily. That is the compatibility break we chose when the parser came out, and it is fine - but it should not be a puzzle, so both run targets depend on the disk rather than merely using it, and both READMEs say why. run-voyager boots the same disk on the machine with a screen. It existed only as EMU=../Voyager in front of run-cosmos, which is not a thing anybody should have to know. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
310804e267 |
Give Snake back the speed its comment promised, and present in step with the display
Two things, one certain and one likely. THE CERTAIN ONE IS NOT THE WINDOW'S FAULT. Snake's pause loop said "at the emulated rate this is about an eighth of a second", and it was, when a cycle was one instruction. A cycle became one memory access, every loop in the machine got dearer, and this one silently doubled: the game has been running at half the speed it documents ever since, in a terminal as much as in a window. Measured rather than guessed - the inner loop is a DECA and a BNA, one byte and three, so four cycles a turn, and a whole run went from 3,848,610 cycles to 1,920,504 when the outer count came down from 256 to 122. Almost exactly half, which is what the arithmetic said it would be. That is the cost model change reaching a program nobody thought to re-measure. Worth looking for others: any loop tuned by eye before that change is running at half its intended speed. THE LIKELY ONE is the frame limiter. Without the vsync hint, Raylib sleeps towards sixty frames a second on its own clock, which beats against a display refreshing on its own - frames shown twice or skipped, and the machine handed an uneven number of cycles each time, since it takes its budget from the wall clock. The hint puts presentation in step with the screen. SetTargetFPS stays for a driver that ignores it. Snake is one byte bigger, because RSTB became INIB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
43a05b3df1 |
Replace the escape parser with cursor registers
The console had grown an ANSI parser, and that was the wrong shape. ANSI exists because a screen used to be on the other end of a serial line and a byte stream was the only channel there was. This screen is memory the program can already address, so reaching it by sending characters for a state machine to take apart is a middleman for something the machine does better - and it meant accepting an open protocol somebody else defines, in hardware, with no natural end to it. Everything else on this machine is registers. So the console gets three: cursor row at 0x03, cursor column at 0x04, and a command port at 0x05 where 1 clears the screen. Both cursor registers are READ as well as written, which is the thing an escape cannot do without sending a query and parsing a reply - a routine that wants to put the cursor back where it found it can now ask. Clearing is one command against a thousand cells walked one at a time. Snake and Life are smaller for it: 2,168 bytes to 2,163 and 1,410 to 1,396. A HOST TERMINAL STILL SPEAKS ANSI, and bridging to the host is the emulator's job, the same job it does reading standard input. So the escapes are now GENERATED, outbound, for the set this device chooses, rather than parsed inbound as though the machine were a terminal. The set cannot grow behind our backs because we are the ones saying it. The cursor is announced lazily, at the next character rather than at the register write, so setting a row and a column costs one sequence rather than two. The console's block widens from three ports to six, which registryTest noticed: it had been asking about port 0x05 precisely BECAUSE nothing was there, and the console had just moved in. Re-blessing it would have left it checking nothing, so it asks about 0x80 instead - clear of the console, the disk, the screen, the controller, and the sound device coming to 0x40. Six checks in Tests/video.sh swapped from the sequences to the registers, including that the cursor reads back and that one sent past the edge is clamped rather than refusing. Those checks also stopped counting bytes from the ends of a file, which had quietly started measuring an escape the moment the console began announcing the cursor. SplitLint caught the one thing worth catching in the port: the clear command leaves A at 1 and key mode is also 1, so the second load looks redundant. Acting on it would tie a console command to a console mode by coincidence, and break silently if either ever moved, so it is suppressed with that reason rather than removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
79727044b7 |
Reboot, and the machine device that makes it possible
Until now the only way to restart was to stop the emulator and run it again, which meant the one thing the machine could not do was the thing Once was written for. The loop now closes without leaving it: > Once /System/Boot/bare.bin next start: /System/Boot/bare.bin, once > Reboot starting again stage two just this once: /System/Boot/bare.bin bare metal: no system, just this Writing 1 to port 0x13 asks the machine to start over. A PORT RATHER THAN A SERVICE, because a reset has to work when the system does not: something only askable through SWI would be unavailable in exactly the case that wants it most, and a program that owns the whole machine has no system to ask. It is device class 0x04, in the range kept for the machine rather than among the peripherals, because it is not one - it is not attached to anything and cannot be unplugged. WHAT A RESET REPEATS IS HOW THE MACHINE STARTED. Named an image, the emulator places it again; named none, the ROM is shadowed again and reads the disk. Anything else would mean a reset changed what the machine IS, which is the one thing a reset must not do. Both are tested. Taken between instructions, because a device cannot restart the machine from inside the instruction that asked: the CPU is part way through a step and its state is not yet anything a reset could leave behind consistently. The disk stays attached and keeps everything written to it - that is what warm means. The vector table is cleared, which is the one deliberate departure from leaving memory alone: a vector points into whatever installed it, and after a reset that program is not running, so a handler left behind would aim an interrupt at an address belonging to something gone. It is the argument CosmOS already makes at exit, applied to the machine. Reboot is 45 bytes, most of them the word it prints. |
||
|
|
7b28f48f52 |
Once: start something else on the next start, and only that one
A program that owns the whole machine had nowhere to run. It cannot be started from the shell, because starting it means there is no shell, and pointing boot.cfg at it means a machine that keeps starting it - which is a poor place to find a mistake in something written five minutes ago. Once writes /System/Boot/once.cfg, in the same format as boot.cfg and read with the same routines, because a second format for one setting would be a second format. The loader reads it before boot.cfg and DELETES IT BEFORE IT JUMPS, which is the only moment there is: after the jump the loader does not exist. Consumed by being read rather than by working, so a one shot that hangs cannot hang twice - the request is gone before the image ran, and the next start reads boot.cfg like any other. THE BOOT STATE IS NOT TOUCHED, and the first version got that wrong. It marked the start the way any other start is marked, and then every successful bare metal boot reported that it had never arrived - because a program with the whole machine has no filesystem to clear a mark with and is doing nothing wrong by not having one. Found by running it: the image printed its line and the next start still said the last one did not. Three disks, each a start further along, so none of the tests depends on another having run. The loop is closed on the machine now: write it in Edit, assemble it with Asm, ask for it with Once, restart, watch it own the machine, and the system comes back without being asked. |
||
|
|
89c667848b |
Edit read a file into a buffer it never checked the size of
Opening hello.asm showed a thirty one line file as three, one of them cut short. Opening it again hung the machine: the emulator kept running and nothing ever answered. Entry is the buffer a line is read into, and it is followed in memory by TextHead and ArenaFree - the head of the document, and the pointer its line allocator hands out. The loop that splits a file into lines copied characters in WITH NO BOUND AT ALL, so a 94 character line wrote thirteen bytes over both of them. The list head then pointed into the middle of the text and the allocator handed out an address inside the file, which is why the second open walked a list that led back into itself for ever. Typing was always safe. osReadLine is told how much room there is, so a new document behaved perfectly and a source file did not - which is exactly how the user found it, and why it looked like a mystery rather than a bug. The bound is there now, and the buffer is 128 characters: what a line is everywhere else on this machine, the same number configuration files use, rather than a second answer to a question already answered. hello.asm fits. A file with a longer line is REFUSED rather than shortened. This is an editor - a line cut on the way in would be written back cut, and the file damaged by having been looked at. It says so and exits with a status of one, which it can do since this afternoon; the file is byte identical afterwards, and the test checks that. Opened twice in the test, because once is not enough to see it: the first open does the damage and the second is what never returns. This is the third time this shape has turned up: a buffer written past its end into the variables that happened to follow it. The prompt walked off CwdText into the shell's own command names; the assembler's output ran into its label table. Every one was found by a person using the machine. |
||
|
|
87d819847e |
A program can say how it went
SWI osExit takes a status in A, and the shell keeps it. Fifty eight exits across twenty three programs now say deliberately whether they worked: 25 did what they were asked, 24 did not, 9 were asked wrongly. Compare is the exception and says so - one there means the files differ, which is a result rather than a failure, the way diff has always had it. IN A RATHER THAN Q, which is not a departure from the rule that a service answers in Q. This one takes an ARGUMENT, the way osPrintNumber takes A and B, and it never returns to answer anything. A is free precisely because a return would have put it back - and Q is the ALU's output, so a small number costs four instructions there against one in A. The shell does not print it. A program that failed has already said so in words and a number beside that is noise, so osLastStatus hands it back and Status is the program that shows it. That indirection is the point: the number exists for the thing that cannot read words. MARKING THE EXITS FOUND A DEFECT ON THE FIRST RUN. Type and More printed why they had failed and then fell through into the success exit, reporting that all was well. Nobody had noticed, because while the only reader was a person, the person could see both the complaint and the claim. Two smaller things. Snake sets the console to line mode and then exits with zero, and the linter flagged the second RSTA as redundant - an exit status and a console mode, equal by accident, which is the class that must never be collapsed. And the README still taught answering by writing into the frame, three months of habit that SRET replaced yesterday; that section is gone and the one describing SRET stands in its place. |
||
|
|
b4206673a6 |
The clean install disk is one the machine can start itself from
make run-cosmos handed the emulator a boot image AND a disk, so it took the direct path every time: memory placed from outside, nothing on the disk consulted about it. The whole boot chain was built and tested and then not used by the one command that runs the system. The disk is formatted with a boot area now, stage two goes into slot zero as raw bytes, and the system is an ordinary file at /System/Boot/cosmos.bin - which is what boot.cfg would choose between if there were one. There is not: stage two falls back to that name when the file is missing, and a clean install with nothing to configure is the right default. run-cosmos names no image, so the machine starts itself. The old behaviour is run-cosmos-direct, which is worth keeping and worth naming honestly: it is what a debugger does, and it is what to use when the thing being debugged is the boot chain, since it skips the boot chain. /Source gains stage1.asm and stage2.asm, so the machine can rebuild what starts it. Everything stage two includes is already in /Lib, so Asm stage2.asm on the machine produces the bytes a boot slot takes. Stage one is the exception and always will be: it is the ROM, and the one part of this that a disk cannot replace. Second slot left empty, which is what makes replacing the first survivable. |
||
|
|
f1cc2e56b2 |
The last handler that knew a frame offset
handleReadLine used DP1 for the frame and the conversion only looked for DP2, so it kept the whole dance - and the dance had become a no-op that looked like work: MVQA copied Q into A, A was written over the saved Q, and RETI restored it, which is where it started. readLine leaves the length in Q and SRET keeps Q, so the answer simply stands. No handler in CosmOS knows what an interrupt frame looks like now. The 0d02 and 0d05 offsets still in the file are structures - an SBEX header, an entry in the instruction table - and not frames. |
||
|
|
c8c9f0b363 |
SRET: a handler answers the way a subroutine does
CALL saves A, B and Data Pointers 0 to 2 and nothing else, which is exactly why Q and DP3 are how a subroutine hands something back. An interrupt saves all of it, so a service with an answer had to reach into its own frame and un-save two fields by hand: MVSD.2 DPUP.2 0d02 ; the saved Q, by an offset it had to know STA.2 RETI Thirty places in CosmOS did that. Every one knew the frame's layout by heart, and all thirty would have gone quietly wrong the day the frame gained a field - the same duplicated fact this project keeps being bitten by, except duplicated into thirty places AND into the CPU. SRET is 0x76, in the seat the block split left for it. It is RETI's frame with RET's rule applied: A, B and DP0 to DP2 come back, the saved Q and DP3 are dropped, and the Interrupt Flag is restored from the frame - only that bit, so carry survives a service the way it survives a call, and there is one rule rather than two. RETI stays exactly as it was: a hardware handler has nothing to say and must leave no trace. CosmOS is 10,969 bytes against 11,122, and no handler knows a frame offset. TWO MISTAKES WORTH RECORDING, both mine, both caught by tests. The first conversion matched STA.2 with a regular expression that did not allow a trailing comment, so it ran past the end of one handler and into the next. The second understood the pattern and still got it wrong: the old frame write carried the answer from A into the saved Q slot, so simply deleting the write left Q holding whatever it happened to hold. Services that answer by calling something were fine - Q already had it - and services that set A directly silently reported success for every failure. cosmosCwd is what noticed, by saying "cannot go there" about a directory that was there. Sixteen handlers move the answer into Q now. Seven MVQA went with it. They copied Q into A so the frame write could carry it; SRET puts A back, so they moved a value nobody would ever read. |
||
|
|
cd5f548736 |
Move the opcode map: nothing in 0x0X, and room for a return variant
Three blocks move and nothing else changes. Branches take 0x60, subroutines take 0x70, and the ALU moves up into the 0x10 block the two of them used to share. Order within each block is preserved exactly - this relocates them, it does not rethink them. WHAT IT BUYS IS AN EMPTY 0x00 TO 0x0F. Program Memory that was never written, or a load that stopped part way and left zeroes in its tail, used to read as a long run of ADDs: the machine carried on through them, arrived somewhere unpredictable, and whatever broke there was a long way from the byte that caused it. Now it faults where it is met: Fault: 0x00 at Program Address 0x0004 is not an instruction. That is the address of the byte after the last real instruction, which is the difference between a diagnosis and a search. Reserving the whole nibble rather than just 0x00 means a run into blank memory faults wherever it starts rather than only when it lands on the right byte. runOffTest records it, and the block is left empty for whatever turns out to want it. The other half is room: branches and subroutines had filled 0x10 to 0x1F between them, so a service return that keeps Q and DP3 had nowhere to sit next to its family. It has 0x76 waiting now. Five places wrote an opcode down that the scripted remap did not reach, and four of them were found by tests rather than by looking: - secondPass.c lists which opcodes take an address, and firstPass.c knows SWI by number. Missing those made XOR read as a branch. - Asm.asm knows SWI by number too, being the other assembler. Missing it made the native and host assemblers disagree byte for byte, which is exactly the check that exists to catch a thing known in two places. - loaderTest.asm carries a hand written payload, and its RETI was 0x19. To the assembler those are numbers and to the program they are data, so nothing but running it could notice. It says so in a comment now. - The Assembler Manual prints the bytes hello.asm assembles to, and two of them were branches. The monitor's recorded disassembly moved by exactly the bytes it should: 18 became 72 wherever SWI appears, with SETD and INIB untouched and every disassembled line still reading the same. |
||
|
|
ce2a2cd7e6 |
Settle is a program, and a machine with no fallback still starts
The boot state opened a loop that could not be closed from inside: the machine said "settle it to try again" and gave you no way to do so. Settle closes it, in 349 bytes. A PROGRAM RATHER THAN A SHELL WORD. The shell is for the things that cannot be done without it, and this is not one - it reaches the system through SWI like anything else, which means it can be replaced, left off a disk, or called by whatever comes to call programs in turn. That last one is the point: a shell word is not callable by anything. Two services for it. osBootState answers in Q, and a machine with no disk answers settled, because there is nothing there to be unsettled about. osBootSettle puts it back. SETTLING IS THE ONLY WRITE A PROGRAM GETS - marking a start as trying or fallen back is the loader's business, and a service that let a program claim either would let it lie about something the loader has no way to check. And a hole the tests walked into, which was mine rather than theirs. With no fallback configured, a failed start left the machine unable to start at all: the mark said do not use the system, and there was nothing else to use. That turns "the last start failed" into "no start is permitted", which is worse than the problem the mark was added to solve. With nothing to fall back to it now tries the configuration again and says so - a failure that was passing recovers, and one that is not leaves the machine exactly where it would have been without any of this, which is the most that can be promised when there is only one thing to start. docs.sh caught both new services having no row in the services table before anything else did. |
||
|
|
dc74149321 |
B4: the disk remembers whether the last start arrived
The loader marks the superblock before it hands over and the system clears the mark when it reaches its prompt, so a system that crashes on the way there leaves it set. The loader finding it still set next time is how a machine that will not start says so to the only thing in a position to do anything about it. Without that, pointing boot.cfg at something that dies before the shell is a machine that can never be told anything again - the shell is the only way to change the file, and the file is what stops the shell from starting. Three states rather than two, and the third is the one worth having: 0 settled the last start arrived; use the configuration 1 trying handed over, and nothing came back to say it got there 2 fell back a try failed and the fallback was used, until settled With only 0 and 1 the machine alternates for ever: fall back, reach a prompt, clear the mark, retry the broken system, crash, fall back. State 2 stops that. A system known not to start is not tried again until somebody says the situation has changed. REACHING THE PROMPT IS A DELIBERATE THRESHOLD. It is not a claim that the system works - a shell can be reached by something broken in every other way. It is the point where a person can type, which is exactly what the fallback exists to give back: anything wrong past there is fixable from the prompt and nothing wrong before it is fixable at all. The routines live in sbfs.asm because both the loader and the system read and write this byte, and two pieces of code with their own idea of where a byte lives is what this format has two implementations and a byte for byte comparison to avoid. And the trap this system documents in its own manual caught me anyway: the first version handed the state back in A, which CALL restores, so every read got whatever the caller happened to be holding. It comes back in memory now, and the comment says why. Three disks differing only in the state on them, so the tests read as three consecutive starts of one machine while none depends on another running. |
||
|
|
546f336823 |
Configuration files, and boot.cfg as the first of them
One setting to a line: a key, a space, the rest of the line is the value. A semicolon starts a comment. The format was noticed rather than designed - textSplit already cuts the first word off a line and leaves the rest, and textSame already insists two strings end together, so reading a setting is those two routines and a loop. It is also what the shell reads, which makes a configuration line a command line the machine reads instead of a person typing one. The format was chosen by asking what the BOOT LOADER could manage, because it is the worst case in every direction: a few kilobytes, no operating system to report to, and if it fails the machine does not start. Two formats would be worse than one and the loader cannot have the richer one. CONFIGURATION IS ADVICE. A missing file, a missing key, an unusable value, a line too long to read: all of them mean use the default and none is a failure. BUT QUIET IS NOT SILENT - a setting somebody meant, which did not take effect, says so. That was the user's addition and it is the better rule: the default alone leaves the only symptom being that the machine did not do what somebody asked. So two routines. cfgGet reads and says nothing, because reading three settings should not report one bad line three times. cfgCheck reads the file once and reports, and is handed the caller's list of keys - whether a key means anything is the only part of this a shared reader cannot judge. /System/Boot/ holds the boot files, and stage two reads boot.cfg for what to start, with a fallback to try if it does not work and a name compiled in for when the file says nothing. THE TEST FOUND A REAL BUG, and it is the interaction I would not have thought to look for. First-match-wins met an empty value: a file with system system /System/Boot/bare.bin matched the first line, handed back nothing, and the machine tried to start a file with no name while a good setting sat underneath. An unusable value is an absent one - which is what "configuration is advice" says, and this is where it earns its keep. cfgBare starts an image with no operating system in it at all, which is what loading an ordinary boot image buys: a program wanting the whole machine is a file like any other, chosen the same way the system is. Three disks differing ONLY in boot.cfg, so each is a test of the file rather than of the machinery under it. |
||
|
|
c312853f8e |
The machine starts itself
stage two CosmOS > saved it read it back, 22 bytes: Stage one hands over to stage two out of a boot slot; stage two mounts the filesystem, finds /System/cosmos.bin, takes the image apart and places its code, its data and its vector table, and jumps to the entry point the vectors named. Nothing placed memory for it. What it loads is an ORDINARY BOOT IMAGE, the same SPBT file the emulator has always been handed. That was the user's call and it is the whole trick: a second stage that loads the machine's normal image format is not a boot-specific mechanism, so bare metal SplitBit stops being a special case. A program wanting no operating system under it is just an image, written under CosmOS like any other, and startable because it is a file. Three things in it worth knowing: - THE ENTRY POINT IS CAUGHT ON ITS WAY PAST. Program Memory cannot be read back, so the boot vector cannot be looked up after being installed; the vector loop notices the one addressed at 0xFC00 and keeps it. - A missing "VEC" is not a fault. An image written before vectors existed simply ends after its data, and then the entry point is zero, which is what every such image has always relied on. - Feature flags that are set mean an image asking for a machine this may not be, and the honest answer to a request that cannot be understood is to refuse rather than to run it anyway. The test records that the system WORKS afterwards rather than that it started. A loaded program running is what says the vector table arrived, because a program reaches the system through SWI and nothing else; the file written and the directory entered say the filesystem and the console came up with it. A second disk has a boot slot and nothing to start, and says so rather than jumping somewhere. |
||
|
|
82adeeb193 |
A boot payload can arrange its own Data Segment
Stage one places Program Memory and nothing else, because knowing where a payload's data ends and its code begins would mean knowing a format, and knowing formats is what ROM must do as little of as possible. But the real second stage needs a Data Segment: sbfs.asm has variables and a string it compares against. The answer needs nothing new. A loadable image is written into the slot as code followed by data, so the data image is already in Program Memory just past the code - and the payload's first instructions blit it down to where it was assembled for. Proved by slotData.asm, which prints from a string it placed itself. The padding is the part worth recording. The blit needs a length and the assembler will not work out the difference between two labels, so the segment is padded to a round number and that number is what gets copied. The first draft padded to 257 and copied 256, and the byte that did not arrive was padding, so it worked by luck. It is exact now and says why. This is the shape the user asked for and it goes further than the mechanism: the second stage becomes a loader for the machine's ORDINARY image format rather than for anything boot-specific, so bare metal SplitBit stops being a special case. A program that wants no operating system is just an image, developed under CosmOS like any other, and selectable at boot because it is a file. |
||
|
|
d07b23f90b |
Rung 2: the machine starts itself off a disk
Stage one exists and works. It is 330 bytes of program and everything it knows is a thing that will be true forever: which port the disk is on, that a SplitBit disk begins with its own name, and where two numbers sit in that first block. Not what a file is, not what a directory is, not that SBFS has versions. It reads the live boot slot into Program Memory, jumps to the first byte, and prints one character and halts if there is nothing there. It is an ordinary boot image for now, so the whole chain runs on machinery that already exists and the emulator has not been touched. Nothing about it changes when it moves into ROM except who puts it in memory. SplitDisk gained "boot" to write a slot and "bootslot" to choose one, kept apart on purpose: writing a slot and starting from it are different decisions, and joining them would make every write a commitment. A slot is always written WHOLE, because one still holding the tail of what was there before is one whose contents depend on its history, and stage one reads all of it without knowing where the file stopped. Three recorded tests, and the pair is the point: two disks differing only in which slot the superblock names, with payloads that say different things. One prints "booted" and the other does not, so this is a test of CHOOSING a slot rather than a test that some bytes were read. The third boots a disk with no boot area and gets the one character a ROM has room for. Eight more host checks, including that a slot is padded whole. Two things worth recording. The first draft used #Align to put the scratch buffer at 0x8000 and produced a 33K file - thirty two kilobytes of zeroes in something meant to be a ROM. It is an address, not storage, which is exactly what the assembler's own scratch map exists to say. And SplitLint caught the second in code written an hour after the baseline that catches it. In the blit set-up, RSTA writes a source address of zero and then RSTA writes a bank number of zero - two unrelated quantities that are equal by accident, in the most safety critical file in the repository. It is marked with a reason rather than removed. |
||
|
|
9c144469b4 |
Take the SplitLint findings that are one operation, leave the rest
Twenty four more sites, and the interesting part is which ones were left alone. A rule emerged while reading them and it held all the way through: apply where the repetition is INSIDE one operation, skip where the author's own structure says it is a new thought, and never where two equal values mean different things. Taken: - Five registers reassigned to a value they already held, where both are the same quantity: two masks in one expression in Snake, two spaces printed by the monitor, both halves of block zero in waitTest, and a RSTA in Pour that the very next instruction overwrote. - Eighteen SETDs that reload a pointer inside one operation - a store back into the variable just read, or an INCD stepping to the second byte of a two byte value. Those read correctly without the reload. - sbfsNext, which branched to the label on the line below it. Left, with reasons that are the useful part of this: - Eight registers where the same number means two different things. CosmOS and the loader set A to 1 for a blit command and then to 1 again for a bank number; Asm compares a type against 3 and then a status against 3. Removing those couples one quantity to another that is equal by accident and would part company silently. - Ten RSTAs that open the RSTA/RSTB/CCF/ADD "return zero" block. The redundancy is what makes that idiom self contained; taking it out makes the return value depend on the line above. - Eleven SETDs that begin an arm of a comparison chain. Each arm loads, compares and branches, and they get reordered - the repetition is the reason a new arm can be dropped in anywhere. - Twenty five SETDs separated from their pointer by a blank line or a comment, which is the author saying a new thought starts here. - Two CCFs before arithmetic, which this codebase writes unconditionally. - Three redundant branches in test programs whose recorded output includes addresses, where three fewer bytes moves what the test demonstrates. Nine recorded outputs moved and every one is a size in a listing or, for Life, five more generations inside the same cycle budget. Behaviour is unchanged everywhere: cosmosSnake and cosmosEdit pass byte for byte while Snake loses eight bytes and Edit twelve. CosmOS is 10,902 bytes of program against 10,937, and the native assembler 12,173 against 12,183. The CosmOS README's size for Edit moved twice in one sitting, and this morning's check caught it both times - which it could not have done before that claim was reworded to name what it was about. |
||
|
|
e1273337c4 |
Two mechanical fixes SplitLint found: MVQA, and RSTA for zero
Twenty four places moved Q into A or B by pushing it and popping it back. That is four bus cycles and two bytes to do what MVQA does in one of each, and several of them are inside loops - Life, the calculator, int8. Nineteen more loaded zero with INIA 0d0 where RSTA says the same thing in one byte. Both are equivalent at the CPU rather than by assertion: RSTA and INIA both leave Status alone, and PSHQ followed by POPA nets to A = Q with the Stack Pointer where it started. The one difference is that the pair leaves a copy of Q in memory just below the Stack Pointer and MVQA does not, which nothing here reads. Five recorded outputs moved and every one of them says the change worked: - 16x16Life fits five more generations into the same cycle budget, the first 457 lines identical, because the loop got cheaper. - Life.sbx is 1409 bytes rather than 1411, in three tests that list it. - Edit.sbx is 1995 rather than 1996. That last one broke a check I added this morning, and the hole is worth recording: the CosmOS README's claim about Edit's size did not have the word "Edit" on the same line as the number, because the subject was in the sentence before, so the check that measures quoted sizes skipped it silently. The sentence now names what it is talking about, which makes it both checkable and clearer, and the check fails on a wrong number there. Comments on either half of a replaced pair are carried onto the instruction that replaces them, so nothing anybody wrote was lost. |
||
|
|
8f4cc5878d |
Repair a table cut in half, and measure the numbers that had drifted
The README's emulator options table was split by forty lines of prose: two rows, then the whole discussion of the cost model, then five more rows with no header above them. Markdown renders that second half as something other than a table, so three of the seven options were not being shown as options. The rows are back together and the prose follows them. Four numbers had gone stale, in three different ways, and none was noticed: - "Five more scripts run alongside it" - there are six, and lint.sh had no bullet saying what it was for. - "rebuild all three tools" - there are four. - Files.asm quoted at 645 bytes in two places; it is 665. - Edit quoted at 1,983; it is 1,996. The last two are the most quotable sentences in the CosmOS README and the least likely to be rechecked by hand: the programs kept being made better and the sentences about how small they are stayed where they were. So docs.sh measures all four now. It counts the scripts in Tests/ that are not the driver or the disk builder and checks the README says that many and explains each one; it counts what the makefile's all target builds and checks the tool count in both phrasings, which took two attempts because one sentence says "the four tools" and the other "all four tools"; and it assembles every app the CosmOS README quotes a size for and compares. Each check was confirmed by making the fact wrong and watching it fail. WAIT also added a second kind of cycle this morning and the cost model section still described only one. It now says what an idle cycle is, why the two are counted apart, and what that distinction is FOR - a machine that slept through a slow disk and one that spun on it take the same elapsed time and print the same characters, and only the split tells them apart. The duplicated sentence about pipelining is gone; it was said twice, eleven lines apart, in nearly the same words. |
||
|
|
2b079324ae |
Add SplitLint, and let it be told when something is deliberate
SplitLint reports valid assembly that has a shorter direct expression: zero loads that could be RSTA or RSTB, Q moved through the stack where MVQA would do, self-cancelling push and pop pairs, assignments overwritten before use, unreachable fallthrough, one-byte pointer moves that could be INCD or DECD, a branch to the label directly below it, a SETD reloading an address the pointer already holds, and branches whose carry is known. Its model is deliberately local and conservative: every label and every directive forgets all known state, so a claim only ever lives inside a straight-line region. It knows the calling convention - CALL forgets DP3 and keeps the rest, RCAL and SWI forget everything - and it shares assembly.o with the assembler, so an added opcode cannot leave it holding a private copy of the instruction table. 260 warnings across the corpus, of which three were wrong in the way that matters: branchTest.asm and interruptFlagTest.asm exist to check that a branch whose carry is known behaves correctly, so a diagnostic saying the outcome is known is exactly right and exactly unwanted. A line whose comment says "splitlint: <reason>" is now not reported on. THE REASON IS REQUIRED and a bare marker is refused, because a suppression nobody explained outlives whatever made it necessary. Suppressed warnings are not counted, so --fatal-warnings does not fail on one, and the number of them is printed at the end so the claim is visible rather than silent. Tests/lint.sh checked a TOTAL: twenty three warnings expected, twenty three found. That number stays right while the thing behind it goes wrong - a rule that stopped firing while another fired twice would pass, and so would a rule reporting at the wrong line. It now checks which warning came out and at which line, that nothing else came out, and that the four lines meant to stay quiet did. Confirmed by breaking one rule's message and watching it name that rule: the old assertion passed the same sabotage, because the warning still fired and the count never moved. Written with the user while I was away; my part is the suppression mechanism, the harness rewrite, and the three marks in the test programs. |
||
|
|
c216c83e12 |
The filesystem waits for the disk instead of asking it repeatedly
sbfsWaitDisk spun on the status port. Read ahead had already hidden about
three quarters of the latency, and what remained was still 11.5% of Type
over a 14K file on a ten thousand cycle disk - all of it memory traffic
spent finding out that nothing had happened yet.
It tests the port first and only waits if the disk is still busy, and that
order is the whole of what makes it safe: the disk raises its line when it
finishes, so a disk that finished in the gap between the test and the WAIT
has its line standing already and the WAIT does nothing rather than
sleeping through the answer. No handler and no vector - the shell keeps the
Interrupt Flag down, and a WAIT wakes on a line whether or not anybody
means to answer it.
Printing a 14K file, by where the cycles go:
cycles a block total bus waiting
0 922,570 922,570 0
2,000 946,474 922,702 23,772
10,000 1,042,474 922,702 119,772
The bus column stops moving. What the program costs in memory is now the
same whatever the disk does. On this emulator nothing observable changes;
on hardware it is a CPU standing out of the way of the memory controller
rather than competing with it for every one of those 119,772 cycles.
The first version cost 660 cycles more at latency zero because it read the
status port again on the way out. AND writes to Q and leaves A alone, so
the status was already there - which is what the original said in its own
comment, and what I stopped believing while rewriting around it.
|
||
|
|
c3188ed657 |
Seventy becomes seventy one: a machine that can wait
HALT is terminal - stepCPU returns at once when the Halt Flag is up, so a halted machine does not execute, service devices, or take an interrupt - and that has to stay true, because every test ends with a halt and "halted" is how a program says it has finished. The consequence was that SplitBit had no way to wait at all. Every wait was a spin, and a spin is bus traffic: 11.5% of Type over a 14K file on a disk of ten thousand cycles, after read-ahead had already hidden three quarters of the latency. WAIT is 0xFE, one byte, no operands, sitting under HALT where the instruction that almost stops the machine belongs. Three decisions in it: - A line already standing means there is nothing to wait for, so WAIT does nothing. That is what makes test-then-wait race-free. - Any line ends the wait, masked or not, so a program can sleep on a device it has no handler for and read its status afterwards. Masking says who answers a request, not whether it happened. - A line that wakes the CPU without being dispatched is taken down by the WAIT. Left standing it would be found by the next WAIT, which would return at once - the program would spin exactly as before while looking as though it slept. Waiting is NOT a Status bit, and that is the trap avoided rather than a gap: Status rides into the interrupt frame and comes back out, so a machine interrupted mid-wait would return from its handler still waiting, and wait again for what it had already been given. An internal field instead. Idle cycles are counted apart from bus cycles and the halt line says so when there are any, which is what makes the difference observable at all - with the line-clearing removed the total moves by ONE cycle, 20,100 against 20,099, and only the idle half changes, halving to 9,976. A test on totals could never have seen it. Tests/terminal.sh asks that question, being the file for things a recorded output cannot see, and fails with the clear removed while "both reads finished" still passes. Three collisions, all found by building it: - 0xFE was the assembler's "not an instruction" sentinel. getOpcode now answers a negative NOT_AN_OPCODE, which is outside the range of every possible answer instead of inside the unused part of it. - 0xFE was also what faultTest and faultResumeTest executed to provoke a fault. They now use 0xFD and say why, because they did not fail when it became an instruction - they HUNG, having started sleeping instead. - Keys.asm has had a label called "wait" for a year, and mnemonics are matched uppercased. What that reported was "Branch without label" at the BRQ thirty lines away. The assembler now refuses a label that is already an instruction, at the label, by name; every instruction added takes a word out of the space of label names, so this will happen again. |
||
|
|
6b41354f8f |
Delete a comment describing a design that was removed, and check the rest
Two comments in the native assembler survived the changes that made them false, and both are the kind that misleads rather than merely ages. Asm.asm still explained an eighteen kilobyte buffer that the whole output had to fit in "because a file is written in one call and there is nowhere to put half of one" - which stopped being true when the assembler learned to stream, and the variable it described, ImgRoom, does not exist any more. It was sitting in front of the comment that replaced it, so the paragraph a reader met first described the design that had been taken out. Replaced by what is actually there: no limit but the disk, one block at a time through a window in scratch. scratch.asm said the system keeps below 0x1000 twelve lines above the paragraph explaining that the system's half of Data Memory was doubled to 0x1FFF. A stale number next to its own correction is worse than a stale number alone, because which one a reader believes is a coin toss. docs.sh now checks both of the facts scratch.asm asserts about the machine around it: the floor it claims the system keeps below, against the CosmOS README, and the address its buffers start at, against where the assembler's own data actually ends. Neither is enforced by a line of code anywhere - the map is a comment, deliberately, because reserving the buffers would put 22K of zeroes in the file and the assembler could not load itself - so a check is the only thing that can hold them. Both fail when broken; the second reports the exact overlap. |