2e9cb9af7e2bff55dc4e2f43fb725294e501f8ca
64
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b1dde7908c |
Lunar Porter flies on a controller
A held thruster burns every tick it is held for, which is the whole reason the pad exists: the console can only say a key went down, so a thruster driven by it could be pumped and never leaned on. The burn happens on the same tick gravity does, and for the same reason - a sixteenth of a pixel is the smallest step this arithmetic takes, and applied sixty times a second it is an enormous acceleration. On the tick, thrust and gravity are two numbers whose RATIO is the whole feel of the thing. Position still moves every frame; only the acceleration is stepped, and nothing can see that. Two against gravity's one, so climbing and falling are the same speed. Three was the first try and it left the moon after about a second of holding. If there is a pad the console's arrows are ignored, because under a window the same keypress reaches both - the pad as a level, the console as a byte - and a thruster that fired twice for one press would be a mystery to anybody tuning it. q still quits, since a pad has no letter for it. With no pad the arrows still burn once a press, which is the most that can be done down a wire. And break.sh now rebuilds the disk images as well as the binaries. Half the things worth breaking here are SplitBit assembly rather than C, and those live on the fixture disks - so an edit to a .asm file changed nothing the suite could see, and the tool reported that nothing caught the break. That is the exact lie it was written to prevent, turning up in a new place. With the disks rebuilt it catches this one: the lander falls between the two captures instead of climbing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
88ecb208f4 |
The coarse scroll register was being sent the wrong register
CALL eighth OUTA 0x36 RET puts A back the way it found it, so the column origin was written the high byte of the position that had been passed in, and the answer the subroutine had worked out went nowhere. The fine register was computed inline with OUTQ and was correct, which is exactly what it looked like from the outside: smooth scrolling within a cell that never advanced one. Q is the only register that crosses a RET. Every other answer in this program already came back in it; this one had been written as if A would do, and A very nearly does, which is what makes it worth a comment rather than a fix. Gravity was Jupiter's. A sixteenth of a pixel per frame per frame is the smallest step this arithmetic can take and it crossed the screen in a second, so it is applied one frame in six instead - which divides the pull by six and costs a byte and a compare. The alternative was a finer unit for velocity than for position, and that means a shift every time one is added to the other, twice a frame, for ever. And the check that catches all this now looks 1.5 million cycles in rather than twelve. The first number came from assuming a program that saves a whole screen takes a long time to start; it does not, and by twelve million the lander had flown seven hundred frames and left the picture. A capture near the beginning is worth more than a tuned one - there is less between it and the start that can move. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
d6c81fa32c |
Lunar Porter, rung one: it flies
A lander over a moon that wraps. Landing, crashing, fuel, cargo and bases are not here - this rung exists to answer whether it FEELS right, because everything after it is bookkeeping and none of it is worth building on a lander that is no fun to fly. The moon comes for free. The map's column origin is a ring in hardware, so 128 cells is 1024 pixels of surface with no edge and no seam to cross. Position and velocity are sixteen bit in SIXTEENTHS OF A PIXEL, and the unit is the design: gravity is a small number added to a velocity and a velocity is a number added to a position, with no multiply or divide anywhere. 1024 pixels is 16,384 sixteenths, which is 2^14 - so going all the way round is an AND with 0x3FFF rather than a comparison, and it is never wrong at the seam. The lander never moves sideways. The world scrolls under it and it sits at the middle of the screen, which is a byte a frame instead of two and is also what makes the wrap invisible: there is no moment where it jumps. One key is one burn. The console says which key went down and there is no such thing as a key coming up, so a thruster cannot be held - a press adds to the velocity once. That is a property of the machine rather than a choice this program made, and it reads as pumping the engine. Four bugs found by running it, all worth keeping written down: B CANNOT BE A LOOP COUNTER here. Every comparison is an INIB, so the count was overwritten by whichever bound was last tested and the loop reset itself for ever. Counters that outlive arithmetic live in memory. A subroutine answers in Q, and the AND after it read A. The moon came out flat because it was testing the height against 1 instead of the random number. Row minus height, not height minus row: they are equal at the surface, equal does not borrow, and the surface row has to be ground. And the shift register has A as its HIGH half. Written the other way, the view scrolled by 256 cells for every one it should have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
fba1b553d2 |
Depth: a ball behind the near pillars and in front of the far ones
The demo for what V5 added. Four pillars at four distances, each ONE 8 by 8 tile stretched to its own width and height, and a ball walking past all of them at a distance between two. What it shows is the thing an ordering cannot. The ball is sprite NOUGHT and every pillar is numbered after it, so table order puts the ball in front of all four - and it is still hidden behind two of them, because the depth buffer is asked per column. Caught mid-straddle in the checks: the ball is 48 wide and the pillar 32, so it shows on both sides and nowhere across the middle. Writing it found the conceptual trap in the feature, which is now written down where somebody will hit it. The pillars first carried their own distance in their entries AND wrote that same distance into their columns, so each was asked whether it was in front of itself - and 20 is not nearer than 20, so all four vanished. THE BUFFER IS WHAT HAS BEEN DRAWN AND A SPRITE'S DEPTH IS A QUESTION ASKED OF IT. Scenery writes it; it does not ask. Also found that a scheme only gives a colour to index one. The default palette sets each scheme's paper and ink and nothing between them, so art drawn in index two comes out black until a program writes a palette. And the fixture disk's root directory was full: four blocks, 32 entries, all taken, so adding an app failed the whole disk build. Loudly, which is the right way round - but it is a wall that moves for free, so it is eight blocks and 64 entries now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a916103a7f |
Sprites: things that move without the screen moving
Everything drawn on this machine was in a cell. Something between two cells meant rewriting both; something moving a pixel at a time meant rewriting them sixty times a second, which is affordable for one thing and not for twenty. A sprite is put at a pixel and the device draws it over whatever is behind, so moving it costs two bytes. MADE OF TILES, which is the decision the rest follows from: m by n taken in reading order from one index, so there is no second pixel format, no second kind of memory, and nothing a sprite can show that the map cannot. A 16 by 16 character is four tiles and the background can name the same four. 256 entries of 8 bytes at 0xC000 in the atlas - eight so the entry address is a shift, the same no-multiply argument as the palette's four. Position is signed and sixteen bits, because 640 by 400 does not fit in a byte and a sprite has to be able to sit half off the left rather than appearing whole at the edge. A PIXEL OF ZERO IS NOT DRAWN, or every sprite is a rectangle. Tested before the attribute is added, so a hole belongs to the art and not to the colour scheme. The same rule the other way round is what "behind" means: drawn only where the background pixel was zero, so a thing walks behind a pillar and in front of the floor in one frame. All of them draw, every frame, so they cannot flicker. Real machines dropped them per scanline because they had a fixed number of shift registers; this has a loop. The limit is the size of the table, which is a constant rather than a property of what is on screen. And the system takes them down at exit. The sprite table sits in the gap the screen save walks around - to the end of the map, then the palette - and that is right, because nothing the shell draws is a sprite: there is nothing to give back, only something to take away. Otherwise a program that put a ball up and left would leave it over the prompt, in front of everything, with nothing able to type it away. Sprite.asm deliberately leaves its own, because a program that faulted could not have cleared it. Every check here was re-broken and failed: transparency, reading order, draw order, priority, and size. Size needed breaking twice - the first attempt did not compile, and a silent build failure had left the old binary passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
eee95ef0ce |
Flip says the true thing, and the checks that let it lie
Two bugs, both in what the demo claimed rather than in the device. The assembler has no string escapes, so the "\n" written in a literal printed as a backslash and an n. A newline is a byte; Say.asm has always written one as 0x0A 0x00 and this now writes it out of the console port. And the line whose whole job was to still be there afterwards was wiped out on the way back, because the program called osTakeScreen - which restores the screen AS IT WAS BEFORE, so the tidy-up erased the one thing the demo was pointing at. It did not need saving: nothing it touches is the shell's. A program that damages nothing should not ask, and asking anyway costs it the screen it was standing on. Which turned out to be untrue as written, and that is the third thing. Flip drew with a tile of its own, and the system copies the font back over every tile at exit - so the filled screen went blank the moment the program left, and the check that the system put the display back could not tell a restored screen from an abandoned one. It passed with the restore deleted. So did the check that a program can show the other screen at all: a blank screen counts as one colour just as well as a filled one does. Now it fills with 0x0A, which is an asterisk in one of the reversed colour schemes: paper is the colour and ink is black, so a whole screen is drawn with NO TILE REDEFINED and it survives leaving. Both checks ask for the commonest colour in the picture rather than counting colours or naming a pixel - the font's only blank glyph is the space, whose attribute nibble is nought, so a filled screen is always a pattern and which pixel lands on paper depends on the character. Both were re-broken afterwards and both failed this time. Also cosmosFlip, a transcript test, which is what would have caught the printed backslash in the first place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
023362b05a |
A second screen, and one port to say which is shown
A screen drawn where it can be seen is seen half drawn. A program that moves forty things and rewrites the map underneath them is wrong for as long as it takes to put them all right, and at a megahertz that is long enough to look at. So the device brings a second screen bank, on port 0x3B, and port 0x3C says which of the two is displayed. Everything a program draws into the other one is invisible until one byte shows the whole of it at once. ONE REGISTER IS ENOUGH, where the hardware this imitates needed two. The other said which screen the CPU's window pointed at; there is no window here, because a program reaches a bank through the memory controller by its number. Writing to the screen that is not shown is a matter of naming its bank, and the device never has to be told. And a flip cannot tear: a frame is drawn from one bank in one go, so a flip either happened before that frame or happens before the next. There is nothing to race, where the real machines had to catch the few lines between frames to swap in. The console draws into whichever screen is displayed rather than one of its own, so a fault message lands where somebody can read it even if a game had flipped. And CosmOS puts the displayed screen back at exit, the way it already puts back the cursor and the ink: a program that faulted while flipped could not have, and a shell that only came out right for programs which remembered would come out wrong the day one crashed. Flip.asm is the worked example. It deliberately does NOT restore the display itself - that is the point of the paragraph above, and it is what makes the system's guarantee the thing under test rather than the program's good manners. Written the other way round first, where it passed with the guarantee deleted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
66e7b84272 |
The screen is two banks: an atlas and a screen
Tiles and colours are written when a program loads; the map is written whenever anything moves. Sharing one 64K bank made them compete for room neither needed all of, and had a worse consequence than being cramped: a bitmap covers the whole bank, so entering bitmap mode destroyed the font. A program could not draw a picture and then say anything about it. Split, each gets a whole bank. The atlas holds the tiles and the palette, the screen holds the map or a bitmap, and a picture now costs the map and nothing else. It also leaves 48K free in the atlas, which is where the sprite table and a second page of tiles are going. No new mechanism was needed. A bank is registered by naming the port that owns it, so a device with two banks needs two ports that own memory: the base port keeps the atlas, since tiles have been at 0x0000 since there was a screen at all, and 0x3A owns the screen. The registry now answers honestly about which ports in the block bring memory, where it used to say all sixteen did. CosmOS never addresses video memory except in one place - the screen save, which walks 196 pages of it. The page number already says which bank a page is in, so screenBankFor works it out rather than keeping a second list beside screenPageFor. Grid and picture.asm register both banks; colours.asm only touches the palette and needed none of it. Tests/video.sh names the memory every write is for, because an address cannot: tile 5 and bitmap pixel 5 are both 0x0005, and a helper that guessed would be right for the tiles and silently wrong for a picture. And picture.asm gained a check, because this change broke it and nothing noticed - registering the second bank leaves DestBank pointing at it, so the palette went into the wrong one and the picture came out black. It was the only thing here found by looking rather than by a test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
2abc8281df |
Loops, and the scripting language is a language
while and for. Both only mean anything in a script, because a loop goes back to the line that opened it and a prompt has no line to go back to - and both say so rather than doing something surprising. THE SCRIPT READER KEEPS THE POSITION OF EVERY LINE before reading it, which is what makes any of this possible: by the time a line has been read the reader is past it, and a line is not a fixed size to subtract. Three words per line, and the block is read again on the way back so the pointer into it means what it meant - the same thing nesting one script inside another already did, for a different reason. THE TWO LOOPS END DIFFERENTLY, and that is the design rather than an accident. A while is taken away at its end and its own line asks the question again, so nothing has to be remembered. A for is not: how many words it has used is kept in the block, and its line reads itself again and counts one more off the front. That is a byte in a block instead of a copy of the word list in every one of them. Blocks grew from a byte to a record of sixteen - state, kind, words used, and where the line that opened it was - and sixteen because A and B are a shift register, so four rotations turn a block number into its offset. The history and the variables are addressed the same way for the same reason. Nested loops, an if inside a loop, a loop inside a branch nobody takes, and a for with no words: the last two run no times rather than once, which is the case worth having a test for. Three things found by running it: textSame asks whether two WHOLE strings are the same, so "in red green blue" is not "in". The word has to be split off before it is compared. A for typed at a prompt complained about while, because both arrive at the same place. One message that names neither is better than one that names the wrong one. And docs.sh caught a naming convention nobody had written down: it recognises a packed name by its label ending in "Name", so ForName2 was silently not counted. It failed the right way round - saying the run was shorter than the count claims rather than passing - but the convention now lives where the names are and not only in the checker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
16f8232a35 |
Lines that are only run sometimes
if, else, end, and same. IF TAKES A COMMAND, which is one rule rather than two and is why comparing values needs no syntax of its own: "same" is an ordinary command that fails when its two words differ, so "if same $a $b" falls out of the rule instead of being an exception to it. Anything else that can fail is a question too - "if load Snake.sbx" is a perfectly good one. The shell already had the other half. LineFailed exists because a script stops at the first line that did not work, so every command was already saying whether it had, for a different reason entirely. A BLOCK HAS TWO KINDS OF NOT-RUNNING. One where an else would turn it on, and one where it would not - which is what an if pushes when something above it is already being skipped. That is what makes nesting need no looking down the stack: the top of it says everything. A branch nobody is taking is not even looked at. The skipping happens BEFORE the names are filled in, so a variable mentioned in a branch that is not running is not an error - a line nobody runs must not be able to fail. AND LINES MAY BE INDENTED, which they could not be before there was anything to indent inside. Nobody writes an if inside an if without indenting what is in them, and a leading space used to make the first word empty and match nothing. Found by writing the test script the way anybody would write one. CALL commandFailed became BRI commandFailed in nine places. It never returns - it marks the line and branches to the prompt - so calling it was a lie that cost a Stack frame each time, and fourteen other sites already branched. THE LINT RULE FOUND THIS, three days after I wrote the rule and on my own code: two false positives that were really the linter being right about a CALL that is not one. It does not fix the leak on its own, since a failure inside any called routine still abandons that frame, but it removes the cause of the commonest case and makes the code true. The mechanical edit then left a BRI prompt stranded behind one of them, and the linter caught that too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a8707f29f0 |
Names for things
"set apps /Apps", and then "$apps" anywhere on a later line stands for it. A name stops where a name stops - letters and digits - so it composes into a path without anything having to be quoted, which is the whole reason a script would want one. THE SUBSTITUTION HAPPENS ON EVERY LINE THE SHELL IS ABOUT TO RUN, typed or read out of a file, so the two behave the same and no command below has to know that variables exist. Same shape as the line editing: one place the whole system already flows through, rather than a decision made twenty times. A NAME NOTHING WAS SET TO DOES NOT RUN THE LINE. Every other shell expands it to nothing, and that is the wrong answer here: a mistyped name would quietly become an empty path, which is the class of silent wrong answer the rest of this system spends its effort refusing. It says so and the line counts as failed, which stops a script - and the test proves that by running one, where the line after it must not appear. Somebody who wants an empty value writes "set name" and gets one, so the escape hatch exists and has to be asked for. A NAME TOO LONG IS AN ERROR RATHER THAN A SHORTER NAME. Cutting it off at fifteen characters was the first version, and it is the same fault wearing a different coat: two names differing only after the fifteenth would be one variable, and the complaint about a missing one printed a word nobody typed. Eight slots of sixty four bytes - sixteen of name, forty eight of value - and sixty four rather than eighty because A and B are a sixteen bit shift register, so two rotations turn a slot number into its offset. The same trick the history uses, and the reason neither needs a multiply this machine has not got. TWO THINGS I GOT WRONG AND ONE I FOUND: doSetVar ended in RET. It is BRANCHED to from the dispatch, not called, so that RET went wherever the Stack happened to point - the same fault that formatted a disk last week, in a command written three days after the rule was named. The new lint rule does not catch this shape: it fires on falling INTO a subroutine, not on a branch target that ends like one. And a test of the expansion's answer, which is dead code: commandFailed does not return. It marks the line and branches to the prompt, the way every failure in this shell is reported, so the only way out of the expansion is the one where it worked. Which turned up a real leak, measured and not yet fixed: every failure that goes through commandFailed abandons the frames between the prompt and the call. SP goes from FFFD to FE6D over twenty of them, twenty bytes each. Its own commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
b04e4b7d1c |
Finish making the tune a program
The user's conversion, which I reverted while I was working out whether it was half done or broken. It was half done: an sbx application wants its #Include above #Program, because services.asm ends in a #Vectors block and a #Base written after that has no segment to be the base of. So the include moves up, the bases move a page with everything else, and the test starts it from the shell instead of booting it. It plays for 9,469,987 cycles, 9,423,527 of them waiting, which is the same 567 frames of music it played as a boot image. BEING A PROGRAM MEANS ITS VECTOR IS THE SYSTEM'S TO INSTALL. It brings the screen's, so that it has a beat to play to, and CosmOS puts it in when the tune starts and takes it back out when it stops - a thing a boot image never had to have right, and the second program here to exercise the version two format at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
3449405b18 |
Give the system another page of each memory
CosmOS had 1,161 bytes of Program Memory left before the address applications load at, and Tab completion is not going to fit in that with anything to spare. So the wall moves up one page: the system keeps below 0x4FFF and 0x2FFF, and an application is based at 0x5000 and 0x3000. A PAGE IS A CHEAP THING TO GIVE IT AND AN EXPENSIVE THING TO RUN OUT OF. An application still has 44K of Program Memory before the vector table and the largest one here uses 7.5K, so what was taken from applications is space nothing has ever asked for - while what the system gained is the difference between building the next thing and counting bytes while building it. Not doubling, which was the version that would have cost application space worth minding. One page, and the same again when it is needed. Nothing in the machine knows where the wall is, so this is 34 #Base lines, one threshold in the fault handler, and the table in the CosmOS README that Tests/docs.sh reads its limits out of. The native assembler's scratch map had to move with it, and docs.sh said so before anything ran: its data reached 0x40D6 and its buffers began at 0x4000, so they were sitting on its variables. That file already carries a paragraph about the floor coming up and the map staying where it was. It has happened twice now, and been caught by a check the first time wrote. Twenty three recordings are the same runs a page higher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
fd9c4c75f8 |
Tell the person where it hurts
A fault stopped the machine and printed a line to standard error. On a terminal that is a diagnosis. Behind a window it is a frozen picture and no reason at all, because the message went somewhere nobody was looking - the machine looked hung and was not. It had stopped, and said so invisibly. CosmOS catches all five faults now and says what happened on the screen, with the address, in red. A FAULT ENDS THE PROGRAM, NOT THE MACHINE. That is the answer to "carry on or start again", and it is not a compromise: a bare RETI from most of these meets the instruction that failed and fails again, so carrying on was never on offer. But the machine is almost never what is broken. Everything the shell puts back when a program exits - the Stack, its vectors, the drive, the working directory, the console, the screen - is exactly what wants putting back after one dies, so the handler sets a status and joins handleExit. You are back at the prompt, and the program is recorded as having STOPPED rather than finished, because saying "finished" under a red fault message would be the shell contradicting itself. A fault below where programs load is the system's own, and there is nothing to go back to. That one says so and stops. THE SCREEN GOES BACK TO A MODE TEXT CAN BE SEEN IN, and that is the part that matters rather than the part that is prettiest. A program that faulted in bitmap mode left the console with no text rows, so it draws nothing at all: the message would be perfectly correct and completely invisible, which is the one thing it must never be. Two palette entries go back for the same reason, since a program that wrote its own colours can leave every ink the same as every paper. Only the two the message needs, so the rest of what the program chose is left alone. Both halves are checked by looking at the PICTURE, because the serial line was never where the problem was. Crash blind ruins the palette and drops into bitmap mode before it faults; without the mode the screen comes back 320 by 200 with nothing on it, and without the palette it is the right size with the message present and unreadable. Each break loses the red on its own. Crash is also a program worth having: it breaks in whichever of the five ways you name, so a fault screen can be looked at without having written a bug first. Two things found on the way: The native assembler keeps its OWN copy of the reserved vector names, so it did not know NoHandler or NoDevice and built a cosmos.bin that differed from the host assembler's. Caught by native.sh, which is exactly the drift that test exists for. And cosmosMonitor had dead input. It assembles code into 0x8000 and runs it, and that code faults - which used to kill the machine, so everything after it in the file had never run. It runs now, and the recording grew by sixty lines of monitor session that had been unreachable since the day the fault was put there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |