ee77d79780c2965a4914f414e7a158f5f44d762e
30
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
caf5e1f99d |
A base speaks in the window, not into the world
Two bugs with one cause. The console draws into the map, so a message printed while flying was a message the lander then flew over - and printing scrolls, so every one of them moved the whole world up a row. The window is at a screen position and forty cells wide, and neither is true of it. So the window is two rows now: the gauge, and whatever there is to say. The letters are ordinary tiles, because the character generator starts at the space and glyph n is character n less thirty two. The rest of the row is blanked after every message, or a short one would leave the tail of a long one behind it. Opening the throttle wipes the line, because a message that outlived the moment would be read as describing this one. The crash still goes to the console, deliberately: it is the last thing the program says and it should survive the program. A or Start continues from a message as readily as a key does. Somebody flying on a controller should not have to reach for the keyboard to say they have read something. AND TWO TESTS WENT WITH IT, which is the interesting part. cosmosLanderSoft and cosmosLanderPadOne asserted on lines in a transcript, and the lines moved off the console - so both went on passing while checking nothing at all. A test that asserts a side effect rather than the thing itself is always one refactor from being decorative. What they were for is now checked in the picture, where the message actually is. The lander check moved earlier too. The window grew to two rows, so by 1.5 million cycles the lander had climbed behind the status bar - the window doing exactly what it should, and leaving the check counting six pixels of a forty pixel lander. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
bfb515e23b |
Cargo: bases with names, and a landing that is not an ending
Four bases, told apart by the scheme their pad is drawn in, so "the cyan one" is a thing a person can say and a thing the machine already knows. Yellow is missing on purpose: it is the lander, and a base the same colour as the thing landing on it would be a poor joke. Land empty at a base and it loads cargo for the base ACROSS THE MOON, two along - so the pairs are cyan with red and green with blue, and the wrapping surface is a route rather than scenery. Land carrying at the right one and it takes the cargo and pays eighty units of fuel. Land at the wrong one and nothing happens, which is why the destination will want to be on the screen. A LANDING NO LONGER ENDS THE RUN. The lander rests where it is, exactly on the surface with both speeds zeroed, until the throttle opens again - which is the only way to stop being landed. Gravity does not pull on something already sitting down, and a base does not hand out cargo sixty times a second to a lander parked on it. The pad array holds the base's number plus one rather than a flag. Nought still means no pad, so it is still one lookup, and a flag would have to be followed by "and which of the four" - the same walk done twice for an answer already in hand. Pads are eight columns rather than four. Four was 32 pixels in a moon 1024 round, which is a target somebody flying by feel misses over and over. WHAT IS NOT COVERED, and why: the delivery and wrong-base paths need a lander flown from one base to another, and hand-authoring a recorded pad input that hits an eight column pad across a 128 column moon is a piloting exercise rather than a correctness one. Several attempts got within two columns. Loading, crashing, landing off a pad and running dry are all covered; delivery is built and flown by hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
7257ad369c |
Landing pads, carved rather than looked for
A random walk does not leave flat ground and a lander wants some. Four pads are cut into the moon after it is generated, each four columns levelled to whatever height its first column happened to have - so they sit in the landscape rather than on a shelf above it. The moon decides where they are; this only decides that they are flat. Searching for flat spots was the alternative and it can fail, which means a fallback that carves anyway - the carving, plus a search nobody needed. They are marked by an ATTRIBUTE and not a tile of their own, which costs no art at all: a nibble is added to every index in a tile, so one solid block is grey moon or a cyan pad depending on the byte beside it. Which columns are pads is an array, because asking has to be one lookup. Four comparisons per column per row is 12,800 of them for one screen, and the landing verdict asks the same question again. THE LANDER STARTS ABOVE ONE, because that is where a porter's day begins. Starting in the middle of nowhere meant a straight descent landed in the middle of nowhere, which is a fine thing to be able to do and a poor thing to have to. That change cost the crash test its teeth, and the way it did is worth keeping. It held nothing at all and let the lander fall - and a short drop onto the high ground of the base you started above is survivable, which is correct, and left the test saying nothing. It holds Right now: lateral speed has no limit and nothing slows it, so a slide always ends badly however the rest is tuned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
2274be4b68 |
Lunar Porter, rung three and a half: fuel
Every thruster costs a unit every tick it fires, so holding two at once costs two - the honest price, and it makes a drift you corrected expensive in a way a drift you avoided is not. AN EMPTY TANK IS NOT AN ENDING. There is no message and nothing stops: a lander with no fuel is still flying, it just cannot do anything about where. What happens next is gravity, and gravity is patient. The test for it holds the thruster from the first frame to the last and crashes anyway, which is what says the fuel is real - a lander that could hold Up for ever would land every time, and the economy this is the first half of would have nothing to buy. The gauge is in the window, which is what the window was built for two commits ago: a bar at a SCREEN position, so the moon turning underneath does not carry it off. Thirty five cells after a label, redrawn whole every frame because seventy bytes out of one port is cheaper than working out which of them changed. A byte of fuel, and a byte is enough. Over eight it is a bar of up to thirty one cells - a shift, because there is no divide - and at a unit a thruster a tick it is about forty seconds of holding the engine open. Sixteen bits would be more arithmetic for a number nobody reads to the unit. Cargo and the bases are the other half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
e3eccd17a8 |
Settle the view before saying how the landing went
The console draws into the map and the map is what is being scrolled, so a message printed while the view was three pixels into a cell came out three pixels off the top, with as much of its first row missing as the cell above it had lost. The flying is over by then, so the fractional part of the view has no more work to do. Putting it back is what makes the whole message visible. This is not the general problem. A status bar that has to stay readable WHILE the map moves is a different thing entirely, and nothing here solves it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a074a831f0 |
The shell scrolls, and the moon was drawn where nobody was looking
Lunar Porter never put the row origin back. The map is a ring 128 rows tall that the screen shows 25 of, and the shell leaves that origin wherever its last command finished - so a moon drawn into rows nought to 24 while the screen is reading from row forty is a moon nobody can see. It came out as terrain missing, or half there, depending on how far down the prompt had got. Running Pad first was enough; so was holding Return. Nothing here is tidiness. It is the difference between the rows a program WRITES and the rows the screen READS, and only one of those is under the program's control. Grid has always known this; Lander did not. The check for it needed writing twice. Forty returns caught nothing, because the shell runs an eighty column screen which is FIFTY rows tall - forty returns fill it and never scroll it, so the origin was still nought and the test passed against a build with the fix taken out. The screenful that matters is the one the shell is using, not the one the program is about to ask for. At eighty it is 28,608 pixels of moon with the fix and none at all without it. break.sh is what said so. The first version of this check looked exactly like a passing test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
f7be843ed9 |
Lunar Porter takes any controller, not the first one
A controller does not always arrive on pad nought. The front end hands out the numbers the host gave it, so a game that reads only the first one works on the machine it was written on and silently does nothing on the next - which is the shape of "the pad is detected, Pad shows it, and the game ignores it". Four reads and three ORs. One person flies this and which socket they plugged into is not a thing they should have to know. Presence is any of the four bits rather than the low one, for the same reason. The manifest's pad column takes several fixtures now, comma separated, and they fill the pads in turn. So cosmosLanderPadOne holds nothing on pad nought and flies the whole landing on pad one - a test that fails on the version of this program that shipped an hour ago. Also confirmed while looking: raylib 6 does refresh which gamepads are ready every frame in PollInputEvents, so a hot-plugged pad should be seen. Whatever is stopping that is above us and worth a separate look. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
df50c2f0f8 |
The pad was working; the game was told there was not one
0x64 counted only the RECORDED pads. So a controller plugged into Voyager reported its buttons perfectly, and every game asking whether there was a controller was told no - which is exactly what Lunar Porter asked, once, at startup, before falling back to the console for the rest of the run. The cause is worth naming: a front end calls padSet every frame for every pad, so "held nothing" is the commonest thing it says and cannot also mean "there is no pad here". Connected is said separately now. Pad nought is always there behind a window, because the keyboard is behind it - which is the useful answer rather than the literal one. And Pad.asm, which is what should have existed before any of that guessing began. It prints a line whenever a pad changes, and tells apart the three states that look identical from inside a game that will not respond: one nobody noticed, one mapped to nothing, and a mapping that is wrong. WHY A PROGRAM AND NOT A PRINT IN THE FRONT END: because the question is what the MACHINE can see. A front end reporting what it thinks it is sending answers a different question, and the gap between those two is the whole of this bug. It also found that osPrintNumber takes A as the HIGH half - the same way round as the shift register and every other pair here, and not what a byte in A wants. Every value came out 256 times too big. Gravity is one frame in ten rather than six. The ratio between thrust and gravity is the feel; how often the tick comes round is how fast that feel arrives, and one in six was still touchy. Same lander, more time to think. And the verdict waits for a key. It printed and left immediately, taking the screen with it - so the one thing worth seeing, the lander sitting on the ground it had just reached, was gone before it could be looked at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
8eb4e4d67e |
Lunar Porter, rung two: it lands, or it does not
The terrain is an array in Data Memory rather than something read back out of the map, and that is the whole reason this is cheap: the ground under the lander is one index into 128 bytes, where asking the screen would be a transfer through the controller every frame. The column is the world position over eight, masked to the moon's 128. The surface is that column's row times eight - three turns left of the shift register, since a row is at most 24 and 192 fits in the low half. The feet are the lander's top plus its eight pixels. WHAT DECIDES IS THE SPEED AT THE MOMENT IT ARRIVES. Both of them, and both have to be gentle: three quarters of a pixel a frame downwards and half of one sideways. Sideways is the tighter on purpose, because a landing that was soft downwards and sliding is a lander on its side - which is the interesting half of the difficulty, and the half the drift bar was blind about until it existed. Two fixtures say it works, and they differ only in what was held: one holds nothing and falls the whole way, the other pulses the thruster six frames in sixteen and survives. Same terrain, same seed, same keys. Also: the gamepad did nothing, and the reason is that the four direction buttons are the D-PAD. A lot of controllers made this century have one nobody uses - the thumb goes on the stick, which reports as an axis rather than a button - so a pad that was plugged in and working correctly did nothing at all. The stick counts as held past halfway now. Untested here, because there is no controller in this environment and the suite runs headless; Voyager also says at startup which controllers it can see, so a pad that still does nothing can be told apart from one nothing noticed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
db0c26e13f |
A bar for the drift, and a lighter touch sideways
A moon has no air, so a sideways drift never stops by itself and stopping one means cancelling the velocity EXACTLY. That is not hard to do; it is hard to do blind, which is what it was - a number nothing on the screen said anything about. So sprite one is a bar whose width is the drift. It runs right from the middle of the screen for a rightward one and left for a leftward one, so which way is as plain as how fast, and stopped is the one state with nothing drawn at all. The whole of it is a target width written once a frame; the device stretches one tile into it and the program draws nothing. Sideways thrust is one a tick rather than two. At two, the smallest correction available was twice the size it needed to be and overshooting was the normal outcome. WHICH ZERO MEANS NOTHING TURNED OUT TO MATTER. A target width of nought is the NATURAL width, not an empty sprite - so a bar with no drift in it came out eight pixels wide, sitting at the middle of the screen, saying "stopped" in the same shape it says "drifting slightly". What draws nothing is a SIZE of nought, which is the other zero in the other byte. Both meanings are deliberate and documented and it still caught me out inside a week of writing them down. The check for it earned its place by failing on that before it was found, which is the best evidence a check can offer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
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 |
||
|
|
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 |
||
|
|
81e544eb3d |
Load a program that has no data
A five instruction program that writes one port and exits has no Data Segment at all, and the loader stopped the machine dead on it. It asked the memory controller to move a segment of no bytes, and a length of zero asks for the whole 64K - which is the machine's rule, and a reasonable one, since two bytes cannot say 65536 and a transfer of nothing is not usually what anybody meant. It is exactly what was meant here. 64K did not fit, the controller refused, and the load stopped half done. ON A TERMINAL THAT PRINTS A FAULT WITH AN ADDRESS. Behind a window it is a frozen picture and no reason at all, which is how it was found and is a separate problem from this one. The header says how long each segment is, so the loader knows before it asks. Both bytes are already in hand, so the test costs one OR. Nothing is lost by skipping the transfer: a blit leaves the controller's addresses past whatever it touched, and a blit of nothing would have left them where they already are, which is where the vectors are read from next. Guarded for the code segment too. A program with no code is equally assemblable and would have stopped in exactly the same place. Mode.sbx is the fix's test and a program worth having on its own: forty columns or eighty, whichever the screen is not in, which is what a person wanting Snake drawn twice the size actually needs. Ten instructions and no data, deliberately - it prints its two digits a register at a time rather than from a string, so it stays the smallest shape a loadable program can take. Nothing else on that disk had ever been that shape, which is why nothing had ever tried it. Reported by the user, who wrote the program. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
736037462e |
The shell edits the line it is given
Three different things used to do this job, and which one you got depended on where the machine was running. On a terminal the host held the line and did the echoing and the backspacing; behind a window the console's own gatherer did it; from a file nothing did it at all. One job, three implementations, none of them in the system - which is why there was no way to move about in a line and nowhere for a history to live. So editLine does it. Key mode while a line is being read and line mode straight after, so nothing else in the system and no program calling osReadLine notices anything changed. Left and Right, Home and End, Backspace for the character before the cursor and Delete for the one under it, and anything typed goes in where the cursor is with the rest of the line moving along. Ctrl-D means the end of input again, on an empty line, because that was a thing the terminal did while it was holding the line and it is not holding it now. Same trade as the echoing. MOST KEYSTROKES DRAW NOTHING BUT THEMSELVES. A character typed at the end of a line needs no cursor moved: printing it is the whole change, and a backspace there is three ordinary bytes. That matters beyond speed - moving the cursor by hand is what a terminal is TOLD about, in an escape sequence, so redrawing on every keypress would fill every recorded transcript in this suite with them. The line is only reprinted when something happened in the middle of it. Where the line STARTS is worked out backwards from where printing ended, rather than trusted from what was remembered. That is what makes it survive the screen scrolling: a line printed on the bottom row moves everything up by one, and a remembered row would be one too low from then on. The command line holds 127 characters, up from 63. The limit started to be felt the moment a line could be moved about in. 58 recordings changed, and every one of them by the echo. THE PROOF IS NOT A HEURISTIC: a CosmOS built with the echo silenced reproduces 187 of the 188 recordings byte for byte. The one exception is cosmosTyped, the backspace test, where the rub-out marks now come from the shell instead of from the console's gatherer - same marks, different author. cosmosEditKeys is the new test, and every line in it is typed wrong and then corrected with a different key. Its last line is eighty six characters at a prompt in column two on an eighty column screen, so the line runs onto the row below and the shell has to find the start of something it can no longer see; breaking either half of that arithmetic fails it. Also: agree.sh looked for "> the same", anchored to a prompt that no longer precedes what a command prints. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
b3726c950a |
Deliver the keys that are not characters
An arrow key has never reached this machine. Voyager threw it away for want of anywhere to put it, and a terminal sent ESC [ A, which arrived in the middle of whatever was being read and made it unrecognisable - typing Up at the CosmOS prompt put three bytes in the command line and got "I do not know". So the console names them: one byte each, 0x80 upward, above ASCII so nothing written before them can collide. Up, Down, Left, Right, Home, End and forward Delete, with room above for the paging and function keys. The console normalises, which is what it already does. Behind a window it turns the key somebody pressed into a byte; on a terminal it turns the sequence into the same byte. That is the act it has always performed on Return and Backspace, one layer further along, and it is why a program need not know which of the two it is talking to. What a key MEANS is not the console's business - that belongs to whoever is reading, the same way what is on a disk belongs to the system and what a drive is belongs to the machine. Translated only when standard input really is a terminal. Nothing else sends these sequences, a pipe holds exactly the bytes somebody put in it, and it keeps the Escape-or-Up timing problem out of every test here: a test writes the key values themselves. Line mode drops them, in both front ends, because line mode delivers characters and a line somebody else has finished editing cannot be moved about in. Press.sbx says what it was handed, in hexadecimal and by name, and reads a line before it reads keys so both halves of that rule are checked. Two recordings, one fed as standard input and one as a keyboard, agreeing byte for byte; each break fails exactly one of them. Three checks in terminal.sh type real escape sequences at a pseudo-terminal, which is the only place they are ever read as sequences: that they arrive as keys, that Escape alone is still Escape, and that a character typed straight after an escape is held rather than swallowed. Five recordings re-blessed for Press.sbx appearing on the shared disk, and the whole of that diff is the file's own line and the counts above it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
2a29cebc6b |
Make the screen come back on the machine people actually run
Found by running it: Grid exits and the shell prints its prompt into the grid, with the view up to seven pixels out of alignment. Three faults, and the first is the one that made the other two visible. MAKE RUN-VOYAGER HAD NO SCRATCH DRIVE. It gives drive 1 to Disks/personal.img, which is a file and not volatile, so there was nowhere to keep a screen - osTakeScreen answered no and the whole feature silently did not happen. It was tested with --ram-disk and shipped without one, which is as good a description of testing the wrong machine as I can write. There is now a RAM disk in drive 2, after the personal disk so that drive 1 stays the one that is yours. A PROGRAM TOLD NO MUST COPE. A refusal is not a fault, it means doing what the program did before there was anywhere to save a screen. Grid deleted its own tidying up when it started asking, so being refused left the grid on screen with a prompt written into it. It clears up again when refused, and only then. AND THE SYSTEM ALWAYS LEAVES THE SCREEN USABLE. The fine scroll registers go back to zero at every program exit, whether or not the picture could be saved: the console draws in whole cells, so a view three pixels into one puts every character three pixels out for ever. That is not part of saving a screen and should never have depended on it. Both paths are checked now. With a scratch drive the screen comes back cell for cell; without one, no grid is left behind. Breaking either fails one of them and not the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
ab72443b99 |
Give the screen back: osTakeScreen, and the RAM disk earns its keep
A program that takes the whole screen leaves the shell a blank one, and
whatever was on it is gone. There was nowhere to put 48K of video memory on
a machine with 64K of Data Memory that CosmOS already lives in.
A DRIVE MADE OF MEMORY IS SOMEWHERE. The screen goes to a file on the
scratch drive - the first volatile drive found at boot - like any other
file, and comes back from handleExit alongside the vectors and console mode
already put back there. The filesystem does the allocating, so this
invented nothing: it is 196 pages of tiles, map and palette, with a block
on the front holding the cursor, the four scroll registers and the mode.
NOT AUTOMATIC, and that is the whole design. Saving on every program start
would be cheap enough; restoring on every exit would be wrong, because dir
and Files and Say print and stop and their output is the reason you ran
them. A program says it took the screen, and one that says nothing behaves
exactly as every program did before this existed.
It deleted thirty lines of Grid, and they were all wrong anyway: four
scroll registers put back by hand, the map filled with spaces, the cursor
sent home, palette bank 0 written out - and the other fifteen banks kept
Grid's colours, because there was nowhere to have kept the real ones. Grid
is 64 bytes smaller and gives back what was actually there.
The check compares the screen before against the screen after, CELL BY
CELL, and allows only the rows around the cursor to differ - found from
where the text ends rather than guessed at, because the first version
assumed the cursor was near the bottom of the screen and let three real
differences through.
Two things cost time and neither was the feature:
- An edit adding "SWI osTakeScreen" to Grid was in the same script as a
failing s.index, so the file was never written - and the COMMENT
describing the call did land, from a later edit. Grid documented a call
it did not make, and read as though it should have worked.
- docs.sh caught osTakeScreen having no row in the services table, which
is the check the service layer added for exactly this and the second
time it has earned itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
e4f4bae762 |
Work across two disks: copy between them, and run a program from one on
files from the other Two things anybody expects of a second disk, and each needed something different. COPYING NEEDED TWO THINGS TO REMEMBER A DRIVE. The write stream is the only thing here that lives across service calls, so it is the only thing whose drive can change underneath it: every osFileBlock names its source path again and goes back to the source drive, and then osFileWrite has to come home. It records the drive it was opened on and returns there. And the file lookup CACHE. It keeps the last path resolved so a reader walking a file does not re-walk the directory for every block - and skipping the walk skipped the drive the path named, so block one of a cross-drive copy read the source's block numbers off the DESTINATION disk. It only showed on files of more than one block, because a file of one is never looked up twice. One block worked and two did not, which is a suspicious enough shape to have suspected sooner. RUNNING A PROGRAM FROM ELSEWHERE NEEDED A THIRD PLACE TO LOOK, and two restorations. The shell tried where you are and /Apps on the disk you are on. It now tries /Apps on drive 0 as well, which is what makes the system's programs work from a disk of your own - one with your files on it and no system, which is most of the point of having a second disk. The drive goes back after the load, because by then the program is in memory and the blocks it came from mean nothing; and again when it exits, because a program that copies between disks moves the drive as its own paths need to and being left wherever it finished is not what was asked for. Copy 1:/a 0:/b now leaves you exactly where you were. The fixture disk grew an /Apps, because it kept its programs at the root and so could not exercise the third place at all. Two hours of the debugging above were spent on a stale disk image. The machine boots the system that is ON the image, so a rebuilt cosmos.bin means nothing until the image is rebuilt too - and the trace said my new code never ran, which was true. Third time this project has been misled by one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
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 |