Compare commits

..
121 Commits
Author SHA1 Message Date
AnachronautandClaude Opus 5 09c72e4951 srcNext asks its own questions instead of calling for the answers
Two calls per character of every pass: numCompare to ask whether the
buffer is used up, and numStep to move one further into it. Inlined, the
compare settles almost every time on the high bytes and settles them
FIRST - a full block is 256, so SrcCount's high byte is one and SrcAt's
is nought until the last sixteenth of the block. Six instructions where
the call was twenty.

And the newline test uses A, which still holds the character, where it
used to fetch it back out of the SrcChar it had just been put in.

              before        after
  hello      586,184      574,737
  Say      4,506,702    4,333,689
  Files    7,188,351    6,863,744
  Keys    15,069,880   14,399,801
  cosmos 789,982,899  772,826,841

Two to four and a half per cent, which is worth having and is much less
than expected - AND THAT IS THE INTERESTING PART. It says the per
character cost is not in the reading. Now that comments no longer come
through here at all, what is left is the tokenizer above it, and the
remaining time is there rather than in getting the bytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 21:43:29 -04:00
AnachronautandClaude Opus 5 2e9cb9af7e Comments are thrown away by the reader, not read one byte at a time
A comment used to walk through tokGet and srcNext and be classified,
character by character, on the way to being discarded. Comments are most
of what this assembler reads: 87 per cent of what Say.asm pulls in once
services.asm is counted, 59 per cent of CosmOS. The house style here is
dense commentary, so the assembler is penalised more than most by its own
sources.

The saving is not the classifying, it is the BOOKKEEPING. srcNext loads
and stores the walking pointer through memory for every character and
asks numCompare whether the buffer is used up. srcSkipComment keeps the
pointer in a data pointer for a whole run and the newline in B, so a
comment byte costs a load, a compare and two steps. A run is capped at
255 so one byte can count it, which is the only reason it loops.

Measured, each version with its own rebuilt images:

              without        with
  hello       791,957     586,184
  Say       9,924,401   4,506,702   2.20x
  Files    13,197,710   7,188,351   1.84x
  Keys     23,091,447  15,069,880   1.53x
  cosmos  886,498,996 789,982,899   1.12x

Which tracks the comment ratios: Say gains most and cosmos least, in
proportion to how much of each is prose.

---- And two mistakes worth keeping ----

The scratch went among the READER'S STATE, which is a block copied whole
by a count written down somewhere else - so every saved file lost the
last four bytes of itself and an include came back with its pointer
wrong. The comment above that block says not to do this, in capitals.
That is twice this week: scriptCopyState had the same shape this morning.

And a file that ends inside a comment has to put back the file that
included it, exactly as srcAtEnd does for a character. NOTHING IN THIS
REPOSITORY ENDS THAT WAY - every source here ends on a line of code with
a newline after it - so break.sh could not catch an error in that path
because nothing reached it. tail.asm is generated with no newline on its
last line for that reason, and usestail.asm names a label after the
include, which is what goes missing when the include never returns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 21:33:15 -04:00
AnachronautandClaude Opus 5 e20c9bac1f The label table is kept in order, and halved instead of walked
labFind walked the index from the front, so every use of every label
cost a scan of every label defined so far, each with a string compare.
The cost grew with the program being built, which is what made it hurt:
assembling CosmOS on the machine took 1,833,691,267 cycles against the
assembler assembling itself at 57,257,133 - three times the source for
thirty two times the time.

Sorted and halved, the same build is 886,498,996. THE WALK WAS 52 PER
CENT OF THE WHOLE ASSEMBLY, which settles a suspicion this project has
carried unverified for weeks and puts a number on it.

Eleven comparisons against two thousand entries where a walk averaged six
hundred and seventy. The search hands back where a name WOULD go, which
is what adding one needs and what a walk could never have offered, so
labAdd gets its insertion point for nothing.

sameText was already an ordering and did not have to change: Q is the
difference at the first character that differed, and the Carry Flag from
that same subtraction survives the return because nothing puts the Status
register back. A name that runs out while the other carries on borrows
against the other's character, which sorts the shorter first.

Small programs pay about a tenth more - 57.3M to 63.5M for the assembler
on itself - because adding a label now moves the tail of the index up and
a short table was never expensive to walk. That is the right way round
for a trade to fall.

numHalve and numBack are new: a rotate right on a CIRCULAR sixteen bit
register brings bit nought back in at the top, so halving means taking
that bit off again, and the low half has to go down first because the
mask wants B.

WHICH END THE TABLE IS SORTED FROM DOES NOT MATTER. labAdd takes its
insertion point from labFind, so the comparison that decides the order is
the same one that searches it - turn it round and the table is built
backwards and read backwards and no output changes. Tests/break.sh says
so, correctly, by not noticing.

Verified by CosmOS builds CosmOS and second generation staying byte
identical. An indexing bug cannot hide behind a fixed point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 21:02:05 -04:00
AnachronautandClaude Opus 5 563bc20a75 Tab lists its matches in columns, and says what it can in colour
Tab's listing was six instructions: print the name, print two spaces. It
was the crown of this shell for a while and looks plain next to ls.

Columns cost NOTHING EXTRA. Tab already walks its candidates twice - once
to find the answer, once to show the matches, and tabRunSources exists
because those are the same walk asked two questions. The longest match is
counted on the first, which already visits every one of them, so the
listing needs no walk of its own to know how wide a column should be.
Padding goes before a name rather than after, so a row ends on a name.

Directories are blue and the shell's own commands are green. Both are
free: Tab appends the separator itself, and a built-in is not a file at
all - which is also the only way anybody could know it will run.

A FILE IS NOT COLOURED, and that is a decision. Whether a file will run
is a read of its first block, which is what ls does and what makes ls
cost twice what it otherwise would. Here it would be worse than slow:
candidates are offered from inside a directory walk, and looking a file
up would overwrite the very fields holding the walk's own position. Doing
it safely means holding every candidate name in memory, which is a buffer
the shell would carry whether anybody pressed Tab or not - and ls is one
keystroke away. A Tab press already costs about 200,000 cycles, so the
read was not the objection.

---- And the machine could no longer build itself ----

Found by make test, not by reading. The native assembler ran out of room
for label names on cosmos.asm: 16,758 bytes against 16,384. The index was
1,341 of 1,536 in the same breath.

Which is scratch.asm's own warning happening a second time - "two
ceilings a hundred bytes apart look like one ceiling until the first is
lifted" - so both were raised, out of the seventeen kilobyte page that
file deliberately left unclaimed against exactly this. Names to 26,624
and the index to 2,048, both left about a third clear, with 1,792 bytes
still unclaimed for the same reason.

A name is thirteen bytes on average and an index entry is four, so the
arena will always be the one that speaks first. That is now written down
where the two numbers are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 20:18:56 -04:00
AnachronautandClaude Opus 5 eaeb473176 ls says in green what the shell will start
Directories were already blue. Something the shell will run is green now,
and it is asked by LOOKING rather than looked up.

This was priced once as needing a runnable bit in the directory entry,
set by the filesystem from a list of magic numbers it would have to be
taught - declined twice, for putting format knowledge in the filesystem
and giving two implementations a registry to keep in step.

It costs nothing of the sort any more. The shell decides what to run by
reading a file's first block, so "will this run" is a question with an
answer already, and this asks the same one the shell would: SBEX for a
program, "#!" for a script. Nothing is written down and nothing has to
agree about anything.

The price is a block read per file, which is exactly what the bit existed
to avoid: a listing of thirty seven files went from 260,593 cycles to
635,321. It is paid in ls and not in dir on purpose - dir is the listing
you audit and is built into the shell, this is the one you read and was
loaded off the disk anyway. The fast one stays fast.

plain.script is the proof on the test disk: it sits among a dozen scripts
that are green and is not one, because it is the fixture with no shebang
and the shell will not start it. The extension is decoration and the
colour is the truth, which is the whole of what running by content means,
finally visible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 19:53:33 -04:00
AnachronautandClaude Opus 5 afda6ca83b Finishing says nothing, which is what finishing looks like
The shell printed "finished" every time a program gave the machine back.
That made sense when starting a program and getting the machine back was
most of what the machine did and the prompt was the only other thing on
the screen. Under a listing, between two commands, it is noise.

And it was printed whatever the program made of it, so a program that had
just explained what went wrong was answered with the word "finished" -
"there is no such directory: nowhere" and then, immediately, "finished".

The prompt coming back is what says a program is over. It is the same
argument osLastStatus is made of: a program that failed has already said
so in words, and anything the shell adds beside that is the shell talking
over it. A program that FAULTED still says so, because the fault screen
printed in red above and a program that is gone should not look like one
that ended.

A NEWLINE ONLY IF ONE IS WANTED. A program that stopped part way along a
line would leave the prompt sitting in the middle of its last output,
which the word used to prevent by accident. The console knows which
column the cursor is in, so the shell asks - where a blank line printed
every time would be right about half of the time, and an empty directory
listed with ls would be followed by a blank line for no reason.

Fifty nine recorded sessions lose the word. Two of them move a cursor up
a row with it, which is the same fact seen from the screen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 19:49:43 -04:00
AnachronautandClaude Opus 5 a13bbeb4db ls, a listing laid out to be read
> ls
  Copy.sbx   Say.sbx    Where.sbx  Walk.sbx   ls.sbx     Lander     where.sh

dir says what is there one line each, with sizes and a tally of the disk.
This says the names in columns and nothing else, which is what you want
ninety times in a hundred. Two programs rather than one with a switch,
because they answer different questions and neither answer is a worse
version of the other.

AND THIS IS A PROGRAM WHERE DIR IS BUILT IN, which is the difference that
matters when the disk is what you are doubting: dir is already in memory
and this has to be loaded off the disk it is about to list. The daily
driver and the diagnostic.

Two passes over the directory and no buffer at all. Columns need the
longest name before the first line can be printed, which usually means
holding every name - twenty four bytes each against a format that allows
1,024 entries. Walking twice costs a read of each directory block, into a
buffer that is already there.

Across and not down. Real listings go down the columns so that names next
to each other alphabetically are next to each other on the screen, and
that reason depends on sorting - which nothing here does. With the order
arbitrary, down-and-across buys nothing and costs a division.

Directories in blue and wearing a separator, unfinished saves in red,
everything else plain: whether a file is runnable cannot be known without
opening it, and opening every file in a directory to colour a listing is
a price nobody agreed to pay. The colour reaches a terminal as well as
the screen, so it is one mechanism and not two.

---- Three bugs, and one of them is this machine's oldest trap ----

nameLength answered in A, and a RET puts A back the way the caller had
it. So it answered with nothing, and every gap between the columns came
out the same width because the padding was subtracting whatever A
happened to hold. Q is the ALU's output and nothing puts it back, which
is why every answer here comes home in it.

The padding subtracted the other way round - the name from the cell -
which borrowed on every name that was not the longest, so all of them
took the "wider than its cell" path and the listing came out separated by
one space.

And padding after a name left trailing spaces on every line that did not
fill its last column. It goes before the next name now, so spaces only
ever fall between two things and a line ends on a name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 19:29:02 -04:00
AnachronautandClaude Opus 5 323d7a0330 A listing says what is left of the disk, and what will fit
dir said what was there and nothing about what was left. SplitDisk has
printed the free figure since it was written, so the machine's own
listing was the poorer of the two implementations at describing the same
disk.

  43 files, 3 directories
  46 of 64 entries, 1893 blocks free

Entries first, because they are the ceiling nobody notices until they hit
it: a disk of small files runs out of directory slots long before it runs
out of blocks.

COUNTED RATHER THAN ASKED. The superblock keeps a free count and this
file calls it "a note rather than the truth" in three places. sbfsSpace
reads the whole directory table instead, which costs a read per directory
block and is the answer rather than a guess. SplitDisk goes on reading
the note and saying when it is stale, which is the right place for that
check - the host tool is what you audit a disk with.

---- And it is a fact about the disk, not about where you are ----

The first version added the blocks up as the LISTING walked past them,
which cost no extra read and was wrong: that walk stops only on entries
in the working directory, so the same disk came out as 1,996 blocks free
from the root and 2,025 from /Apps. Comparing against SplitDisk is what
said so, which is what having two implementations is for.

---- The longest run, which is what decides whether a file fits ----

  4 of 16 entries, 37 blocks free
  the longest run is 25

Files are laid down contiguously, so the free total does not say whether
a file will fit. Both implementations learn it, from one specification.

Said only when it differs from the free total. Deleting is what fragments
a contiguous store, and a disk that has only been appended to has one gap
at the end - so on a healthy disk this is silent, and a line that appears
only when something is wrong is a line somebody reads.

There is no sort on this machine and the entries are in no order, so a
candidate walks the disk: each pass finds the used extent nearest at or
after it, and anything the candidate stands inside pushes it to the far
end and starts the pass again. The same trick allocating uses. So it
costs a pass per gap rather than per file - nearly nothing on a disk with
one gap, more the more fragmented the disk is, which is the right way
round.

holes.img is six files with the second and fourth deleted, because no
other disk here can show any of this: none of them has ever had anything
deleted from it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 18:19:04 -04:00
AnachronautandClaude Opus 5 2ad8edf9bc A program can see a directory
Every file service took a name a program already knew - read it, save it,
rename it, delete it, ask how big it is - and none of them could find out
what names there are. dir could list only because it lives in the shell
and calls the filesystem directly. So a file manager, a backup, and the
package manager still to come were each unwritable for want of this.

osDirFirst and osDirNext. DP0 says where to put the name and B how much
room, the same bargain osArgument and osWhereAmI offer.

Q ANSWERS THE KIND rather than a yes or no, so one value says both
whether there is an entry and what it is: 0 a file, 1 a directory, 2 a
save that stopped before it committed, 0xFF nothing more. A caller that
only wants names tests for 0xFF and ignores the rest.

The size is deliberately not in it. A walk hands back a name, and a
program that wants the size asks osFileInfo about that name - the
alternative being a record in memory whose shape both sides have to agree
on, which services.asm went out of its way to avoid for file sizes.

Walk.asm is the first program that can see a directory, and it asks
osFileInfo about each entry BETWEEN two steps of the walk. That is the
hazard rather than decoration: where a walk has got to and where the last
file asked about lives are both held by the system, and two things
sharing one position would show as a listing that stopped early or said a
name twice. Then the same walk in /Apps, since one that only ever ran at
the root would not have proved it walks where you are.

A directory is not asked about at all. osFileInfo answers for one
perfectly well and says nought blocks, which is true and reads as a size
- and nought is a size a file can genuinely have.

The lint baseline moves by one. The kind is decided by a chain of bit
tests in the shape dir already uses five hundred lines away, and arms of
a comparison chain each loading the same variable are the case this
project's own rule says not to collapse: the repetition is what lets a
new arm be dropped in anywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 17:55:41 -04:00
AnachronautandClaude Opus 5 b83ba5bf7a A colour reaches a terminal as well as the screen
The console's attribute has always meant something to the screen and
nothing to the serial line: its low nibble picks one of sixteen ink and
paper pairs, and only videoPutCell ever read it. So the fault screen's
red was red in the window and grey down the wire, and Examples/colours
printed " ordinary  highlighted " with nothing to tell them apart.

It is said in ANSI now, on the same terms the cursor is said in: a
register write only marks it and the next character sends it, so setting
a scheme and printing nothing says nothing, and setting the same scheme
twice costs one sequence rather than two. Only the scheme nibble crosses
- the page bits say which tiles a cell draws from, which is a fact about
the screen's own art.

THE ORDER WAS ALREADY RIGHT, which is worth saying because it looks like
a borrowing and is not. Both sets enumerate a three-bit colour, red green
blue counted in binary: one is red in both, three is yellow in both, six
is cyan in both. The same arithmetic done twice, forty years apart. The
one place they differ is slot 0, and that difference is forced - this
screen is ink on black, so ink cannot be black, and slot 0 is grey where
ANSI's is black.

Every sequence begins with a reset, so going from bank 8 to bank 1 does
not write red on the grey paper bank 8 left behind.

Scheme 0 is a bare reset rather than grey on black, and a terminal is
assumed to start plain - so a machine that never asks for a colour says
nothing at all, and one that does put the terminal back on its way out.
Two ways out, because the two endings have different rules: stopping on
purpose goes through stdio, since atexit runs BEFORE the buffer is
flushed and a reset written to the file descriptor would arrive in front
of the text it is meant to follow. Dying on a signal writes the four
bytes directly and accepts that the buffer may be lost.

colourTest walks all sixteen and then halts WITH ONE STILL SET, so the
recording shows the reset after the halt line rather than before it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 17:24:44 -04:00
AnachronautandClaude Opus 5 94bdf71356 Lunar Porter moves into a directory of its own
The first application with things of its own, and the first customer for
all three rungs before it at once.

  /Apps/Lander                      a launcher
  /Packages/app.Lander/Lander.sbx   the program
  /Packages/app.Lander/splash.tune  its tune

Typing "Lander" starts the launcher, which needs no new rule: the shell
runs what it reads, so a launcher beginning "#!" and Say.sbx beginning
"SBEX" go down the same road and neither the shell nor the person has to
know which kind of thing they started. It passes on what it was told with
$args, and says #quiet, because a launcher is machinery and not
narration. The game then asks where it came from and joins its tune's
name to that, so nothing anywhere names /splash.tune.

Every recorded Lander test still types just "Lander" and knows nothing
about any of this, which is exactly the claim.

---- Why the directory is not in /Apps ----

A launcher and a directory of the same name cannot both be there; SBFS
refuses the second. And /Apps is the directory the shell walks for every
word it does not know and Tab walks for every first word, so doubling
what is in it is a cost on the path that runs most.

The "app." goes in FRONT rather than behind because Tab matches the start
of a name: a prefix is a namespace and a suffix is a collision. Copy.app
beside Copy.sbx makes "Copy" and Tab complete to the shared "Copy." and
hand you a broken word.

The launcher names drive 0, so a game started from a disk of your own is
looked for where the game is rather than where you are - its lines run on
your disk, which is what makes everything else in a script work.

/lander.state stays at the root. A saved position belongs to whoever
saved it and is found where they are standing; a tune belongs to the
program and is found beside it. That pair is the whole distinction.

---- And what it cost ----

Two hundred thousand cycles, about twelve frames, between the machine
starting and the game drawing: a script opened, a deeper path walked. Six
video captures moved out by that much. The drift bar needed something
else - its pad is counted from the MACHINE starting rather than the game,
so the burn is now twelve frames shorter from the game's point of view,
and the recording holds the button longer instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 15:55:19 -04:00
AnachronautandClaude Opus 5 2defbb49e2 A program can ask where it came from
SWI osWhereAmI hands back the path the program was loaded from, on the
same terms as osArgument, and Libraries/path.asm joins a name to the
place another thing is in. Between them an application can find its own
assets: ask where you are, then pathBeside that and the file's name.

The answer FOLLOWS THE PROGRAM AND NOT THE PERSON, which is the whole
point and the reason the working directory could not serve. A program's
assets are relative to the program and its arguments are relative to
whoever ran it, and cwd can only be one of them - setting it to the
program's own would mean "Play mytune.tune", typed by somebody in their
own directory, looked in Play's.

It is made absolute before the program starts, because the path the
search settled on may be a bare name: a program found where somebody was
standing is named by the word that was typed, and a bare name means the
working directory - which a program is entitled to move out of. Worked
out once, at the start, since where a program came from is a fact about
its start and cannot change afterwards.

Joining is a LIBRARY and not a service. A service that opened a file
relative to the program would need a twin for every file operation there
is - read, save, info, block, start, write, done, delete, rename - while
one service handing back a path composes with all of them.

---- And the root's own path was "//" ----

Found by the first caller that asks. shellPath prepends a separator in
front of whatever string it is given, so being handed the separator
itself wrote two of them. Nothing saw it while the only caller was the
prompt, which asks where it is only when that is not the root. It is
handed an empty string now, and cosmosWhere runs a program from the root.

Where.sbx exists to be run rather than read, and is on the test disk
twice: at the root, where it is found by the bare word typed, and in
/Apps, where it is found by a path that already says where it is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 15:27:57 -04:00
AnachronautandClaude Opus 5 d9ebc76cf5 A script is given words, and keeps them
"do build.sh cosmos.asm" looked for a file called "build.sh cosmos.asm",
because scriptOpen copied the whole rest of the line into the name. So a
script could be told nothing, and a launcher - a script whose entire job
is to hand on what it was told - could not exist.

Now the name is cut off the front and what follows is kept whole. $1 to
$9 are the words of it, walked out on demand, and $args is all of them.
Nothing is stored per parameter, so there is no limit on how many a
script may be handed and no second number to keep in step. Both ways of
starting a script pass them on: "do" and typing the name.

A word that was not given is an error that stops the script, like every
other name this shell does not know. Expanding it to nothing would let a
command run with an argument missing and then report success, which is
what stop-on-failure exists to prevent. $args is always set inside a
script, empty if it was given nothing, so "if same $args" can be asked.

---- And the count that describes the block was already wrong ----

Found while adding a field to it. The state one script keeps for another
is saved by a single copy of a fixed number of bytes, and that number was
71 against a block of 77: six bytes of line position had been added in
the middle of it years after the count was written. So the tail of every
saved script was never saved, and #quiet in a helper stayed behind in the
script that called it - the opposite of what this file's own comment
promises and the README documents.

The unsaved line position turned out not to matter, because a loop keeps
its own copy in the block record. Nothing said so. Three numbers describe
this block and all three now say so in a comment, and cosmosScriptNest
ends on a helper that goes quiet and a caller that must not stay that
way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 15:06:32 -04:00
AnachronautandClaude Opus 5 06af7e7fbb Reading a script must not move the person who started it
A script is fetched a block at a time while its lines run, and its name
is resolved afresh for every block. A name with a drive in front of it
moves the machine to that drive on the way past - sbfsWalk calls sbfsUse
- so a script found in the system's place on drive 0, started by somebody
standing on a disk of their own, ran its lines on the system disk.

Always possible with "do 0:/Apps/setup.sh", and reachable by typing a
name now that the search finds scripts the same three places it finds
programs. The drive is kept across each fetch and put back after it, at
both places a script's name is resolved.

The test has to work for it. A script that fits in one block is read
entirely while it is being opened, and the opening was never the hard
part; and the keep in scriptFill cannot be broken on its own, because
scriptOpen has already written the variable down. So the script on the
disk crosses two block boundaries and moves itself between them: what it
says about where it is standing is 1 before the move and 0 after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 13:23:31 -04:00
AnachronautandClaude Opus 5 fd962f0084 The shell runs what it reads, not what a name ends in
A typed word had ".sbx" pasted on the end of it before anything went
looking, which is why "notes.txt" sent the shell after a notes.txt.sbx
that was never going to exist, and why a script could only be started
with "do". The extension was what made a file reachable by name.

Now the word as typed is asked for first and the word with the extension
on it only after that misses. What comes back is dispatched on what is
inside it: SBEX loads and starts, "#!" is read as lines. The loader
already refuses anything that is not SBEX and the script reader already
refuses anything without the shebang, so the two kinds of runnable file
turn each other away and neither has to know the other exists.

The suffix can only ever be a second guess, so a file that is really
there always beats one that would have to be invented and every program
already on a disk still starts by the short name people type for it. It
costs a second walk of each directory when a word is not found in it.

Tab completion offers a file under the name it actually has, and the
suffix stripper is gone: a first word can now be any file at all, and
offering only the ones ending .sbx would hide the scripts.

The video capture windows moved out a hundred thousand cycles, because
starting Lander now walks four names where it walked two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 13:23:14 -04:00
AnachronautandClaude Opus 5 4e61158b11 The splash stops sounding before it ends, so listen earlier
The check asked whether music was still playing four seconds in, and the
score it listens to ends its every voice on a rest - twelve ticks of
silence that hold the logo up while the voices decay. So it was measuring
exactly the quiet the score asks for and calling a perfect tune a failure.

Three seconds instead, which is still twice the second and a half the
silent path holds the logo for, and which is what the check is actually
about: that this is the tune and not the hold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 13:22:56 -04:00
AnachronautandClaude Opus 5 f351ee8844 Every score on the disk, not a named one
The makefile compiled splash.score by name, so intro.score sat beside it
turning into nothing - which is exactly the failure the mirror three
lines above exists to prevent, and its comment says so: a list in a
makefile goes stale the moment somebody adds a file, and what they forgot
is invisible until they go looking for it on the machine.

Found by the user going looking for it on the machine.

Every .score in Programs/Tunes is compiled and put on the disk now, and
every patch in Programs/Sounds is converted first so a score can name any
of them. The source is mirrored to /Source/Tunes with everything else
that was written; the tune goes to the root, where a program looking for
one expects it - which is worth writing down, because the two being in
different places is the thing that sent somebody hunting.

two.score moved to Tests. It is a fixture, it names patches that only
exist in the test build, and it was in the music tree only because that
is where scores were when it was written - which the game disk build
found immediately by failing to compile it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 11:15:47 -04:00
AnachronautandClaude Opus 5 dfc9d9ef7f A score is written and a tune is what the machine reads
The convention, at the user's asking, and it is the one this project
already has everywhere else: a .asm is written and a .sbx or a .bin is
what the machine loads. A .score and a .tune are the same pair one
subject along.

It is not only tidiness. I read the user's splash.tune as a compiled tune
yesterday, dumped its header, and got a tick of seven and a half million
cycles and ninety seven patches out of what was plainly a text file.
Different names make that a thing nobody has to notice.

AND THE SPLASH WAS SILENT ON THE DISK THAT MATTERS. The play disk mirrors
every .asm and puts every app, and nothing on it put a compiled tune - so
make run-cosmos and make run-voyager both booted a Lander that read
/splash.tune, did not find one, and held the logo in silence. Only the
test disk had it, because I had added it there and stopped.

The makefile now compiles Programs/Tunes/splash.score with TuneC and puts
the result on the disk, and the mirror's prerequisite list learned about
.score files so that changing the music rebuilds the disk. That is the
same failure the mirror was built for, in a file type the mirror did not
know about yet: tune.asm went into Examples once, the image was not
remade, and it was simply not there.

Measured on the real disk: music from 0.9 s to 9.6 s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 10:24:39 -04:00
AnachronautandClaude Opus 5 00b31b88af Lander opens on a splash, with music
The first customer for the player outside the program it was pulled out
of, and the argument for doing it: a splash needs no resident player at
all. Nothing else is happening while the logo is up, so Lander owns the
timer and all four channels exactly as Play does. Music UNDER a running
game is still deferred, and still the harder problem.

It polls the timer rather than being interrupted by it. Lander has no
vector segment and waits for the screen by reading port 0x30, so it waits
for a beat by reading port 0x50 - the same shape, and it brings no
handler that would have to be taken away before the game starts.

The tune is read off the disk. A missing one means the logo and silence
and the game starts anyway, the way a missing /lander.state means the
defaults stand.

FOUR THINGS THIS COST, each found by running it:

A subroutine cannot answer in A. CALL saves and restores it, so splashSkip
handed its caller back the A it already had - the channel number of the
last stepVoice - and the splash ended on its first pass whatever anybody
pressed. Q is what survives a RET, which nextRandom says twenty lines
away and I did not read.

Port 0x3D is how many window rows are SHOWN, and it is two once putGauge
runs and nothing before. A line written to row eleven went somewhere real
and was displayed nowhere.

A nought is not a keypress. It is what a recorded keyboard file holds
while nobody is typing, and a splash that took it for a key is one no
test could ever watch.

And skipping has to be free. Asked after blanking the window and reading
the file, a skipped splash still cost a fifth of a second - enough to
push the thruster test's early capture past the frame it looks at. Asked
first, it costs a pad read.

player.asm no longer asks a caller for Order0 to Order3: the voice records
name NoOrder instead, so a program whose tune comes from a file does not
have to define four order lists it never uses. That was the file case
finding a wart in the contract.

Every other Lander test now skips the splash with a space - a key the
game itself ignores, since it answers to q, z and the arrows - so they go
on testing what they tested. The one in sound.sh presses nothing, which
is what makes it the one that hears the music.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 10:06:20 -04:00
AnachronautandClaude Opus 5 6bb1565dea A tune's tick is the tune's own
useTune took the period out of a file's header and then Play wrote its
own straight over it, so every tune played at a sixteenth note at 120
beats a minute whatever it asked for. A tune with a #Tick of 0d250000
lasted half as long as it said.

Nothing noticed because every fixture in the suite asked for exactly the
tick Play had written into itself. A test that agrees with the bug by
coincidence is not a test, and the way to find out is a fixture that
wants something else - so slow.tune is two.tune with twice the period and
nothing else changed, and it has to last twice as long.

The period now belongs to whoever supplied the tune: useTune sets it from
the header, useBuiltIn sets its own, and the start code writes only the
control byte - which has to come after either of them, because writing
control with the run bit set is what loads the period.

Found while reading Play to see how a splash screen would drive the
player, which is a reminder that the second reader of a piece of code is
worth more than the first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 09:37:08 -04:00
AnachronautandClaude Opus 5 87467a7d3a Lander starts a flight where a flight starts
run does not reload, and that is right: run's job is to run the program
that is loaded, which is why it is a separate word from load and why
typing a program's name does both. What follows is that the numbers the
assembler wrote into a Data Segment are LOAD-time values, true once, and
a program that wants them true at every start has to say so itself.

Lander did not. Flying is 0x01 in the data and is only ever cleared, so a
second run began with the loop already over: the program started and
handed the machine straight back. Reproduced by running it, quitting, and
running again - the leftover game keys land at the shell prompt, which is
what a program that never read them looks like.

AND THE TEST FOUND A SECOND ONE, quieter and worse. The terrain seed is a
written number, so a fresh load always walks out the same moon - but
nextRandom moves it, and a second run generated a DIFFERENT moon. Nobody
decided that. A test comparing the whole screen found it without anybody
having had to think of it in advance, which is the argument for comparing
the picture rather than the variables somebody remembered to check.

Only what a flight needs to begin is put back. Anything not on the list
keeps what the last run left it, which is deliberate - state surviving a
run is sometimes exactly what is wanted, and the way to have that is for
the list to be a decision rather than a sweep. The things that are simply
nought are a table of NAMES that the assembler turns into addresses, so
adding a variable that must start empty means adding it there and nowhere
else.

video.sh now runs Lander, quits, runs it again, and requires the same
picture a hundred frames in - the same moon, the same lander in the same
place, the same gauges. break.sh confirms it by taking the seed line back
out: 37,995 bytes of the picture differ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 09:02:50 -04:00
AnachronautandClaude Opus 5 b160c66110 The arpeggio's first chord is the note its comment says it is
tune.asm wrote 0x2A four times, labelled "; Gs" each time. 0x2A is F#2.
G# is 0x2C. The file has disagreed with itself since it was written.

It is not a near miss. The progression is Ab, Bb, C - a flat six and a
flat seven resolving home - and the other two chords are right: 0x2E is
A# and 0x30 is C. With F# the first one is a tritone against its own
third and stops being major at all.

Found by the user transcribing the piece into the new tune language,
where writing the notes out again by ear is what made the wrong one
audible. The number was carried across faithfully; the intent was not in
the number.

Nothing churns: it is one value and not a length, so the binary is the
same size and the recordings are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 22:15:58 -04:00
AnachronautandClaude Opus 5 fee2b1ef10 One missing patch says one thing
A patch file that could not be read left the name unregistered, so every
#Voice naming it failed as well, and then the check that a voice has an
instrument failed for each of those. One wrong path produced seven
messages and only the first was worth reading.

A patch that cannot be read is still a patch that was NAMED. It is
registered either way now, with its bytes marked missing, so everything
below resolves the name and says nothing. Nothing is written regardless -
one problem is enough to stop that - so a patch with no bytes never
reaches a file.

The damage from the old behaviour was not the extra lines. It is that a
compiler which says one thing seven ways teaches people to read the last
line, which is the one that matters least.

Checked by counting: one missing patch, three voices using it, and the
count of messages mentioning it has to be one. break.sh confirms it by
putting the old behaviour back on the failure path alone - the first
attempt at that break stopped every tune compiling and the disk build
failed before any test ran, which is break.sh being right about a break
that proved nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 22:09:18 -04:00
AnachronautandClaude Opus 5 fb672d7761 A manual for writing tunes
The fifth document, and the one somebody would want first if they had a
piece of music and this machine. What a tune is, how to write one, what
the compiler refuses and why, how to build and play one, and the bytes it
holds for anything that would rather write one itself.

The last of those matters more than it looks: Tests/maketune.py writes
tunes without going through TuneC, and the suite checks the two agree
byte for byte. A format with two implementations needs a specification
they are both held to rather than one of them being the specification.

The refusals get a table of their own with the reason beside each,
because every one of them is something the PLAYER cannot notice - it has
no names, no lengths, and no way to tell "no starting instrument" from
"instrument nought" once a tune is loaded. Documented as reasons rather
than as rules, so that somebody meeting one knows what it saved them
from.

docs.sh now settles the manual against the compiler both ways: every
directive TuneC takes has a row, and the limits the manual quotes are the
compiler's own #defines. Verified with break.sh - a directive removed
from the manual and a limit raised in the compiler are both caught.

One that was not caught first time and should not have been: removing the
#Use row from one table left it documented in the other, which is the
check being right and my break being wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 21:20:55 -04:00
AnachronautandClaude Opus 5 2808688fa1 TuneC: a written tune becomes the bytes the player reads
The compiler, and the last thing the ladder was waiting for. A tune names
its instruments, writes sequences of notes and durations, and gives each
voice an order list of sequence names - which is where repetition comes
from, since a phrase played four times is written once and named four
times.

  #Tick 0d125000
  #Patch Oboe oboe.patch
  #Voice 0d0 Oboe
  #Sequence Verse
    0d64 0d4  0d67 0d4  0d72 0d8
  #Order 0d0
    Verse Verse Ending

"#" is a directive and ";" is a comment, exactly as in SplitBit assembly
and in the shell's scripts, and numbers are written the way the assembler
writes them. One rule across the machine rather than a third dialect -
and the rule earned itself immediately: the first tune I wrote said
"#Voice 0" and was refused, correctly, for a bare number.

WHAT IT REFUSES IS EVERYTHING THE PLAYER CANNOT NOTICE. The machine has
no names, so it cannot say a sequence does not exist. It has no lengths,
so it cannot say the voices will come apart four bars after the mistake.
A duration of nought is counted down to 255 and held, which sounds like a
hang rather than an error. And by the time a tune is loaded, "no starting
instrument" and "instrument nought" are the same byte - so the user's
ruling, that a voice with a part and no instrument is an error, can only
be kept here.

SoundPatch gains --blob, writing the same table as raw bytes. It stays
the only thing that reads soundThing's JSON: a second program parsing
that format is a second opinion about what a patch means, and the seam
between two opinions is where the LFO bug lived for a fortnight. Patches
are found beside the tune and then on a -I path, the way an include is.

THE TEST IS THAT TWO IMPLEMENTATIONS AGREE. maketune.py lays the fixture
out by hand and TuneC compiles a written source, and the suite checks
they match byte for byte - the discipline SplitDisk and sbfs.asm are held
to, for the same reason: either alone is only self-consistent. The
fixture predates the compiler, so this is also TuneC checked against
something written before it existed. Four more checks cover the four
refusals.

Also: the SoundPatch binary was tracked, alone among the six tools, and
.gitignore lists every other one. Untracked, and TuneC added beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 21:11:36 -04:00
AnachronautandClaude Opus 5 bfc46d982e Play reads a tune from a file
The loader, and the reason it is small: everything in a tune is an offset
from wherever it was put, so taking one means adding the base to two
tables and pointing four voices at their order lists. No sequence is
walked. Nothing inside one is an address to be found and corrected, which
is the difference between a malformed tune that plays wrongly and one
that takes the loader with it.

  "SBTU", version, the tick in cycles, how many patches and sequences,
  offsets to the two tables, an order list each, and the patch each
  voice starts on.

The magic is checked before anything else, because from there on the
loader follows what the offsets name. break.sh shows what that guard is
worth: without it, handing Play a PROGRAM runs the machine away until the
cycle limit, rather than saying it is not a tune.

PatchTable, SequenceTable and VoiceStart became pointers, so the engine
does not care whether a tune came out of a file or was assembled in.
Play's built-in tune now hands over the same three addresses a loaded one
would, in eight lines - which is what keeps the two paths from drifting,
and what made this rung change no scheduler code at all.

Tests/maketune.py lays the fixture out byte by byte. IT IS NOT THE
COMPILER: the sequences and the patches are literal bytes and the only
thing computed is where each piece lands. That is the point - the loader
is checked by something that does not share its idea of the format, which
is the same reason SplitDisk and sbfs.asm share nothing but a
specification.

Both notes in the fixture are number 60, so the octave between them is a
0x80 command loading the second patch out of the file: the header, the
tick, the relocation, an order list and a patch from a file, measured in
one go. Play and the tune live on quiet.img rather than cosmos.img, so a
fixture does not move ten recordings every time it changes size.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 20:58:24 -04:00
AnachronautandClaude Opus 5 5d9b39514b The player speaks indices, which is the shape a file has to be
Order lists hold one-byte sequence indices and 0x80 holds a one-byte
patch index. Two tables, PatchTable and SequenceTable, are the only
places an address lives.

THAT IS WHAT MAKES A TUNE LOADABLE WITHOUT WALKING IT. Nothing inside a
sequence or an order list is an address, so putting one in memory means
adding the load address to two arrays and nothing else. The alternative
is a loader that parses every sequence looking for addresses to correct,
which is a loader a malformed file can walk off a cliff.

Doing it now, while the tune is still assembled in, means the file form
and the assembled form are the same shape - so reading a tune from a file
will change no engine code at all. The whole point of the rung.

The patch each voice starts on moved from four calls in a row into
VoiceStart, four declared bytes, and loadStartPatches reads them. A
starting instrument is state and belongs where state goes: the user's
ruling is that a voice with undefined state is an error, prompted by
noticing that a program run a second time starts with the memory the
first run left, because loading is what initialises and running is not.

Order lists also halved in size, which was not the reason but is welcome.

Hand-writing the two tables is exactly the tedium the compiler exists to
remove - every sequence counted into its place, and moving one means
renumbering. Better to feel that here than after a tool has baked the
shape in.

Verified by rendering: bar for bar the same piece. break.sh confirms the
scaling, since an index is doubled to reach a two-byte entry and halving
that step lands on the wrong sequence and fails four checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 20:45:40 -04:00
AnachronautandClaude Opus 5 8029063bdc Sequences, order lists, and a command that changes the instrument
The rung between M3 and M4, and the point of doing it before the format:
the engine learns the tracker's model with the tune still assembled in,
so M4 becomes serialising a thing that exists rather than designing a
thing that does not.

A voice no longer walks one long track. 0xFF now means THIS SEQUENCE
ended, and the voice takes the next address from an order list of its
own. That is where repetition comes from, and it costs no notation: the
bass plays the same sequence in the first bar and the last and it is
written once. Per voice rather than one shared table of four-column rows,
because a voice's order cursor is then a pointer it advances by itself -
the same LDD and STD move everything else here makes. Four columns is how
it reads, not how it is stored.

Sequences also carry COMMANDS, which take no tick: the reader acts and
reads the next event on the same boundary. One is defined, 0x80, which
plays the rest of that voice on another patch, and the other 125 values
are left alone. A patch change reshapes whatever is still ringing on the
voice and nothing can be done about that - a channel has one set of
parameters and a note in its release is using them - so it is a fact
about the hardware and the cure is a rest, which is the composer's.

The engine moved to Libraries/player.asm rather than being copied into
the test a second time, now that it is big enough to drift. Play supplies
the tune and the beat; the library supplies the scheduler, the patch
loader and a voice's state. Verified by rendering: bar for bar identical
across the move.

AND THE TEST FOUND A REAL FLAW IN THE FORMAT, which is the whole argument
for building the reader first. The order list ended with 0x0000, on the
reasoning that no sequence could live below the 0x3000 this program is
based at. True of a loaded program, false of a boot image whose data
starts at zero - so the first test written against it read its own first
sequence as the end of the list and played nothing at all. It is 0xFFFF
now, which mirrors the 0xFF ending a sequence and is impossible
everywhere: a sequence at 0xFF00 or above has fewer bytes left than it
needs. An address is a poor place to hide a flag unless the address is
impossible in every program, not just this one.

One order list in the test now really ends, because otherwise nothing
reached the terminator at all: every voice sat on a long rest and the
break went unnoticed. With it, breaking the test reads garbage past the
end and the counter sums two notes at 781 hertz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 19:54:38 -04:00
AnachronautandClaude Opus 5 f9b08cf7f9 CosmOS lets every voice go when a program stops
A gate is a register on the sound device and only a program can drop one.
A program that has stopped cannot: it is gone. So a note left held
sustained until something else said otherwise, and nothing else did -
one program could leave the machine sounding for as long as it ran, with
nothing the person at it could do.

The shell already puts back the Stack, the vectors, the drive, the
working directory and the screen. This is the same list and the same
argument, and the fault path calls it too, for the stronger version of
the argument: a program that CRASHED is exactly the one that cannot tidy
up after itself, and a machine that will not stop humming is a poor place
to read an error message.

It does not make the device silent at once and does not pretend to.
Dropping a gate RELEASES a note rather than stopping it, so the patch's
release still runs. A bounded tail rather than an endless one is the part
the system can be responsible for without knowing what instrument the
program had built.

Hum exits while holding a note; Pause makes no sound and takes a couple
of million cycles, because the machine stops the moment the shell runs
out of input and a note quietened at that instant leaves no samples
behind to say whether it was. They are on a disk of their own so that a
fixture does not move the ten recordings that quote cosmos.img's listing.

THE CHECK CAUGHT ITSELF PASSING WRONGLY FIRST. Pause was missing from the
disk, the machine halted immediately, and the window that should have
held the tail was past the end of a render a twentieth of a second long -
an empty window's peak is nought, which is indistinguishable from silence.
So the sample count is asserted before anything is read from it. Verified
with break.sh: without the call the note is still ringing at 9869.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 18:54:46 -04:00
AnachronautandClaude Opus 5 7f96499438 Waiting for a key is not the same as time stopping
A console read that had nothing to hand over blocked in read(), and
while it was blocked nothing told the devices any time had gone by. The
whole machine stood still: the clock, the cursor, a disk settling, a note
decaying. Measured, four real seconds of sitting at the prompt bought
zero cycles.

A window has never done this. consoleGatherLine and the key mode loop
both spend a frame and then say so, and the comment on consoleWaited
gives the reason - a display controller blinking a cursor does not stop
because the processor is waiting on a key, and neither does a disk
finishing a read. The terminal path simply never got it.

How it showed was a note left in its release after a program exited.
Frozen mid-decay, it came out a snippet at a time, one per keystroke,
because a keystroke was the only thing that let the machine run at all.

So a terminal read now polls a frame at a time and tells the devices
about each one. Four seconds now buys 3.85 million cycles, which is the
wall clock, which is what the window has always done.

ONLY WHEN THERE IS REALLY A TERMINAL, the same rule the escape sequences
and the erase character follow. A file or a pipe keeps blocking: every
recorded test on this machine is reproducible because emulated time does
not depend on how fast the other end of a pipe is writing, and that must
stay true. The full suite is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 18:54:31 -04:00
AnachronautandClaude Opus 5 41d65d6346 Play waits for its own sound to finish before handing back
The gates all go down on the tick the tracks end, but a gate down is a
note RELEASED and not a note stopped: the oboe and the strings have a
release left to run. Rendered and measured, the tail dies eight tenths of
a second after the last event, and the ring was eight ticks - one second,
which sounds like enough and was two tenths short.

A program that exits with sound still in the air leaves nothing able to
end it: the program is gone and cannot drop a gate. What that becomes
depends on the front end. Behind a window the tail finishes on its own.
On a terminal it does not, because emulated time stops while the machine
blocks on a key - measured, and the reason the leftover sound came out a
snippet per keystroke rather than fading.

Twelve ticks, which is a second and a half and covers the measured tail
with room over. It is a number about THESE FOUR INSTRUMENTS: a patch with
a longer release would want more, and the general answer is for the
system to quieten the device when a program stops, the way it puts the
screen back. That is not built and is worth deciding on rather than
guessing at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 16:13:10 -04:00
AnachronautandClaude Opus 5 ce1c0517aa M3: an instrument for each voice
Play loads a patch per channel before a note is played: an oboe for the
melody, strings under it, a square wave for the bass and a kalimba for
the arpeggio. It reads a count and that many parameter and value pairs -
the format SoundPatch writes - and understands nothing else about them,
which keeps SoundPatch the only thing that knows what soundThing's JSON
means.

FOUR PATCHES CAN BE UP AT ONCE, and that is the whole rung. It is the
first use of d361ea1: before it the LFOs belonged to the whole device, so
whichever patch loaded last owned them for every voice. Kalimba has LFO 0
switched off and the other three have it on, so on the old device this
piece would have played all four parts with the arpeggio's setting -
which is exactly the fault that stopped Lunar Porter's low fuel warning
trilling after the first landing of a run.

The eight patches are the user's, brought over from soundThing; four are
used here and the rest are a palette for the next piece.

The test now asks BOTH VOICES FOR NOTE 60 and gets an octave, because
their patches differ in one parameter and the lower is loaded first. A
device where a patch was global would have the second overwrite the first
and the two would answer in unison. Verified with break.sh: loading both
patches onto one channel flips the octave onto the wrong voice and the
ratio inverts to 0.499.

The test patches carry a release, and that was measured rather than
assumed: without one the first voice's note was still fading into the
second voice's window, which read 634 hertz - not a note, not an octave,
and a reminder that a crossing counter given two notes answers with
neither.

Play.sbx went 379 -> 740 bytes, which is four patches of 47 pairs each,
and is why ten recordings moved. makedisks.sh needed the Sounds include
path that Lander's line already had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 16:02:06 -04:00
AnachronautandClaude Opus 5 1687422619 A voice starts pointed at its track, without code to point it
There is no assembler bug. I reported one and was wrong.

A label written in the Data Segment does come out as its address, two
bytes, most significant first - implemented in populateOutputBuffers,
documented in the Assembler Manual, and correct. What misled me was the
test I checked it with: the label was the first thing in an unbased
Data Segment, so its address really was 0x0000, and I read the right
answer as an unfilled placeholder.

So Play was doing at run time what the assembler had already offered to
do at assembly time. The voice records now carry their track labels
directly, which is exactly the shape LDD reads, and nothing relocates on
this machine so the address written is the address it will have. That
takes out startVoice, its four call sites, and the eight SETDs that fed
them: 450 bytes to 379, and the initial state of a voice is now
something you can read rather than something you have to follow the code
to work out.

The comment claiming otherwise is gone from Play.asm, and the same
change is made in fourVoiceTest.

The music is unchanged - bar by bar the render matches to within one per
cent, which is the program loading a shade sooner because it is smaller.
Ten recordings moved for the same reason: 446 to 379, and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 15:24:02 -04:00
AnachronautandClaude Opus 5 79d1e2639b M2: four voices on one clock
Play is the scheduler: one tick, four cursors. Every voice keeps its own
place in its own track and its own count of how much longer the note it
is holding lasts, so a voice playing whole notes and a voice playing
eighths cost the same and never have to know about each other. They
share the tick and nothing else.

That is what makes the tick the smallest subdivision in the piece rather
than a note length - it is the only unit four parts can agree on.

A track is pairs of bytes: what to play, then how many ticks it lasts.
MIDI notes stop at 127, so the top of the byte was free and neither the
rest nor the end marker had to be invented - 0 is a rest and 0xFF ends
the track. Examples/tune.asm spent zero on its end marker and so could
not write a rest at all, which one voice can live with and four cannot:
the silences are what make them separate parts rather than a chord.

The state is four bytes a voice, cursor first because that is what LDD
and STD move - a pointer through a pointer, which is what lets this be a
loop over four voices instead of the same code four times. CALL preserves
A and DP0-2, so a caller says which voice it means in two instructions.

The piece is four bars of C, F, G, C with the parts moving at four
different rates, because that is the thing one channel cannot do. All
four channels get the same instrument, which is exactly what M3 replaces.

fourVoiceTest staggers two voices so each gets a stretch alone: middle C
while the other rests, the octave while the first is silent, then both.
The first two are measured for pitch and the third for level, because
TWO NOTES CANNOT BE ASKED THEIR PITCH - the crossing counter adds them
and answers 785 hertz, which is 262 plus 523 and a fact about nothing.
Verified with break.sh: dropping the channel select trips one check,
pointing both voices at one cursor trips three.

It shares Play's design and not its code, and is smaller - no track ends
in it, so there is no live flag and no count of what is still playing.

Play.sbx on the disk is why ten recordings moved: one added line each,
and the file count with it. Nothing else in them changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 15:03:12 -04:00
AnachronautandClaude Opus 5 1b251fb290 M1: the tune keeps a beat it sets, instead of one it borrowed
tune.asm counted the screen's frames, because until 6b51d63 the frame
was the only regular beat on the machine. It is now driven by the timer.

THE DURATIONS ARE NOT A CONVERSION OF THE OLD ONES. A frame is 16,667
cycles, so a sixteenth note at 120 beats a minute is seven and a half of
them and could not be asked for at all: the arpeggio was written as
seven whole frames, which is six and a half per cent fast, and the rest
of the piece was written to a tempo picked so its subdivisions landed on
whole frames. The tick is now 125,000 cycles, which IS that sixteenth
note, and the durations are what the music wanted in the first place -
an eighth is two ticks, a quarter four, the arpeggio one each.

The player's loop did not change shape: a note table, a wait on each
beat, the gate dropped at the end. Only what it waits on. The handler
stays a bare RETI, because taking the interrupt is what brings the line
down - a program that POLLED the timer would have to read 0x50, and the
comment now says so, since that is the trap.

Three checks in sound.sh, on a program that is the player's loop rather
than the tune: eight ticks last eight periods to within a tenth of a per
cent, and the seventh and eighth notes are measured where the ticks put
them. The middle one is the one that fails on the old timing - at seven
whole frames the boundary slides under the window and it reads 384 Hz,
which is neither note and is what a crossing counter says when given
two. Verified with break.sh by moving the timer's period a byte.

tune.sbx grew 306 -> 318 bytes, which is why ten cosmos recordings moved:
they quote a directory listing. Nothing else in them changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 14:42:45 -04:00
AnachronautandClaude Opus 5 35c6708e46 The shell takes spaces off both ends of a line, not just the front
Tab completion leaves a space after the word it finished, which is right
when another word is coming. When nothing else was coming, that space
stayed on the end of the line and a command taking a file name looked
for one whose name ended in a space:

  load greet.sbx      loaded, starting at 5000
  load greet.sbx      no such file      (the same line, finished with Tab)

Which took most of the good out of completion, since finishing a name
and pressing Return is the whole of what it is for. The existing tab
tests all typed more characters after completing, so none of them ever
submitted a completed line.

lineTrim already took the leading spaces off for indented blocks, so the
trailing ones come off in the same place, on the whole line, rather than
at each of the dozen commands that take a name. Walking back needs no
guard against running off the front: by then the first character cannot
be a space, and a line that was nothing but spaces has already become an
empty one.

CONSEQUENCE WORTH SEEING: echo no longer prints a trailing space it was
given, which is why cosmosTabPath's recording moved. That is the
conventional behaviour and it means what echo is handed is what somebody
would have typed, but it is a real change and not only a bug fix.

cosmosTrim covers a line completed and run straight away, the same
trailing space typed by hand, spaces at both ends at once, and a line of
nothing but spaces, which still has to do nothing rather than fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 12:23:28 -04:00
AnachronautandClaude Opus 5 8631a78229 Backspace, whatever the terminal calls it
CosmOS's line editor looks for 0x08, which is what Voyager's keyboard
sends. A POSIX terminal sends its own erase character instead, and on
most of them that is 0x7F.

It stayed hidden while the terminal was doing the editing: canonical
mode consumes the erase character itself and hands over a finished
line. Key mode turns ICANON off, which is the point of it, so from the
day the shell started editing its own line - 7360374 - Backspace worked
in Voyager and did nothing at all in the console-only emulator. The
Programming Manual already claimed the console normalised Backspace on a
terminal the way it normalises the arrow keys; the code did not.

The erase character is read from the terminal's own VERASE rather than
assumed to be 0x7F, because some terminals are set to 0x08 and a person
who has moved their erase key has said where it is. Forward Delete is
untouched and stays 0x86: two keys, two values.

Only when standard input really is a terminal. A file or a pipe holding
0x7F holds a byte somebody wrote, not a key somebody pressed.

Three checks in terminal.sh under a pseudo-terminal whose VERASE is set
on the slave side: erase at 0x7F arrives as Backspace, erase at 0x08
still does, and a 0x7F read from a file is left alone. The last one is
what fails on the obvious wrong fix of translating 0x7F unconditionally,
which was verified with break.sh along with the fix's removal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 11:56:17 -04:00
AnachronautandClaude Opus 5 019c93a587 The console draws a tab instead of dropping it
consoleDraw gave meanings to newline, carriage return and backspace and
dropped every other byte below the first glyph. A tab was one of those,
so it left no mark on the screen at all - while the same byte went down
the serial line, where a host terminal laid it out perfectly.

That is why a tab separated file read correctly and displayed wrongly.
Type and More were never at fault: they hand the file's bytes to the
console unchanged, and the console is where the tabs stopped. An
assembler symbol table came out with its fields run together.

A tab now moves the cursor to the next stop, eight columns apart, and
wraps when the next stop would reach or pass the last column - which is
what an ordinary character does at the edge, rather than a rule only
tabs obey. It MOVES rather than writing spaces, the way a terminal does:
a carriage return followed by a tab steps over what is on the line and
leaves it.

Kept in the console rather than expanded by Type, More, and every future
program that prints text.

Three checks in video.sh, each of which fails on a different mistake: a
tab renders the same screen as the spaces it stands for, one that runs
off the edge renders the same screen as a newline, and sixteen letters
tabbed across still have their ink - which is the one that fails if a
tab is implemented by writing spaces. Verified with break.sh both ways.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 11:55:23 -04:00
AnachronautandClaude Opus 5 7dca141aae The vector's number goes beside its kind, not at the end of the line
Kind and Number together are what names a thing: Vector 16 and Device 32
are identifiers in a way that Vector on its own is not. Everything after
them - where it lives, what it is called, where it was written - says
something about it rather than naming it, so with the number at the far
end a line opened with a bare kind and closed with the fact that would
have told you what you were reading.

Fields are now Kind, Number, Address, Name, File, Line. A label still
carries a dash where its number would be, which reads as "this kind is
not numbered" rather than as a field that went missing.

Manual and the docs check follow. Verified with break.sh by swapping the
number and the address back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 11:03:11 -04:00
AnachronautandClaude Opus 5 f3d8985bc4 Vectors in the symbol table, with both of the numbers they have
A vector is the one thing about a program that nothing else can tell
you. A pinned vector has its number in the source that pinned it, but a
vector the assembler numbered has that number nowhere at all - not in
the source, not in the binary in any form a reader can find. Until now
there was no way to learn that a vector became number 64.

It also cost two hops to follow by hand. The name in "SWI osPrintString"
is not the name of the routine that implements it, so finding the code
meant searching for the vector, reading the handler's name off the
Vector Segment, and searching again. A vector row now names the handler
and gives the line the two were tied together on.

Both numbers, at the user's asking, because neither can be worked out
from the other without knowing which table the vector is in: the Number
is what a program writes and the machine dispatches on, the Address is
where the handler's address is stored, base plus twice the number. The
slot is computed with the same expression the loader is given, so what
the table says and what gets written there cannot drift apart. A vector
a program only declares is listed too - that is how a program says which
vectors it calls, and how two programs can be checked against each other
for agreeing about a number.

A device has no name of its own, being named by the port it is plugged
into, so it is listed under its handler.

The first field is now Kind rather than Memory, because Vector and
Device are not memories. Sorted Program, Data, Vector, Device.

docs.sh checks the six fields against the manual and against real dumps
of two programs - Keys, a loadable program with all four kinds, and
cosmos, a boot image whose segments both start at zero. It now also
checks that a row's name really appears on the line the row names, which
is what catches the string-newline bug fixed in ca6c8ca coming back.
Verified with break.sh four ways: wrong slot arithmetic, vectors
dropped, a field renamed in the manual, and that bug reintroduced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 10:51:50 -04:00
AnachronautandClaude Opus 5 ca6c8ca6ad A newline inside a string is still a newline
readToken consumed a string literal character by character and counted
none of the newlines in it, so every line number after a multi-line
string was short by one - and by one more for each one after that.

cosmos.asm has seven such strings spanning thirteen lines, so by the end
of the file the assembler was reporting labels thirteen lines early.
That was never only a cosmetic problem: the same counter is what every
error message names, so an error anywhere after the help text was
pointing at somebody else's code, thirteen lines away, with no sign that
it was doing so.

Found because the symbol table started publishing line numbers, which
made a wrong one something you could look at. Verified against all 797
of cosmos.asm's labels, every one of which now names the line it is
actually on. The check that keeps it that way comes with the next
commit, which is what gives it something to check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 10:51:36 -04:00
AnachronautandClaude Opus 5 3527812c41 A symbol table says which memory, and where the name was written
The dump was an address and a name. Both of the questions it gets asked
were only half answered.

"What is at this address" was ambiguous, because Program and Data are
separate memories and an address alone does not say which one. That is
easy to miss in a loadable program, where the segments are usually based
far apart - and immediate in a boot image, where both start at zero:
replCalculator has a Program 0003 and a Data 0003 and the old file
printed both as "0003 <name>".

"Where is this defined" was not answered at all, and it is the one that
matters more as a program grows. A name defined once and called in forty
places is hard to find by searching. Lander's table names five files
besides its own; CosmOS and its libraries define over a thousand names
across a dozen.

So: memory, address, name, file, line, separated by tabs, sorted by
memory and then address with Program first. Tabs because that makes it a
table cut, awk and sort already read, and no heading line because
nothing should have to know to skip one. Everything needed was already
being passed to addLabel and thrown away; the file name points at the
copy the include list owns, which outlives the label table.

The manual describes the five fields, and docs.sh now settles that
description against a real dump - the shape, not the values, so that an
example cannot go stale and turn editing a program into editing a
manual. Verified with break.sh three ways: a reordered field, a dropped
field, and a field renamed in the manual.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 10:21:06 -04:00
AnachronautandClaude Opus 5 7a55cfe151 Say that an option's file is one the assembler writes, and check we said it
-S was added without a row in the Assembler Manual, and the usage it
printed listed a bare "-S <file>" with no long name and no statement of
what the file is for. That is not merely incomplete, it is misleading:
"-S <file>" reads just as naturally as "dump the symbols of <file>", and
asking for it that way hands the source to -S, leaves nothing positional
behind it, and is answered with "No source file specified" on a command
line that plainly names a source. The error described the hole the
mistake left and hid the mistake.

So the usage now prints the long names, says outright that every <file>
is a path it writes and the source is the last argument on its own, and
ends with a whole example command. When the source is missing and a
file-taking option was given, the error says which options take a path
to write. The manual gains the -S row it never had, a warning in the
same words, and a sentence on what a symbol dump is for.

Documenting it twice is how it went wrong once, so docs.sh now settles
both against getopt's own option table: every option the assembler takes
has a row in the manual and a line in its own usage. Verified with
break.sh against the manual row and the usage line separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 09:40:21 -04:00
AnachronautandClaude Opus 5 cdee9acfae break.sh believes a suite's exit status, not the shape of its output
It decided whether a suite noticed by grepping its output for a
"N passed, M failed" summary line. That is right for the suites that
print one, since they print it only when something failed - and
impossible for the three that never print one at all. docs, terminal
and voyager report in their own words, so a break any of them caught
loudly was reported as "NOTHING CAUGHT THE BREAK".

That is the one wrong answer the tool exists never to give, and it was
turning up in a third place: the header already tells the story of the
first two. It made the whole docs suite unverifiable by the harness the
project uses to decide whether a check is worth having.

Every suite already exits nonzero when it fails, so that is the signal
now. The summary line is still printed where a suite keeps a count, and
the suite's own last line stands in where it does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 09:40:10 -04:00
Anachronaut d361ea1e46 An LFO belongs to its channel, not to the whole device
The two LFOs lived in the Synth, so four channels shared them and whichever patch
loaded last owned them for every voice at once. A sound with its LFO switched off
silenced the trill under a sound that was still playing - which is what made Lunar
Porter's low fuel warning intermittent: the first landing, docking or crash of a run
took its trill away, and it was right again next time the machine started.

The engine fix went upstream to soundThing and has come back. synth.c and synth.h
are re-vendored at 71e3cb2, character for character bar the ASCII transliteration,
and now carry two changes: the LFOs moved into the Voice, and synthSyncVoices
carries a free LFO's cycle down alongside its rate. That second hunk does nothing
here - it only matters to a caller that syncs voices, and this device never does,
because syncing would flatten four channels into one instrument. It is taken so the
vendored file stays identical in both trees, and it is commented as such.

Upstream also found a bug in the original patch, in patchLoad, which is soundThing's
own file and does not travel.

Downstream the LFO parameter groups 0x60 and 0x70 now read the selected channel like
every parameter beside them, so an LFO written to one channel is inaudible on the
other three. Everything else about the device is unchanged.

Lunar Porter keeps loading each patch immediately before its note, but for the
smaller reason that now applies: the bang and the latch share channel three, and a
channel used by two sounds has to be told which of them it is about to be. The
comment that said otherwise, and the manual's warning about sharing, are rewritten
as history rather than as a caveat.

Tests/sound.sh's shared-LFO check is inverted to assert the fixed behaviour, with a
third leg added: after proving another channel's patch leaves this one alone, it
switches this channel's OWN LFO off and requires the pitch to move. Without that,
both checks would pass on a device where writing an LFO did nothing at all. Routing
either group back to voice 0 is caught.

Cost, measured: four channels sounding continuously for 400 seconds of audio takes
5.0 s of wall clock against 4.59 s before, about 9% of total emulator time. Half of
that is wasted on voices that cannot sound, since VOICE_COUNT is 8 and there are
four channels; recovering it would mean diverging the vendored file, which is not
worth it at this price.
2026-09-04 22:20:26 -04:00
Anachronaut 3ca5f193e6 The warning's trill, and why it only sometimes came out
An LFO belongs to the DEVICE and not to a channel. There are two of them
against four voices, and a patch carries LFO settings the way it carries
everything else - so whichever patch was loaded last owns both of them,
for every voice at once.

The warning's trill is a saw LFO on the pitch. The patches on channel
three, the bang and the latch, carry an LFO that is switched off, and
they load at the moment they are used. So the first landing, docking or
crash of a run took the trill away and left a plain tone, and it was
right again next time the machine started. Correct until something
unrelated plays is the worst shape a fault can have.

The same thing had already happened silently at startup: the instruments
were set up in order, so the warning's LFO settings, written last, sat on
top of the rumble's and the rumble never had its own at all.

So nothing is set up once any more. Each sound loads its patch
immediately before its note - forty-odd writes at a moment already making
a sound - and is then whatever its patch says, whatever played before it.
Measured after a landing: 1056, 660, 516 hertz, then up to 1698 and down
again. Two sweeps of the saw, which is the trill.

What it does not fix, because it cannot: two sounds overlapping still
share the LFOs, so a warning going off mid-burn re-tunes the rumble for
as long as it lasts. With two between four that is the device.

Three checks at the device level, where the trap can be stated exactly: a
routed LFO bends a pitch (184 Hz against the 262 the note asked for),
another channel's patch takes it away (272, the note itself), and saying
it again gets it back (184). Written up in the Programming Manual beside
the LFO mode, since the next program to want two sounds will meet it too.
2026-09-04 19:44:02 -04:00
Anachronaut 5e85356245 Every thruster that catches pops, not only the first
A pilot already burning upwards who then adds a sideways thruster has lit
an engine, and that is what an engine lighting sounds like. So what is
watched is now the SET of thrusters rather than whether any of them is
lit: the bits on now that were not on before, which is an exclusive or
and an and and no comparison at all.

The rumble still asks the old question, because it is the right question
for it - struck when the first lights and not again until every one has
gone out. Restriking it when a second joins would start its attack over,
which is a stutter rather than an engine, so it keeps a flag of its own.

Pitches are as tuned by ear: 36 for the pop, 60 for the rumble.

Checked by two flights of the same length, one thruster held throughout
against one that gains a second in the middle. The rumble is identical in
both, so the whole of the difference is the extra pop: peak 11,452 against
15,540, and the recordings diverge at 3.19 seconds, which is the frame the
second thruster lights.
2026-09-04 18:30:17 -04:00
Anachronaut 388faafd04 A thruster you can hear, and a latch on arriving and leaving
Three patches, and the last of the sounds that were asked for.

The thruster bangs when it lights and rumbles while it burns. The rumble
is this game's first HELD note: its gate goes down when a thruster lights
and does not come up until every one is out. Only the edges matter - a
rumble restruck every frame would never get past its own attack, and a
bang struck every frame is a buzz - and the condition is the flames':
a held button with a dry tank is a pilot doing nothing.

Arriving latches two notes quickly. Middle C then the C above for taking
hold of the station, the same pair reversed for letting go, and two
octaves lower for the ground - the same shape in a different register,
because setting down and taking hold are the same kind of event. The
second note is PENDING rather than played: at an undocking the pilot is
mid-burn, and stopping the world for an eighth of a second to fit a note
in would be felt as the controls sticking. Where the game is stopping
anyway, runPend simply pumps it out.

Four channels for five sounds. The bang and the latch share one, because
a lander arriving either arrives or does not, and a crash ends the run.
That leaves the thruster its two, which it needs: a held note struck on
the same channel as the ignition bang would cut the bang off at the
moment it was meant to be heard.

---- A held note outlives the loop that was holding it ----

Landing while the thruster was still down ended the flying and then
waited to be told the message had been read, so the frame that would have
noticed the button coming up never ran. The engine roared under the
verdict and went on roaring until the machine stopped. Anything that
stops to wait hushes it now, and forgets last frame with it, so a thruster
still held when the waiting ends counts as lighting again.

---- Three checks that had to be rebuilt around the new noise ----

A landing is not silent any more, so the crash is measured against the
second BEFORE it rather than against a quiet landing. The dust samples
moved eight frames later, because the latch plays first. And the warning
check lost its measure twice: counting bursts could not tell a beep from
a nag, and measuring total length stopped working the day the thruster
got a rumble. It burns, stops, and listens AFTER - warned once there is
nothing left, nagging the last beep is still fading. Nought against 5,679.
2026-09-04 17:34:16 -04:00
Anachronaut 6fe898b5fc A quarter-tank warning that says it once, and a gauge that keeps saying it
Two halves of the same number. The warning is the moment it happened and
the colour is how things stand: it fires on the way down through a
quarter of a tank and then holds its peace, and the gauge stays red until
a base fills the lander up, which also allows the warning again.

Once, because a lander is at its most careful in the last few seconds
before it touches, and something repeating in its ear through that is not
a warning, it is a distraction.

Checked where the fuel actually moves rather than once a frame - the tank
only changes in takeFuel and payFuel, so there is nowhere else it can
cross a threshold.

Channel two, with the crash on three. Separate channels rather than one
reused, because a crash while the warning is still sounding should not cut
it off, and on a machine with four voices there is no reason to be clever.

Half and a tenth are the obvious next thresholds and are deliberately not
here: one is enough to find out whether being told at all is welcome.

---- And the crash sound is the one that was designed for it ----

Crash.json, which carries voice_gate itself, so the program's own trigger
write is gone - it would have masked a deliberate choice rather than
backing one up. The note moved from 24 to 60 by ear, and the comment
saying it was low went with it.

---- A check that could not tell a beep from a nag ----

Counting bursts of sound cannot: without the latch the warning fires
every tick the tank drops, far faster than the sound decays, so the beeps
run into each other and a burst counter sees one long burst either way.
It measures the LENGTH now - 26,944 samples against 490,348 - which is
the difference between six tenths of a second and ten seconds of it.
2026-09-04 16:45:26 -04:00
Anachronaut de765267c5 Lunar Porter's crash is a designed sound now, not a guessed one
Crash1.json, made in soundThing and converted by SoundPatch. Two noise
oscillators fifteen cents apart, a filter opened 11.6 kHz by the
modulation envelope, and the LEVEL shaped by that same envelope rather
than by the amplitude one - which is exactly the freedom the routed level
was for, and not a thing the engine could express a week ago.

It peaks at 10,501 and is gone in about six tenths of a second, inside
the eight tenths the pieces are in the air. The guessed one wandered
between 1600 and 8200 and sounded like a drain.

The patch predates the gate field, so the program's own trigger write is
what makes it a one-shot - which is the argument for having put it there
rather than leaving it to the patch. LowFuel.json is here too, saved from
the newer soundThing with the gate and LFO mode in it, and is not wired
to anything yet.

Both JSONs live beside the tables they produce. The build has never
needed soundThing and still does not, but a sound can now be regenerated,
or simply read, without going to find the editor first.
2026-09-04 16:13:44 -04:00
Anachronaut b2ff8d64e5 Fold soundThing's changes back down, and expose the two new switches
The three changes that went up came back as part of soundThing, along
with two more that they made possible. The engine here is now b73e5c0
character for character, except that em-dashes and arrows in comments are
written as ASCII because this tree is ASCII only - a local rule, not an
improvement, and not sent up.

So synth.h's "what was changed" list is gone. There is nothing to list:
what has to be kept current is only that if either copy changes, the
other one has to be told.

---- What came back ----

A VOICE CAN END ITSELF. Naming the level's source said what shapes a
voice; nothing said what ends one, so the only thing that could ever
finish one was a key coming up. A game is nearly all one-shots and not
one of them wants its length decided by how long a note was held. Exposed
as parameter 0x51: 0 gated, 1 triggered.

AND A ONE-SHOT IS THE SAME ONE-SHOT TWICE. A triggered voice re-arms its
oscillators, and an LFO can be told to start over with each voice -
parameter 3 of either LFO. Both halves are needed and the check proves
it: with the LFO left free, two triggered hits still differ.

Their note warned that whatever applies a patch to a channel has to set
these or they hold synthInit's defaults. Checked: Voyager never calls
synthSyncVoices, so their 0001 is a no-op here as they predicted, and
nothing reaches into an LFO's phase, so the struct split is safe.

---- What it is for ----

Lander's crash is a triggered voice now, so boomOff is gone. Nothing has
to remember to end a bang.

SoundPatch learnt voice_levelSource, voice_gate and lfo<N>_mode, which
the new soundThing writes - without that it would have refused every
patch saved from it, since an unknown field stops the tool on purpose. A
patch from before those fields still converts, and says in its own
comments that it predates the level routing.

Three checks, each seen to fail on its own break: a gated voice still
sounding with nothing holding it, a triggered one down to nothing with no
gate ever dropped, and two hits identical sample for sample.

One test bug worth keeping: the first version of the repeatability check
struck the second note while the first was still ringing, so what it
found and compared as "the second hit" was a point in the middle of the
first one's tail. It now looks for sound after SILENCE rather than sound
after an offset.
2026-09-04 16:02:40 -04:00
Anachronaut a8b6b09a59 SoundPatch: design a sound where it can be heard, then convert it
The bang was guessed at directly in bytes and came out as a low gurgle,
which is what guessing at bytes gets you: a cutoff of 40 looks small and
is 57 Hz, so the filter sweep ended almost shut. Voyager's sound device
is soundThing's voice engine with the editor taken off, so a patch
designed in soundThing - where there is a screen, a keyboard and a pair
of ears - makes the same sound here. What differs is only how it arrives.

SoundPatch converts one into the other. Every parameter the device takes
is a documented function of a natural value and all of them invert: times
are squared into four seconds, cutoff and rate are exponential, depths
and detune are centred on 128. It writes a table of a count and that many
parameter and value pairs, and playPatch hands it to the device - so
every sound after this costs a table and a call rather than forty lines
of its own.

Two things the conversion has to say out loud. soundThing has no field
for the level routing, because there it is always the amplitude envelope,
so the table says so explicitly - a channel keeps its patch between notes
and a leftover from a previous one would otherwise be carried in. And a
field the tool does not recognise STOPS it: a patch format that has moved
on would otherwise produce a table that quietly means something else.

THE BUILD DOES NOT DEPEND ON IT. soundThing lives in its own repository
and is not needed to build anything here; the tables are checked in and
the tool is for when a sound is being changed.

Lander's crash now plays Kick808 as a stand-in until a bang is designed
for it, and the difference is the point: the hand-guessed patch wandered
between 1600 and 8200 for eight tenths of a second, and this decays
3492, 2743, 2037, 1515, 1040, 614, 87, nothing.

The docs check caught the tool count in two manuals, which is what it is
for.
2026-09-04 13:00:18 -04:00
Anachronaut 9ae59bfccb A bang for the crash, which is this game's first sound
Noise through a low pass that the modulation envelope shuts as the level
falls, so the bright part is only at the front of it: a boom rather than
a hiss. Noise because every other waveform here has a pitch, and a
pitched bang is a note.

The instrument is built at startup the way the tiles are, and a crash
only says "this channel, this note". That is what the selector and value
registers are for - a patch is twenty odd writes and a note is two - and
it is the shape every sound after this one should take.

CHANNEL THREE. There are four, and effects count down from the top so
that music, if it ever arrives, can take nought and count up and the two
never have to negotiate. This is also the first program to drive the
sound device while also doing something else; the only other customer is
the patch editor, whose whole job is the device.

---- A byte of envelope is not seconds ----

It is squared and scaled to four of them, so the decay first written here
was 200 - which is two and a half seconds. Over the eight tenths of a
second the pieces are in the air that is not a bang fading, it is the
FRONT THIRD of one, and it both sounded and measured as a flat wash of
noise. Ninety is about half a second and it fades to silence with time to
spare.

Two checks, each seen to fail on its own break. The first is against
SILENCE - a landing in the same conditions makes no sound at all, so it
is measuring the crash and not the machine humming - and the second is
that it is louder in its first half than its second, which is the
difference the decay was getting wrong.
2026-09-04 12:35:29 -04:00
Anachronaut 0264b19a3d Dust on landing, gas on letting go, and a lander that stays let go
One particle system, three uses now: a lander coming apart, dust kicked
up by a landing, and gas out of a docking port on release. Where they
start, how fast they go, what colour they are and how long they last are
arguments; everything else is shared.

Dust goes sideways and UP off the lander's feet, because that is where
kicked dust goes and there is ground in the way of the rest of it. Gas
goes evenly in every direction, because nothing is in the way of a
docking port. Neither happens on docking - a dock is a catch and not a
touchdown, and there is nothing under it to kick.

---- Ticked from the frame loop, not run in place ----

The explosion can afford to stop the world; there is nothing left to fly.
The undocking puff cannot, because it goes off on the frame a thruster is
pressed, and freezing a quarter of a second exactly then is felt as the
controls sticking. So a burst advances one frame at a time from the main
loop, and the two that can afford to wait just pump that same tick until
the air is clear.

---- And letting go did not let go ----

Which the puff is what found. A docked lander sits EXACTLY one tile under
the station, so releasing upwards moved it towards the station and it
docked again on the very next frame: took the fuel again, said so again,
and waited to be told the message had been read - which reads as the
controls locking up the instant they are used.

It has to get clear now before it can take hold again, and the two
distances have to differ: docking wants one tile, re-arming wants two.
A single distance re-armed on the frame it let go, because a tile is
exactly where it was sitting.

Three checks, each seen to fail on its own break. The last of them
measures HOW FAR the lander has got and not merely that it moved: a
sixteen frame pause on release still leaves it climbing, four rows short
of a free run, so "it moved" would pass for a stall that has been slept
through.
2026-09-04 12:23:31 -04:00
Anachronaut 115efa1fa1 A crash takes the lander apart, instead of just saying so
The verdict used to be the whole of it: a line of text and a lander still
sitting there in one piece, so somebody watching a recording had to read
the words to know what had happened. That is the same problem the flames
were for.

The lander goes, and both flames with it, and six pieces of it leave in a
rough hexagon at its own colour for about a second. Then they go too,
rather than hanging over the words. The world is not running while it
plays: it is a loop of its own, so nothing else in the program has to
know how to be half destroyed.

Yellow, which is the lander's own colour, because it IS the lander - and
it is only free to use because the lander itself is hidden by then. The
check that counts exactly forty pixels of yellow looks at a flying frame.

---- Two mistakes worth keeping ----

The block went in between touchdownCrash and touchdownStop, so a crash
fell into the explosion and returned from there - no verdict, no end of
run, and the lander sitting there being crashed into the ground again
every frame. The linter caught it as a subroutine nothing called walking
into, which is exactly what it was.

And copyWord goes DP0 to DP1, so setting the pieces off from the lander's
position had the pointers the wrong way round: it copied the empty pieces
OVER ShipX. Since that is in the view block, it took the lander's own
column with it, and the one piece that could be seen drifted out of the
top left corner of the screen.

Three checks, each seen to fail on its own break. They measure the SPREAD
and not the count: two of the six leave the top of the screen on the way,
so the count drops from twenty four to sixteen, which is correct and
would make an exact count a check that breaks the day a lander crashes
somewhere else.
2026-09-04 12:06:00 -04:00
Anachronaut c408fc6cf6 Thruster flames, so a watcher can see what the pilot is doing
Every reading on this screen is a number drawn as a bar - how fast
sideways, how fast down, how much sky, how much tank - and all of it says
what is happening TO the lander. None of it says what the pilot is doing
about it, so somebody watching over a shoulder has to read gauges to work
out that a thruster is even lit.

A plume hangs off whichever side the engine is pushing from: under the
lander to lift, over it to retro, and on the far side from the way it is
being pushed sideways, since that is the side the gas leaves. One tile
does up and down, because a vertical flip turns one into the other, and a
second does the sides, because a flip cannot rotate a tile a quarter
turn. Twenty four pixels each, on purpose, so a count of them means
something.

HELD, NOT FIRED. The engine fires one frame in ten, because that is the
tick gravity is applied on, and a flame that honest would be one frame of
light six times a second - a fault lamp, not a rocket. What is drawn is
the button being down, which is the truthful answer to "is the pilot
burning": the tick is how the sum gets done, not what is happening.

An empty tank draws nothing, and nor does the retro thruster on the
ground, because in both cases the button really is doing nothing. The
second of those was a lie the first version told.

Red, and that is by elimination again: white is the ceiling warning, cyan
the landing pads, magenta the instruments, blue the station, yellow the
lander itself - and the lander is counted as exactly forty pixels of
yellow, so a yellow flame would have broken it.

Three checks, each seen to fail on its own break. Two things the fixtures
taught: a lander placed at the world's origin sits BEHIND the two row
window, which reads exactly like a flame that is not drawn; and red has
to be looked for in a box round the lander rather than a column, because
the speed bars go red and one of the four pads is red too.
2026-09-04 11:49:38 -04:00
Anachronaut 418631a221 The orbit check is back, and osFileRead says it reads in blocks
Placing a state retired the reason the orbit check was deleted. Reaching
a given orbit through the controls takes a sustained burn while holding
height, and the phase of that burn against the gravity tick - one frame
in ten - decides whether the thruster is seen at all, so two pad files a
frame apart fly differently. The old check passed against one disk and
failed against another, which is a check measuring the boot time rather
than the physics.

Placed at eighty sideways it climbs to row 51, falls to row 190, and
climbs again to row 20 - and the turning points are BROAD, tens of pixels
across, so the samples have nothing like the margin problem the old one
had. Three claims: it climbs, it turns over on its own, and it comes
round again no lower than the first time.

Both halves of the mechanic are separately caught. Without the outward
push it sinks and lands and never climbs; without the exchange it climbs
away and never comes back, which is the one way trip the whole thing
exists to prevent.

---- And osFileRead writes whole blocks, which nothing said ----

A disk is read a block at a time, so a sixteen byte file still puts 256
bytes where it is told to. Reserving exactly the file's length writes
over whatever follows - a quiet corruption rather than a refusal, and it
looks like a bug somewhere else entirely. It cost an afternoon here: the
state buffer sat in front of the view tables, so the program read its
state, wiped the numbers every gauge draws from, and left immediately.

Said now in services.asm beside the vector and in the CosmOS manual,
along with the pattern that works: reserve the length rounded up to the
next 256, read into that, and copy the parts wanted where they are wanted.

The orbit fixture also needs its own keyboard file. The shared one holds
a key down every forty eight bytes for the held-thruster check, which
would fly this orbit as well as measure it.
2026-09-04 11:31:33 -04:00
Anachronaut 7e82b64d47 A dock that slides into line, and settles squarely on the port
Two things, and the second is the one that was actually wrong.

The lander is slid into place at half a pixel a frame rather than put
there. A dock is allowed eight pixels out in either direction, so
snapping moved it a whole tile in a single frame - a jump, at the very
moment the player was being told they had been careful. The worst gap
closes in about half a second, which reads as the two of them settling
together.

And it settles SQUARELY now. It used to come to rest four pixels out
however carefully it was flown, because the lander is drawn from half a
screen less half a tile - which is what centres an eight pixel lander on
the middle - while the station was drawn from half a screen exactly, so
its left edge sat where the lander's centre was. Both come off the same
origin now, and a gap of nothing puts one exactly above the other.

stationGap is factored out along the way. Three callers wanted it: the
one that draws the station, the one that decides whether it can be docked
with, and now the one that slides the lander in under it. The sideways
ease goes through that wrapped gap rather than the raw positions, because
a dock made either side of the moon's seam has a raw difference of most
of a moon.

---- And the fixtures moved to frame 400 ----

A dock waits to be told its message has been read, and reading a state
file costs a disk read, so a placed run starts a good deal later than a
plain one. The acknowledgement was at frame 100 and stopped working the
day the state file arrived: the program had not reached the dock yet and
the press went by unheard, which shows up as every sprite missing and
reads like a drawing bug. Everything after it is sampled well clear of
both ends.

Two checks, each seen to fail on its own break - the alignment settles at
4,-8 without the shared origin, and the slide reads 0,-8 the whole way
without the easing. Which of the two axes each one actually watches is
written down beside them, because it is not the one you would guess.
2026-09-04 11:23:54 -04:00
Anachronaut 1928275f87 A state can be placed, which four things were queued behind
Lander reads sixteen bytes from /lander.state if the disk has one and
starts from those: position, velocity, where the station is, fuel, and
which screen to be in. A disk without the file is the game as it always
was, which is every other flight in the suite.

It exists because some states cannot be flown to. A successful dock needs
the lander alongside the station and matched, and NINETY SIX pad files
failed to get there - not for want of trying, but because climbing spends
sideways speed, so a lander cannot rise while matched and arrives slower
than orbital every time. Reaching it wants two burns and a phase. The
orbit check had already been deleted for the same reason, and the strike
check was flown on a nineteen frame window, which is the sort of fixture
that ends up measuring the boot time rather than the physics.

Binary, because that is what a file is on this machine. A tool to build
one from readable text is a small job for another day; until then the
tests write the bytes with the fields named, which reads plainly enough.

---- The buffer is a whole block, and that is not caution ----

osFileRead lands a file in Data Memory and a file is stored in blocks of
256, so reading sixteen bytes into sixteen bytes of room writes over
whatever follows. It did: the first version put the buffer in front of
the view tables, and the program read its state, wiped the numbers every
gauge draws from, and left immediately - "finished" and back to the
prompt, with nothing on the screen to say why.

Four checks, each seen to fail on its own break: a state file places the
lander (row 192 and twelve cells of gauge, against thirty one with no
file), a matched approach docks and is paid once and not once a frame, it
then rides the station a tile under it, and the same approach unmatched
is a wreck. The flown strike fixture and its narrow window are gone.
2026-09-04 10:51:01 -04:00
Anachronaut 804dd3040e Docking, and a ceiling that is no longer the old screen's edge
The station can be docked with: close enough, and slow enough RELATIVE TO
IT, or it is a wreck. Its speed is orbital speed, which is what the marks
on the drift bar point at, so the instrument for this was on the screen
before there was anything to dock with. A dock pays eighty units of fuel,
once and not once a frame, and holds the lander a tile under the station
until a thruster lets go - checked before the speeds are put back, or a
burn would be wiped on the frame it was made and the lander could never
leave.

---- And the ceiling went up, which is what made it work ----

Sixty four pixels was the whole of the sky a forty column screen had over
the world's origin. It was never a fact about the world, it was a fact
about the view, and the wide one starts twenty four rows higher: a lander
stopped at the old line was stopped a long way short of the top of its own
picture for no reason it could see. It is 192 pixels now, the top of the
wide view.

The station went from four rows up to twelve - about two thirds of the way
from the ground to the ceiling. At four it sat exactly where anything
climbing away from the surface had to pass, and being run down there is
not a hazard, it is a toll: ELEVEN of this suite's flights were being run
down as collateral, including the ceiling check, which has to climb past
it to reach the ceiling at all. Moving both fixed all eleven at once.

The height bar divides by sixty four rather than thirty two, because the
band it describes is 368 pixels now and half a pixel of bar to a pixel of
sky would stand 184 tall and run off the top of the forty column screen it
is drawn beside.

---- What is checked, and what is written down instead ----

The wreck is flown. The fixture is narrow - the window measured 496 to 514
frames of climb and it sits at 505 - and that is said in place, along with
the instruction to re-measure before believing the code is broken. It was
not always narrow: at the old altitude any climb from 135 to 300 frames
struck. Narrow is the right way round, because it means the station is
hard to blunder into.

The successful dock is NOT flown, and ninety six pad files failed to find
it. That is not the search's fault: climbing spends sideways speed, so a
lander cannot rise while matched. It is measured working instead - tank
100 to 180, message up, and the pair holding together one tile apart for
two hundred and forty frames.
2026-09-04 10:37:52 -04:00
Anachronaut 922511c8a7 A retro thruster, at half the strength of the one that lifts
Arresting a rise meant a sideways burn and a wait for the orbit to come
back round. That is how a rendezvous really is flown and it is a lot to
ask of somebody who has not flown one before, so down now makes the
correction directly.

HALF THE STRENGTH, deliberately: one sixteenth a tick against two, which
is exactly gravity's own step. So it can stop a climb and it can hurry a
descent, and it can never turn a landing approach into a crash faster
than simply letting go would - the cheap way out of a mistake stays the
expensive one. Four against eight on a keyboard, which is the same ratio.

It does NOTHING to a lander on the ground, and that guard is load
bearing rather than tidy. touchdown has already had its say and returns
early once a lander is down, so there is nothing underneath to stop it
and no crash to say it happened: measured without the guard, holding it
drives the lander clean off the picture and spends a fifth of the tank
doing it.

---- And the fixture that hid all of that ----

The first version of the landed check passed just as happily with the
guard deleted, and the reason is worth writing down. A lander that has
landed WAITS to be told its message has been read, and the keyboard
fixture pads with NULs, which are not keys. So the program sat in that
loop for ever and the picture was frozen at the moment of touchdown -
every thruster held afterwards did nothing, which reads exactly like a
working guard. The pad now presses A to get past the wait before the
check tests anything, and both the row AND the fuel are read, because an
engine that fired and moved nothing would keep the row and one that moved
the lander for free would keep the fuel.

The altitude bar check upstream leans on that same freeze for its stable
end, and its comment said "stays landed" when what it means is "is not
running any more". Corrected, because that is the sort of comment that
sends the next person looking in the wrong place.
2026-09-03 23:02:19 -04:00
Anachronaut 800f555ffb A station in orbit, going round and not yet dockable
S1 of the station: it exists, it orbits, it wraps, it is drawn. Docking
is deliberately not here - the point of stopping at this rung is to fly
up and find out whether matching a four pixel a frame target feels good
before any rules are written about what happens when you reach it.

IT NEEDS NO PHYSICS OF ITS OWN. A body at 64 sixteenths is exactly what
a circular orbit is under the rules already here: the pull and the swing
cancel at that speed at ANY height, because gravity never falls off and
the moon is a cylinder that does not rotate. So the station is a
position, a constant, and the same fourteen bit wrap the lander uses.
That also means there is no prograde or retrograde to choose - nothing
privileges a direction, which is what makes a second station going the
other way a thing that can exist later.

Four rows above the world's origin: off the top of a forty column screen
and comfortably inside a wide one, so it is somewhere to go that the zoom
is needed to see. It starts half a moon away and a lap is 256 frames, a
little over four seconds, so it has to be found but will not stay lost.

Its column is the middle of the screen plus how far round it is from the
lander, wrapped to fourteen bits - which measures the long way round
whenever it is behind, so anything past the half way point becomes a
negative offset instead. Off the edge needs no test at all: a sprite's X
is signed and sixteen bits, so a station three hundred pixels to the left
is asked for at minus a hundred and forty and the device declines.

Blue, and that is not taste. White is counted to find the ceiling
warning, cyan to find the landing pads, magenta is the instruments and
yellow is the lander. Blue is the one ink no check measures, and picking
a measured one has broken a test twice already.

toPixelsSigned is factored out of showLander, since the station wants the
same sign-extended conversion.

Four checks, each seen to fail on its own break. Measured going round at
244 pixels in sixty frames, off the far side of the moon, and back on the
other edge.
2026-09-03 22:41:28 -04:00
Anachronaut 32559ce872 The room a zoom buys goes to the sky, not to the moon
Zooming out drew fifty rows DOWN from the world's origin, which put
exactly the same sky on the screen as before with twice as much moon
under it. Measured: 47 per cent rock zoomed in and 71 per cent zoomed
out. A zoom that shows you more of the thing you cannot fly through is
not worth a button.

The eighty column screen is fifty rows and the flyable band is thirty -
the ceiling is eight rows above the origin and the deepest valley is
twenty two below it - so the twenty rows a zoom buys have to go
somewhere. They go above. The wide view starts twenty four rows over the
origin, the ground sits near the bottom, and the whole band is on the
screen: 23 per cent rock instead of 71.

Which needed three things. The moon is drawn from row minus twenty four
rather than from nought, because rows above the origin are sky by
definition and because whatever the shell left in them is otherwise still
there. The row origin comes out of the view block like every other screen
number. And the lander's own Y moves with the view, which meant making
toPixels' answer SIGNED at last: it masks to twelve bits, so a lander
above the origin came back as a large positive number rather than a small
negative one - harmless while such a lander was off the picture either
way, and wrong the moment the view moved up to include it.

That is the point of the button. A lander at the ceiling is off the top
of a forty column screen, which is where the orbit lives and why the
altitude bar had to exist; zoomed out it is at row 149 and you can watch
the whole orbit.

Four checks, and the proportion is deliberately not on its own: measured
alone it passes for a moon floating over a void, because pointing the
view back at the origin leaves the rows under the terrain simply never
drawn, and black counts as sky. Both breaks that matter went straight
through it. What catches them is that the ground has to reach the bottom
of the screen and the sky has to be empty - the latter only on a shell
scrolled a hundred and eighteen lines deep, since that is what it takes
to get anything into the rows the wide view moves into.

The tap fixture also moved to frame 100. Fifty rows of moon take longer
to draw than twenty five, and a six frame tap at frame thirty now lands
before the program is reading a controller at all, which reads exactly
like a button that has stopped working.
2026-09-03 22:14:02 -04:00
Anachronaut 437ddf8ebe z zooms too, so a keyboard is not shut out of it
B was the only way to swap the view, and the game is meant to be flyable
without a controller - the arrows fly it when there is no pad. Somebody
without one had no way to zoom at all.

Tested ABOVE the pad test rather than beside the arrows, and that is the
distinction: which way the lander is flown is a question a controller
answers better, so the arrows stand aside for one. How much of the moon
is on the screen is not that kind of question, and a player with a pad
may still have a keyboard in front of them.

No edge to remember here either. The console delivers a key ONCE, which
is the whole difference between a key and a held button.

The check puts the z forty bytes into the keyboard file, because the
console hands over one key a frame: a z two hundred bytes in is a z two
hundred frames away, which is past the end of the capture and reads
exactly like a key that does nothing. It cost a wrong answer first time.
2026-09-03 21:54:12 -04:00
Anachronaut a02d701efe Two zoom levels on a button, which the device already had
Forty columns and eighty are the same map, the same 8x8 cells and the
same engine; only how many of them fit differs. The map is 128 by 128
either way and the moon is exactly 128 columns of it. And the front end
scales whatever it is handed by the largest whole number that fits, so
320 by 200 at four times and 640 by 400 at twice fill the same glass.

So the two modes ARE two zoom levels and nothing in the video device had
to change to get them: forty columns shows under a third of the moon at
twice the size, eighty shows nearly two thirds. Out for the orbit, in for
the landing, and B says which.

What did have to change is every screen coordinate in Lander, because the
middle of the screen is 160 on one and 320 on the other. They now live in
one block that setView copies over from whichever of two tables matches
the mode, so a gauge reads a variable and never has to know which screen
it is on. Several coordinates became sixteen bit on the way, since 620
will not go in a byte. follow already read HalfScreen and showLander
already writes the world position straight through, so the lander itself
needed nothing but its resting column.

The moon is redrawn on a swap because it is filled as many rows deep as
the mode shows, and a moon drawn 25 deep on a screen showing 50 floats
over nothing.

The swap is edge triggered. A pad is LEVEL and not an event, so a view
that swapped while B was down would swap sixty times a second - which is
not a zoom, it is a strobe, and it redraws the whole moon each time. The
check for that holds the button for three hundred frames and requires the
lander to land where a six frame tap left it; with the edge dropped it
ends seventeen pixels adrift, which is the strobe costing it frames.

Four checks, each seen to fail on its own break. The README's Lander
entry also still described the relief orbit that the previous commit
replaced, and now describes the one that is there.
2026-09-03 21:48:32 -04:00
Anachronaut 85029d3b85 An altitude bar, and orbital speed marked on the drift bar
The orbit takes the lander off the top of the screen, and the panel only
worked while the ground was in sight. Both other bars are rates: they say
how fast, and neither says where.

The altitude bar is height above the surface underneath, up the left edge,
half a pixel of bar to a pixel of sky - the flyable band is about 256
pixels and the screen is 200 tall, so pixel for pixel would run off the
top of the very screen it describes. Above the surface rather than above
some fixed line, so it reads NOUGHT the moment the lander is down.

The marks say where 64 sixteenths is. Without one that number is folklore:
a pilot can feel that somewhere around here the falling stops and has no
way to see where. The bar is a pixel a sixteenth from the middle at 160,
so the marks sit at 224 and at the two pixels before 96, adjacent rather
than overlapping - a bar at orbital speed would otherwise hide the thing
it is being measured against.

Both in magenta. Red and green are taken and they MEAN something here,
how fast and whether it can be landed with, and an altitude is neither.
White was the first choice and the ceiling check counts white to find its
warning, so it read a warning that was never up - the suite caught that.
Cyan was the second and it is the colour of a landing pad, which is
checked as whole cells of it.

groundLevel is factored out of restOnSurface, which had the same sum.

Three checks, each seen to fail on its own break. Two flights, because no
one flight holds both ends of the bar well: the climb reads 219 pixels and
the descent lands and then stays landed. They fly on their own keyboard
file - the shared one holds a key down every forty eight bytes for the
held-thruster check, so borrowing it flew the lander from the keyboard and
the controller at once and turned the gentle descent into a crash.
2026-09-03 20:57:16 -04:00
Anachronaut c514d5328e An orbit that comes back round, out of gravity minus the swing
The relief version could never make one. It only ever SUBTRACTED from
gravity, so a lander a little too slow sank for ever and one a little
too fast rose for ever - nothing in it could turn a fall around, because
nothing in it ever pushed up. There was no periapse to have.

Gravity minus the swing outwards has a sign change in it, and that is
the whole mechanic. Below orbital speed the pull wins and the lander
falls; above it the swing wins and the lander climbs; at 64 sixteenths
they cancel and it circles. Falling buys sideways speed and climbing
spends it, so a fall carries the lander past orbital and turns into a
climb, and the climb pays it back and turns into a fall.

The trade is the quarter square multiply, because the rate has to be
the PRODUCT of the two speeds. Set by the vertical speed alone it drained
a climb to nothing, and any minimum to stop that became a trap the climb
spent its way into - measured freezing at 23 with a gate of 24 and at 3
with a gate of 4. With the product in it there is no gate: as the
sideways speed goes to nothing the trade stops by itself.

Half the product rather than a quarter or the high half. The high half
alone is nought below a product of 256, which is a dead patch exactly
where the turn begins, and a quarter still ran the lander into the roof
before it came round - the whole sky is about 145 pixels.

Measured, placed at 80 sideways and left alone: apoapse at -1024 with 59
sideways, periapse at -124 with 71, and round again at 165, 241, 299 and
369 ticks with no sign of decay. A period of about 22 seconds.

The ceiling also spends one sideways when it wipes a climb. Without that
it was a trap with no way out: the pin wipes the climb, so the trade sees
neither fall nor climb and never touches the speed that is pushing the
lander up. Measured pinned at the top with 117 sideways, unmoving, for
the whole of a four minute flight.

Tests/makedisks.sh needed the library path, since Lander now includes
math.asm, and the app went from 2,924 bytes to 4,651 - mostly the 1,022
byte table - which is what the listing expectations move for.
2026-09-03 19:15:50 -04:00
AnachronautandClaude Opus 5 f5642f52b5 A ceiling that pins rather than ends
Climbing made a sixteen bit height count down past nought and round to
65535, so a lander that kept going came back through the bottom and hit
the ground FROM ABOVE. Two thousand pixels of climb, which a full tank
reaches easily.

Pinned instead, and told so in the window. Leaving upward is RECOVERABLE -
gravity is always there and a lander with fuel can always come back - so
ending the run would punish a state the player can fly out of. What kills
you out here is running dry a long way from the ground, which is a death
somebody flew into rather than one a boundary handed them.

TWO DIFFERENT LINES, and both were got wrong before they were got right.
The warning covers being AT the ceiling or above it: compared against the
ceiling itself, a pinned lander read as back inside the world the next
frame and the warning was written and wiped sixty times a second, so it
never appeared at all. The pin covers being STRICTLY above it: including
the ceiling dragged the height back every frame and the lander could never
descend, which is a lid nobody can leave and worse than the wrap.

And only the climb is spent, never the fall. Zeroing the speed outright
pinned it there for ever - gravity adds once a tick and a clamp running
every frame wiped the pull nine times out of ten.

The orbit check is gone, and the reason is in video.sh. Every window where
the difference showed turned out to be a few frames wide: hold the
thruster and both landers are pinned with their climbs spent, ease off and
both land and freeze. A version of it passed against one disk and failed
against another, which is a check measuring the boot time rather than the
physics. Orbit is verified by measurement and said to be so, rather than
left looking tested.

cosmosLanderDry wants seventy million cycles now instead of forty: it
spends the whole tank at the ceiling before it falls the length of the
world.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-03 18:42:34 -04:00
AnachronautandClaude Opus 5 d896c9d433 Multiplying, on a machine with no multiplier
a * b = qs[a + b] - qs[|a - b|]     where qs[n] is n squared over four

because (a+b)^2/4 minus (a-b)^2/4 is exactly a*b, and the halves the
flooring throws away cancel between the two terms. A multiply is two
lookups and a subtract.

AND THE TABLE IS BUILT BY ADDING, which is the part that makes it fit a
machine with no multiplier at all. A table of squares would need squaring
to fill; this one does not, because qs[n] = qs[n-1] + n/2, and n/2 goes 0,
1, 1, 2, 2, 3 - a number that steps up on every even n. So the whole thing
is a running total and a toggle, and nothing harder than an add appears
anywhere in building the thing that does the multiplying.

511 entries of two bytes, because a byte plus a byte reaches 510. That is
1,022 bytes of Data Memory, and it is the price: a kilobyte traded for an
operation the hardware has not got.

The operands go in memory rather than in registers. B cannot be stored and
a product does not fit in one byte anyway, so two in and two out would
spend more instructions shuffling than the multiply costs.

Checked against nought, the commutation both ways round, a square, and 255
times 255 - which is 0xFE01 and the largest product two bytes hold. The
square is the case the identity leans on hardest: the difference term is
nought and the whole answer comes out of one entry.

Wanted for Lunar Porter's orbit, where the trade between height and speed
has to be proportional to vx times vy and could not be. Useful well beyond
it: this is the routine every fixed point sum on this machine has been
doing without.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-03 18:07:07 -04:00
AnachronautandClaude Opus 5 a069ee7a00 Orbit, and two bars that answer instead of reporting
Going sideways lifts the moon off you. Not because gravity weakened -
because at speed the surface falls away underneath as fast as the lander
falls towards it, which is what an orbit is.

A GRADIENT OUT OF INTEGER ARITHMETIC. Gravity is one sixteenth of a pixel
a tick and there is nothing between that and nothing, so it cannot be
scaled down. Instead four times the sideways speed goes into a byte every
tick and the tick's gravity is skipped whenever that byte carries: the
fraction cancelled is the speed over 64, smoothly, with no multiply and no
divide. At four pixels a frame it carries every time. That is the linear
approximation; the honest one is the square, and wants a table.

It did nothing at all for its first two versions. Once because the relief
was a 256th a tick, so orbit wanted a speed no lander would reach; and
once because A IS THE HIGH HALF of the shift register, so multiplying by
four left the answer in A while the code read B, which is nought. The same
trap as the scroll register and the pixel conversion before it.

And a bar for the vertical speed beside the one for drift, both GREEN
WHILE A LANDING WOULD SURVIVE AND RED WHILE IT WOULD NOT. That turns two
numbers into one question - can I put down - and answers it at a glance.

WHAT THIS COST: the flown delivery check. A recording is a list of buttons
and not a flight, so replaying it under different gravity flies somewhere
else; the delivery became a crash two columns short. The fixture is still
there and is still a faithful record of what somebody did, and is no
longer a record of what happens.

That is the standing cost of a flown fixture, and it is worse than the
transcript tests dropped earlier: those broke when an output moved, and
this breaks whenever a NUMBER moves. Making the delivery reachable without
flying - a way to start already carrying, or at a chosen base - is what
would fix it properly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-03 16:25:56 -04:00
AnachronautandClaude Opus 5 4b1c3d8e3f A missing file is an error, not a core dump
Naming a pad file that is not there printed the error and then said the
machine had STARTED. MACHINE_OK is nought and the code returned nought, so
the front end ran a machine whose clock had never been set up and divided
by it: a typo in a path came out as a floating point exception and a core
dump.

The trap is two functions in one file with opposite conventions -
machineStart returns MACHINE_OK for worked, machineRestart thirty lines up
returns 1 for worked - and this copied the nearer one. Both of the returns
I added last week had it.

Checked now for all three files the replay suite is about, because the
same mistake fits all of them, and re-broken to be sure: the check comes
back exit 136, which is a signal 8, which is the crash.

Found by somebody typing a path that was not there, which is the fourth
thing this week that no test would have reached.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-03 15:50:40 -04:00
AnachronautandClaude Opus 5 60e0196fe2 A delivery, flown by hand and kept
Cyan base to red base: twenty five seconds of steering, recorded with
--record-pad and replayed as a test. THE FIRST FIXTURE HERE THAT WAS
PLAYED RATHER THAN WRITTEN.

It is the only check that a cargo ever reaches anywhere. Several attempts
at authoring a flight like it by hand got within two columns and no
closer, which is a piloting exercise rather than a test - and the whole
reason the recorder exists.

What is checked is the FIRST LETTER of what the base answers. Delivered,
Loaded, Nowhere and Not are 30, 22, 37 and 37 pixels of white in that
cell, so a D is a delivery and nothing else is. Counting the whole message
would pass on any message of the same length, and comparing the picture
would fail the next time anything about a font changed.

It took three flights to get here and two of them were lost to bugs in the
recorder: one that recorded the wrong pad, and one that recorded a pad
sampled on a different clock from the one the machine read. Both were
found by somebody watching a replay and saying it was not what they flew,
which nothing in this suite could have said.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-03 14:54:00 -04:00
AnachronautandClaude Opus 5 ee77d79780 A pad is sampled once a frame, and the recorder writes what was sampled
The live state is written by whatever watches real hardware, on ITS clock:
a window polls its keyboard once a HOST frame, which is not a machine
frame. Read straight through, that made a pad whose value could change in
the middle of a machine frame - breaking the one promise the device makes,
that asking twice in a frame gives the same answer both times. The manual
said it could not happen and the code allowed it.

It also made recordings that were not of the flight. The recorder sampled
on a frame boundary and the program read whenever it read, so the two saw
different bytes. A replay of that is a DIFFERENT FLIGHT, faithfully
reproduced: it flew a lander off the top of the screen that had never gone
there, and every check said the replay was deterministic and re-recorded
as itself, because it was. Both were true and neither was the point.

So the live state is latched once a frame. What the machine reads and what
the recorder writes are now the same thing by construction rather than by
two clocks happening to agree. Real hardware latches a controller once a
frame for the same reason.

WHAT IS STILL NOT COVERED: the latch itself. Every recorded pad already
changes only on a frame, so the tests cannot tell a latched live pad from
an unlatched one - the case that went wrong is the one with a real hand on
a real controller, which is the case a headless suite has none of. Said
here rather than left to look tested.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-03 14:43:58 -04:00
AnachronautandClaude Opus 5 de1857f5f7 Record every pad, not the one that happened to be first
The first recording ever made with this came back 1,766 frames of nothing.
It recorded pad NOUGHT and the controller was somewhere else - which pad
one lands on is an accident of the host, the same accident that made Lunar
Porter read all four in the first place - and a flight flown for the
purpose was lost to it.

So every pad is or-ed into the byte. A demo is a record of what somebody
DID, and on a machine one person is playing the number it arrived on is
not part of that. It plays back on pad nought, where --pad puts the first
file given, and any program that reads more than one pad reads them or-ed
anyway for exactly the same reason.

--record-pad takes one file now rather than filling pads in turn, because
there is nothing left for the second one to mean.

The check for it plays a recording on pad ONE with nought holding nothing
and requires the bytes back. That is the case that was missing: the round
trip was tested and passed, on pad nought, which is the only pad it could
not have gone wrong on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-03 14:27:58 -04:00
AnachronautandClaude Opus 5 eb695a3f3b --keyboard was a flag that did nothing behind a window
Voyager installed its own input hook after machineStart, which had already
installed the keyboard file's - so naming a keyboard file and running the
window silently got the window, and the flag said nothing about being
ignored.

Both together is the combination a demo wants. Recording a flight needs
the typing that STARTS it to be the same every time, because a human
reaching the shell a moment later shifts every frame of the recording
after it - while the flying itself has to come from whatever is actually
in somebody's hands.

So the window only takes the keyboard when no file was named.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-03 12:52:17 -04:00
AnachronautandClaude Opus 5 a163c670d0 A demo recorder: --record-pad writes what --pad reads
One byte a frame, in exactly the format the player takes, so a recording
needs no conversion and there is no second format to keep in step. That
symmetry is the feature, and it makes the strongest form of the claim
testable: a recording is made OF a playback, and the bytes coming out have
to be the bytes that went in.

It exists because some inputs cannot sensibly be written by hand. Flying a
lander from one base to another is a few hundred frames of steering that
has to arrive somewhere eight cells wide, and several attempts at
authoring one got within two columns and no closer. That is a piloting
exercise rather than a test. Playing it once and keeping what happened is
the answer.

A BYTE FOR EVERY FRAME, written inside the loop that advances the
recordings rather than after it, so a machine that jumped several frames
at once still writes one for each. A recording is a timeline: one that
skipped the frames nobody looked at would play back faster than it was
flown.

What is recorded is what the DEVICE WOULD REPORT, not the live state - a
recording of a playback that wrote the live state would be a file of
noughts. And it is flushed as it goes, because a recording is usually
stopped by whoever is playing rather than by the program ending, and a
demo lost to a buffer is a demo flown twice.

Tests/replay.sh is where this and whatever follows it are checked. Twelve
scripts now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-03 12:38:43 -04:00
AnachronautandClaude Opus 5 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
2026-09-03 12:23:12 -04:00
AnachronautandClaude Opus 5 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
2026-09-03 12:04:27 -04:00
AnachronautandClaude Opus 5 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
2026-09-03 11:45:30 -04:00
AnachronautandClaude Opus 5 6073086584 The system takes a leftover window down
Lunar Porter put a fuel gauge up and never took it away, so the shell came
back with FUEL across the top and the cursor underneath. Clearing did not
help: a window is a layer at a SCREEN position that does not scroll, which
is exactly what makes one left behind unpleasant - it sits over whatever
comes next and cannot be scrolled off, cleared away or typed past.

Taken away rather than given back, like the sprite table and for the same
reason: nothing the shell draws is a window, so there is nothing to
restore. And a program that FAULTED while one was up could not have taken
it down itself, which is why this belongs to the system rather than to
whichever programs remember.

The check for it needed writing twice, and the first version was the
familiar kind of wrong. It counted the gauge BAR's colour - and the bar
disappears on its own whatever happens, because it is drawn with a tile
the screen save puts back, so the check passed with the teardown deleted.
What actually survives is the LABEL, in font tiles the shell needs anyway.

So the screen is cleared afterwards and read cell by cell. With the window
down a cleared row is "> " and a cursor; with it up the same row is F, U,
E, L. Cells one and three being empty is the whole difference, and it is
29 and 22 pixels of it rather than a threshold somebody has to believe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-03 00:50:42 -04:00
AnachronautandClaude Opus 5 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
2026-09-03 00:35:34 -04:00
AnachronautandClaude Opus 5 17c8da111f A window: a layer that does not scroll
The map moves and this does not, which the map alone cannot express. The
scroll registers move ALL of it, so a score printed into the map slides
away, and one printed into whichever rows the view happens to be showing
jumps a pixel at a time as the fine offset changes.

Port 0x3D is how many rows tall and 0x3E is which row it starts at. Nought
tall is no window, so a cleared screen has none and every program written
before this means what it meant. A start row is a register because a
status bar along the bottom is as common as one along the top.

IT HAS ITS OWN MEMORY, and that is the argument for it. The cheaper design
draws the top rows of the MAP without the scroll applied - no new memory,
one register - and makes those rows part of the playfield's ring, so a
game that scrolls vertically has to route its world around its own
scoreboard for ever. The point of a status bar is that it is not somewhere
in the level. Lunar Porter does not scroll vertically today and will the
moment an orbit is a thing you can reach.

0xC000 in the screen bank, which the map does not reach: it ends at
0xBFFF. Same cells, same tiles, same pages, same schemes. Being in the
screen bank makes it per screen, so flipping the buffer flips the status
bar with it - what a double buffered game wants, and surprising the other
way round.

Drawn over everything, sprites included. A sprite that could cover the
fuel gauge would be a bug in every game that had both.

Tile modes only. In bitmap mode the picture is using that memory, so a
bitmap program pins things to the screen with sprites, which are in screen
coordinates for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-03 00:19:09 -04:00
AnachronautandClaude Opus 5 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
2026-09-03 00:02:59 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 23:51:48 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 23:36:05 -04:00
AnachronautandClaude Opus 5 6def343e97 Find Raylib's link flags instead of guessing one set
Raylib 6 calls X11 directly from GetClipboardImage, so a static libraylib
needs -lX11 on a Linux desktop where 5.6 did not - and because it is an
archive, one function nobody calls drags in the whole object and every X
symbol with it.

THE OLD SHAPE FAILED IN THE WORST AVAILABLE WAY. One hardcoded guess was
test-linked, and when the guess went short the probe reported that Raylib
was not installed - so make quietly stopped building Voyager and said so
in the words it keeps for a machine with no graphics library at all. The
answer looked like an absence and was a missing flag.

So the candidates are tried in order and the first that links is the
answer. The probe and the flags are the same thing now rather than two
facts that can disagree, which is the property that was actually missing:
a probe that tests something other than what gets built can only ever be
a coincidence.

Verified against 6.1-dev: Voyager builds and all 210 tests pass under it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-02 23:24:06 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 22:48:24 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 22:33:32 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 22:14:36 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 21:58:31 -04:00
AnachronautandClaude Opus 5 fed1453e6e Controllers: four pads that say what is held
The console says WHICH KEY WENT DOWN, which is the right shape for typing
and the wrong one for playing. A game wants to know what is being held,
this frame, possibly several things at once, and a stream of presses
cannot say that: a key that is down and staying down sends nothing at all.
Lunar Porter's thrust is a burn per press for exactly that reason.

So a pad is its own device on ports 0x60 to 0x6F, reporting a LEVEL. One
read gives every button at once, holding is the natural thing to express,
two directions together cost nothing, and reading does not consume it - a
game may ask twice in a frame and be told the same thing both times.

Four of them, because a party is four. They cost a port each and nothing
at all when unused. The directions are the low nibble so "which way" is an
AND with 0x0F; the buttons are the high nibble for the same reason. 0x64
says which are really there, so a game can ask for a controller rather
than sitting silent while somebody presses things at it. They never
interrupt: a game polls once a frame because that is when it draws.

KEY-UP ON THE CONSOLE WAS THE OTHER WAY TO DO THIS AND WAS REJECTED. A
terminal hands over characters and can never report a key coming up
however it is asked, so it would have been a thing that worked behind a
window and silently did not down a wire. A separate device can honestly
say it is not there.

Voyager drives pad nought from the keyboard as well as from any real
controller, OR-ed rather than chosen between, so a game written for a pad
is playable on a machine with none and unplugging one mid-game does not
leave somebody holding nothing.

And a recorded path, which is what makes any of it testable: --pad names a
file of one byte a frame, and the manifest has an eighth column for it.
A BYTE A FRAME AND NOT A BYTE A READ - a level asked twice in one frame
has to answer the same both times, and a file that advanced per read would
depend on how the program happened to be written. Voyager's own tests run
headless with nobody holding anything, so without this the device would be
exercised only by somebody playing: the state the console's line editing
was in when it broke twice in two days.

0x50 is the timer, not free. The block this went in was chosen after
looking rather than before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-02 21:44:55 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 21:13:09 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 16:52:01 -04:00
AnachronautandClaude Opus 5 5222c85100 More fills the screen it is on, not the screen it was written for
Twenty two lines was right when there was one screen size. It still is on
the forty column screen and wastes three fifths of the eighty column one,
so More asks the rows register instead - which is readable for exactly
this sort of reason.

Rows minus three is twenty two on a twenty five row screen, so nothing
changed underneath anyone already reading files this way. It fills a
bigger screen and leaves a smaller one alone.

A bitmap screen has no rows and says so with a nought, which through an
eight bit subtraction would be 253 lines. Anything under five falls back.

The existing test stopped testing paging the moment this worked: 32 lines
fits in a 47 line page, so the file never paged and the recording lost the
prompt entirely. The fixture is 60 lines now - the INPUT needed moving,
not just the output, which is the failure this project keeps meeting.

And cosmosMoreNarrow, which runs Mode first and pages the same file on the
forty column screen. Two recordings of one file at 47 lines and at 22: a
More that went back to a constant would make them the same length.

Both were verified with break.sh, and the first attempt was a bad break
rather than a bad test - it replaced one of two reads of the rows port and
the other still fetched the real value. Which is a fair argument against
reading a port twice, so it is read once and kept now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-02 16:18:37 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 15:58:34 -04:00
AnachronautandClaude Opus 5 cb898450b5 Sprites that scale, and a depth buffer to hide them behind
A target size in PIXELS rather than a multiplier, which is the whole of
why this is usable here. A billboard at distance d wants to be k/d pixels
tall, and that is a number a program has anyway - out of a lookup table,
most likely. A multiplier would have to be a fixed point fraction arrived
at by dividing, and this CPU cannot divide.

Zero on an axis means the natural size, so every sprite written before
scaling existed still means what it meant.

The two axes are independent, and that shape - one tile wide at its own
size, stretched to whatever height a distance says - is a wall column in a
pseudo-3D game. Measured: a DDA step costs 85 cycles, so 80 columns of ray
casting is about 85,000 cycles, or 12fps. Drawing those walls from the CPU
instead would be 256,000 writes, fifteen frames of cycles for one frame of
screen. The device doing the pixels is what makes such a game possible at
all here, not merely faster.

And a depth buffer, one byte a screen column at 0xD000, written by the
program. A sprite with a depth draws only in the columns it is in front
of. PER COLUMN, and that is the point: a billboard is nearer than the wall
at one end of itself and further at the other, and no ordering of the
table can say that. Table order settles sprites against each other; the
buffer settles them against the scenery. Zero means no test at both ends,
so a program that never writes it behaves as it did before it existed.

The entry grew from 8 bytes to 16 - now, while two programs use the table,
rather than once a game is written on it. Bytes 0 to 7 kept their
meanings, so Sprite.asm needed no change.

The pass is rewritten to walk where a sprite is GOING rather than where it
came from, which is what makes a stretch and a squash one operation. It
also made flipping fall out: turning the source coordinate round mirrors
the tile order and the pixels inside each tile in one step, where drawing
tile by tile had to be told to do both. All 111 checks passed unchanged at
natural size, which is what says the rewrite changed nothing it should not.

Clipping moved out of the inner loop and had to: a target size is sixteen
bits, so a sprite asked to be 60,000 pixels tall would have been sixty
thousand turns of a loop that drew eight rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-02 14:07:38 -04:00
AnachronautandClaude Opus 5 f8c3db5d56 A tool for breaking things, since doing it by hand went wrong twice
A check that passes proves nothing until it has been seen to fail. Doing
that by hand failed twice in two days, and BOTH TIMES IT LOOKED LIKE A
RESULT - the suite ran, went green, and read exactly like "this check does
not catch that".

Once the edit produced code that would not compile, make failed, the exit
status was not looked at, and the previous binary ran the suite. Once the
anchor was right and the filename was wrong, so nothing was edited at all.

Neither had anything to do with header dependencies, which have always
worked: DEPFLAGS is -MMD -MP and every .d is included. What was missing
was a harness that refuses to report a result it did not earn.

So Tests/break.sh checks every step of its own work and treats anything
unexpected as a hard error rather than a green run. Not finding the break
is the answer it exists to give, and it is worthless if it can also be the
answer when the break never happened. It restores the file on the way out,
including on an interrupt.

It is not in the suite and docs.sh does not count it, for the reason
makedisks.sh is not counted turned round - but being left out of the count
is not being left out of the manual, and that gap is where a script goes
undocumented for months. So docs.sh now requires both of them to be
described, and caught this one being missing.

Also: video.sh reads the fixture disks and does not build them, so after
make sanitize clears the build directory it reported SEVEN product-looking
failures for a missing file. It builds them now and says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-02 13:19:22 -04:00
AnachronautandClaude Opus 5 9eed23120f Four pages of tiles, in bits that were already there
A tile number is a byte and a byte reaches 256, which is not many once a
font has taken 135 of them and a game wants a character, a background and
a wall. Bits 4 and 5 of the attribute now say which page of 256 the
number is in - bits already written on every cell and every sprite, and
reserved for this since the attribute was defined.

Four pages of 16K is 64K, which is the whole atlas, so THE FOURTH PAGE IS
THE MEMORY THE SPRITE TABLE AND THE PALETTE ARE IN. That is not a hole in
the design; it is the answer shared video memory has always given, and it
is checked rather than forbidden. The atlas is 1024 tiles, and what a
program spends on sprites and colours comes out of them: no sprites means
page 3 is art, and sprites means 768 tiles and a reason.

The page is a property of the CELL and not a mode, so one screen shows
tiles from all four at once and nothing has to decide which page it is in.

Both places a tile is drawn from now ask one function where the art is.
They would otherwise drift: the sprite pass was written days after the map
pass and neither is where the other is looked at.

Nothing in CosmOS changes. The shell draws from page 0, which the screen
save covers; a tile left in another page is invisible unless a map cell
names that page, and the map is given back or cleared.

Both breaks were tried and both failed the checks - and the second had to
be tried twice, because the constant it needed lives in video.h and the
harness was only editing video.c. That is the same silent no-op as
yesterday's uncompiled break, in a different disguise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-02 12:59:02 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 11:42:11 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 11:08:09 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 10:31:29 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 10:09:53 -04:00
AnachronautandClaude Opus 5 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
2026-09-01 20:50:05 -04:00
AnachronautandClaude Opus 5 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
2026-09-01 20:33:33 -04:00
AnachronautandClaude Opus 5 4b109f704c The shell starts each line with the Stack where it left it
Every failure in this shell abandons a frame. commandFailed is reached with CALL
and never returns: it marks the line and branches to the prompt, which is the
idiom every command uses and is why a failure needs no unwinding anywhere. What
it costs is the frame of that call and of everything between the prompt and it -
twenty bytes for a name that was never set, more from somewhere deeper - and
nothing ever gave them back.

MEASURED BEFORE IT WAS FIXED. Twenty failed lines moved the Stack Pointer from
FFFD to FE6D, and it only ever went one way.

Nothing had noticed because it takes thousands of failures to reach anything and
nobody types thousands of anything. A loop in a script would, which is why this
is worth doing before there are loops rather than after.

So the loop starts each turn from a known place. SystemStack is NOT that place:
it is taken when a program starts, so that the shell's Stack can be given back
when the program stops - which means it holds wherever the shell had got to at
that moment, the value that needs correcting rather than the one to correct
from. ShellStack is taken once, at boot, when nothing is happening.

Second use of MVDS in the system, and it earns it for the same reason as the
first: a Stack that is right by construction beats one that is right because
everybody remembered.

Break prints the registers, so the test is two dumps with eight failures between
them and a requirement that they agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-01 19:29:20 -04:00
AnachronautandClaude Opus 5 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
2026-09-01 19:23:55 -04:00
AnachronautandClaude Opus 5 3c76934a9a Tab reaches the disk
Paths and programs, which is the half that makes it worth having. The first word
of a line is a command or a PROGRAM, offered under the name somebody would type
- the extension taken off - and anything after it is a file, offered as it
really is. A separator anywhere in the word says which directory to look in.

A directory answers with a separator on the end instead of a space, which says
what it is and lets the next part be typed straight away. The answer ending in
one is also what stops a space being added, so that is one test rather than a
flag.

PROGRAMS ARE LOOKED FOR WHERE THE SHELL WOULD LOOK to run one: where you are,
/Apps on the disk you are on, and /Apps on drive 0. Offering something the shell
would not find would be finishing a word into a thing that then does not work.
Drive 0's is skipped when that is already the drive, or every program in it
would be offered twice and nothing would ever be the only match.

Walking somebody else's directory means standing in it, which is the only way to
walk one here, so where the person was and which drive they were on are put down
first and restored whatever happens.

Three bugs, all found by running it:

THE DIRECTORY TEST WAS INVERTED. dir asks the same question the same way round
four hundred lines further up, which is what made it obvious once looked at.

THE /Apps WALK OVERWROTE THE TYPED PATH. The whole search runs a second time to
list the matches, and by then TabDir said "/Apps" - so a word that had named
nowhere went looking in the wrong place and listed nothing at all. Two ways into
the walk now, and the typed path is never written over.

AND LISTING ONLY KNEW ABOUT COMMANDS, because it was a second copy of the walk.
It is the same walk with a flag now: finding the answer and showing the matches
are the same question asked twice.

Also cosmosMonitor, which had been RE-BLESSED INTO MEANINGLESSNESS by the wall
move. It disassembles a loaded program, at an address the input names - and that
address moved a page while the recording was simply re-recorded to whatever came
out, which was a page of zeroes. It is pointed at 5000 again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-01 18:10:40 -04:00
AnachronautandClaude Opus 5 bb065fe221 Tab finishes a word somebody started
The first word of a line, against the shell's fifteen commands. One match goes in
with a space after it, because a word that can only be one thing is finished.
Several are folded into their longest common prefix and that goes in, which is
the most that can be said without guessing which was meant - and if that adds
nothing, the matches are listed and the line put back underneath.

THE LINE COMING BACK IS THE HALF I EXPECTED TO BE HARD and it was already
solved. The prompt has been reprinted somewhere else entirely, so the editor's
idea of where the line begins is wrong - but editAnchor works that out backwards
from where printing ended, precisely so it survives the screen moving. Listing is
a redraw it already knew how to do.

editInsert became editPut, a routine, because completing a word puts in several
characters and every one of them is that. Which cost a bug immediately: the old
inline code left the insertion point in A, and a RET puts A back to what the
caller had.

Two more bugs worth naming, both mine and both the same shape - a pointer that
had moved:

THE CANDIDATE'S START HAS TO BE KEPT. The comparison walks DP3 through the name
as it matches, so by the time a match is declared, DP3 points at the part AFTER
what was typed - and that is what got copied. "he" completed to "he" because the
answer taken was "lp".

AND THE INSERTION STOPS AT OR PAST, not exactly equal. With the wrong answer the
two counters passed each other and the loop ran off the end of the buffer,
filling the line with whatever was next in memory. They cannot pass each other
now, and the branch stays, because the cheaper failure is worth nothing.

MY OWN TEST HAD A HOLE and breaking the code found it. The later-word case
pressed Tab after a space, where there is nothing to finish anyway, so it passed
whether or not the shell checked which word it was on. It types "echo he" now,
which would become "echo help" if it did not.

The assembler's label table went past 1024 and is doubled. A ceiling reached
once will be reached again, and it is pointers into source already in memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-01 17:43:48 -04:00
AnachronautandClaude Opus 5 749fef8ce2 The shell's own words, as a table and not just a chain
The dispatch is a run of "is the line this name" comparisons. That is fine to
execute and impossible to WALK, and completing a half typed command needs to
walk them - so the names have to be data as well as code.

They nearly were already: DirName through ExitName were fourteen zero
terminated strings sitting back to back, which is a table by accident of layout.
This makes it deliberate. MonitorName joins them, the run is labelled, and a
count goes underneath because a run of strings does not say where it stops.

WHAT MAKES IT A TABLE IS THE ZEROES. Each name ends in one, so the next begins
after it: no pointers, no lengths, and adding a command costs a line.

Tests/docs.sh reads both the dispatch and the run and compares them, because the
two can disagree and every way they do is quiet. A command added to the dispatch
and not to the run simply never completes, which nobody would think to check by
hand. Something put BETWEEN the strings is worse: the walk ends there and takes
every command after it, and the machine goes on working perfectly except that
Tab knows about six things instead of fifteen.

All three break that way and say something useful. Putting one byte in the
middle of the run reports that it holds ten names against the fifteen claimed,
which points at roughly where.

Groundwork for Tab completion. Nothing uses it yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-01 17:21:30 -04:00
AnachronautandClaude Opus 5 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
2026-09-01 16:56:17 -04:00
AnachronautandClaude Opus 5 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
2026-09-01 16:49:12 -04:00
AnachronautandClaude Opus 5 f97d15de08 The font comes from a chip, not from RAM that remembers
videoReset zeroed video memory and then wrote the font and the sixteen colour
schemes into it, and the comment above that said out loud what was wrong with
it: "everything here is ordinary video memory". RAM does not wake up with
anything in it. That was the last piece of magic in this device, and it looked
harmless until something wanted the font BACK - a program that redefines a glyph
had destroyed the only copy there was.

So the device has a character generator, the way the machines this one is
pretending to be really did, and the copy into RAM is a thing it DOES rather
than a state it mysteriously starts in. Command port 0x39: bit 0 for the font,
bit 1 for the schemes.

THE RAM IS STILL RAM. A program may overwrite every glyph and every colour and
should be able to, which is what makes this a tile engine rather than a text
display. What changed is that it is no longer a one way door.

NEITHER COMMAND CLEARS WHAT IT DOES NOT OWN. The font used to clear the whole of
tile memory before writing itself, which was harmless while it happened only at
reset and is wrong the moment a program can ask: a program that defined a tile
of its own and then wanted its text back would have paid for it with the tile.

The reason it is a chip rather than a file on the disk, which was the other
candidate: the boot chain prints before CosmOS exists. Stage one prints "?" when
there is nothing to boot, and if the font came off the disk then the message
about the disk having failed would be the one thing that could not be drawn. A
system that wants its own font still loads one over the top - the ROM is the
floor, not the policy.

Two things that had been worked around now simply work. The shell asks for both
whenever a program exits, so a program that redefined a letter no longer leaves
it unable to spell, and Grid no longer needs to have saved the screen to avoid
handing back green text on blue. And the fault screen asks for the glyphs first,
because a message spelled in somebody's tile graphics is no message at all.

Five video checks. Two runs each for the font and the schemes, since the map
holds a tile NUMBER and the glyph is looked up when the frame is drawn - so
restoring changes every cell using it, including ones drawn before, and what the
two runs differ by is the command. The third guards the decision not to clear:
tile 200 has to survive the font coming back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-01 16:21:24 -04:00
AnachronautandClaude Opus 5 925388c2f2 Keep the personal disk three starts back
Copied before every start of the machine, and kept several deep rather than
copied over one file.

ONE BACKUP TAKEN AT EVERY START IS WORSE THAN NONE. The way a disk is lost is
that something goes wrong - and the very next thing anybody does is start the
machine again to see how bad it is, which is exactly when a single backup gets
overwritten by the wreckage. Three deep means the damage has to happen and then
be started past three times before the copy that would have helped is gone.

AND IT IS NOT GUARDED BY A CHECK THAT THE DISK STILL LOOKS RIGHT, because no
such check can be written. The disk that went missing this week was a perfectly
valid and perfectly empty filesystem: the format had succeeded, and there is
nothing to look at that says a disk has lost anything. That is the whole reason
to keep the old ones rather than to judge the new one.

The backups sit outside clean's reach like the disk itself. A backup a rebuild
deletes is not one.

The message the disk prints when it is first made said "nothing in this makefile
will touch it again", which stopped being true the moment this was added. It now
says written to, and says where the copies go.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-01 15:54:55 -04:00
AnachronautandClaude Opus 5 c1b3c4c156 A rule for the bug that formatted a disk
falls-into-subroutine. The code above a label ends without going anywhere and
the label is one something CALLs, so execution walks into the subroutine,
reaches its RET, and returns to whatever the Stack happens to hold - because
nobody called, there is no caller, and it goes somewhere nobody named.

It is worth a rule because the symptom is nowhere near the cause and changes
with the Stack. In CosmOS's monitor it was usually a byte that does not decode,
in the middle of newLine; once it was inside sbfsFormat, and the machine
formatted the disk it had booted from.

Two exemptions, and both had to exist or the rule would have reported well
written code:

A TAIL CALL IS THE SAME SHAPE AND IS FINE. Falling out of one subroutine into
another means the RET returns to the outer caller, which is real. So it only
fires when nothing since the last branch or return was a call target either -
which is the linter's usual trade of precision for being worth reading.

AND osExit NEVER RETURNS. It is how a loaded program gives the machine back, and
every program here ends with it and then writes its helpers underneath. Without
that, twelve well written programs were reported. It is the one name from the
system this tool knows, and the comment says why it is there.

Also SRET, which stopsFallthrough did not list. It returns from a handler
exactly as RET returns from a call, and leaving it out is a gap in every rule
that asks what reaches an instruction. Load bearing rather than tidy: without it
cosmos.asm reports a handler ending in SRET as falling into the routine written
under it.

A first pass over the file collects call targets, because a subroutine is very
often called from further down than it is written.

The corpus reports none of it, which is the point rather than a disappointment,
and the Test Manual now says so - a baseline entry that is absent is otherwise
indistinguishable from a rule that never runs. Checked against the version of
cosmos.asm from before the fix, where it names the line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-01 15:43:25 -04:00
AnachronautandClaude Opus 5 66be42d7bb A word the monitor does not know goes to the disk, not into the weeds
There was nothing at the end of the monitor's command list. An unrecognised word
fell off it and straight into sayPrompt - which is a ROUTINE, so its RET had
nothing of its own to return to and went wherever the Stack happened to be
pointing.

The user found it by typing a program's name at the monitor prompt, which is an
entirely reasonable thing to do: the monitor is a mode of the shell, so
everything the shell does is meant to work in it. What they got was a fault, and
before that a second prompt printed on top of the first - which is sayPrompt
doing exactly what it is for on its way past, and the tell that it had been
entered rather than called.

WHERE THAT RET WENT DECIDED HOW BAD IT WAS. Usually 0x0003, in the middle of
newLine, and the machine stopped on a byte that is not an instruction. Once it
was inside sbfsFormat, and the machine formatted the disk it had booted from -
the user's would not start again, and neither would mine, which is how I came to
have a reproduction before I had a diagnosis.

Pre-existing, and not recent: it is there at 2a29ceb and every revision I
checked back through.

The fix is one branch. cosmosMonitorRun covers all three cases the monitor now
has to handle - a program started by name, a program that faults, and a word
that is nothing at all - because the first of those is what the user did and the
last is what used to be fatal.

Worth naming as a shape: a run of tests falling through into a subroutine. The
symptom is not at the site, the failure depends on the Stack, and the damage is
whatever the return address happens to land on. SplitLint has no rule for it and
could have one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-01 15:30:04 -04:00
AnachronautandClaude Opus 5 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
2026-09-01 15:08:20 -04:00
AnachronautandClaude Opus 5 000a6d39cb Somewhere to send the fault about there being nowhere to send it
Dispatching through a vector with nothing in it was the one fault this machine
could not hand over, because the thing that would hand it over is the thing that
has just found nothing to hand it to. It stopped the machine and no program
could do anything about it - so calling a service the system does not implement
was fatal, and that is an ordinary mistake to make.

Two new fault vectors: 5 when a software vector was empty, 6 when a device
interrupted and its hardware entry was. Separate, because they are separate
mistakes with separate fixes - one is a program calling something that is not
there, the other a program that asked to be interrupted and forgot the handler.

WHICH ENTRY WAS EMPTY ARRIVES IN Q, and it is the only thing on this machine a
handler is given in a register. Not a fault cause register by another route: the
vector still says what happened and Q says which of the 256 entries it happened
about, which is a parameter and not a cause. It costs no new state at all,
because the frame already saved the Q the interrupted program had and RETI puts
it back.

The escalation happens once. If vector 5 or 6 is itself empty the machine stops
the way it always did, having genuinely run out of places to go.

swiFaultTest is what guards that, and it was written long before any of this: it
installs nothing, so it must still get the old halt. Breaking the escalation
fails the two new tests and not that one; making the escalation unbounded fails
that one and not the two new ones. Each break fails exactly the half it belongs
to.

noDeviceTest is fed no input on purpose. The console raises its line once when
input ENDS as well as when a byte arrives - which exists so a program driven by
interrupts is told when nothing more is coming - so with no input at all, that
end is what turns up.

Groundwork for CosmOS's fault screen, which wanted to catch these two and could
not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-01 14:40:53 -04:00
AnachronautandClaude Opus 5 4d976fc22a A program reading a line gets the editing too
osReadLine goes through the shell's editor now, so anything that asks the system
for a line gets arrows, Home, End and Delete. The editor is a program, and a word
typed with two letters the wrong way round can be put right without starting the
line again.

IT DOES NOT GET THE HISTORY, and that is the interesting half. Edit would
otherwise fill the history with the text of somebody's document, and pressing Up
in the middle of writing one would put "dir" into it. The history belongs to the
thing whose lines are commands. Two entry points rather than a flag the caller
sets first, so a caller cannot forget which it wanted.

And the console is put back the way it was FOUND rather than the way the shell
likes it. A program that had asked for key mode and then read a line through the
system used to be handed back a console in line mode having asked for nothing of
the sort. The status port reports all three things the control port can ask for,
in the same order two bits along, so one shift turns what the console IS into
what to write to make it that again.

Which uncovered a real fault in the console. READING THE STATUS PORT WAS EATING A
KEY: in line mode the poll consumed an arrow key and dropped it, so a program
that looked and then asked for key mode - exactly what reading a line now does -
found the first key it was reaching for already gone. A look must not consume
what it cannot report, because the mode can change. It is held now and delivered
as soon as something will take it. A blocking read still discards it, and must:
that read IS the delivery, and a byte held there would be met again forever.

Four recordings gained a program's echo, and cosmosEdit's went from
"> : : : : > : : > 1: alpha" to a session you can read. VERIFIED THE SAME WAY AS
BEFORE: with only the program side of the echo silenced, all 192 tests pass
against the recordings as they were before this commit, so the echo is the whole
of what changed.

cosmosEditService is the new test and it checks both halves at once. Inside Edit,
Left/Delete/Left puts "alpah" right. Up and Down do nothing there - were a
program's line walking the shell's history, the next line would come out as the
echo command from the top of the file instead of the word. And one press of Up
back at the prompt finds the command typed before Edit was started, which is the
proof that nothing the editor read went into the history at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-01 14:07:09 -04:00
AnachronautandClaude Opus 5 71f6e215f9 The shell remembers what was typed before it
Up walks back through the last eight lines and Down forward again. It exists
only because the keys reach the system now: until A1 and A2 there was nothing to
press Up at, and the line was assembled somewhere the shell could not see.

A RING RATHER THAN A LIST. A ninth line pushes the oldest out by moving where
the ring starts, not by moving any of the lines - so keeping a line costs a copy
of that line and nothing else, however full the history is. Eight is a power of
two, so which slot an entry lives in is an AND. The ISA had the awkward part
already: A and B are a sixteen bit shift register, so one SHR with B empty turns
a slot number into the offset of a 128 byte slot, high byte and low, ready for
DPUW.

A NINTH SLOT HOLDS WHAT WAS BEING TYPED when Up left it, and Down brings it
back. Losing a half written line to a keypress is the sort of small rudeness
that makes a thing unpleasant to use, and it costs one slot to avoid.

An empty line is not kept, and neither is one the same as the line already at the
top. The test proves the second by looking one further back: if a repeated
command were kept twice, the line behind the newest would be the same line
again.

The redraw had to learn to rub out. One space was enough while the only thing
that shortened a line was taking one character out of it; a recalled line
replaces the whole of it, and a short line over a long one left the tail of the
long one on screen looking like part of what you were typing. It now covers
exactly what was lost - which turned out to be one space fewer than before in
the cases that GREW, so two lines of cosmosEditKeys lost a trailing space that
was never doing anything.

Costs 1157 bytes of Data Memory, taking CosmOS to 6220 of the 8192 it has before
a loaded program's data begins. Worth writing down: that is the budget, and this
is the largest single thing in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-01 12:24:23 -04:00
AnachronautandClaude Opus 5 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
2026-09-01 11:47:20 -04:00
AnachronautandClaude Opus 5 373454ec00 A fresh disk for every test, fixtures included
A fixture built by makedisks.sh was handed to each test where it lay. Twenty
four tests name disks/cosmos.img and several of them write to one, so a test
could hand the next one a disk with its leavings on.

romBoot is what found it. Its recorded output described a directory that
selfBoot had made earlier in the same run, so it passed in a full run and failed
on its own - which is the worst way round for a test to be wrong, because the
form nobody runs is the one telling the truth. Its recording now says "made"
like selfBoot's, which is what running the same input on the same disk should
always have said.

Fixed as a class rather than as an instance: run.sh copies a fixture before
attaching it, the same way it already removed a scratch image. Then every one of
the 138 run and rom tests was run on its own to see whether anything else was
leaning on what ran before it. Nothing was, before or after.

Also, cosmosEditKeys.in was written by Python's write_text, which encodes as
UTF-8, so every key byte was 0xC2 and then the key. The test passed anyway,
because the shell ignores a byte it has no use for - a fixture working for a
reason it was not built on, which is exactly the thing that stops working
without anybody touching it. Written as bytes now; the recording is unchanged,
which is the proof the stray bytes were being ignored.

docs.sh is what caught that, and it turns out to draw the line in the right
place by construction: a deliberately binary fixture does not decode as UTF-8
and is skipped, while one that is accidentally UTF-8 decodes and is reported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-01 11:22:43 -04:00
254 changed files with 25330 additions and 817 deletions
+7
View File
@@ -9,6 +9,12 @@
/SplitDisk /SplitDisk
/SplitLint /SplitLint
/Voyager /Voyager
/SoundPatch
/TuneC
# A .score is written and a .tune is what TuneC makes of it, so a .tune is a build product
# the way a .sbx is. Not anchored: they turn up wherever somebody is composing.
*.tune
/CLAUDE.md /CLAUDE.md
/claudeResume.sh /claudeResume.sh
/codexResume.sh /codexResume.sh
@@ -40,3 +46,4 @@ Source/Emulator/rom.c
# The personal disk 'make run-voyager' puts in drive 1. Made on demand, never rebuilt, and # The personal disk 'make run-voyager' puts in drive 1. Made on demand, never rebuilt, and
# not cleaned - it is the one place in here where something made ON the machine can live. # not cleaned - it is the one place in here where something made ON the machine can live.
/Disks/ /Disks/
*.sym
+2 -2
View File
@@ -39,7 +39,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
SETD.0 Banner SETD.0 Banner
@@ -72,7 +72,7 @@ deeper:
#Data #Data
#Base 0x2000 #Base 0x3000
Banner: Banner:
"two stops, and what the registers were at each "two stops, and what the registers were at each
+2 -2
View File
@@ -19,7 +19,7 @@
#Include services.asm #Include services.asm
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
; One block, and ten bytes after it. ; One block, and ten bytes after it.
@@ -81,7 +81,7 @@ noWrite:
SWI osExit SWI osExit
#Data #Data
#Base 0x2000 #Base 0x3000
Name: Name:
"claim.dat" "claim.dat"
+2 -2
View File
@@ -9,7 +9,7 @@
#Include services.asm #Include services.asm
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
SETD.0 Arguments SETD.0 Arguments
@@ -236,7 +236,7 @@ secondReadFailed:
SWI osExit SWI osExit
#Data #Data
#Base 0x2000 #Base 0x3000
Arguments: Arguments:
#Reserve 0d256 #Reserve 0d256
+2 -2
View File
@@ -14,7 +14,7 @@
#Include services.asm #Include services.asm
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
SETD.0 Arguments SETD.0 Arguments
@@ -232,7 +232,7 @@ doneFailed:
SWI osExit SWI osExit
#Data #Data
#Base 0x2000 #Base 0x3000
Arguments: Arguments:
#Reserve 0d256 #Reserve 0d256
+145
View File
@@ -0,0 +1,145 @@
; Breaks on purpose, in whichever of the four ways it is asked for.
;
; Every one of these used to stop the machine and print a line to a standard error that
; nobody behind a window is looking at, so the machine appeared to hang. The system catches
; all of them now and says what happened and where, and this is what says so - and what a
; person can run when they want to see the fault screen without having written a bug first.
;
; Crash opcode a byte in the middle of the code that does not decode
; Crash service a SWI naming a service the system does not implement
; Crash bank a transfer out of a bank that has nothing registered in it
; Crash device asking the console to interrupt, with no handler installed
; Crash blind the same bad byte, but from a screen with nowhere to print at all
;
; The last one is the odd one: it is not something this program does wrong, it is something
; it fails to have done. The interrupt arrives from outside once the console has anything to
; say, and with no input it is the END of input that arrives.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x5000
start:
SETD.0 Argument
INIB 0d15
SWI osArgument
SETD.0 Argument
SETD.1 WordOpcode
CALL textSame
BRQ crashOpcode
SETD.0 Argument
SETD.1 WordService
CALL textSame
BRQ crashService
SETD.0 Argument
SETD.1 WordBank
CALL textSame
BRQ crashBank
SETD.0 Argument
SETD.1 WordDevice
CALL textSame
BRQ crashDevice
SETD.0 Argument
SETD.1 WordBlind
CALL textSame
BRQ crashBlind
SETD.0 Usage
SWI osPrintString
INIA 0x0A
OUTA 0x00
INIA 0x01
SWI osExit
; ---- The case the fault screen exists for ----
;
; Bitmap mode has no text rows, so the console draws NOTHING there: a program that faults
; here leaves the system with a message to print and nowhere to print it. Putting the screen
; back into a mode that has characters in it is the difference between a diagnosis and a
; machine that appears to have hung.
crashBlind:
; ---- And with the colours ruined as well ----
;
; A known mode is only half a known screen. This makes the ink of attribute one the same as
; its paper, which is what a program that wrote its own palette can easily leave behind -
; and a message printed into that is perfectly present and completely invisible.
INIA 0d4
OUTA 0xE3
INIA 0x30
OUTA 0xE2
INIA 0x03
OUTA 0xE8 ; Video memory as bank four.
INIA 0d4
OUTA 0xE3
INIA 0xFC
OUTA 0xE4
INIA 0x40
OUTA 0xE5 ; 0xFC40, the two entries attribute one draws from.
RSTA
INIB 0d8
crashBlindWipe:
OUTA 0xE9
DECB
BNB crashBlindWipe ; Both of them black, ink and paper alike.
INIA 0x02
OUTA 0x31
crashOpcode:
0x00 ; Not an instruction, and never will be.
crashService:
SWI 0d40 ; Forty is nobody's.
crashBank:
INIA 0d9 ; Nothing is registered there.
OUTA 0xE0
RSTA
OUTA 0xE1
OUTA 0xE2
INIA 0d1
OUTA 0xE3 ; Into Data Memory.
RSTA
OUTA 0xE4
OUTA 0xE5
OUTA 0xE6
INIA 0d16
OUTA 0xE7
INIA 0x01
OUTA 0xE8 ; Blit, from a bank that is not there.
crashDevice:
INIA 0x02 ; Interrupt me when the console has something to say.
OUTA 0x02
SIF
crashWait:
; Never touches the console, so whatever happens next came from outside.
BRI crashWait
#Data
#Base 0x3000
WordOpcode:
"opcode"
WordService:
"service"
WordBank:
"bank"
WordDevice:
"device"
WordBlind:
"blind"
Usage:
"Crash opcode | service | bank | device | blind"
Argument:
#Reserve 0d16
#Include text.asm
+377
View File
@@ -0,0 +1,377 @@
; A ball that goes behind some pillars and in front of others.
;
; Four pillars, each ONE 8 by 8 TILE stretched to its own width and height, at four different
; distances. One ball, moving across all of them. Which pillars it passes behind is decided
; per screen column by the depth buffer, and that is the thing worth watching: the ball is in
; front of the far pillars and behind the near ones IN THE SAME FRAME, without the program
; sorting anything or drawing anything twice.
;
; ---- Why that needs a buffer rather than an ordering ----
;
; Sprites are drawn in table order, so a program can put one in front of another by numbering
; them. That is enough when things are flat. It stops being enough the moment a thing is
; nearer than one part of the scenery and further than another - a ball halfway past a pillar
; is in front of the floor beside it and behind the pillar itself, and there is no order of
; two sprites that means "behind, on those columns only".
;
; So the scenery says how far away it is, a column at a time, and the ball says how far away
; IT is. The device compares them per pixel.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x5000
start:
; The atlas: the tiles, the sprite table and the depth buffer are all in it.
INIA 0d4
OUTA 0xE3
INIA 0x30
OUTA 0xE2
INIA 0x03
OUTA 0xE8
SETD.0 Message
SWI osPrintString
INIA 0x0A
OUTA 0x00
CALL putArt
CALL clearDepth
CALL putPillars
CALL putBall
INIA 0x01
OUTA 0x02 ; Key mode.
everyFrame:
CALL waitFrame
CALL stepBall
CALL moveBall
INA 0x01
INIB 0x01 ; READY
AND
BRQ everyFrame
INA 0x00
RSTA
OUTA 0x02 ; Line mode. The sprites and the buffer are the system's to clear.
SWI osExit
; ---- Two tiles ----
;
; Tile 200 is the ball, which has a shape and so comes out of the Data Segment. Tile 201 is
; the pillar, which is a solid block and so is a Fill: 64 bytes of index one, no art needed.
putArt:
INIA 0x01
OUTA 0xE0 ; SourceBank: Data Memory.
SETD.1 BallArtAt
SETD.0 BallArt
STD.0.1
LDA.1
OUTA 0xE1
INCD.1
LDA.1
OUTA 0xE2
INIA 0d4
OUTA 0xE3
INIA 0x32
OUTA 0xE4 ; Tile 200 is at 200 times 64, which is 0x3200.
RSTA
OUTA 0xE5
OUTA 0xE6
INIA 0x40
OUTA 0xE7
INIA 0x01
OUTA 0xE8 ; Blit.
INIA 0x32
OUTA 0xE4
INIA 0x40
OUTA 0xE5 ; Tile 201 is 64 bytes further on, at 0x3240.
INIA 0x01
OUTA 0xE2 ; Index ONE, which is the only index a scheme gives a colour to:
; the default palette sets each scheme's paper and ink and
; nothing between them.
RSTA
OUTA 0xE6
INIA 0x40
OUTA 0xE7
INIA 0x02
OUTA 0xE8 ; Fill.
RET
; ---- The buffer, emptied ----
;
; Nought in a column means nothing is there. It is the program's buffer and nobody clears it
; between programs, so a program that means "nothing yet" has to say so.
clearDepth:
INIA 0d4
OUTA 0xE3
INIA 0xD0
OUTA 0xE4
RSTA
OUTA 0xE5
OUTA 0xE2 ; Fill takes the byte it writes from SourceLow.
INIA 0x02
OUTA 0xE6
INIA 0x80
OUTA 0xE7 ; 640 bytes, one a column.
INIA 0x02
OUTA 0xE8
RET
; ---- The pillars ----
;
; Four records of seven bytes: x, y, height, depth. Each becomes a sprite entry and a run of
; the depth buffer saying how far away that part of the scenery is.
putPillars:
INIA 0d4
SETD.0 PillarLeft
STA.0 ; Sprite one is at 0xC010; sprite nought is the ball.
INIA 0x10
SETD.0 EntryLow
STA.0
SETD.3 Pillars
putOnePillar:
; ---- The entry, written straight through ----
;
; Sixteen bytes out of one port with the address named once, because the controller's Data
; port steps on after every byte. DP3 walks the record and survives the calls below.
INIA 0d4
OUTA 0xE3
INIA 0xC0
OUTA 0xE4
SETD.0 EntryLow
LDA.0
OUTA 0xE5
INIA 0xC9
OUTA 0xE9 ; Tile 201, the pillar block.
INIA 0x01
OUTA 0xE9 ; Attribute one.
; Kept as they go past, because putPillarDepth needs the column and a Data Pointer cannot
; be walked backwards to find it again.
LDA.3
OUTA 0xE9 ; X low.
SETD.0 PillarXLow
STA.0
INCD.3
LDA.3
OUTA 0xE9 ; X high.
SETD.0 PillarXHigh
STA.0
INCD.3
LDA.3
OUTA 0xE9 ; Y low.
INCD.3
LDA.3
OUTA 0xE9 ; Y high.
INCD.3
INIA 0x11
OUTA 0xE9 ; One tile by one, which is the art. The size below is the look.
RSTA
OUTA 0xE9 ; No flags.
INIA 0d32
OUTA 0xE9
RSTA
OUTA 0xE9 ; Thirty two pixels wide, whatever the tile is.
LDA.3
OUTA 0xE9 ; Height low.
INCD.3
LDA.3
OUTA 0xE9 ; Height high.
INCD.3
; ---- And a depth of NOUGHT, which is not the pillar's distance ----
;
; It is tempting to put the pillar's own distance here, and it is wrong: the depth buffer
; is what has already been DRAWN, and a sprite's depth is a QUESTION ASKED OF IT. A pillar
; that both wrote 20 into its own columns and carried 20 would be asked whether it was in
; front of itself, and 20 is not nearer than 20, so it would vanish. Which it did.
;
; Scenery writes the buffer. It does not ask.
RSTA
OUTA 0xE9
CALL putPillarDepth
; On to the next record and the next entry.
INCD.3
SETD.0 EntryLow
LDA.0
INIB 0d16
CCF
ADD
STQ.0
SETD.0 PillarLeft
LDA.0
DECA
STA.0
BNA putOnePillar
RET
; ---- How far away those thirty two columns are ----
;
; A Fill, because the whole run is one number: the pillar is flat on, so every column of it
; is the same distance. DP3 is on the depth byte of the record and stays there.
;
; The buffer begins at 0xD000, whose low byte is nought - so the column's low byte IS the
; address's low byte, and its high byte only has to be added to 0xD0. No sixteen bit sum.
putPillarDepth:
LDA.3
OUTA 0xE2 ; The byte to fill with, which is the depth.
INIA 0d4
OUTA 0xE3
SETD.0 PillarXHigh
LDA.0
INIB 0xD0
CCF
ADD
OUTQ 0xE4
SETD.0 PillarXLow
LDA.0
OUTA 0xE5
RSTA
OUTA 0xE6
INIA 0d32
OUTA 0xE7 ; Thirty two columns of it.
INIA 0x02
OUTA 0xE8
RET
; ---- The ball ----
;
; Sprite nought, so it is in front of every pillar as far as the TABLE is concerned. What
; puts it behind some of them is its depth and nothing else, which is the whole point.
putBall:
INIA 0d4
OUTA 0xE3
INIA 0xC0
OUTA 0xE4
RSTA
OUTA 0xE5
INIA 0xC8
OUTA 0xE9 ; Tile 200.
INIA 0x02
OUTA 0xE9 ; Attribute two.
RSTA
OUTA 0xE9
OUTA 0xE9 ; X, low then high.
INIA 0d230
OUTA 0xE9
RSTA
OUTA 0xE9 ; Y.
INIA 0x11
OUTA 0xE9 ; One tile by one.
RSTA
OUTA 0xE9 ; No flags.
INIA 0d48
OUTA 0xE9
RSTA
OUTA 0xE9
INIA 0d48
OUTA 0xE9
RSTA
OUTA 0xE9 ; Forty eight by forty eight, from eight by eight of art.
INIA 0d45
OUTA 0xE9 ; And forty five away, which is between the pillars.
RET
waitFrame:
INA 0x30
INIB 0x01
AND
BRQ waitFrame
RET
; Two pixels to the right, wrapping past the far side. Sixteen bits in two bytes, so the high
; one steps only when the low one came round to nought.
stepBall:
SETD.0 BallX
LDA.0
INCA
INCA
STA.0
BNA stepCheck
SETD.0 BallXHigh
LDA.0
INCA
STA.0
stepCheck:
SETD.0 BallXHigh
LDA.0
INIB 0x02
CCF
SUB
BNQ stepDone ; Not past 0x0200, so nowhere near the end.
RSTA
SETD.0 BallX
STA.0
SETD.0 BallXHigh
STA.0
stepDone:
RET
moveBall:
INIA 0d4
OUTA 0xE3
INIA 0xC0
OUTA 0xE4
INIA 0x02
OUTA 0xE5 ; X is bytes two and three of the entry.
SETD.0 BallX
LDA.0
OUTA 0xE9
SETD.0 BallXHigh
LDA.0
OUTA 0xE9
RET
#Data
#Base 0x3000
Message:
"A ball, behind the near pillars and in front of the far ones. Press a key."
; x, y, height, depth. Nearer pillars are taller, which is the only thing making this look
; like distance rather than like four bars.
Pillars:
0x50 0x00 0x64 0x00 0xF0 0x00 0d20 ; x 80, y 100, 240 tall, near
0xC8 0x00 0xDC 0x00 0x78 0x00 0d60 ; x 200, y 220, 120 tall, far
0x68 0x01 0xA0 0x00 0xB4 0x00 0d30 ; x 360, y 160, 180 tall, nearish
0xF4 0x01 0x04 0x01 0x50 0x00 0d80 ; x 500, y 260, 80 tall, furthest
; Where putPillarDepth reads the column from. DP3 is on the record's depth byte by then, and
; a Data Pointer cannot be read backwards, so the two halves are kept here as they go past.
PillarXLow:
0x00
PillarXHigh:
0x00
PillarLeft:
0x00
EntryLow:
0x00
BallArt:
0x00 0x00 0x01 0x01 0x01 0x01 0x00 0x00
0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x00
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x00
0x00 0x00 0x01 0x01 0x01 0x01 0x00 0x00
BallArtAt:
#Reserve 0d2
BallX:
0x00
BallXHigh:
0x00
+2 -2
View File
@@ -51,7 +51,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
; ---- Room for a PATH, not a name ---- ; ---- Room for a PATH, not a name ----
@@ -869,7 +869,7 @@ addByteToWord:
#Data #Data
#Base 0x2000 #Base 0x3000
Break: Break:
0x0A 0x00 0x0A 0x00
+2 -2
View File
@@ -4,7 +4,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
; Swap ValueB and ValueA. ; Swap ValueB and ValueA.
@@ -72,7 +72,7 @@ start:
#Data #Data
#Base 0x2000 #Base 0x3000
ValueA: ValueA:
; Low byte, high byte. ; Low byte, high byte.
+2 -2
View File
@@ -4,7 +4,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
; Swap ValueB and ValueA. ; Swap ValueB and ValueA.
@@ -128,7 +128,7 @@ start:
#Data #Data
#Base 0x2000 #Base 0x3000
ValueA: ValueA:
; Lowest byte ... Highest byte. ; Lowest byte ... Highest byte.
+2 -2
View File
@@ -4,7 +4,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
; Load our initial values into A and B. ; Load our initial values into A and B.
@@ -38,6 +38,6 @@ start:
#Data #Data
#Base 0x2000 #Base 0x3000
#Include print.asm #Include print.asm
+2 -2
View File
@@ -24,7 +24,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
; ---- Write it ---- ; ---- Write it ----
@@ -133,7 +133,7 @@ noDelete:
#Data #Data
#Base 0x2000 #Base 0x3000
Name: Name:
"kept.txt" "kept.txt"
+148
View File
@@ -0,0 +1,148 @@
; The screen nobody is looking at.
;
; The screen brings two map banks and shows one of them. Everything this program draws goes
; into the other, so the picture does not change at all while it is being built - and then
; one byte out of one port shows the whole of it at once.
;
; ---- Why that is worth a port ----
;
; A screen drawn where it can be seen is seen half drawn. A game 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. The machines this one is pretending to
; be had the same problem and solved it the same way, except that they had to catch the few
; lines between one frame and the next to do the swap in. Here a frame is drawn from one bank
; in one go, so a flip cannot land halfway through one and there is nothing to race.
;
; ---- What it costs ----
;
; A whole bank, which is 64K of somebody's memory - and nothing, which is the point. The
; second screen is memory the device brought, the same as the first, so a program that wants
; it registers it and a program that does not never pays for it.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x5000
start:
; ---- The one bank this touches ----
;
; Six: three is the disk's and four and five are the ones the system registers to save a
; screen with, so six is the first number free. Nothing hands these out - see the table in
; the CosmOS README.
INIA 0d6
OUTA 0xE3
INIA 0x3B
OUTA 0xE2
INIA 0x03
OUTA 0xE8
; ---- And it does NOT take the screen ----
;
; osTakeScreen saves what is on the screen and puts it back at exit, and this program
; called it at first. That was wrong in a way worth keeping written down: it saves the
; screen AS IT WAS BEFORE, so the line printed below - the line whose whole job is to still
; be there afterwards - was wiped out by the restore that was meant to be tidy.
;
; It was not needed either. NOTHING THIS TOUCHES IS THE SHELL'S: the only memory it writes
; is the map of the screen nobody is looking at, and the only register it leaves changed is
; which screen that is - which the system puts back itself.
;
; A program that wants a screen saved should ask. A program that damages nothing should
; not, and asking anyway is not free - it costs the screen it was standing on.
SETD.0 Message
SWI osPrintString
INIA 0x0A
OUTA 0x00 ; The assembler has no escapes; a newline is a byte.
; Key mode, so a key arrives when it is pressed rather than when Return is.
INIA 0x01
OUTA 0x02
; ---- Every cell of the other screen, in one command ----
;
; TEN, and the reason is worth the paragraph. Fill writes one byte, and a cell is two, so
; whatever is filled with is BOTH the tile and the attribute - there is no filling a map
; with a tile and a colour that are different numbers.
;
; Ten as a tile is an asterisk, because the font begins at the space and glyph n is
; character n plus thirty two. Ten as an attribute is one of the eight REVERSED schemes,
; whose paper is a colour and whose ink is black. So the screen comes out green, covered in
; black asterisks, without a single tile being redefined.
;
; The high nibble stays at nought, which the manual asks for: it is reserved. That is the
; real constraint on which byte this can be - it has to be under sixteen to leave the
; nibble alone, and eight or over to land on a reversed scheme whose PAPER is the colour.
;
; Which means THIS PROGRAM WRITES NOTHING BUT THE SCREEN NOBODY IS LOOKING AT. It was
; drawn with a tile of its own first, and that tile was one the system copies the font back
; over at every exit - so the screen it had filled turned blank the moment it left, and a
; check that it had been put back could not tell a restored screen from an abandoned one.
; A picture that depends on the atlas does not survive leaving.
INIA 0d6
OUTA 0xE3
INIA 0x40
OUTA 0xE4
RSTA
OUTA 0xE5
INIA 0x0A
OUTA 0xE2 ; Fill takes the byte it writes from SourceLow.
INIA 0x80
OUTA 0xE6
RSTA
OUTA 0xE7 ; The whole map, which is 0x8000 bytes.
INIA 0x02
OUTA 0xE8
; NOTHING HAS CHANGED ON THE SCREEN. Every byte of that went where nobody can see it, and
; the line printed above is still sitting there to prove it - which is the reason this
; waits here rather than flipping straight away. What a back buffer is for is not visible
; in the flip; it is visible in the time before one.
CALL waitKey
; And this is the whole of showing it.
INIA 0x01
OUTA 0x3C
CALL waitKey
; ---- Which screen is showing is NOT put back here ----
;
; On purpose, and it is the one thing in this program worth arguing about. The system
; restores it at exit, the same way it restores the cursor and the ink, and for the same
; reason: a program that FAULTED while flipped could not have put it back, and a shell that
; only came out right for programs which remembered would be a shell that came out wrong
; the day one crashed. What the person is looking at belongs to the system.
;
; The console mode below IS put back, because that is this program's own borrowing rather
; than something the system hands out.
RSTA
OUTA 0x02 ; Line mode again.
SWI osExit
; ---- A key, asked for rather than waited on ----
;
; The console holds one until somebody wants it, so nothing pressed while the map was being
; filled is lost - it is sitting there and this returns immediately, which is right. A key
; pressed is a key meant for this program.
waitKey:
INA 0x01
INIB 0x01 ; READY
AND
BRQ waitKey
INA 0x00 ; Taken, so the shell is not handed a key meant for this.
RET
#Data
#Base 0x3000
; One line and no newline in it. A string literal is terminated where it ends, so a second
; one after a 0x0A byte would never be reached - printing stops at the first terminator. The
; newline is written to the console directly instead.
Message:
"A screen is drawn where you cannot see it. A key shows it, another comes back."
+16 -5
View File
@@ -23,7 +23,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
; ---- Reaching video memory ---- ; ---- Reaching video memory ----
@@ -37,12 +37,23 @@ start:
; fail - it succeeds, and the disk's buffer quietly becomes the screen. Every read the ; fail - it succeeds, and the disk's buffer quietly becomes the screen. Every read the
; filesystem made after that came out of video memory, so the shell found an empty disk ; filesystem made after that came out of video memory, so the shell found an empty disk
; and could not start anything by name. Nothing said a word. ; and could not start anything by name. Nothing said a word.
; TWO BANKS, because the screen has two. The atlas holds the tiles and the palette and is
; written when a program starts; the screen holds the map and is written as things move.
; They are separate memories, so a bank number reaches one or the other and never both -
; which is the whole of what this program had to learn when they were split apart.
INIA 0d4 INIA 0d4
OUTA 0xE3 ; DestBank: the number it will answer to. OUTA 0xE3 ; DestBank: the number it will answer to.
INIA 0x30 INIA 0x30
OUTA 0xE2 ; SourceLow: the port of the device that owns it. OUTA 0xE2 ; SourceLow: the port of the device that owns it.
INIA 0x03 INIA 0x03
OUTA 0xE8 ; RegisterBank. OUTA 0xE8 ; RegisterBank. Four is the atlas.
INIA 0d5
OUTA 0xE3
INIA 0x3A
OUTA 0xE2 ; And the port that owns the screen.
INIA 0x03
OUTA 0xE8 ; RegisterBank. Five is the map.
; ---- Asking for the screen back afterwards ---- ; ---- Asking for the screen back afterwards ----
; ;
@@ -153,7 +164,7 @@ finished:
OUTA 0x34 OUTA 0x34
OUTA 0x36 ; The origins, or the shell looks at a corner of the map. OUTA 0x36 ; The origins, or the shell looks at a corner of the map.
INIA 0d4 INIA 0d5
OUTA 0xE3 OUTA 0xE3
INIA 0x40 INIA 0x40
OUTA 0xE4 OUTA 0xE4
@@ -350,7 +361,7 @@ putMap:
STA.0 STA.0
everyRow: everyRow:
INIA 0d4 INIA 0d5
OUTA 0xE3 OUTA 0xE3
SETD.0 MapRow SETD.0 MapRow
LDA.0 LDA.0
@@ -411,7 +422,7 @@ everyCell:
#Data #Data
#Base 0x2000 #Base 0x3000
ScreenKept: ScreenKept:
0x00 0x00
+2 -2
View File
@@ -17,7 +17,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
CIF ; Nothing arrives until there is something to catch it. CIF ; Nothing arrives until there is something to catch it.
@@ -85,7 +85,7 @@ keyStop:
#Data #Data
#Base 0x2000 #Base 0x3000
Banner: Banner:
"keys, by interrupt. q stops." "keys, by interrupt. q stops."
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -32,7 +32,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
CALL seedGlider CALL seedGlider
@@ -350,7 +350,7 @@ delayDone:
#Data #Data
#Base 0x2000 #Base 0x3000
RowCount: RowCount:
0x00 0x00
+50
View File
@@ -0,0 +1,50 @@
; Forty columns or eighty, whichever the screen is not in.
;
; A machine wakes up in the forty column mode and CosmOS asks for eighty, because that is
; what its own output was written for. A game is the other way round: Snake on a forty column
; screen is the same board drawn twice the size, which is what a person sitting in front of
; it actually wants.
;
; ---- The smallest program this system can load ----
;
; Ten instructions and NO DATA AT ALL, which is not a curiosity: it is the shape that found
; a bug in the loader. Every program written for CosmOS until this one had something in its
; Data Segment, so the loader had never been asked to move a segment of no bytes - and a
; length of zero asks the memory controller for the whole 64K, which does not fit, which it
; refused, which stopped the machine in the middle of loading. On a terminal that printed a
; fault. Behind a window it looked exactly like a hang.
;
; So this is kept dataless on purpose. The two digits it prints are put in a register one at
; a time rather than being a string, which is the only reason it can say anything at all.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x5000
start:
INA 0x31 ; Which mode the screen is in now.
BRA modeWide ; Nought is the forty column one, so go the other way.
; Anything else becomes forty, and that deliberately includes bitmap mode. A program that
; left the screen with no text on it left nowhere to print, so coming back to a mode that
; has characters in it is more use than refusing.
RSTA
OUTA 0x31
INIA 0x34 ; '4'
BRI modeSay
modeWide:
INIA 0x01
OUTA 0x31
INIA 0x38 ; '8'
modeSay:
OUTA 0x00
INIA 0x30 ; '0'
OUTA 0x00
INIA 0x0A
OUTA 0x00
RSTA
SWI osExit
+33 -5
View File
@@ -1,14 +1,15 @@
; Read a text file one screen at a time. ; Read a text file one screen at a time.
; ;
; Twenty two lines are shown before a prompt. Space advances another screen, Return one ; A screenful of lines is shown before a prompt - as many as the screen has, asked for rather
; line, and q gives the machine back to CosmOS. This is forward-only on purpose: the file ; than assumed, so that the eighty column mode is not read three fifths empty. Space advances
; another screen, Return one line, and q gives the machine back to CosmOS. This is forward-only on purpose: the file
; stream holds one block and never asks the whole document to fit in memory. ; stream holds one block and never asks the whole document to fit in memory.
; ;
; Written by ChatGPT for Anachronaut's SplitBit ; Written by ChatGPT for Anachronaut's SplitBit
#Include services.asm #Include services.asm
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
SETD.0 Name SETD.0 Name
@@ -100,11 +101,38 @@ pauseQuit:
INIB 0x01 INIB 0x01
OR OR
RET RET
; ---- As many lines as the screen has, less the prompt ----
;
; Twenty two was written when there was one screen size. It is still right on the forty
; column screen and wastes half of the eighty column one, so this ASKS: the rows register
; says how tall the screen is, and it is readable for exactly this sort of reason.
;
; Rows minus three is twenty two on a twenty five row screen, so nothing changed underneath
; anyone who was already reading files this way - it fills a bigger screen and leaves a
; smaller one alone.
;
; A BITMAP SCREEN HAS NO ROWS AT ALL and says so with a nought, which would come out as 253
; lines through an eight bit subtraction. Anything under five falls back, because a page of
; two lines is not a page and a program should not be the thing that discovers this.
fullPage: fullPage:
INA 0x33
SETD.3 LinesLeft SETD.3 LinesLeft
INIA 0d22 STA.3 ; Kept, because the sums below want A for themselves.
INIB 0d5
CCF
SUB
BRC fullPageFallback ; Borrowed, so there are fewer than five rows.
LDA.3
INIB 0d3
CCF
SUB
MVQA
STA.3 STA.3
RET RET
fullPageFallback:
INIA 0d22
STA.3 ; DP3 is still LinesLeft, from above.
RET
noName: noName:
SETD.0 Usage SETD.0 Usage
@@ -139,7 +167,7 @@ finished:
SWI osExit SWI osExit
#Data #Data
#Base 0x2000 #Base 0x3000
Usage: Usage:
"more: give me a file name "more: give me a file name
" "
+2 -2
View File
@@ -25,7 +25,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
SETD.0 Wanted SETD.0 Wanted
@@ -114,7 +114,7 @@ measureDone:
#Data #Data
#Base 0x2000 #Base 0x3000
Prefix: Prefix:
"system " "system "
+162
View File
@@ -0,0 +1,162 @@
; What the controllers are doing, as they do it.
;
; Prints a line whenever any pad changes: which pad, and the byte it is now reporting. That is
; the whole of the diagnostic, and it is enough to tell apart the three things that look
; identical from inside a game which is not responding.
;
; Nothing printed at all, and nought pads present: the front end never saw a controller.
; A pad present but no lines: it is seen, and the buttons are mapped to nothing.
; Lines that do not match the buttons pressed: the mapping is wrong rather than missing.
;
; ---- Why a program rather than a print in the front end ----
;
; Because the question is what the MACHINE can see. A front end that printed what it thought
; it was sending would answer a different question, and the gap between those two is exactly
; where a controller that was detected, mapped and reported still did nothing: the port that
; says which pads exist counted only recorded ones, so a game asked whether there was a
; controller and was told no while the buttons underneath worked perfectly.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x5000
start:
SETD.0 Intro
SWI osPrintString
; How many are there, before anybody presses anything. One bit a pad.
SETD.0 PresentText
SWI osPrintString
INA 0x64
CALL sayByte
INIA 0x0A
OUTA 0x00
INIA 0x01
OUTA 0x02 ; Key mode, so q arrives without a Return.
everyFrame:
INA 0x30
INIB 0x01
AND
BRQ everyFrame ; A frame, which is when a pad's recording steps.
RSTA
SETD.2 Which
STA.2
eachPad:
CALL onePad
SETD.2 Which
LDA.2
INCA
STA.2
INIB 0d4
CCF
SUB
BNQ eachPad
; q gives the machine back. Anything else typed is ignored, because a pad test that
; stopped on a stray keypress would be a poor thing to lean on.
INA 0x01
INIB 0x01
AND
BRQ everyFrame
INA 0x00
INIB 0x71 ; q
CCF
SUB
BNQ everyFrame
RSTA
OUTA 0x02
SWI osExit
; ---- One pad, printed only when it changes ----
;
; A line a frame for four pads would be two hundred and forty lines a second and unreadable.
; What is worth seeing is the moment something goes down or comes up.
onePad:
; The port is 0x60 plus the pad number, and a port is an immediate byte inside the
; instruction that names it - so it cannot be computed, and the four are written out.
SETD.2 Which
LDA.2
BRA padZero
DECA
BRA padOne
DECA
BRA padTwo
INA 0x63
BRI padGot
padZero:
INA 0x60
BRI padGot
padOne:
INA 0x61
BRI padGot
padTwo:
INA 0x62
padGot:
; Against what it was last time. Last is four bytes, one a pad, so DP1 walks to this one.
SETD.1 Last
PSHA
SETD.2 Which
LDA.2
DPUA.1
POPA
LDB.1
CCF
SUB
BRQ padSame ; The same as last frame, so there is nothing to say.
STA.1 ; Remembered, so the next frame has something to compare with.
PSHA
LDA.2 ; DP2 is still Which, from working out where in Last to look.
INIB 0x30 ; '0'
CCF
ADD
OUTQ 0x00
INIA 0x3A ; ':'
OUTA 0x00
INIA 0x20
OUTA 0x00
POPA
CALL sayByte
INIA 0x0A
OUTA 0x00
padSame:
RET
; ---- One byte, as a number ----
;
; osPrintNumber takes A and B TOGETHER, A being the high half - which is the same way round as
; the shift register and every other pair on this machine, and is not what a byte in A wants.
; Passed as it stood, every value came out two hundred and fifty six times too big.
sayByte:
RSTB
CCF
ADD
MVQB ; The byte, in the low half where it belongs.
RSTA ; And nothing in the high one.
SWI osPrintNumber
RET
#Data
#Base 0x3000
Intro:
"Press buttons on a controller. q quits.
"
PresentText:
"Pads present, as a bit each: "
; One byte a pad, holding what it said last frame.
Last:
0x00 0x00 0x00 0x00
Which:
0x00
+411
View File
@@ -0,0 +1,411 @@
; Play.asm
; Four voices on one clock, which is what music is and one channel cannot be.
; Written by Anachronaut
;
; ---- One tick, four cursors ----
;
; The sound device has four channels and no idea when. The timer has a period and no idea
; what. This is the thing between them: on every tick it walks four voices, and each voice
; counts down the note it is holding and reads the next one when the count runs out.
;
; THE VOICES SHARE NOTHING BUT THE TICK. Each keeps its own place in its own track and its
; own count of how much longer the current note lasts, so a voice playing whole notes and a
; voice playing eighths cost the same and never have to know about each other. That is why
; the tick is the smallest subdivision in the piece rather than a note length: it is the
; only unit all four can agree on.
;
; ---- What a sequence is ----
;
; Pairs of bytes: what to play, then how many ticks it lasts.
;
; 0x01 to 0x7F a MIDI note. 60 is middle C and every 12 is an octave.
; 0x00 a rest - the ticks pass with nothing sounding
; 0x80 to 0xFE a command, which takes no time at all. See below.
; 0xFF the sequence is over
;
; MIDI NOTES ONLY REACH 127, so the top of the byte was free and none of that had to be
; invented. Examples/tune.asm spent zero on its end marker and so had no way to write a rest
; at all, which one voice can just about live with and four cannot: voices do not all play at
; once, and the silences are what make them separate parts rather than a chord.
;
; A note's duration is its whole life. The gate goes down when the count runs out and the next
; event begins on the same tick, so a gap between two notes is WRITTEN, as a rest, rather than
; invented by the player out of some fraction it decided on.
;
; ---- Commands, of which there is one ----
;
; A command consumes no tick: the reader acts on it and reads the next event straight away, so
; commands sit BETWEEN notes in time rather than needing a place of their own.
;
; 0x80 <address> play the rest of this voice on that patch.
;
; A patch change reshapes whatever is still ringing on the voice, and nothing can be done
; about that here: a channel has one set of parameters and a note in its release is using
; them. Deferring the load to the next note would reshape the same tail. So it is a fact about
; the hardware rather than a choice about the format, and the cure is a rest long enough for
; the release, which is the composer's to write.
;
; ONE COMMAND, and the other 125 values left alone. A tempo change, a volume ramp and a note
; slide are all easy to add and impossible to remove, and no piece of music has asked for one.
;
; ---- Sequences, and the order they go in ----
;
; A sequence is one voice's phrase. Each voice has an ORDER LIST of its own - a table of
; sequence addresses, ending in a zero - and when a sequence runs out the voice takes the next
; address from it. The voice stops when the list does.
;
; THAT IS WHERE REPETITION COMES FROM, and it costs no notation: the bass below plays the same
; sequence in the first bar and the last, and it is written once. A thirty-two bar piece that
; reuses four phrases is four phrases and a list.
;
; PER VOICE RATHER THAN ONE SHARED LIST, because that is the shape this machine likes. A
; voice's order cursor is a pointer it advances by itself, which is the same LDD and STD move
; everything else here makes; a shared table of four-column rows would have every voice
; indexing into one place, which is a worse fit for a machine with four data pointers. The
; four columns are how it READS, and how a tracker would show it; they are not how it is
; stored. Nothing keeps the voices together except that their sequences add up to the same
; length, which is a thing a compiler can check and a hand-written tune has to get right.
;
; A voice silent through a bar has a sequence that rests for it. There is no need for a way to
; say "nothing here" when a rest already says it.
;
; ---- Where the state lives ----
;
; Six bytes a voice: two of order cursor, two of sequence cursor, one of count, one of live.
; Both cursors are two bytes and at the front because that is what LDD and STD move - a
; pointer through a pointer, which is the whole reason this is a loop over four voices instead
; of the same code written out four times.
;
; A voice starts pointed at Empty, which is a sequence of nothing but its end marker. The
; first tick runs its count out, finds the end, and goes to the order list for the real first
; sequence - so the beginning needs no special case at all.
#Include services.asm
#Program
#Base 0x5000
start:
; ---- An instrument for each voice ----
;
; Four patches, designed in soundThing and converted by SoundPatch, which is the only thing
; here that understands what a patch means. The player reads a count and that many parameter
; and value pairs and knows nothing else about them.
;
; WHICH PATCH EACH VOICE STARTS ON IS DECLARED, in VoiceStart below, rather than being four
; calls in a row here. That is the shape a tune read from a file will have - a starting
; instrument is state and belongs in a header - and saying it once means the two forms do
; not drift.
;
; A PATCH BELONGS TO ITS CHANNEL, which is why four of them can be up at once. It did not
; used to: the LFOs belonged to the whole device, so whichever patch was loaded last owned
; them for every voice. Kalimba has LFO 0 switched off and the other three have it on, so
; under the old device this piece would have played with the arpeggio's setting on all four
; parts - which is exactly the fault that made Lunar Porter's low fuel warning stop
; trilling after the first landing of a run.
; ---- A tune named on the line, or the one in here ----
;
; Nothing after the name plays what this program was built with. A name reads a file and
; plays that instead, which is what makes it a player rather than a program with one song.
SETD.0 TuneName
INIB 0d63
SWI osArgument
SETD.0 TuneName
LDA.0
BRA playBuiltIn ; Nothing was typed after the name.
SETD.1 TuneBuffer ; DP0 is still the name, which is what osFileRead wants.
SWI osFileRead
BNQ playNoFile
SETD.0 TuneBuffer
CALL useTune
BNQ playNotATune
BRI playReady
playBuiltIn:
CALL useBuiltIn
playReady:
INIA 0x60 ; Room over the top for four voices at once.
OUTA 0x46
; ---- The beat ----
;
; ONLY THE CONTROL BYTE. The period was set by whoever supplied the tune, because a tick
; belongs to the tune and not to this program: a file says what it wants in its header.
;
; It used to be written here as well, AFTER useTune had taken it out of the file - so every
; tune played at a sixteenth note at 120 beats a minute whatever its header said. The only
; reason nothing noticed is that the one fixture in the suite asks for exactly that.
;
; Writing control with the run bit set is what loads the period, which is why this comes
; after whoever set it and not before.
INIA 0x07
OUTA 0x51 ; Run, repeat, interrupt.
SIF
; ---- The loop ----
;
; Step every voice, see whether any of them is still going, and then sleep. Stepping before
; waiting is what makes the first note sound on the first tick rather than the second.
tickLoop:
SETD.1 Voice0
RSTA
CALL stepVoice
SETD.1 Voice1
INIA 0d1
CALL stepVoice
SETD.1 Voice2
INIA 0d2
CALL stepVoice
SETD.1 Voice3
INIA 0d3
CALL stepVoice
SETD.1 Playing
LDA.1
BRA finished
WAIT ; Nothing at all until the timer says a tick has gone by.
BRI tickLoop
finished:
; ---- Let the last note finish, and MEASURE how long that takes ----
;
; The gates all went down on the tick the tracks ended, but a gate down is a note released
; rather than a note stopped: the oboe and the strings have a release to run. Rendered and
; measured, the tail dies out eight tenths of a second after the last event, and eight ticks
; of ring is one second - which sounds like enough and is two tenths short of it.
;
; A program that exits with sound still in the air leaves nothing able to end it: the
; program is gone and cannot drop a gate. What that turns into depends on the front end -
; behind a window the tail simply finishes, and on a terminal EMULATED TIME STOPS while the
; machine blocks on a key, so the tail freezes and comes out a snippet per keystroke.
;
; Twelve ticks is a second and a half, which covers the measured tail with room over. It is
; a number about THESE FOUR INSTRUMENTS, and a patch with a longer release would want more -
; the general answer is for the system to quieten the device when a program stops, the way
; it puts the screen back, and that is not built.
INIB 0d12
lastRing:
WAIT
DECB
BNB lastRing
CIF
RSTA
OUTA 0x51
SWI osExit
; ---- What it says when it cannot ----
;
; Before the timer has been started, both of them, so there is nothing to stop and no handler
; to take away.
playNoFile:
SETD.0 NoFileSaid
SWI osPrintString
INIA 0x01
SWI osExit
playNotATune:
SETD.0 NotATuneSaid
SWI osPrintString
INIA 0x01
SWI osExit
; ---- The tune that is in this program ----
;
; What useTune does for a file, said in eight lines for a tune that is already in memory: the
; three tables the player asks for, and an order list for each voice. Nothing is relocated,
; because the assembler already wrote the addresses.
useBuiltIn:
; The tick this tune wants. For a file it comes out of the header; for this one it is here,
; so that both hand the player the same things.
INIA 0x01
OUTA 0x52
INIA 0xE8
OUTA 0x53
INIA 0x48
OUTA 0x54 ; 0x01E848 is 125,000: a sixteenth note at 120.
SETD.0 BuiltInPatches
SETD.1 PatchTable
STD.0.1
SETD.0 BuiltInSequences
SETD.1 SequenceTable
STD.0.1
SETD.0 BuiltInVoiceStart
SETD.1 VoiceStartAt
STD.0.1
SETD.0 Order0
SETD.1 Voice0
STD.0.1
SETD.0 Order1
SETD.1 Voice1
STD.0.1
SETD.0 Order2
SETD.1 Voice2
STD.0.1
SETD.0 Order3
SETD.1 Voice3
STD.0.1
CALL startVoices
RET
; The tick has nothing to do: the loop above is the player and WAIT only needs something to
; have happened. A handler still has to exist, because an interrupt with nothing installed to
; catch it is a fault. Taking it is what brings the line down - a program that POLLED the
; timer would have to read 0x50 instead.
tick:
RETI
#Data
#Base 0x3000
; The name that followed "Play", and room for the tune it names. WHOLE BLOCKS: osFileRead
; puts 256 bytes down whatever the file's length, so the room here is a multiple of that and
; not a guess at how big a tune is.
TuneName:
#Reserve 0d64
TuneBuffer:
#Reserve 0d1024
NoFileSaid:
"no such tune"
NotATuneSaid:
"that is not a tune"
; ---- The order lists ----
;
; A table of sequence addresses each, ending in a zero. Read the four of them across and they
; are the bars; read one down and it is a part.
;
; bar 1 2 3 4
; melody Mel1 Mel2 Mel3 Mel4
; harmony Har1 Har2 Har3 Har4
; bass BassC BassF BassG BassC
; arp ArpC ArpF ArpG Silent
;
; THE BASS PLAYS BassC TWICE and it is written once. That is the whole return on having an
; order list, and it is worth noticing how little it cost: a table of labels the assembler
; fills in, and ten instructions in stepVoice.
Order0:
0d0 0d1 0d2 0d3 0xFF
Order1:
0d4 0d5 0d6 0d7 0xFF
Order2:
0d8 0d9 0d10 0d8 0xFF ; BassC twice, written once.
Order3:
0d11 0d12 0d13 0d14 0xFF
; ---- The tables, which are the only places an address lives ----
;
; Writing these out by hand is exactly the tedium a compiler exists to remove: every sequence
; has to be counted into its place above, and moving one means renumbering. That it is
; unpleasant is the point of noticing it here rather than after a tool has baked the shape in.
; The tables this tune uses. The player holds POINTERS to them rather than the tables, because
; a tune read from a file has its tables wherever the file was put - so a built-in one hands
; over the same three addresses a loaded one would.
BuiltInPatches:
OboePatch StringsPatch SquarePatch KalimbaPatch
BuiltInSequences:
Mel1 Mel2 Mel3 Mel4 ; 0 to 3
Har1 Har2 Har3 Har4 ; 4 to 7
BassC BassF BassG ; 8 to 10
ArpC ArpF ArpG Silent ; 11 to 14
; The patch each voice starts on. Not assumed, and not four calls in a row: a starting
; instrument is state, and state belongs somewhere it can be read.
BuiltInVoiceStart:
0d0 0d1 0d2 0d3
; ---- Four bars of C, F, G, C ----
;
; A bar is sixteen ticks, and every sequence in a column adds up to sixteen. Nothing enforces
; that here: get one wrong and the voices come apart, quietly, some bars later. It is the
; first thing a compiler should check.
; The melody, in quarters and halves.
Mel1:
0d64 0d4 ; E
0d67 0d4 ; G
0d72 0d8 ; C, held
0xFF
Mel2:
0d65 0d4 ; F
0d69 0d4 ; A
0d72 0d8 ; C
0xFF
Mel3:
0d62 0d4 ; D
0d67 0d4 ; G
0d71 0d8 ; B
0xFF
Mel4:
0x80 0d1 ; Patch one is Strings: the last bar is a swell, not a reed.
0d72 0d16 ; and home
0xFF
; A second part underneath it, in halves.
Har1:
0d60 0d8 ; C
0d64 0d8 ; E
0xFF
Har2:
0d57 0d8 ; A
0d60 0d8 ; C
0xFF
Har3:
0d59 0d8 ; B
0d62 0d8 ; D
0xFF
Har4:
0d64 0d16 ; E
0xFF
; The bass, one note to a bar - and the first bar's is the last bar's.
BassC:
0d48 0d16 ; C
0xFF
BassF:
0d41 0d16 ; F
0xFF
BassG:
0d43 0d16 ; G
0xFF
; And an arpeggio in eighths, which is the part that proves the others are not waiting for it.
ArpC:
0d72 0d2 0d76 0d2 0d79 0d2 0d76 0d2
0d72 0d2 0d76 0d2 0d79 0d2 0d76 0d2
0xFF
ArpF:
0d77 0d2 0d81 0d2 0d84 0d2 0d81 0d2
0d77 0d2 0d81 0d2 0d84 0d2 0d81 0d2
0xFF
ArpG:
0d79 0d2 0d83 0d2 0d86 0d2 0d83 0d2
0d79 0d2 0d83 0d2 0d86 0d2 0d83 0d2
0xFF
Silent:
0d00 0d16 ; Out for the last bar, so it is the other three.
0xFF
; The scheduler, the patch loader and the state a voice keeps. The tune above is this
; program's; everything that plays it is shared with whatever else wants to.
#Include player.asm
; The instruments, at the bottom because each brings its own #Data and a base has to come
; before anything is in the segment it bases.
#Include oboe.asm
#Include strings.asm
#Include square.asm
#Include kalimba.asm
#Vectors
Boot start
Device 0x50 tick
+2 -2
View File
@@ -12,7 +12,7 @@
#Include services.asm #Include services.asm
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
SETD.0 Argument SETD.0 Argument
@@ -213,7 +213,7 @@ doneFailed:
SWI osExit SWI osExit
#Data #Data
#Base 0x2000 #Base 0x3000
Name: Name:
"poured.dat" "poured.dat"
+2 -2
View File
@@ -19,7 +19,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
SETD.0 LineText SETD.0 LineText
@@ -141,7 +141,7 @@ keySay:
#Data #Data
#Base 0x2000 #Base 0x3000
LineText: LineText:
"a line, then keys. q stops." "a line, then keys. q stops."
+2 -2
View File
@@ -27,7 +27,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
SETD.0 Saying SETD.0 Saying
@@ -45,7 +45,7 @@ start:
#Data #Data
#Base 0x2000 #Base 0x3000
Saying: Saying:
"starting again "starting again
+2 -2
View File
@@ -16,7 +16,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
SETD.0 Given SETD.0 Given
@@ -45,7 +45,7 @@ sayEnd:
#Data #Data
#Base 0x2000 #Base 0x3000
SaidText: SaidText:
"it says: " "it says: "
+2 -2
View File
@@ -23,7 +23,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
SWI osBootState SWI osBootState
@@ -71,7 +71,7 @@ alreadySettled:
#Data #Data
#Base 0x2000 #Base 0x3000
WasTrying: WasTrying:
"the disk says a start is still in progress "the disk says a start is still in progress
+2 -2
View File
@@ -9,7 +9,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
RSTA RSTA
@@ -140,7 +140,7 @@ printCandidateHex:
#Data #Data
#Base 0x2000 #Base 0x3000
; Segment has to begin on a page boundary, and now says so itself rather than relying on ; Segment has to begin on a page boundary, and now says so itself rather than relying on
; whatever happens to have been assembled before it. The marking loop adds the prime to ; whatever happens to have been assembled before it. The marking loop adds the prime to
+2 -2
View File
@@ -4,7 +4,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
; Search the list until we find a prime. ; Search the list until we find a prime.
@@ -46,7 +46,7 @@ start:
#Data #Data
#Base 0x2000 #Base 0x3000
; The table of our prime candidates. It has to begin on a page boundary: marking walks ; The table of our prime candidates. It has to begin on a page boundary: marking walks
; the pointer's low byte and treats the carry out as running off the end of the table, ; the pointer's low byte and treats the carry out as running off the end of the table,
+2 -2
View File
@@ -38,7 +38,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
; The two pointers whose low byte is a square number. Both regions are page aligned, so ; The two pointers whose low byte is a square number. Both regions are page aligned, so
@@ -615,7 +615,7 @@ pauseInner:
#Data #Data
#Base 0x2000 #Base 0x3000
HeadCell: HeadCell:
0x00 0x00
+247
View File
@@ -0,0 +1,247 @@
; A thing that moves without the screen moving.
;
; Everything drawn on this machine before sprites was in a CELL. Something between two cells
; meant rewriting both of them, and 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 already there. Moving
; it costs two bytes: the low and high halves of where it now is. That is the whole of the
; loop below, and it is why this program can run over the shell's own text without disturbing
; a single character of it - nothing underneath is written to at all.
;
; ---- What it leaves behind ----
;
; The sprite, still in the table. On purpose, and for the same reason Flip leaves the screen
; it flipped to: a program that FAULTED would have left it too, and a system that only tidied
; up after programs which remembered would be one that left a ball sitting over the prompt
; the first time somebody's game crashed. The table is the system's to clear.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x5000
start:
; The atlas, which is where both the tiles and the sprite table live. Four is what CosmOS
; uses for it; see the table in the CosmOS README for who owns which number.
INIA 0d4
OUTA 0xE3
INIA 0x30
OUTA 0xE2
INIA 0x03
OUTA 0xE8
SETD.0 Message
SWI osPrintString
INIA 0x0A
OUTA 0x00
CALL putBall
CALL screenWidth
CALL putSprite
; Key mode, so a key arrives when it is pressed rather than when Return is.
INIA 0x01
OUTA 0x02
everyFrame:
CALL waitFrame
CALL stepBall
CALL moveSprite
; Anything typed ends it. Asked for and never waited on, so a key pressed between frames
; is still there when this looks.
INA 0x01
INIB 0x01 ; READY
AND
BRQ everyFrame
INA 0x00 ; Taken, so the shell is not handed a key meant for this.
RSTA
OUTA 0x02 ; Line mode again. The sprite is left where it is.
SWI osExit
; ---- The art, into a tile above the font ----
;
; Two hundred, which is well clear of the 135 glyphs the character generator copies back, so
; nothing here costs the shell a letter. Blitted rather than poked: it is already sixty four
; bytes of Data Segment and the controller moves it in one command.
putBall:
INIA 0x01
OUTA 0xE0 ; SourceBank: Data Memory.
; The pointer written down before it is read out of memory a byte at a time, because a Data
; Pointer's two halves cannot be got at any other way.
SETD.1 BallArtAt
SETD.0 BallArt
STD.0.1
LDA.1
OUTA 0xE1
INCD.1
LDA.1
OUTA 0xE2
INIA 0d4
OUTA 0xE3
INIA 0x32
OUTA 0xE4 ; Tile 200 begins at 200 times 64, which is 0x3200.
RSTA
OUTA 0xE5
OUTA 0xE6
INIA 0x40
OUTA 0xE7 ; Sixty four bytes.
INIA 0x01
OUTA 0xE8 ; Blit.
RET
; ---- How wide the screen is, in pixels ----
;
; Columns times eight, and this machine cannot multiply. A and B are one sixteen bit shift
; register though: with the column count in A and nothing in B, A:B holds columns times 256,
; and five shifts right divide that by thirty two - which is columns times eight, high byte
; left in A and low byte in B.
;
; Asked rather than assumed, because the shell runs eighty columns and a game may well have
; asked for forty before starting this.
screenWidth:
INA 0x32
RSTB
SHR
SHR
SHR
SHR
SHR
SETD.0 WidthHigh
STA.0
; B cannot be stored, and there is no move from it. Adding nothing to it puts it in Q,
; which can be copied to A, which can.
RSTA
CCF
ADD
MVQA
SETD.0 WidthLow
STA.0
RET
; ---- The entry, written straight through ----
;
; The controller's Data port steps its address on after every byte, so all eight go out of
; one port with the address named once.
putSprite:
INIA 0d4
OUTA 0xE3
INIA 0xC0
OUTA 0xE4
RSTA
OUTA 0xE5 ; Sprite nought is at 0xC000.
INIA 0xC8
OUTA 0xE9 ; Tile 200.
INIA 0x01
OUTA 0xE9 ; Attribute one, so the ball comes out in scheme one's ink.
RSTA
OUTA 0xE9
OUTA 0xE9 ; X, low then high.
INIA 0d96
OUTA 0xE9
RSTA
OUTA 0xE9 ; Y, ninety six pixels down.
INIA 0x11
OUTA 0xE9 ; One tile across by one down.
RSTA
OUTA 0xE9 ; Not mirrored, not turned over, not behind.
RET
; A frame, which is the only regular beat this machine has.
waitFrame:
INA 0x30
INIB 0x01
AND
BRQ waitFrame
RET
; ---- One pixel to the right ----
;
; Sixteen bits in two bytes, so the high one is stepped only when the low one came back round
; to nought - which is what a carry is, done by hand.
stepBall:
SETD.0 BallX
LDA.0
INCA
STA.0
BNA stepCheck
SETD.0 BallXHigh
LDA.0
INCA
STA.0
; Round to the left edge at the far side. Both halves have to match, and the high one is
; tested first because it is the one that is usually wrong.
stepCheck:
SETD.0 BallXHigh
LDA.0
SETD.1 WidthHigh
LDB.1
CCF
SUB
BNQ stepDone
SETD.0 BallX
LDA.0
SETD.1 WidthLow
LDB.1
CCF
SUB
BNQ stepDone
RSTA
STA.0 ; DP0 is still BallX, from the comparison just above.
SETD.0 BallXHigh
STA.0
stepDone:
RET
; Two bytes out of one port, which is the whole cost of moving a sprite.
moveSprite:
INIA 0d4
OUTA 0xE3
INIA 0xC0
OUTA 0xE4
INIA 0x02
OUTA 0xE5 ; X is bytes two and three of the entry.
SETD.0 BallX
LDA.0
OUTA 0xE9
SETD.0 BallXHigh
LDA.0
OUTA 0xE9
RET
#Data
#Base 0x3000
Message:
"A ball, over the shell's own words. Nothing underneath is written to. Press a key."
; Index nought is not a colour, it is a hole - so the corners of the tile are what the ball
; is not, and whatever is behind shows through them.
BallArt:
0x00 0x00 0x01 0x01 0x01 0x01 0x00 0x00
0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x00
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x00
0x00 0x00 0x01 0x01 0x01 0x01 0x00 0x00
BallArtAt:
#Reserve 0d2
BallX:
0x00
BallXHigh:
0x00
WidthLow:
0x00
WidthHigh:
0x00
+2 -2
View File
@@ -18,7 +18,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
SWI osLastStatus SWI osLastStatus
@@ -70,7 +70,7 @@ done:
#Data #Data
#Base 0x2000 #Base 0x3000
Prefix: Prefix:
"the last program left " "the last program left "
+2 -2
View File
@@ -36,7 +36,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
; ---- 1. How big is something that will not fit ---- ; ---- 1. How big is something that will not fit ----
@@ -408,7 +408,7 @@ printWhy:
#Data #Data
#Base 0x2000 #Base 0x3000
BigName: BigName:
"big.txt" "big.txt"
+2 -2
View File
@@ -6,7 +6,7 @@
#Include services.asm #Include services.asm
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
SETD.0 Name SETD.0 Name
@@ -70,7 +70,7 @@ finished:
SWI osExit SWI osExit
#Data #Data
#Base 0x2000 #Base 0x3000
Usage: Usage:
"type: give me a file name "type: give me a file name
" "
+137
View File
@@ -0,0 +1,137 @@
; A program that says what is on the disk.
;
; Which no program could do until now. Everything the system offered took a name a program
; already knew - read it, save it, rename it, delete it, ask how big it is - and there was no
; way to ask what names there are. dir could list only because it lives in the shell and calls
; the filesystem directly. So a file manager, a backup, and the package manager still to come
; were all unwritable for want of two services.
;
; This is those two, used as simply as they can be: walk, say what each thing is, and ask
; about it.
;
; > Walk
; f Say.sbx 1
; d Apps 0
; u halfsaved.txt 3
;
; ---- And it asks about each one AS IT GOES, which is the point ----
;
; Where a walk has got to is the system's, and so is where the last file it was asked about
; lives - the same eight bytes of filesystem state. A program that walks a directory and asks
; osFileInfo about each entry is using both at once, which is the obvious thing to write and
; the thing that would quietly go wrong if the two shared a position.
;
; Calling osFileInfo between two steps of the walk is therefore not decoration here. It is the
; check, and a walk that came back wrong afterwards would show up as a short listing or a
; repeated name rather than as an error.
#Include services.asm
#Program
#Base 0x5000
start:
SETD.0 Name
INIB 0d24
SWI osDirFirst
BRI walkCheck
walkStep:
SETD.0 Name
INIB 0d24
SWI osDirNext
walkCheck:
; The kind is written down before anything else wants the registers: the comparison below
; is an ALU operation and Q is where its answer goes.
MVQA
SETD.0 Kind
STA.0
INIB 0xFF
CCF
SUB
BRQ walkDone
; f, d or u. A chain rather than a table, because three is not enough to index - and A is
; still the kind, since SUB writes Q and leaves it alone.
BRA walkFile
INIB 0d1
CCF
SUB
BRQ walkDirectory
SETD.0 Unfinished
BRI walkSay
walkDirectory:
SETD.0 Directory
BRI walkSay
walkFile:
SETD.0 File
walkSay:
SWI osPrintString
SETD.0 Name
SWI osPrintString
SETD.0 Space
SWI osPrintString
; ---- And how big it is, asked BETWEEN two steps of the walk ----
;
; The name is the only thing the walk hands over, which is what keeps it from being a record
; both sides have to agree the shape of. A program that wants more asks about the name, the
; same way anything else does.
;
; A directory is not asked about. osFileInfo answers for one perfectly well and says nought
; blocks, which is true and reads as a size - and nought is a size a file can genuinely
; have, so the two would be indistinguishable in the listing.
SETD.0 Kind
LDA.0
INIB 0d1
CCF
SUB
BRQ walkNoSize
SETD.0 Name
SWI osFileInfo
BNQ walkNoSize
PSHD.3
POPB
POPA
SWI osPrintNumber
BRI walkEnded
walkNoSize:
SETD.0 NoSize
SWI osPrintString
walkEnded:
SETD.0 NewLine
SWI osPrintString
BRI walkStep
walkDone:
RSTA
SWI osExit
#Data
#Base 0x3000
File:
"f "
Directory:
"d "
Unfinished:
"u "
Space:
" "
NoSize:
"-"
NewLine:
0x0A 0x00
Kind:
0x00
Name:
#Reserve 0d24
+2 -2
View File
@@ -10,7 +10,7 @@
#Include services.asm #Include services.asm
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
; Where to go is the argument. Nothing else about this program says a directory name, so ; Where to go is the argument. Nothing else about this program says a directory name, so
@@ -85,7 +85,7 @@ noFile:
SWI osExit SWI osExit
#Data #Data
#Base 0x2000 #Base 0x3000
Where: Where:
#Reserve 0d64 #Reserve 0d64
+100
View File
@@ -0,0 +1,100 @@
; A program that says where it came from.
;
; The dullest thing on the disk, and that is the point of it: osWhereAmI and pathBeside are
; what an application with assets stands on, and both are much easier to check now, with
; nothing depending on them, than later inside a game that has gone quiet.
;
; Where says the path it was loaded from
; Where splash.tune says that, and the path of splash.tune beside it
;
; ---- The thing worth checking ----
;
; The answer follows the PROGRAM and not the person. Run this from the root and it says one
; thing; walk somewhere else and run the same copy and it says the same thing, because the
; system worked it out when the program started and a program's own place cannot move under
; it. A working directory is where the person is standing, and it is what the argument means:
; "Where notes.txt" asks about a notes.txt in the program's place, not in yours, which is
; exactly the distinction an application needs and the reason cwd could not serve both.
#Include services.asm
#Program
#Base 0x5000
start:
SETD.0 MyPath
INIB 0d192
SWI osWhereAmI
SETD.0 FromText
SWI osPrintString
SETD.0 MyPath
SWI osPrintString
CALL newLine
; With nothing after it, that is the whole of what it has to say.
SETD.0 Given
INIB 0d64
SWI osArgument
SETD.0 Given
LDA.0
BRA whereDone
SETD.0 MyPath
SETD.1 Given
SETD.2 Beside
INIB 0d192
CALL pathBeside
BNQ whereTooLong
SETD.0 BesideText
SWI osPrintString
SETD.0 Beside
SWI osPrintString
CALL newLine
whereDone:
RSTA
SWI osExit
whereTooLong:
; A path that did not fit is a wrong answer, and saying it would be worse than saying so.
SETD.0 TooLongText
SWI osPrintString
CALL newLine
INIA 0x01
SWI osExit
newLine:
SETD.0 NewLine
SWI osPrintString
RET
#Data
#Base 0x3000
FromText:
"loaded from "
BesideText:
"beside it: "
TooLongText:
"that path would not fit"
NewLine:
0x0A 0x00
MyPath:
#Reserve 0d192
Beside:
#Reserve 0d192
Given:
#Reserve 0d64
; ---- Included last, after both segments have been based ----
;
; A #Base says where a whole segment begins and so has to be the first thing in it, and a
; library brings code and data of its own. Put in front, its #Data would be the first thing
; in the Data Segment and this program's own #Base would be too late. This is where Lander
; keeps its seven, for the same reason.
#Include path.asm
+2 -2
View File
@@ -17,7 +17,7 @@
#Program #Program
#Base 0x4000 ; Above the system, which keeps below here. #Base 0x5000 ; Above the system, which keeps below here.
greet: greet:
SETD.0 Opening SETD.0 Opening
@@ -44,7 +44,7 @@ greet:
#Data #Data
#Base 0x2000 ; And its data above the system's. #Base 0x3000 ; And its data above the system's.
Opening: Opening:
"a program, loaded off a disk, running on the system that loaded it "a program, loaded off a disk, running on the system that loaded it
+2 -2
View File
@@ -6,7 +6,7 @@
#Program #Program
#Base 0x4000 ; Change two: #Base 0x5000 ; Change two:
SETD hello ; Change three SETD hello ; Change three
Start: Start:
@@ -26,6 +26,6 @@ End:
#Data #Data
#Base 0x2000 ; Five, adjust the base of the data segment. #Base 0x3000 ; Five, adjust the base of the data segment.
hello: ; Throw a label here so we can explicitly point at this data. Six, actually. hello: ; Throw a label here so we can explicitly point at this data. Six, actually.
"Hello, World!" "Hello, World!"
+458
View File
@@ -0,0 +1,458 @@
; A listing laid out to be read.
;
; > ls
; Copy.sbx Say.sbx Where.sbx
; Walk.sbx Lander where.sh
;
; > ls /Apps
;
; dir says what is there, one line each, with sizes and a tally of the disk. This says the
; names and nothing else, in columns, which is what you want ninety times in a hundred - and
; the two are separate programs rather than one with a switch because they answer different
; questions and neither answer is a worse version of the other.
;
; ---- Why this is a program and dir is not ----
;
; dir is built into the shell, and that is what makes it the one to trust when the disk is
; the thing being doubted: it is already in memory. This has to be loaded FROM the disk, so it
; cannot list a disk too broken to load it. The daily driver and the diagnostic, and it is
; worth having both.
;
; ---- Two passes, and no buffer at all ----
;
; Columns need the longest name before the first line can be printed, which usually means
; holding every name in memory - twenty four bytes each, and this format allows 1,024 entries
; on a disk. So instead the directory is walked TWICE: once to find the longest name and count
; them, once to print. A walk costs a read of each directory block and the disk buffer is
; already there, where a buffer for the names would be memory this program has to have whether
; the directory is large or not.
;
; ---- Across and then down, rather than down and then across ----
;
; Real listings go down the columns, so that names next to each other alphabetically are next
; to each other on the screen. THAT IS A REASON THAT DEPENDS ON SORTING, and nothing here
; sorts: entries come back in the order the directory holds them, which is the order they were
; made. With the order arbitrary, down-and-across buys nothing and costs a division to work
; out how many rows there are - on a machine that cannot divide.
;
; So this goes across. If sorting ever arrives, that is the moment to change it, and this
; comment is the reason why.
#Include services.asm
#Program
#Base 0x5000
start:
; A directory to list, if one was named. The shell puts the working directory back when a
; program exits, so this can walk off somewhere and not tidy up after itself.
SETD.0 Given
INIB 0d64
SWI osArgument
SETD.0 Given
LDA.0
BRA lsHere
SWI osChangeDir
BNQ lsNoSuchPlace
lsHere:
; ---- Pass one: how wide the widest of them is ----
RSTA
SETD.0 Longest
STA.0
SETD.0 Seen
STA.0
SETD.0 Name
INIB 0d24
SWI osDirFirst
BRI lsMeasure
lsMeasureNext:
SETD.0 Name
INIB 0d24
SWI osDirNext
lsMeasure:
MVQA
SETD.0 Kind
STA.0
INIB 0xFF
CCF
SUB
BRQ lsMeasured
SETD.0 Seen
LDA.0
INCA
STA.0
CALL nameLength
MVQA
; A directory wears a separator, which is a character of width like any other.
SETD.0 Kind
LDB.0
DECB
BNB lsMeasureWidth
INCA
lsMeasureWidth:
SETD.0 Longest
LDB.0
CCF
SUB
BRC lsMeasureKeep ; Borrowed, so this one is not the longest.
STA.0
lsMeasureKeep:
BRI lsMeasureNext
lsMeasured:
; Nothing at all is said with nothing at all, which is what an empty directory looks like.
SETD.0 Seen
LDA.0
BRA lsDone
; ---- How many of them fit across ----
;
; Two spaces between columns, so that names never touch. Worked out by taking the cell width
; off the screen width until there is not room for another, because this machine cannot
; divide and the answer is never more than a handful.
SETD.0 Longest
LDA.0
INCA
INCA
SETD.0 Cell
STA.0
INA 0x32
SETD.1 Across
STA.1 ; What is left of the line, counted down.
RSTA
SETD.1 Columns
STA.1
lsFitting:
SETD.1 Across
LDA.1
SETD.1 Cell
LDB.1
CCF
SUB
BRC lsFitted ; Borrowed, so there is not room for another column.
MVQA
SETD.1 Across
STA.1
SETD.1 Columns
LDA.1
INCA
STA.1
BRI lsFitting
lsFitted:
; A name wider than the screen still gets a column of its own. One that runs over is better
; than one that is not shown.
SETD.0 Columns
LDA.0
BNA lsPrint
INIA 0d1
STA.0
lsPrint:
RSTA
SETD.0 At
STA.0
SETD.0 Name
INIB 0d24
SWI osDirFirst
BRI lsSay
lsSayNext:
SETD.0 Name
INIB 0d24
SWI osDirNext
lsSay:
MVQA
SETD.0 Kind
STA.0
INIB 0xFF
CCF
SUB
BRQ lsLastLine
; ---- The cell the last name started, finished now rather than then ----
;
; Padding AFTER a name puts spaces at the end of every line that does not fill its last
; column, and trailing whitespace on a line is the kind of thing that is invisible until
; something else reads the output. Padding before the next name instead means the spaces
; only ever go between two things, and a line ends on the last character of a name.
SETD.0 At
LDA.0
BRA lsColour ; First on its line, so there is nothing to finish.
CALL padOut
lsColour:
; ---- What it is, said in colour ----
;
; The attribute reaches a terminal as well as the screen now, so this is one mechanism and
; not two. A directory in blue, something that will run in green, an unfinished save in red,
; and everything else plain.
SETD.0 Kind
LDA.0
BRA lsFileInk
DECA
BRA lsBlue
INIA 0d1 ; An unfinished save, which is worth looking at.
BRI lsInk
lsBlue:
INIA 0d4
BRI lsInk
lsFileInk:
; ---- Whether it will run, asked by looking ----
;
; This was priced once as needing a RUNNABLE BIT in the directory entry, set by the
; filesystem from a list of magic numbers it would have to be taught - which was declined,
; twice, for putting format knowledge in the filesystem and giving two implementations a
; registry to keep in step.
;
; It costs nothing of the sort now. The shell decides what to run by READING the first block
; of a file, so "will this run" is a question with an answer already, and asking it is the
; same question the shell would ask. Nothing new is written down and nothing has to agree
; about anything: if the shell learns a third kind of runnable file, this is wrong until
; somebody adds two lines, and wrong in the direction of a listing that is less colourful.
;
; The price is a block read per file, which is what the bit existed to avoid. It is paid
; here rather than in dir on purpose: dir is the listing you audit and is built into the
; shell, and this is the one you read and is a program that was loaded off the disk anyway.
CALL runnable
BNQ lsPlain
INIA 0d2
BRI lsInk
lsPlain:
RSTA
lsInk:
OUTA 0x06
SETD.0 Name
SWI osPrintString
CALL nameLength
MVQA
SETD.0 Kind
LDB.0
DECB
BNB lsWidth
PSHA
SETD.0 Slash
SWI osPrintString
POPA
INCA
lsWidth:
; How much of the cell this name took, for whoever finishes it.
SETD.0 Used
STA.0
; Back to plain before anything else is written, so that a scheme with paper in it would
; not paint the gap between the columns.
RSTA
OUTA 0x06
SETD.0 At
LDA.0
INCA
STA.0
SETD.0 Columns
LDB.0
CCF
SUB
BNQ lsSayNext ; Room for another on this line.
RSTA
SETD.0 At
STA.0
CALL newLine
BRI lsSayNext
lsLastLine:
; A line with something on it needs ending. One that ended on its last column does not, and
; a blank line under a listing looks like a listing with a gap in it.
SETD.0 At
LDA.0
BRA lsDone
CALL newLine
lsDone:
RSTA
SWI osExit
lsNoSuchPlace:
SETD.0 NoSuchPlace
SWI osPrintString
SETD.0 Given
SWI osPrintString
CALL newLine
INIA 0x01
SWI osExit
; ---- How wide the name in the buffer is ----
;
; Q COMES BACK THE COUNT, not A. A RET puts A back the way the caller had it, so a subroutine
; that answers in A answers with nothing - and this one did, which made every gap between the
; columns the same width because the padding was subtracting whatever A happened to hold.
; Q is the ALU's output and nothing puts it back, which is why every answer on this machine
; comes home in it.
nameLength:
SETD.0 Name
RSTA
lengthNext:
LDB.0
BRB lengthDone
INCA
INCD.0
BRI lengthNext
lengthDone:
RSTB
CCF
ADD
RET
; ---- Will the shell start this? ----
;
; Q is zero if it will. The same two things the shell itself looks for in a first block: SBEX
; for a program, "#!" for a script. A file too short to have a first block at all cannot be
; either, and osFileBlock saying no is that answer arriving for free.
runnable:
SETD.0 Name
SETD.1 Block
RSTA
RSTB
SWI osFileBlock
BNQ runnableNo
; The shebang first, because two bytes settle it.
SETD.0 Block
LDA.0
INIB 0d35 ; '#'
CCF
SUB
BNQ runnableExec
INCD.0
LDA.0
INIB 0d33 ; '!'
CCF
SUB
BRQ runnableYes
runnableExec:
SETD.0 Block
SETD.1 ExecText
INIA 0d4
SETD.2 Left
STA.2
runnableSame:
LDA.0
LDB.1
CCF
SUB
BNQ runnableNo
INCD.0
INCD.1
SETD.2 Left
LDA.2
DECA
STA.2
BNA runnableSame
runnableYes:
RSTA
RSTB
CCF
ADD
RET
runnableNo:
INIA 0x01
RSTB
CCF
ADD
RET
; A is how much of the cell the name took. Fills the rest of it with spaces.
padOut:
; The cell less what the name took, and IN THAT ORDER. Written the other way round first,
; which borrowed on every name that was not the longest - so every one of them took the
; "wider than its cell" path below and the whole listing came out separated by one space.
SETD.0 Cell
LDA.0
SETD.0 Used
LDB.0
CCF
SUB
BRC padDone ; Wider than its cell, so one space keeps the names apart.
MVQA
BRA padOne
padLoop:
PSHA
SETD.0 Space
SWI osPrintString
POPA
DECA
BNA padLoop
RET
padOne:
padDone:
SETD.0 Space
SWI osPrintString
RET
newLine:
SETD.0 NewLine
SWI osPrintString
RET
#Data
#Base 0x3000
ExecText:
"SBEX"
Slash:
"/"
Space:
" "
NoSuchPlace:
"there is no such directory: "
NewLine:
0x0A 0x00
Kind:
0x00
Longest:
0x00
Cell:
0x00
Columns:
0x00
Across:
0x00
At:
0x00
Seen:
0x00
Used:
0x00
Left:
0x00
Name:
#Reserve 0d24
Given:
#Reserve 0d64
; A whole block, because osFileBlock writes one whether the file fills it or not.
Block:
#Reserve 0d256
+9 -3
View File
@@ -38,7 +38,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
SETD.0 Argument SETD.0 Argument
@@ -2180,7 +2180,7 @@ report:
#Data #Data
#Base 0x2000 #Base 0x3000
Argument: Argument:
#Reserve 0d23 #Reserve 0d23
@@ -2258,7 +2258,7 @@ ReservedLeft:
ReservedWalk: ReservedWalk:
0x00 0x00 0x00 0x00
ReservedCount: ReservedCount:
0d5 0d7
VecHandlerName: VecHandlerName:
#Reserve 0d23 #Reserve 0d23
@@ -2285,6 +2285,12 @@ ReservedNames:
"BankFault" "BankFault"
#Reserve 0d5 #Reserve 0d5
0d4 0d4
"NoHandler"
#Reserve 0d5
0d5
"NoDevice"
#Reserve 0d6
0d6
ProgPut: ProgPut:
0x00 0x00 0x00 0x00
+124 -16
View File
@@ -1,5 +1,22 @@
; The label table: the only thing that survives between the two passes. ; The label table: the only thing that survives between the two passes.
; ;
; ---- KEPT IN ORDER, so that a lookup can halve it ----
;
; It used to be walked from the front, and that was most of the assembler. Every use of every
; label paid a scan of every label defined so far, so the cost grew with the program being
; built: assembling CosmOS on the machine took 1.83 billion cycles against the assembler's own
; 57 million - three times the source for thirty two times the time.
;
; Sorted and halved instead, it is 886 million: the walk was 52 per cent of the whole
; assembly. Small programs pay about a tenth more, because adding a label now moves the tail
; of the index up to make room and a short table was never expensive to walk. That is the
; right way round for a trade to fall.
;
; WHICH END IT IS SORTED FROM DOES NOT MATTER, and that is not carelessness. labAdd takes its
; insertion point from labFind, so the comparison that decides the order is the same one that
; searches it; turn it round and the table is built backwards and read backwards and nothing
; else can tell. Which is why breaking that comparison on purpose changes no output at all.
;
; Names are packed end to end in an arena and each index entry holds a pointer into it, ; Names are packed end to end in an arena and each index entry holds a pointer into it,
; rather than every entry carrying a field wide enough for the longest name. MEASURED on ; rather than every entry carrying a field wide enough for the longest name. MEASURED on
; CosmOS, which is the biggest thing this will ever be asked to assemble: 453 labels ; CosmOS, which is the biggest thing this will ever be asked to assemble: 453 labels
@@ -72,10 +89,50 @@ labAddFresh:
CALL numCompare CALL numCompare
BRC labAddCrowded ; The arena is smaller than where this name would end. BRC labAddCrowded ; The arena is smaller than where this name would end.
; The index entry: where the name is about to go, and what it means. ; ---- Room made for it where it belongs ----
;
; The index is kept in order so that looking a name up can halve the table instead of
; walking it, and labFind left LabLo at the place this name sorts to. Everything from there
; on moves up four bytes, from the END backwards so that nothing is written over before it
; has been read.
;
; It costs about a third of the table copied per label added, which against the walk it
; replaces is nothing: the walk was paid once per USE of a label and this is paid once per
; label, and there are several uses of each.
SETD.0 LabWhich SETD.0 LabWhich
SETD.2 LabCount SETD.2 LabCount
CALL numSet CALL numSet
labAddShift:
SETD.0 LabWhich
SETD.2 LabLo
CALL numCompare
BRQ labAddPlace ; Down to where it goes, so there is nothing left to move.
; The one below this place, moved up into it.
CALL labEntryAt
SETD.1 LabEntry
LDD.1.1 ; DP1 is where it goes.
SETD.0 LabWhich
CALL numBack
CALL labEntryAt
SETD.0 LabEntry
LDD.0.0 ; DP0 is where it comes from.
INIB 0d4
labAddShiftByte:
LDA.0
STA.1
INCD.0
INCD.1
DECB
BNB labAddShiftByte
BRI labAddShift
labAddPlace:
SETD.0 LabWhich
SETD.2 LabLo
CALL numSet
CALL labEntryAt CALL labEntryAt
SETD.1 LabEntry SETD.1 LabEntry
@@ -149,14 +206,39 @@ labAddNo:
labFind: labFind:
SETD.2 LabSought SETD.2 LabSought
STD.0.2 STD.0.2
SETD.0 LabWhich
CALL numZero
labFindLoop: ; ---- Halving the table rather than walking it ----
SETD.0 LabWhich ;
; The names are kept in order, so a lookup is eleven comparisons against two thousand
; entries where a walk was six hundred and seventy. THAT WAS MOST OF THE ASSEMBLER: every
; use of every label paid it, and it grew with the program being built, so assembling
; CosmOS on the machine cost 1.83 billion cycles against the assembler's own 57 million -
; three times the source for thirty two times the time.
;
; LabLo is left at the place the name WOULD go, which is what adding one needs and what a
; walk could never have handed back.
SETD.0 LabLo
CALL numZero
SETD.0 LabHi
SETD.2 LabCount SETD.2 LabCount
CALL numSet
labFindStep:
SETD.0 LabLo
SETD.2 LabHi
CALL numCompare CALL numCompare
BNC labFindMissing ; Walked the whole table without a match. BNC labFindMissing ; Nothing left between them, so it is not here.
; The one in the middle. Adding two numbers under two thousand cannot overflow sixteen
; bits, so there is nothing to carry into.
SETD.0 LabWhich
SETD.2 LabLo
CALL numSet
SETD.0 LabWhich
SETD.2 LabHi
CALL numAdd
SETD.0 LabWhich
CALL numHalve
CALL labEntryAt CALL labEntryAt
SETD.1 LabEntry SETD.1 LabEntry
@@ -169,16 +251,33 @@ labFindLoop:
INCD.0 INCD.0
STB.0 STB.0
; ---- sameText is already an ordering ----
;
; Q is the difference at the first character that differed, so zero is a match - and the
; Carry Flag from that same subtraction says which way round they were, and survives the
; return because nothing puts the Status register back. A name that runs out while the
; other carries on borrows against the other's character, which sorts the shorter first,
; which is what anybody means by alphabetical.
SETD.1 LabNamePointer SETD.1 LabNamePointer
LDD.0.1 LDD.0.1
SETD.1 LabSought SETD.1 LabSought
LDD.1.1 LDD.1.1
CALL sameText CALL sameText
BRQ labFindGot BRQ labFindGot
BRC labFindAbove ; The one in the table is the smaller, so look above it.
SETD.0 LabWhich SETD.0 LabHi
SETD.2 LabWhich
CALL numSet
BRI labFindStep
labFindAbove:
SETD.0 LabLo
SETD.2 LabWhich
CALL numSet
SETD.0 LabLo
CALL numStep CALL numStep
BRI labFindLoop BRI labFindStep
labFindGot: labFindGot:
SETD.1 LabEntry SETD.1 LabEntry
@@ -285,6 +384,12 @@ LabEntry:
0x00 0x00 0x00 0x00
LabOffset: LabOffset:
0x00 0x00 0x00 0x00
; Where a halving search has got to, and where a name that is not there would go. LabLo is
; what labAdd makes room at, which is the whole reason the search hands it back.
LabLo:
0x00 0x00
LabHi:
0x00 0x00
LabWhich: LabWhich:
0x00 0x00 0x00 0x00
LabLength: LabLength:
@@ -297,18 +402,21 @@ LabEnd:
; How many labels there may be, and how many bytes of name between them. ; How many labels there may be, and how many bytes of name between them.
; ;
; SIZED FOR THE ASSEMBLER ITSELF, which turns out to be the largest thing it is asked to ; SIZED FOR THE ASSEMBLER ITSELF, which turns out to be the largest thing it is asked to
; build: 555 labels and about 6,800 bytes of name, against CosmOS's 650 and 8,081 - CosmOS is ; build: the assembler is about 555 labels, against CosmOS's 1,341 and 16,758 bytes of name
; the bigger of the two now, and was the smaller when this was written. Running into either ; measured 2026-09-06 - CosmOS is much the bigger of the two now, and was the smaller when
; this was written. Running into either
; limit says so rather than writing past the end of the table, and that is what it did. The buffers live ; limit says so rather than writing past the end of the table, and that is what it did. The buffers live
; above the program rather than inside it - see the scratch map in Asm.asm. ; above the program rather than inside it - see the scratch map in Asm.asm.
; Fifteen hundred and thirty six names, and sixteen kilobytes to hold them in. Both were ; Two thousand and forty eight names, and twenty six kilobytes to hold them in. Both have
; half that, and the names ran out first: 8,081 bytes of 8,192, which is a hundred and ; now been doubled twice, and BOTH TIMES THE NAMES RAN OUT FIRST with the index a couple of
; eleven - and the thing that ran into it was one ordinary piece of work adding eighteen ; hundred behind them - 8,081 of 8,192 the first time, 16,758 of 16,384 the second. A name
; labels. THESE TWO MUST AGREE WITH THE SCRATCH MAP, which says where the room actually is. ; is thirteen bytes on average and an index entry is four, so the arena will always be the
; one that speaks; raising it alone would buy a few hundred labels and then the other wall.
; THESE TWO MUST AGREE WITH THE SCRATCH MAP, which says where the room actually is.
LabLimit: LabLimit:
0x06 0x00 0x08 0x00
LabRoom: LabRoom:
0x40 0x00 0x68 0x00
LabNamed: LabNamed:
": " ": "
+39
View File
@@ -112,6 +112,45 @@ numCompare:
numCompareDone: numCompareDone:
RET RET
; Takes one off the two byte number at DP0. The mirror of numStep, and wanted for the same
; reason: walking an index backwards is what moving a run of entries up needs.
numBack:
INCD.0
LDA.0
BNA numBackLow ; The low half has something to take, so the high half is safe.
DECD.0
LDA.0
DECA
STA.0
INCD.0
LDA.0
numBackLow:
DECA
STA.0
RET
; The two byte number at DP0 becomes half of itself.
;
; A and B are a CIRCULAR sixteen bit register, so a rotate right brings bit nought back in at
; the top rather than dropping it - which is a halving only once that bit is taken off again.
; The numbers this is asked about are index positions, well under thirty two thousand, so the
; top bit was nought before the rotate and clearing it afterwards loses nothing.
numHalve:
LDA.0
INCD.0
LDB.0
SHR
; The low half goes down FIRST, because taking the wrapped bit off the high half wants B
; for the mask and there is nowhere else to keep it.
STB.0
INIB 0x7F
AND
MVQA
DECD.0
STA.0
RET
; Q is zero if the strings at DP0 and DP1 are the same, both ending in a zero byte. ; Q is zero if the strings at DP0 and DP1 are the same, both ending in a zero byte.
; ;
; Down here rather than with the label table, where it started, because four separate ; Down here rather than with the label table, where it started, because four separate
+2 -2
View File
@@ -14,7 +14,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
SETD.0 Wanted SETD.0 Wanted
@@ -60,7 +60,7 @@ noFile:
#Data #Data
#Base 0x2000 #Base 0x3000
Wanted: Wanted:
#Reserve 0d23 #Reserve 0d23
+33 -15
View File
@@ -12,7 +12,7 @@
; ;
; None of this is initialised data. It is scratch, wanted only while the assembler is ; None of this is initialised data. It is scratch, wanted only while the assembler is
; running, and while it is running everything above its own data is free: the system keeps ; running, and while it is running everything above its own data is free: the system keeps
; below 0x1FFF, the staging area is only in use during a load, and the Stack comes down ; below 0x2FFF, the staging area is only in use during a load, and the Stack comes down
; from the top. So the addresses are written down here and the file carries none of it. ; from the top. So the addresses are written down here and the file carries none of it.
; ;
; That sentence said 0x1000 for a while after the system's half of Data Memory was ; That sentence said 0x1000 for a while after the system's half of Data Memory was
@@ -20,10 +20,10 @@
; bad enough; a stale number sitting next to the correction is worse, because whichever ; bad enough; a stale number sitting next to the correction is worse, because whichever
; one a reader takes is a coin toss. ; one a reader takes is a coin toss.
; ;
; 0x4000 6144 the label index, 1536 entries of four ; 0x5000 8192 the label index, 2048 entries of four
; 0x5800 16384 the label names, packed end to end ; 0x7000 26624 the label names, packed end to end
; 0x9800 256 one block of the output file, on its way to the disk ; 0xD800 256 one block of the output file, on its way to the disk
; 0x9900 18176 free ; 0xD900 1792 free
; 0xE000 1792 the vector names, 64 entries of twenty eight ; 0xE000 1792 the vector names, 64 entries of twenty eight
; 0xE700 2048 the reader's stack, six levels of 301 ; 0xE700 2048 the reader's stack, six levels of 301
; 0xEF00 368 which files have been included, sixteen names of 23 ; 0xEF00 368 which files have been included, sixteen names of 23
@@ -35,9 +35,15 @@
; and the map stayed where it was, leaving sixteen kilobytes between the two that nothing ; and the map stayed where it was, leaving sixteen kilobytes between the two that nothing
; touched. ; touched.
; ;
; Starting at 0x4000 takes that back. The assembler's data is a little over four kilobytes ; Starting above the assembler's own data takes that back. It began at 0x4000 with the data
; from 0x2000, so there is still nearly four kilobytes of slack in front of this - and room ; from 0x2000, and moved to 0x5000 when the system was given another page and every
; for its data to double before the two would meet. `make test` measures that gap now ; application's data moved to 0x3000 with it - THE FLOOR CAME UP A SECOND TIME, exactly as
; the paragraph above says it did the first, and this time the check below said so before
; anything ran: the assembler's data reached 0x40D6 and the index began at 0x4000, so the
; buffers were sitting on the variables.
;
; There is still nearly four kilobytes of slack in front of this, and room for the data to
; double before the two would meet. `make test` measures that gap now
; rather than trusting this paragraph, and measures the floor above as well, because both ; rather than trusting this paragraph, and measures the floor above as well, because both
; of those numbers describe the machine AROUND this file and neither is enforced by a line ; of those numbers describe the machine AROUND this file and neither is enforced by a line
; of code anywhere. ; of code anywhere.
@@ -63,6 +69,17 @@
; among buffers that do not need it, because an unclaimed page is available to whichever ; among buffers that do not need it, because an unclaimed page is available to whichever
; one turns out to want it. ; one turns out to want it.
; ;
; ---- And one turned out to want it ----
;
; Sixteen kilobytes of names was half full when that was written and ran out on 2026-09-06,
; on an ordinary day's work on the shell: 16,758 bytes of name against 16,384. The index was
; 1,341 of 1,536 in the same breath, which is the paragraph above this one happening again -
; two ceilings, and lifting only the one that spoke would have bought two hundred labels.
;
; So both, out of the free page, and both left at about a third clear rather than at half:
; the names to 26,624 and the index to 2,048. What is left over is 1,792 bytes, kept
; unclaimed for the same reason the seventeen kilobytes were.
;
; That ends at 0xF070, with the Stack coming down from 0xFFFF above it - nearly four ; That ends at 0xF070, with the Stack coming down from 0xFFFF above it - nearly four
; kilobytes, against the tens of bytes of CALL frames this ever nests. ; kilobytes, against the tens of bytes of CALL frames this ever nests.
; ;
@@ -72,16 +89,17 @@
; list moved up rather than the stack simply being asked to fit. ; list moved up rather than the stack simply being asked to fit.
; ;
; THE TWO THINGS THAT DECIDE THESE SIZES are the largest program it will be asked to build ; THE TWO THINGS THAT DECIDE THESE SIZES are the largest program it will be asked to build
; and the largest one it will be asked to read. CosmOS is 475 labels and 9,564 bytes of ; and the largest one it will be asked to read. Measured 2026-09-06: CosmOS is 1,341 labels
; output; the assembler itself is 555 labels, about 6,800 bytes of name and 11,648 of output. The ; and 16,758 bytes of name, and the assembler itself is far smaller. It was the other way
; second is bigger than the first, which is worth knowing: the hardest thing this assembles ; round when this file was written - "the hardest thing this assembles is not the operating
; is not the operating system, it is itself. ; system, it is itself" - and the system has grown past it since. The numbers here are worth
; measuring again rather than believing, which is why they carry a date.
ScratchLabIndex: ScratchLabIndex:
0x40 0x00 0x50 0x00
ScratchLabArena: ScratchLabArena:
0x58 0x00 0x70 0x00
ScratchWindow: ScratchWindow:
0x98 0x00 0xD8 0x00
ScratchVecNames: ScratchVecNames:
0xE0 0x00 0xE0 0x00
ScratchSrcStack: ScratchSrcStack:
+194 -10
View File
@@ -105,6 +105,143 @@ srcRewindNo:
ADD ; Q is not zero: it is not. ADD ; Q is not zero: it is not.
RET RET
; ---- Everything up to and including the next newline, thrown away ----
;
; Q is zero if a newline was found, and something else if the file ended first.
;
; THIS IS MOST OF WHAT THE ASSEMBLER READS. Measured on this repository: 87 per cent of what
; Say.asm pulls in is comment and blank line, and 59 per cent of CosmOS - the house style is
; dense commentary, so the assembler is penalised more than most by its own sources. Every one
; of those bytes used to go through tokGet and srcNext and be classified and thrown away.
;
; The saving is not the classifying, it is the BOOKKEEPING. srcNext loads and stores the
; walking pointer through memory for every single character, and asks numCompare whether the
; buffer is used up. Here the pointer lives in DP0 for a whole run and the newline lives in B,
; so a comment byte costs a load, a compare and two steps.
;
; A run is capped at 255 so that one byte can count it, which is why the outer loop exists at
; all: a full block is 256, one more than a byte holds.
srcSkipComment:
SETD.0 SrcEnded
LDA.0
BNA srcSkipEnded
srcSkipBlock:
; What is left of the buffer.
SETD.0 SrcSpan
SETD.2 SrcCount
CALL numSet
SETD.0 SrcSpan
SETD.2 SrcAt
CALL numTake
SETD.0 SrcSpan
LDA.0
INCD.0
LDB.0
OR
BRQ srcSkipRefill ; Used up, so the next block or the end.
SETD.0 SrcSpan
LDA.0
BNA srcSkipCapped ; A high half at all means 256, which is one too many to count.
INCD.0
LDA.0
BRI srcSkipCounted
srcSkipCapped:
INIA 0xFF
srcSkipCounted:
SETD.0 SrcRun
STA.0
SETD.0 SrcTaken
STA.0 ; Where the run began, to work out what it swallowed.
SETD.2 SrcPointer
LDD.0.2
SETD.1 SrcRun
INIB 0x0A ; The newline, held in B for the whole run.
srcSkipChar:
LDA.0
XOR
BRQ srcSkipNewline
INCD.0
LDA.1
DECA
STA.1
BNA srcSkipChar
; The run ended without a newline in it, so the rest of the block is comment too.
SETD.2 SrcPointer
STD.0.2
CALL srcSkipAccount
BRI srcSkipBlock
srcSkipNewline:
; Past the newline itself, which belongs to the comment.
INCD.0
SETD.1 SrcRun
LDA.1
DECA
STA.1
SETD.2 SrcPointer
STD.0.2
CALL srcSkipAccount
; A newline is what makes the next character part of the next line, counted as it goes past
; exactly as srcNext counts it.
SETD.0 SrcLine
CALL numStep
RSTA
RSTB
CCF
ADD
RET
srcSkipRefill:
CALL srcLoad
BRQ srcSkipBlock ; Another block of the same file.
; ---- A file that runs out inside a comment ----
;
; The same thing srcAtEnd does for a character, and it has to be done the same way here: an
; included file ending puts the one that included it back on, and the comment carries on
; into it exactly as it did when this was read a character at a time.
;
; NOT DOING THIS WAS THE WHOLE OF THE FIRST VERSION'S BUG. services.asm ends inside a
; comment, so an include of it never came back, and every label after the #Include line -
; the whole of the program doing the including - was quietly never defined. It surfaced as
; "no label of that name is defined anywhere" pointing at a line whose label was three
; lines from the end of the file.
INIA 0d1
SETD.0 SrcEnded
STA.0
SETD.0 SrcDepth
LDA.0
BRA srcSkipEnded ; Nothing included this, so there is no more source anywhere.
CALL srcPop
BRI srcSkipComment
srcSkipEnded:
INIA 0x01
RSTB
CCF
ADD
RET
; How much of the run went by, added to how far into the buffer we have read.
srcSkipAccount:
SETD.0 SrcTaken
LDA.0
SETD.0 SrcRun
LDB.0
CCF
SUB
MVQA
SETD.0 SrcAt
CALL numAddByte
RET
; The next character of the file, into SrcChar. Q is zero if there was one, and something ; The next character of the file, into SrcChar. Q is zero if there was one, and something
; else at the end of the file. ; else at the end of the file.
srcNext: srcNext:
@@ -112,11 +249,29 @@ srcNext:
LDA.0 LDA.0
BNA srcAtEnd BNA srcAtEnd
; Is the buffer used up? SrcAt counts how far into it we have read and SrcCount how many ; ---- Is the buffer used up? ----
; of its bytes are the file's, which is 256 for every block but a short last one. ;
; SrcAt counts how far into it we have read and SrcCount how many of its bytes are the
; file's, which is 256 for every block but a short last one.
;
; ASKED HERE RATHER THAN BY numCompare, because this is the hottest question in the
; assembler - every character of every pass asks it - and a call to answer it costs more
; than the answer. The high bytes settle it almost every time and settle it FIRST: a full
; block is 256, so SrcCount's high byte is one and SrcAt's is nought until the last
; sixteenth of the block. Six instructions, where the call was twenty.
SETD.0 SrcAt SETD.0 SrcAt
SETD.2 SrcCount SETD.2 SrcCount
CALL numCompare LDA.0
LDB.2
CCF
SUB
BNQ srcHaveByte
INCD.0
INCD.2
LDA.0
LDB.2
CCF
SUB
BNQ srcHaveByte BNQ srcHaveByte
CALL srcLoad CALL srcLoad
BNQ srcAtEnd BNQ srcAtEnd
@@ -129,19 +284,31 @@ srcHaveByte:
STD.0.1 STD.0.1
SETD.0 SrcChar SETD.0 SrcChar
STA.0 STA.0
SETD.0 SrcAt
CALL numStep
; A newline is what makes the next character part of the next line. Counting it here, ; ---- A newline is what makes the next character part of the next line ----
; as it is handed out, means the line number always describes the character just given. ;
SETD.0 SrcChar ; Counted here, as the character is handed out, so the line number always describes the
LDA.0 ; character just given. A still holds it - nothing since the load has wanted it - where
; this used to fetch it back out of SrcChar it had just been put in.
INIB 0x0A INIB 0x0A
XOR XOR
BNQ srcNextDone BNQ srcNextStep
SETD.0 SrcLine SETD.0 SrcLine
CALL numStep CALL numStep
srcNextStep:
; And one further into the buffer, counted the same way and for the same reason.
SETD.0 SrcAt
INCD.0
LDA.0
INCA
STA.0
BNC srcNextDone
DECD.0
LDA.0
INCA
STA.0
srcNextDone: srcNextDone:
RSTA RSTA
RSTB RSTB
@@ -576,6 +743,23 @@ SrcStateBytes:
SrcDepthLimit: SrcDepthLimit:
0d6 0d6
; ---- OUTSIDE the state block above, and that is not an accident ----
;
; What is left of the buffer, how much of it one run may take, and where that run began.
; They live for the length of one call to srcSkipComment and never across an include, so they
; are not part of what a file remembers while another one is being read.
;
; PUT AMONG THE STATE FIRST, which added four bytes to a block that is copied whole by a
; count written down somewhere else - so every saved file lost the last four bytes of itself
; and an include came back with its buffer pointer wrong. The comment two paragraphs up says
; not to do it, and says it in capitals.
SrcSpan:
0x00 0x00
SrcRun:
0x00
SrcTaken:
0x00
; Where an include is looked for when it is not beside you. One fixed place rather than a ; Where an include is looked for when it is not beside you. One fixed place rather than a
; list somebody sets, for the same reason the shell has one fixed place for programs: a ; list somebody sets, for the same reason the shell has one fixed place for programs: a
; list would need somewhere to live between one boot and the next. ; list would need somewhere to live between one boot and the next.
+10 -6
View File
@@ -54,13 +54,17 @@ tokSkip:
BNQ tokBegin BNQ tokBegin
tokComment: tokComment:
CALL tokGet ; ---- Thrown away by the reader, not read one character at a time ----
;
; This used to walk the comment through tokGet and srcNext, classifying every byte of it on
; the way to discarding it. Comments are most of what this assembler reads - 87 per cent of
; what a small program pulls in, once the libraries it includes are counted - so it was most
; of the work, and all of it thrown away.
;
; A pushed back character cannot be sitting there: this is only reached from tokSkip, which
; got the semicolon out of tokGet and did not put anything back.
CALL srcSkipComment
BNQ tokEnded BNQ tokEnded
SETD.0 TokChar
LDA.0
INIB 0x0A
XOR
BNQ tokComment ; Everything up to the newline belongs to the comment.
BRI tokSkip BRI tokSkip
tokBegin: tokBegin:
+2 -2
View File
@@ -15,7 +15,7 @@
#Program #Program
#Base 0x4000 #Base 0x5000
start: start:
SETD.0 Wanted SETD.0 Wanted
@@ -128,7 +128,7 @@ noFile:
#Data #Data
#Base 0x2000 #Base 0x3000
Wanted: Wanted:
#Reserve 0d23 #Reserve 0d23
+16
View File
@@ -0,0 +1,16 @@
#! script
; The launcher for Lunar Porter, which is what typing "Lander" actually starts.
;
; An application with things of its own gets a DIRECTORY of its own, and a typed word names
; a file rather than a directory - so what sits in /Apps is this, and the game sits with its
; tune in /Packages/app.Lander. The shell decides what to run by reading it, so this file
; beginning "#!" and Say.sbx beginning "SBEX" go down the same road and neither the shell nor
; the person has to know which kind of thing they started.
;
; That indirection is the point of it. Where the game physically lives stops being part of
; its name, so an installer may put it on another disk and change only this line.
;
; #quiet, because a launcher is machinery and not narration. Without it, starting a game
; prints the path of the thing it is about to start.
#quiet
0:/Packages/app.Lander/Lander.sbx $args
+614 -33
View File
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+290
View File
@@ -1916,6 +1916,278 @@ sbfsEntryBounds:
sbfsBoundsDone: sbfsBoundsDone:
RET RET
; ---- What the whole disk is holding ----
;
; SbfsUsedBlocks becomes what every entry on the disk holds between them, and SbfsUsedEntries
; how many slots are filled. Q is zero if the disk could be read.
;
; EVERY ENTRY, NOT EVERY ENTRY IN A DIRECTORY. Free space is a fact about the disk and not
; about where you are standing, so this walks the directory table itself the way allocating
; does, rather than using sbfsFirst and sbfsNext - which stop only on entries whose parent is
; the working directory, and would have called the same disk emptier from one directory than
; from another.
;
; A DIRECTORY HOLDS NO BLOCKS and adds nought, which is true by its fields rather than by a
; test here. An unfinished save holds all of its, and is counted, because the blocks are
; spoken for whatever the entry is called - and a listing that left them out would promise
; room that is not there.
;
; The count in the superblock is not consulted. sbfs.asm calls it "a note rather than the
; truth" in three places, and this is the truth: it costs a read of every directory block,
; which is what the honest answer costs.
sbfsSpace:
RSTA
SETD.0 SbfsUsedBlocks
STA.0
INCD.0
STA.0
SETD.0 SbfsUsedEntries
STA.0
INCD.0
STA.0
SETD.0 SbfsDirStart
SETD.1 SbfsBlock
CALL sbfsCopyWord
SETD.0 SbfsDirBlocks
INCD.0
LDA.0
SETD.1 SbfsLeft
STA.1
sbfsSpaceBlock:
CALL sbfsReadBlock
BNQ sbfsSpaceFailed
SETD.1 SbfsBuffer
CALL sbfsBufferOut
SETD.2 SbfsBuffer
INIA 0d8
SETD.1 SbfsCount
STA.1
sbfsSpaceEntry:
LDA.2
INIB 0x01
AND
BRQ sbfsSpaceNext ; A free slot holds nothing and fills nothing.
SETD.0 SbfsUsedEntries
CALL sbfsStepWord
; The blocks this entry holds, which are at the same offset in an entry as they are
; everywhere else. sbfsEntryBounds wants DP2 on the entry and gives back both ends, and the
; length is the difference - worked out that way rather than read directly, so that one
; routine owns where in an entry those fields are.
PSHD.2
CALL sbfsEntryBounds
SETD.0 SbfsEntryEnd
SETD.2 SbfsEntryStart
CALL sbfsSubWord
SETD.0 SbfsUsedBlocks
SETD.2 SbfsEntryEnd
CALL sbfsAddWord
POPD.2
sbfsSpaceNext:
DPUP.2 0d32
SETD.1 SbfsCount
LDA.1
DECA
STA.1
BNA sbfsSpaceEntry
SETD.0 SbfsBlock
CALL sbfsStepWord
SETD.1 SbfsLeft
LDA.1
DECA
STA.1
BNA sbfsSpaceBlock
RSTA
RSTB
CCF
ADD
RET
sbfsSpaceFailed:
INIA 0x01
RSTB
CCF
ADD
RET
; ---- The longest run of free blocks there is ----
;
; SbfsBiggest becomes the largest number of blocks that are free AND NEXT TO EACH OTHER.
; Q is zero if the disk could be read.
;
; This is the number that says whether a file will fit. Files are laid down contiguously -
; first fit, with the directory itself as the map and no allocation table anywhere - so a disk
; with a thousand blocks free in ten scattered pieces will refuse a file of two hundred, and
; the free total gives no hint of it.
;
; ---- The same trick allocating uses, for the same reason ----
;
; There is no sort on this machine and the entries are in no order, so the gaps cannot simply
; be listed. Instead a candidate walks the disk: each pass looks for the used extent nearest
; at or after it, and anything the candidate is standing inside pushes the candidate to the
; far end of it and starts the pass again.
;
; So the cost is a pass of the directory table per gap, rather than per file - and it is a
; measurement that costs nothing on a disk with one gap at the end, which is what a disk that
; has only been appended to looks like, and costs more the more fragmented the disk is. Which
; is the right way round: it is slow exactly when it has something to say.
sbfsLargestRun:
RSTA
SETD.0 SbfsBiggest
STA.0
INCD.0
STA.0
CALL sbfsFirstData
sbfsRunPass:
; Nothing is in the way yet this time round, and the nearest thing ahead is the end of the
; disk - which is the answer when there is nothing ahead at all.
RSTA
SETD.0 SbfsRunMoved
STA.0
SETD.0 SbfsRunNext
SETD.2 SbfsDiskBlocks
CALL sbfsSetWord
SETD.0 SbfsDirStart
SETD.1 SbfsBlock
CALL sbfsCopyWord
SETD.0 SbfsDirBlocks
INCD.0
LDA.0
SETD.1 SbfsLeft
STA.1
sbfsRunBlock:
CALL sbfsReadBlock
BNQ sbfsRunFailed
SETD.1 SbfsBuffer
CALL sbfsBufferOut
SETD.2 SbfsBuffer
INIA 0d8
SETD.1 SbfsCount
STA.1
sbfsRunEntry:
LDA.2
INIB 0x01
AND
BRQ sbfsRunNextEntry ; A free slot is in nobody's way.
LDA.2
INIB 0x02
AND
BNQ sbfsRunNextEntry ; Nor is a directory, which holds no blocks - see allocating.
CALL sbfsEntryBounds
PSHD.2 ; The comparisons want DP2, and the walk wants it back.
; Behind the candidate entirely, so it says nothing about what is ahead.
SETD.0 SbfsEntryEnd
SETD.2 SbfsCandidate
CALL sbfsCompareWord
BRC sbfsRunClear
BRQ sbfsRunClear
; Beginning at or before the candidate, so the candidate is standing inside it. It moves to
; the far end and the pass begins again, exactly as allocating does.
SETD.0 SbfsEntryStart
SETD.2 SbfsCandidate
CALL sbfsCompareWord
BRC sbfsRunStraddles
BRQ sbfsRunStraddles
; Ahead of the candidate. Is it the nearest thing ahead so far?
SETD.0 SbfsEntryStart
SETD.2 SbfsRunNext
CALL sbfsCompareWord
BNC sbfsRunClear
SETD.0 SbfsRunNext
SETD.2 SbfsEntryStart
CALL sbfsSetWord
BRI sbfsRunClear
sbfsRunStraddles:
SETD.0 SbfsCandidate
SETD.2 SbfsEntryEnd
CALL sbfsSetWord
INIA 0x01
SETD.0 SbfsRunMoved
STA.0
sbfsRunClear:
POPD.2
sbfsRunNextEntry:
DPUP.2 0d32
SETD.1 SbfsCount
LDA.1
DECA
STA.1
BNA sbfsRunEntry
SETD.0 SbfsBlock
CALL sbfsStepWord
SETD.1 SbfsLeft
LDA.1
DECA
STA.1
BNA sbfsRunBlock
; The pass is over. If the candidate moved, everything measured against where it used to be
; was measured against the wrong place, so the pass is worth nothing and is done again.
SETD.0 SbfsRunMoved
LDA.0
BNA sbfsRunPass
; Nothing moved, so the gap from the candidate to the nearest thing ahead is a real one.
SETD.0 SbfsRunGap
SETD.2 SbfsRunNext
CALL sbfsSetWord
SETD.0 SbfsRunGap
SETD.2 SbfsCandidate
CALL sbfsSubWord
SETD.0 SbfsBiggest
SETD.2 SbfsRunGap
CALL sbfsCompareWord
BNC sbfsRunKept ; What is already remembered is the same or bigger.
SETD.0 SbfsBiggest
SETD.2 SbfsRunGap
CALL sbfsSetWord
sbfsRunKept:
; Past the end of the disk is the end of the walk. Otherwise the candidate goes to the
; thing that stopped it, which the next pass finds itself standing inside and steps over.
SETD.0 SbfsRunNext
SETD.2 SbfsDiskBlocks
CALL sbfsCompareWord
BNC sbfsRunDone
SETD.0 SbfsCandidate
SETD.2 SbfsRunNext
CALL sbfsSetWord
BRI sbfsRunPass
sbfsRunDone:
RSTA
RSTB
CCF
ADD
RET
sbfsRunFailed:
INIA 0x01
RSTB
CCF
ADD
RET
; Finds a run of SbfsWantBlocks free blocks and puts where it begins in SbfsFileStart. ; Finds a run of SbfsWantBlocks free blocks and puts where it begins in SbfsFileStart.
; Q is zero if there was room. ; Q is zero if there was room.
; ;
@@ -3691,6 +3963,24 @@ SbfsScratch1:
SbfsPathWanted: SbfsPathWanted:
0x00 0x00
; What every entry on the disk holds between them, and how many slots are filled. Worked out
; by sbfsSpace, which reads the whole directory table to do it.
; The longest run of free blocks, and the sweep that finds it: where it has got to ahead of
; the candidate, whether anything pushed the candidate this pass, and the gap being measured.
SbfsBiggest:
0x00 0x00
SbfsRunNext:
0x00 0x00
SbfsRunGap:
0x00 0x00
SbfsRunMoved:
0x00
SbfsUsedBlocks:
0x00 0x00
SbfsUsedEntries:
0x00 0x00
SbfsFileStart: SbfsFileStart:
0x00 0x00 0x00 0x00
SbfsFileBlocks: SbfsFileBlocks:
+121 -8
View File
@@ -72,7 +72,7 @@ scriptSlotAt:
DECA DECA
BRA scriptSlotDone BRA scriptSlotDone
scriptSlotStep: scriptSlotStep:
DPUP.3 0d71 DPUP.3 0d205
DECA DECA
BNA scriptSlotStep BNA scriptSlotStep
scriptSlotDone: scriptSlotDone:
@@ -80,7 +80,7 @@ scriptSlotDone:
; Seventy bytes, DP0 to DP1. ; Seventy bytes, DP0 to DP1.
scriptCopyState: scriptCopyState:
INIB 0d71 INIB 0d205
scriptCopyByte: scriptCopyByte:
LDA.0 LDA.0
STA.1 STA.1
@@ -154,8 +154,21 @@ scriptOpenFirst:
INIB 0d63 INIB 0d63
CALL copyText CALL copyText
; ---- And what it was given ----
;
; After the push, so that what the script one level up was given is already somewhere safe.
; A script started with nothing gets an EMPTY one rather than none at all, which is what
; makes "$args" always mean something inside a script while "$1" can be missing.
SETD.1 ScriptArgsFrom
LDD.0.1
SETD.1 ScriptArgs
INIB 0d128
CALL copyText
CALL scriptKeepDrive
SETD.0 ScriptName SETD.0 ScriptName
SWI osFileInfo SWI osFileInfo
CALL scriptGiveDriveBack
BRQ scriptOpenThere BRQ scriptOpenThere
INIA 0x01 INIA 0x01
BRI scriptOpenFailed BRI scriptOpenFailed
@@ -243,6 +256,21 @@ scriptLine:
STD.0.1 STD.0.1
scriptLineAgain: scriptLineAgain:
; ---- Where this line begins ----
;
; Kept before it is read, because a loop has to be able to go back to the line that opened
; it and by the time that line has been read the reader is past it. Three words, and the
; block itself is read again on the way back, which is what scriptReread is for.
SETD.0 ScriptIndex
SETD.1 ScriptLineIndex
CALL sbfsCopyWord
SETD.0 ScriptAt
SETD.1 ScriptLineAt
CALL sbfsCopyWord
SETD.0 ScriptBlocks
SETD.1 ScriptLineBlocks
CALL sbfsCopyWord
SETD.1 ScriptLength SETD.1 ScriptLength
RSTA RSTA
STA.1 STA.1
@@ -411,6 +439,7 @@ scriptFill:
OR OR
BRQ scriptFillNoMore BRQ scriptFillNoMore
CALL scriptKeepDrive
SETD.0 ScriptName SETD.0 ScriptName
SETD.1 ScriptBlock SETD.1 ScriptBlock
SETD.2 ScriptIndex SETD.2 ScriptIndex
@@ -418,6 +447,7 @@ scriptFill:
INCD.2 INCD.2
LDB.2 LDB.2
SWI osFileBlock SWI osFileBlock
CALL scriptGiveDriveBack
BNQ scriptFillNoMore BNQ scriptFillNoMore
; DP3 is how many bytes came back. The nought goes after them. ; DP3 is how many bytes came back. The nought goes after them.
@@ -473,6 +503,40 @@ scriptFillNoMore:
ADD ADD
RET RET
; ---- Reading a script must not move the person ----
;
; The name is resolved afresh for every block, and a name with a drive in front of it moves
; the machine to that drive as a side effect of being resolved - sbfsWalk calls sbfsUse on
; the way past. So a script found in the system's place on drive 0, started by somebody
; standing on a disk of their own, would run its lines on the system disk instead of theirs.
;
; It matters more now than it used to. `do 0:/Apps/setup.sh` was always able to do this;
; typing a name now finds scripts the same three places a program is looked for, and the
; third of them is drive-qualified.
;
; The answer that survives the CALL: Q into A and the byte count onto the stack, because
; sbfsUse writes Q and RET puts DP3 back the way it found it.
scriptKeepDrive:
INA 0x24
SETD.1 ScriptDrive
STA.1
RET
scriptGiveDriveBack:
PSHD.3
MVQA
PSHA
SETD.1 ScriptDrive
LDA.1
CALL sbfsUse
POPA
POPD.3
; A holds what Q held. Adding nought to it is how it becomes Q again.
RSTB
CCF
ADD
RET
; Everything up to and including the next line feed, thrown away. Used for the shebang. ; Everything up to and including the next line feed, thrown away. Used for the shebang.
scriptSkipLine: scriptSkipLine:
CALL scriptByte CALL scriptByte
@@ -526,11 +590,35 @@ LoudWord:
ScriptDepth: ScriptDepth:
0x00 0x00
; ---- Seventy bytes, and they are next to each other on purpose ---- ; The drive the person is standing on, kept across a block being fetched. Not part of the
; saved state below: it is only ever live for the length of one read.
ScriptDrive:
0x00
; Where the arguments are, for the length of one call to scriptOpen. A pointer rather than the
; text, because the text is still sitting in the line that named the script and copying it
; twice would buy nothing.
ScriptArgsFrom:
0x00 0x00
; ---- Two hundred and five bytes, and they are next to each other on purpose ----
; ;
; Name, blocks left, next block, where in the block, and whether it is echoing: the whole of ; Name, blocks left, next block, where in the block, what it was given, and whether it is
; where a script has got to. Saving it is one copy because of this order, and nothing else ; echoing: the whole of a running script. Saving it is one copy because of this order, and
; may be put between them. ; nothing else may be put between them.
;
; ---- THREE NUMBERS DESCRIBE THIS BLOCK AND ALL THREE MUST AGREE ----
;
; scriptCopyState how many bytes one of them is
; scriptSlotAt how far apart two saved ones are
; ScriptSaved four of them
;
; They did not agree, and that is what this warning is for. Six bytes of line position were
; added in the middle of the block and the count stayed at 71, so the last six bytes of every
; saved script were never saved: a nested script's #quiet leaked back out to the script that
; called it, which is the opposite of what this file's own comment promises. The line position
; that went unsaved with it turned out not to matter - a loop keeps its own copy in the block
; record - but nothing said so, and the next field added would have been chosen at random.
ScriptName: ScriptName:
#Reserve 0d64 #Reserve 0d64
ScriptBlocks: ScriptBlocks:
@@ -539,6 +627,19 @@ ScriptIndex:
0x00 0x00 0x00 0x00
ScriptAt: ScriptAt:
0x00 0x00 0x00 0x00
; ---- And where the line being read began ----
;
; A loop goes back to the line that opened it, and by the time that line has been read the
; reader is past it. So the position is kept before every line rather than worked out
; afterwards, which cannot be done: a line is not a fixed size and there is nothing to
; subtract.
ScriptLineIndex:
0x00 0x00
ScriptLineAt:
0x00 0x00
ScriptLineBlocks:
0x00 0x00
; Saved with the rest, so that a quiet script calling a loud one gets its quiet back when ; Saved with the rest, so that a quiet script calling a loud one gets its quiet back when
; the loud one finishes. A new script INHERITS it rather than resetting, because a build ; the loud one finishes. A new script INHERITS it rather than resetting, because a build
; that asked for quiet meant its helpers too; only the first script started from the prompt ; that asked for quiet meant its helpers too; only the first script started from the prompt
@@ -546,10 +647,22 @@ ScriptAt:
ScriptQuiet: ScriptQuiet:
0x00 0x00
; ---- What the script was given ----
;
; The rest of the line that started it, kept whole rather than picked apart, because picking
; it apart costs a place to put every piece and a limit on how many there may be. "$1" walks
; this and copies out the word it wants; "$args" is this.
;
; A WHOLE LINE OF ROOM, so that nothing a person can type can be cut short. The line is 127
; and the script's own name has already come off the front of it, so this can never be filled
; - which is worth 128 bytes a level to never have to explain.
ScriptArgs:
#Reserve 0d128
; Three would do - a save happens on the second script and not the first - but four costs ; Three would do - a save happens on the second script and not the first - but four costs
; seventy bytes and removes an off-by-one from the only place it could hide. ; one more and removes an off-by-one from the only place it could hide.
ScriptSaved: ScriptSaved:
#Reserve 0d284 #Reserve 0d820
ScriptInto: ScriptInto:
0x00 0x00 0x00 0x00
ScriptRoom: ScriptRoom:
+64
View File
@@ -41,6 +41,13 @@
; going out it is A and B together, and neither direction needs a record in memory that ; going out it is A and B together, and neither direction needs a record in memory that
; both sides have to agree on the shape of. ; both sides have to agree on the shape of.
osFileRead 0d20 ; DP0 names it, DP1 says where. Q is zero if it read, DP3 is how many bytes. osFileRead 0d20 ; DP0 names it, DP1 says where. Q is zero if it read, DP3 is how many bytes.
; ---- AND IT WRITES WHOLE BLOCKS ----
;
; A disk is read a block at a time, so a sixteen byte file still puts
; 256 bytes where it is told to. The room given has to be the file's
; length ROUNDED UP to the next 256, and a caller that gives exactly
; the length writes over whatever follows it. DP3 still says how many
; bytes are the file's; the rest is whatever was on the block.
osFileSave 0d21 ; DP0 names it, DP1 is the bytes, A and B are how many. Q is zero if it saved. osFileSave 0d21 ; DP0 names it, DP1 is the bytes, A and B are how many. Q is zero if it saved.
osFileDelete 0d22 ; DP0 names it. Q is zero if it went. osFileDelete 0d22 ; DP0 names it. Q is zero if it went.
osFileRename 0d23 ; DP0 is the name it has, DP1 the name it should have. Q is zero if it moved. osFileRename 0d23 ; DP0 is the name it has, DP1 the name it should have. Q is zero if it moved.
@@ -178,3 +185,60 @@
; NOT AUTOMATIC, and that is the point: dir and Say print and stop, and their output is the ; NOT AUTOMATIC, and that is the point: dir and Say print and stop, and their output is the
; reason you ran them. Only a program that says it took the screen gets the screen put back. ; reason you ran them. Only a program that says it took the screen gets the screen put back.
osTakeScreen 0d36 osTakeScreen 0d36
; ---- Where the program itself came from ----
;
; DP0 says where to put it and B is how much room there is counting the zero, the same
; bargain osArgument offers. What comes back is the whole path the program was loaded from,
; made absolute: a program found where somebody was standing is named by the bare word that
; was typed, and a bare name means the working directory - which a program is entitled to
; move out of.
;
; ---- Why this and not "open a file beside me" ----
;
; A service that opened a file relative to the program would need a TWIN FOR EVERY FILE
; OPERATION there is: read, save, info, block, start, write, done, delete, rename. One
; service handing back a path composes with all of them, and joining a name to a place is
; then a library rather than a service - Libraries/path.asm, beside print.asm and math.asm.
;
; ---- And why not just move the program there ----
;
; Because a program's ASSETS are relative to the program and its ARGUMENTS are relative to
; the person, and the working directory can only be one of them. Setting it to the program's
; own would mean "Play mytune.tune", typed by somebody in their own directory, looked in
; Play's. The working directory stays the person's; this is how a program finds its own.
osWhereAmI 0d37
; ---- Seeing what is on the disk ----
;
; Everything above takes a name a program already knows. NOTHING HERE COULD FIND OUT WHAT
; NAMES THERE ARE - a machine whose programs can read, write, rename and delete files and
; cannot ask what files exist. dir could only list because it lives in the shell and calls
; the filesystem directly; no loaded program could list anything at all.
;
; DP0 says where to put the name and B how much room there is, counting the zero, the same
; bargain osArgument and osWhereAmI offer.
;
; Q ANSWERS THE KIND RATHER THAN A YES OR NO, which is one value carrying both "is there
; one" and "what is it":
;
; 0 a file
; 1 a directory
; 2 a save that stopped before it committed
; 0xFF there are no more
;
; ---- And the size is not in it ----
;
; A walk hands back a NAME, and a program that wants the size of what it found asks
; osFileInfo about that name. The alternative is a record in memory whose shape both sides
; have to agree on, which is exactly what this file went out of its way to avoid for file
; sizes - and most callers of this want names and nothing else.
;
; ---- One walk at a time, and the system holds it ----
;
; Where a walk has got to is the system's, the way an open write is. A program that starts a
; second walk before finishing the first gets the second; there is one position, not a handle
; per caller. That is the same bargain osFileStart makes and for the same reason: the state
; is small, and a program that stops in the middle leaves nothing behind to clean up.
osDirFirst 0d38
osDirNext 0d39
+5 -3
View File
@@ -80,8 +80,10 @@ nextBank:
; three bytes into the palette - which means reaching video memory, which means the ; three bytes into the palette - which means reaching video memory, which means the
; controller. ; controller.
; Give the screen's memory a bank number. The screen answers on port 0x30, and bank 3 is ; Give the screen's memory a bank number. The screen brings TWO banks and this only wants
; the first number software is allowed to hand out: 0, 1 and 2 belong to the machine. ; one of them: port 0x30 owns the atlas, where the tiles and the palette are, and the
; palette is all this touches. Bank 3 is the first number software is allowed to hand out:
; 0, 1 and 2 belong to the machine.
INIA 0d3 INIA 0d3
OUTA 0xE3 ; DestBank: the number being given OUTA 0xE3 ; DestBank: the number being given
INIA 0x30 INIA 0x30
@@ -89,7 +91,7 @@ nextBank:
INIA 0x03 INIA 0x03
OUTA 0xE8 ; Command: RegisterBank OUTA 0xE8 ; Command: RegisterBank
; The palette sits at the top of video memory, at 0xFC00, and entry n is at n times ; The palette sits at the top of the atlas, at 0xFC00, and entry n is at n times
; four. Bank 2's ink is entry 2 * 16 + 1, which is 33, and 33 * 4 is 132 - so 0xFC84. ; four. Bank 2's ink is entry 2 * 16 + 1, which is 33, and 33 * 4 is 132 - so 0xFC84.
INIA 0xFC INIA 0xFC
OUTA 0xE4 ; DestHigh OUTA 0xE4 ; DestHigh
+23 -7
View File
@@ -13,13 +13,13 @@
; animate a whole screen in; it is the mode to draw a picture in and then leave alone, or to ; animate a whole screen in; it is the mode to draw a picture in and then leave alone, or to
; change a corner of. ; change a corner of.
; ;
; It lives over the top of tile memory and the map, because there is nowhere else for it: the ; It lives over the top of the map, in the SCREEN bank, because that is the bank whose
; bank is 65,536 bytes and the picture is 64,000 of them. Going to bitmap mode does not clear ; contents change: a picture and a map are the same memory called two different things in two
; the text screen, it stops calling it one - and coming back finds the tiles holding whatever ; different modes. Going to bitmap mode does not clear the text screen, it stops calling it
; the picture put there. Taking the screen means taking it. ; one - and coming back finds the map holding whatever the picture put there.
; ;
; The palette is the one thing that means the same in both, which is why it sits at the very ; WHAT IT DOES NOT COST IS THE FONT. The tiles and the palette are in the other bank and a
; top, out of the way of everything. ; picture cannot reach them, which is what lets this program draw and then say something.
#Program #Program
@@ -27,17 +27,31 @@ start:
; Video memory is the screen's, not this program's, so it is reached the way every device's ; Video memory is the screen's, not this program's, so it is reached the way every device's
; memory is: given a bank number, then written through the memory controller. Banks 0, 1 ; memory is: given a bank number, then written through the memory controller. Banks 0, 1
; and 2 belong to the machine, so 3 is the first one software may hand out. ; and 2 belong to the machine, so 3 is the first one software may hand out.
; TWO OF THEM, because the screen brings two banks and this program writes to both: the
; palette is in the atlas and the picture is in the screen.
INIA 0d3 INIA 0d3
OUTA 0xE3 ; DestBank: the number being given OUTA 0xE3 ; DestBank: the number being given
INIA 0x30 INIA 0x30
OUTA 0xE2 ; SourceLow: the port that owns the memory OUTA 0xE2 ; SourceLow: the port that owns the memory
INIA 0x03 INIA 0x03
OUTA 0xE8 ; Command: RegisterBank OUTA 0xE8 ; Command: RegisterBank. Three is the atlas.
INIA 0d4
OUTA 0xE3
INIA 0x3A
OUTA 0xE2
INIA 0x03
OUTA 0xE8 ; And four is the screen.
; ---- Two hundred and fifty six colours ---- ; ---- Two hundred and fifty six colours ----
; ;
; Entry n at 0xFC00 plus n times four. Writing the controller's Data port puts a byte at ; Entry n at 0xFC00 plus n times four. Writing the controller's Data port puts a byte at
; the destination and steps it on, so the whole palette is one address and a loop. ; the destination and steps it on, so the whole palette is one address and a loop.
; A already holds 3, because RegisterBank is command 3 and the bank wanted is bank 3.
; Leaving the line out would turn a program that says which bank it means into one that
; works by coincidence, and the coincidence breaks the day either number changes.
INIA 0d3 ; splitlint[redundant-assignment]: said rather than coincided
OUTA 0xE3 ; The ATLAS, which registering the second bank moved off
INIA 0xFC INIA 0xFC
OUTA 0xE4 OUTA 0xE4
RSTA RSTA
@@ -67,6 +81,8 @@ palette:
; DESTINATION PAST WHAT IT TOUCHED, so the address is set once here and never worked out ; DESTINATION PAST WHAT IT TOUCHED, so the address is set once here and never worked out
; again - which matters, because working out where row n begins would be n times 320 and ; again - which matters, because working out where row n begins would be n times 320 and
; this machine has no multiply. ; this machine has no multiply.
INIA 0d4
OUTA 0xE3 ; Into the SCREEN bank now, which is where a picture is
RSTA RSTA
OUTA 0xE4 OUTA 0xE4
OUTA 0xE5 ; Dest 0x0000, the top left corner OUTA 0xE5 ; Dest 0x0000, the top left corner
+106 -85
View File
@@ -8,26 +8,28 @@
; does not interrupt, so a program that only had the sound device could play a tune at ; does not interrupt, so a program that only had the sound device could play a tune at
; whatever speed the machine happened to run at, which is not a tune. ; whatever speed the machine happened to run at, which is not a tune.
; ;
; The screen finishes a frame sixty times a second and will say so. That is the only regular ; The timer is the other half. A period in cycles, a repeat bit, and a line when one has gone
; beat on this machine, and it is counted in the machine's own cycles, so this plays at the ; by: a beat this program SETS rather than one it borrows. Every duration below is a count of
; same speed whether the emulator is running at a megahertz or as fast as it can go. Every ; ticks, and the tick is named in the machine's own cycles, so this plays at the same speed
; duration below is in frames: 30 is half a second. ; whether the emulator is running at a megahertz or as fast as it can go.
; ;
; A programmable timer is the device that ought to be doing this, and it does not exist yet. ; ---- The tick is a sixteenth note, and that is the whole point ----
; Borrowing the screen's frame costs nothing and works, which is the whole reason to notice
; that a beat is a beat wherever it comes from.
; ;
; ---- What borrowing it costs ---- ; 125,000 cycles, which is a sixteenth note at 120 beats a minute.
; ;
; The frame is not slow. It is FIXED, and that is the different complaint. Every duration here ; This program used to count the SCREEN's frames, because until the timer existed the frame
; is a whole number of 16.67 ms, so a note worth a third of a beat cannot be written at all - ; was the only regular beat on the machine. A frame is 16,667 cycles, so a sixteenth note is
; and the way round it is to pick a tempo whose subdivisions happen to land on whole frames, ; seven and a half of them and could not be asked for at all. The arpeggio below was written
; which is making the tune fit the machine rather than the other way round. The theme below ; as seven frames, the nearest whole one, which is six and a half per cent fast; and the way
; was written to the frame and is a few cents of tempo away from what it wants to be. ; round the rest of it was to pick a tempo whose subdivisions happened to land on whole
; frames, which is making the tune fit the machine rather than the other way round.
; ;
; So what the timer wants is an ARBITRARY tick rather than a faster fixed one, and the reason ; SO THE DURATIONS HERE ARE NOT A CONVERSION OF THE OLD ONES. They are what the music wanted
; the screen should not be the clock is that a display refresh and a music routine have no ; in the first place, now that it can be written down: an eighth is two ticks, a quarter is
; reason to share a rate. ; four, and the arpeggio is one each rather than a fast approximation of one.
;
; The reason the screen should not have been the clock is not that a frame is slow. It is that
; a display refresh and a music routine have no reason to share a rate.
; ;
; ---- What a patch costs and what a note costs ---- ; ---- What a patch costs and what a note costs ----
; ;
@@ -35,8 +37,12 @@
; that the inner loop is two: the note, and letting go of it. That split is what the selector ; that the inner loop is two: the note, and letting go of it. That split is what the selector
; and value registers are for - see Making A Noise in the Programming Manual. ; and value registers are for - see Making A Noise in the Programming Manual.
#Include services.asm
#Program #Program
#Base 0x5000
start: start:
; ---- The instrument ---- ; ---- The instrument ----
@@ -139,10 +145,18 @@ start:
; ---- The beat ---- ; ---- The beat ----
; ;
; Ask the screen to interrupt at each frame, and let interrupts in. The screen does not do ; The period first and the control byte second, because writing the control byte with the
; this unless it is asked. ; run bit set is what LOADS the period. Doing it the other way round starts a timer on
; whatever it was holding before.
INIA 0x01 INIA 0x01
OUTA 0x35 OUTA 0x52
INIA 0xE8
OUTA 0x53
INIA 0x48
OUTA 0x54 ; 0x01E848 is 125,000.
INIA 0x07
OUTA 0x51 ; Run, repeat, interrupt.
SIF SIF
; ---- The tune ---- ; ---- The tune ----
@@ -156,11 +170,11 @@ nextNote:
BRA finished BRA finished
OUTA 0x44 ; Writing the note is what starts it. OUTA 0x44 ; Writing the note is what starts it.
INCD.0 INCD.0
LDB.0 ; How many frames it lasts. LDB.0 ; How many ticks it lasts.
INCD.0 INCD.0
holdNote: holdNote:
WAIT ; Nothing at all until the screen says a frame has gone by. WAIT ; Nothing at all until the timer says a tick has gone by.
DECB DECB
BNB holdNote BNB holdNote
@@ -171,9 +185,10 @@ holdNote:
BRI nextNote BRI nextNote
finished: finished:
; Let the last note ring out rather than cutting it off, then put the screen back the way it ; Let the last note ring out rather than cutting it off, then give the timer back - stopping
; was found - and stop asking to be interrupted before taking away what catches it. ; it before taking away what catches it, because this program is about to stop existing and
INIB 0d45 ; an interrupt with no handler installed is a fault.
INIB 0d6
lastRing: lastRing:
WAIT WAIT
DECB DECB
@@ -181,98 +196,104 @@ lastRing:
CIF CIF
RSTA RSTA
OUTA 0x35 OUTA 0x51
HALT SWI osExit
; Sixty times a second, and it has nothing to do. WAIT only needs something to have happened, ; The tick has nothing to do: the loop above is the player, and WAIT only needs something to
; and this is the something. A handler still has to exist: an interrupt with nothing installed ; have HAPPENED. A handler still has to exist, because an interrupt with nothing installed to
; to catch it is a fault. ; catch it is a fault.
frame: ;
; Nothing reads the status port here, and that is not an oversight. Taking the interrupt is
; what brings the line down. A program that POLLED the timer instead would have to read 0x50,
; because looking at it is the only thing that answers a tick nobody was interrupted by.
tick:
RETI RETI
#Data #Data
#Base 0x3000
; ---- Notes and how long they last ---- ; ---- Notes and how long they last ----
; ;
; Pairs: a MIDI note, then a count of frames. 60 is middle C and every 12 is an octave. A zero ; Pairs: a MIDI note, then a count of ticks. 60 is middle C and every 12 is an octave. A zero
; note ends it, which is why there are no rests in here - a rest would want a duration with no ; note ends it, which is why there are no rests in here - a rest would want a duration with no
; note, and this table has no way to say that. Adding one is a byte of flag or a note number ; note, and this table has no way to say that. Adding one is a byte of flag or a note number
; nothing plays, and this program did not need it. ; nothing plays, and this program did not need it.
Tune: Tune:
0d60 0d15 ; C 0d60 0d2 ; C
0d64 0d15 ; E 0d64 0d2 ; E
0d67 0d15 ; G 0d67 0d2 ; G
0d72 0d30 ; C, an octave up, held twice as long 0d72 0d4 ; C, an octave up, held twice as long
0d71 0d15 ; B 0d71 0d2 ; B
0d67 0d15 ; G 0d67 0d2 ; G
0d64 0d15 ; E 0d64 0d2 ; E
0d60 0d45 ; and home 0d60 0d6 ; and home
0x00 0x00
Theme: Theme:
0x30 0d15 ; C 0x30 0d2 ; C
0x35 0d15 ; F 0x35 0d2 ; F
0x3C 0d15 ; C+ 0x3C 0d2 ; C+
0x40 0d30 ; E+ 0x40 0d4 ; E+
0x48 0d30 ; C++ 0x48 0d4 ; C++
0x45 0d30 ; A+ 0x45 0d4 ; A+
0x47 0d30 ; B+ 0x47 0d4 ; B+
0x43 0d45 ; G+ 0x43 0d6 ; G+
; repeat four times. ; repeat four times.
0x2A 0d7 ; Gs 0x2C 0d1 ; Gs
0x33 0d7 ; Ef 0x33 0d1 ; Ef
0x3C 0d7 ; C+ 0x3C 0d1 ; C+
; ;
0x2A 0d7 ; Gs 0x2C 0d1 ; Gs
0x33 0d7 ; Ef 0x33 0d1 ; Ef
0x3C 0d7 ; C+ 0x3C 0d1 ; C+
; ;
0x2A 0d7 ; Gs 0x2C 0d1 ; Gs
0x33 0d7 ; Ef 0x33 0d1 ; Ef
0x3C 0d7 ; C+ 0x3C 0d1 ; C+
; ;
0x2A 0d7 ; Gs 0x2C 0d1 ; Gs
0x33 0d7 ; Ef 0x33 0d1 ; Ef
0x3C 0d7 ; C+ 0x3C 0d1 ; C+
; next chord ; next chord
0x2E 0d7 ; Bf 0x2E 0d1 ; Bf
0x35 0d7 ; F 0x35 0d1 ; F
0x3E 0d7 ; D+ 0x3E 0d1 ; D+
; next chord ; next chord
0x2E 0d7 ; Bf 0x2E 0d1 ; Bf
0x35 0d7 ; F 0x35 0d1 ; F
0x3E 0d7 ; D+ 0x3E 0d1 ; D+
; ;
; next chord ; next chord
0x2E 0d7 ; Bf 0x2E 0d1 ; Bf
0x35 0d7 ; F 0x35 0d1 ; F
0x3E 0d7 ; D+ 0x3E 0d1 ; D+
; ;
; next chord ; next chord
0x2E 0d7 ; Bf 0x2E 0d1 ; Bf
0x35 0d7 ; F 0x35 0d1 ; F
0x3E 0d7 ; D+ 0x3E 0d1 ; D+
; Finally on the C major ; Finally on the C major
0x30 0d7 ; C 0x30 0d1 ; C
0x37 0d7 ; G 0x37 0d1 ; G
0x40 0d7 ; E+ 0x40 0d1 ; E+
; ;
0x30 0d7 ; C 0x30 0d1 ; C
0x37 0d7 ; G 0x37 0d1 ; G
0x40 0d7 ; E+ 0x40 0d1 ; E+
; ;
0x30 0d7 ; C 0x30 0d1 ; C
0x37 0d7 ; G 0x37 0d1 ; G
0x40 0d7 ; E+ 0x40 0d1 ; E+
; ;
0x30 0d7 ; C 0x30 0d1 ; C
0x37 0d7 ; G 0x37 0d1 ; G
0x40 0d7 ; E+ 0x40 0d1 ; E+
; ;
0x18 0d60 ; C bass 0x18 0d8 ; C bass
0x00 0x00
#Vectors #Vectors
Boot start Boot start
Device 0x30 frame Device 0x50 tick
+235
View File
@@ -54,8 +54,243 @@ int16sub:
STQ ; Store the result. STQ ; Store the result.
RET ; Return to the caller. RET ; Return to the caller.
; ---- Multiplying, which this machine cannot do ----
;
; There is no MUL. What there is instead is an identity:
;
; a * b = qs[a + b] - qs[|a - b|] where qs[n] is n squared over four
;
; because (a+b)^2/4 - (a-b)^2/4 is exactly a*b, and the halves that the flooring throws away
; cancel between the two terms. So a multiply is TWO LOOKUPS AND A SUBTRACT.
;
; ---- And the table is built by adding ----
;
; Which is the part that makes this fit a machine with no multiplier at all. A table of
; squares would need squaring to fill, and this one does not:
;
; qs[n] = qs[n-1] + n/2
;
; and n/2 goes 0, 1, 1, 2, 2, 3, 3 - a number that steps up on every even n. So the whole
; table is one running total and a counter, and nothing harder than an add appears anywhere in
; building the thing that does the multiplying.
;
; 511 entries of two bytes, because a and b are bytes and a+b reaches 510. That is 1,022 bytes
; of Data Memory, which is the price: this is a routine that trades a kilobyte for an
; operation the hardware has not got.
; Fills the table. Call once, before the first multiply, and never again.
mulReady:
SETD.0 MulTable
RSTA
STA.0
INCD.0
STA.0 ; qs[0] is nought.
INCD.0
SETD.1 MulTotalLow
STA.1
INCD.1
STA.1 ; And so is the running total.
SETD.1 MulStep
STA.1
SETD.1 MulToggle
STA.1
; Five hundred and ten more entries to write.
INIA 0xFE
SETD.1 MulLeftLow
STA.1
INIA 0x01
SETD.1 MulLeftHigh
STA.1
mulEntry:
; ---- The step goes up on every even index ----
;
; n/2 for n = 1, 2, 3, 4 is 0, 1, 1, 2: it rises at the even ones. A toggle says which this
; is, which is cheaper than halving a sixteen bit counter every time round.
SETD.1 MulToggle
LDA.1
INIB 0x01
XOR
STQ.1
BNQ mulNoStep ; The toggle is one, so this index is odd and the step stands.
SETD.1 MulStep
LDA.1
INCA
STA.1
mulNoStep:
; The running total, plus the step, sixteen bits.
CCF
SETD.1 MulTotalLow
LDA.1
SETD.2 MulStep
LDB.2
ADD
STQ.1
SETD.1 MulTotalHigh
LDA.1
RSTB
ADD ; Nothing but the carry out of the half below.
STQ.1
; And into the table, where DP0 has been walking all along.
SETD.1 MulTotalLow
LDA.1
STA.0
INCD.0
SETD.1 MulTotalHigh
LDA.1
STA.0
INCD.0
; One fewer to do, counted down sixteen bits.
SETD.1 MulLeftLow
LDA.1
BNA mulCountLow
SETD.1 MulLeftHigh
LDA.1
DECA
STA.1
INIA 0xFF
SETD.1 MulLeftLow
STA.1
BRI mulCounted
mulCountLow:
DECA
STA.1
mulCounted:
SETD.1 MulLeftLow
LDA.1
SETD.2 MulLeftHigh
LDB.2
OR
BNQ mulEntry
RET
; ---- One multiply ----
;
; The operands go in MulA and MulB and the sixteen bit product comes back in MulLow and
; MulHigh. IN MEMORY RATHER THAN IN REGISTERS, because B cannot be stored and a product does
; not fit in one byte anyway - two registers in and two out would spend more instructions
; shuffling than the multiply costs.
mul8:
; The sum, which is nine bits: two bytes can reach 510 between them.
SETD.0 MulA
LDA.0
SETD.1 MulB
LDB.1
CCF
ADD
BRC mulSumOver
RSTA
BRI mulSumHigh
mulSumOver:
INIA 0x01
mulSumHigh:
SETD.0 MulSumHigh
STA.0
MVQA ; Q is still the sum's low half; nothing above touched it.
SETD.0 MulSumLow
STA.0
; The difference, without its sign, which is what the identity wants.
SETD.0 MulA
LDA.0
SETD.1 MulB
LDB.1
CCF
SUB
BRC mulOtherWay ; Borrowed, so B is the larger and the sum goes the other way.
MVQA
BRI mulDiffGot
mulOtherWay:
SETD.0 MulB
LDA.0
SETD.1 MulA
LDB.1
CCF
SUB
MVQA
mulDiffGot:
SETD.0 MulDiff
STA.0
; qs[sum]. An entry is two bytes, so the index is doubled - one turn of the shift register,
; where A is the high half and B the low, which is the way DPUW wants them too.
SETD.0 MulSumHigh
LDA.0
SETD.0 MulSumLow
LDB.0
SHL
SETD.0 MulTable
DPUW.0
LDA.0
SETD.1 MulLow
STA.1
INCD.0
LDA.0
SETD.1 MulHigh
STA.1
; Less qs[difference], sixteen bits, the borrow running from one half to the other.
RSTA
SETD.0 MulDiff
LDB.0
SHL
SETD.0 MulTable
DPUW.0
CCF
SETD.1 MulLow
LDA.1
LDB.0
SUB
STQ.1
INCD.0
SETD.1 MulHigh
LDA.1
LDB.0
SUB
STQ.1
RET
#Data #Data
; ---- The multiply's working ----
;
; MulLow and MulHigh are next to each other on purpose: the product is read as a pair.
MulA:
0x00
MulB:
0x00
MulLow:
0x00
MulHigh:
0x00
MulSumLow:
0x00
MulSumHigh:
0x00
MulDiff:
0x00
MulStep:
0x00
MulToggle:
0x00
MulTotalLow:
0x00
MulTotalHigh:
0x00
MulLeftLow:
0x00
MulLeftHigh:
0x00
; 511 entries of two bytes: n from nought to 510, which is as far as a byte plus a byte goes.
MulTable:
#Reserve 0d1022
Int16operands: Int16operands:
0x00 ; Operand A high byte. 0x00 ; Operand A high byte.
0x00 ; Operand A low byte. 0x00 ; Operand A low byte.
+194
View File
@@ -0,0 +1,194 @@
; path.asm
; Putting a name together with the place another thing is in.
;
; DP0 = a path, DP1 = a name, DP2 = where the answer goes, B = how much room, counting
; the zero on the end
; CALL pathBeside Q is zero if the whole of it fitted
;
; Written by Anachronaut
;
; ---- What this is for ----
;
; A program asks the system where it came from - SWI osWhereAmI - and is handed the path it
; was loaded from. What it actually wants is the path of something NEXT TO that: its tune,
; its tiles, its saved state. This is that one step, and it is a library rather than a
; service because it is arithmetic on two strings and touches nothing the system owns.
;
; That split is the whole reason osWhereAmI hands back a path instead of opening files on a
; program's behalf. A service that opened a file relative to the program would need a twin
; for every file operation there is; a path composes with all of them, and the joining
; happens once, here, on a machine that has no string library to do it in each program.
;
; ---- Everything up to the last separator ----
;
; The place a thing is in is everything up to and INCLUDING the last separator in its path,
; so joining is a copy and not a search backwards from the end. A path with no separator in
; it names something in the working directory, and the answer is then the name on its own -
; which means exactly the same thing, in the same place, and needs no special case.
;
; /Packages/app.Lander/Lander + splash.tune = /Packages/app.Lander/splash.tune
; /Lander.sbx + splash.tune = /splash.tune
; Lander.sbx + splash.tune = splash.tune
;
; ---- The room is counted once, in one place ----
;
; Both halves go through pathPut, which counts the room down and always keeps a byte back
; for the zero. So an answer that did not fit is still a STRING, endable and printable, and
; a caller that ignores Q gets a short path rather than a walk off the end of its buffer.
; It gets a wrong answer, which is why Q exists - but not a broken machine.
;
; The answer is built through a stored pointer rather than DP2, because a CALL puts DP0 to
; DP2 back the way it found them and pathPut would otherwise write the same byte every time.
#Program
pathBeside:
SETD.3 PathPut
STD.2.3
SETD.3 PathRoom
STB.3
; ---- How much of the path is the place it is in ----
;
; Counted rather than pointed at, because what is wanted is a NUMBER OF BYTES TO COPY and
; a pointer would have to be turned into one. The count is set afresh at every separator,
; so what it holds at the end is the last one - which is the one that matters.
PSHD.0 ; The front of the path, to come back to.
RSTA
SETD.3 PathCut
STA.3
SETD.3 PathAt
STA.3
pathScan:
LDA.0
BRA pathScanned
INIB 0x2F
CCF
SUB
BNQ pathScanStep
; A separator, so everything up to and including it is the place.
SETD.3 PathAt
LDA.3
INCA
SETD.3 PathCut
STA.3
pathScanStep:
SETD.3 PathAt
LDA.3
INCA
STA.3
INCD.0
BRI pathScan
pathScanned:
POPD.0
SETD.3 PathCut
LDA.3
SETD.3 PathLeft
STA.3
pathPlace:
SETD.3 PathLeft
LDA.3
BRA pathName ; All of the place is written, or there was none of it.
DECA
STA.3
LDA.0
CALL pathPut
BNQ pathNoRoom
INCD.0
BRI pathPlace
pathName:
PSHD.1
POPD.0
pathNameChar:
LDA.0
BRA pathMade
CALL pathPut
BNQ pathNoRoom
INCD.0
BRI pathNameChar
pathMade:
CALL pathEnd
RSTA
RSTB
CCF
ADD ; Q is zero: the whole of it fitted.
RET
pathNoRoom:
CALL pathEnd
INIA 0x01
RSTB
CCF
ADD
RET
; The zero that makes it a string. There is always room for it: pathPut refuses the byte
; that would have taken the last one.
pathEnd:
SETD.3 PathPut
LDD.2.3
RSTA
STA.2
RET
; A holds a character. Puts it where the answer has got to and steps that on. Q is one if
; there is no room for it.
pathPut:
SETD.3 PathHold
STA.3 ; The character, across the pointer being fetched and put back.
SETD.3 PathRoom
LDB.3
DECB
BRB pathPutFull ; Only the zero's worth left, so this byte cannot be written.
STB.3
SETD.3 PathPut
LDD.2.3
SETD.3 PathHold
LDA.3
STA.2
INCD.2
SETD.3 PathPut
STD.2.3
RSTA
RSTB
CCF
ADD
RET
pathPutFull:
INIA 0x01
RSTB
CCF
ADD
RET
#Data
; Where the answer has got to, and how much room is left in it.
PathPut:
0x00 0x00
PathRoom:
0x00
; How much of the path is the place it is in, how far the scan has got, and how much of the
; place is still to be written.
PathCut:
0x00
PathAt:
0x00
PathLeft:
0x00
; One character, across the fetching and putting back of the pointer it is written through.
PathHold:
0x00
+472
View File
@@ -0,0 +1,472 @@
; player.asm
; Four voices on one clock: the part of a music player that is not the music.
; Written by Anachronaut
;
; A program that includes this supplies the tune - four order lists called Order0 to Order3,
; and the sequences they name - and gets the scheduler, the patch loader and the state that
; goes with them. It does NOT supply the beat: setting the timer up and waiting on it belongs
; to the program, because how long a piece rings at the end and what stops it are its business
; and not this file's.
;
; What a sequence is, what a command is and where repetition comes from are all described in
; Apps/Play.asm, which is the first thing to read this.
;
; The caller must define, in its Data Segment:
;
; PatchTable one two-byte address per patch
; SequenceTable one two-byte address per sequence
; VoiceStart four bytes, the patch index each voice starts on
;
; and point each voice's order cursor at a list of one-byte sequence indices ending in 0xFF.
; A tune read from a file needs none of that: useTune does all of it out of the header.
;
; It must call loadStartPatches once, then stepVoice once a tick for each voice, with A
; holding the channel and DP1 the voice's record. Playing counts down as voices run out, and
; reaching nought is the piece being over.
;
; INDICES THROUGH TABLES, RATHER THAN ADDRESSES, because that is the shape a tune read from a
; file has to be: the two tables are the only places an address lives, so loading a tune means
; adding a base to them and nothing else. Keeping the assembled-in form the same shape is what
; makes reading one from a file change no code here at all.
#Program
; ---- One voice, one tick ----
;
; A is the channel and DP1 is the voice's six bytes. Both survive the CALL that got here,
; which is what lets the caller say which voice it means in two instructions.
;
; +0 order cursor +2 sequence cursor +4 count +5 live
stepVoice:
SETD.3 ThisChannel
STA.3 ; A is wanted for other things between here and using it.
DPUP.1 0d5
LDA.1 ; Live?
DPDN.1 0d5
BRA stepDone
DPUP.1 0d4
LDA.1
DECA
STA.1 ; One tick less of whatever is sounding.
DPDN.1 0d4
BNA stepDone ; Still holding it.
; ---- The count ran out, so this is a boundary ----
;
; Select the channel FIRST. Every sound port below writes to whichever channel was last
; named, so a voice that forgot would be playing somebody else's part.
LDA.3
OUTA 0x41
RSTA
OUTA 0x45 ; Let go of the note that just ended.
DPUP.1 0d2
LDD.0.1 ; DP0 is now this voice's place in its sequence.
DPDN.1 0d2
; ---- Read events until one of them takes time ----
;
; A command takes none, and neither does the end of a sequence, so both come back here.
; A sequence of nothing but commands would spin in this loop for ever, which is a hang
; rather than a wrong note - the one malformed sequence worth a compiler refusing.
stepEvent:
LDA.0
INIB 0xFF
XOR ; XOR answers in Q and leaves A holding the event.
BRQ stepSequenceEnd
; The top bit says command, and 0xFF is already dealt with above.
INIB 0x80
AND
BNQ stepCommand
; ---- A note, or a rest, and how long it lasts ----
INCD.0
LDB.0
INCD.0
DPUP.1 0d2
STD.0.1 ; The sequence cursor, moved past this event.
DPUP.1 0d2
STB.1 ; And the count it will be held for.
; A is still the event. Zero is a rest, which is a duration with nothing started.
BRA stepDone
OUTA 0x44 ; Writing the note is what starts it.
stepDone:
RET
; ---- 0x80: play the rest of this voice on that patch ----
;
; The address follows the command. The sequence cursor is put away before the patch is loaded
; and taken out again after, because loadPatch walks DP0 and a CALL hands back the pointer it
; was given rather than the one this needs next.
stepCommand:
INCD.0
LDA.0 ; Which patch: one byte of index, not an address.
INCD.0 ; Past it, on whatever comes next.
DPUP.1 0d2
STD.0.1
DPDN.1 0d2
SETD.2 PatchTable
LDD.2.2 ; The table, wherever this tune put it.
DPUA.2
DPUA.2 ; Twice, because an entry is two bytes.
LDD.2.2 ; And DP2 follows the address it is now holding.
PSHD.2
POPD.0 ; DP0 is the patch.
LDA.3 ; And A the channel, which loadPatch selects.
CALL loadPatch
DPUP.1 0d2
LDD.0.1 ; Back to where the sequence had got to.
DPDN.1 0d2
BRI stepEvent
; ---- The sequence ended, so take the next one from this voice's order list ----
;
; AN ORDER LIST IS INDICES, one byte each, ending in 0xFF - the same byte that ends a sequence.
; SequenceTable is the only place a sequence's address lives.
;
; That is what makes a tune loadable without walking it: nothing inside a sequence or an order
; list is an address, so putting one in memory means adding the load address to two tables and
; nothing else. A loader that had to parse sequences looking for addresses to fix up is a
; loader that a malformed file can walk off a cliff.
;
; It was addresses here, terminated by a zero, on the reasoning that no sequence could live
; below this player's 0x3000 base. True of a loaded program and false of a boot image based at
; zero, so the first test written against it read its own first sequence as the end of the
; list and played nothing at all. An address is not a good place to hide a flag unless the
; address is impossible in every program rather than in this one.
stepSequenceEnd:
LDD.0.1 ; DP0 is the order cursor, which is the first thing in the record.
LDA.0 ; Which sequence comes next.
INIB 0xFF
XOR
BRQ stepOrderEnd
INCD.0
STD.0.1 ; The order cursor, moved past this entry.
SETD.2 SequenceTable
LDD.2.2 ; The table, wherever this tune put it.
DPUA.2
DPUA.2
LDD.2.2 ; And the sequence that index names.
DPUP.1 0d2
STD.2.1 ; And the sequence cursor set to the new one.
DPDN.1 0d2
PSHD.2
POPD.0
BRI stepEvent ; Which has events in it, so read one.
stepOrderEnd:
; Nothing left for this voice. The gate is already down, so its last note is fading.
DPUP.1 0d5
RSTA
STA.1
SETD.3 Playing
LDA.3
DECA
STA.3
RET
; ---- The instrument each voice starts on ----
;
; STARTING STATE IS DECLARED RATHER THAN ASSUMED. A voice whose instrument was never said
; would play on whatever the device woke up with, or worse on whatever the last tune left -
; and that is a fault this machine has met elsewhere, where a program run a second time starts
; with the memory the first run left, because loading is what initialises and running is not.
loadStartPatches:
RSTA
loadStartOne:
SETD.3 ThisChannel
STA.3
SETD.2 VoiceStartAt
LDD.2.2
DPUA.2
LDA.2 ; Which patch this voice starts on.
SETD.2 PatchTable
LDD.2.2
DPUA.2
DPUA.2
LDD.2.2
PSHD.2
POPD.0
LDA.3
CALL loadPatch ; A survives a CALL, so it is still the channel below.
INCA
INIB 0d4
CCF
SUB
BNQ loadStartOne
RET
; ---- base + offset, into wherever the caller wants it ----
;
; DP0 names two bytes of offset and DP1 two bytes to put the address in. They may be the same
; place, which is how a table is relocated in position: the low byte is written before the
; high byte is read, so nothing is clobbered under itself.
;
; The low bytes carry into the high ones, which is the whole reason ADD takes the Carry Flag.
tuneAddr:
SETD.2 TuneBase
CCF
INCD.0
LDA.0 ; The offset, low byte.
INCD.2
LDB.2 ; And the base, low byte.
ADD
INCD.1
STQ.1
DECD.0
LDA.0 ; The offset, high byte.
DECD.2
LDB.2
ADD ; With the carry the low bytes made.
DECD.1
STQ.1
RET
; Every entry of the table at DP0 turned from an offset into an address. B is how many.
tuneReloc:
PSHD.0
POPD.1
CALL tuneAddr ; A CALL hands DP0 and DP1 back, so both are still the entry.
INCD.0
INCD.0
DECB
BNB tuneReloc
RET
; ---- A tune, from wherever it was put ----
;
; DP0 names its first byte. EVERYTHING IN A TUNE IS AN OFFSET FROM THERE, so this adds the
; base to the two tables and the four order lists and nothing else in the file is touched. No
; sequence is walked, and nothing inside one is an address to be found and corrected - which
; is what makes a malformed tune something that plays wrongly rather than something that takes
; the loader with it.
;
; Q is nought if the tune was taken, and anything else if the file was not one.
useTune:
SETD.1 TuneBase
STD.0.1
; "SBTU", and version one. A file that is not a tune has to be refused here, because
; everything below reads offsets out of it and jumps to what they name.
SETD.1 TuneMagic
INIB 0d5
useTuneMagic:
LDA.0
PSHB
LDB.1
XOR
POPB
BNQ useTuneNo
INCD.0
INCD.1
DECB
BNB useTuneMagic
; The tick, straight into the timer. Three bytes, most significant first, which is the
; order the ports take them in.
LDA.0
OUTA 0x52
INCD.0
LDA.0
OUTA 0x53
INCD.0
LDA.0
OUTA 0x54
; How many of each, kept before the pointers move.
INCD.0
LDA.0
SETD.1 TunePatches
STA.1
INCD.0
LDA.0
SETD.1 TuneSequences
STA.1
; The two tables.
INCD.0
SETD.1 PatchTable
CALL tuneAddr
INCD.0
INCD.0
SETD.1 SequenceTable
CALL tuneAddr
; And an order list each.
INCD.0
INCD.0
SETD.1 Voice0
CALL tuneAddr
INCD.0
INCD.0
SETD.1 Voice1
CALL tuneAddr
INCD.0
INCD.0
SETD.1 Voice2
CALL tuneAddr
INCD.0
INCD.0
SETD.1 Voice3
CALL tuneAddr
; The starting instruments are not an offset but a place IN the file, so they are found by
; counting rather than by adding.
SETD.0 TuneBase
LDD.0.0
DPUP.0 0d22
SETD.1 VoiceStartAt
STD.0.1
; Now the tables themselves, whose entries are offsets like everything else.
SETD.0 PatchTable
LDD.0.0
SETD.1 TunePatches
LDB.1
CALL tuneReloc
SETD.0 SequenceTable
LDD.0.0
SETD.1 TuneSequences
LDB.1
CALL tuneReloc
CALL startVoices
; Q is nought, which is how this says it worked. There is no instruction that sets Q: it is
; the ALU's output and nothing else, so saying nought means doing a sum that comes to it.
RSTA
RSTB
XOR
RET
useTuneNo:
; Q is already not nought, because that is what got here.
RET
; ---- Four voices at the beginning of their order lists ----
;
; Whoever supplied the tune has set the order cursors; this sets everything else. A voice
; starts on Empty with a count of one, so its first tick runs the count out, finds the end of
; a sequence, and goes to the order list for the real first one. The beginning of a piece
; needs no special case anywhere.
startVoices:
SETD.1 Voice0
CALL startOneVoice
SETD.1 Voice1
CALL startOneVoice
SETD.1 Voice2
CALL startOneVoice
SETD.1 Voice3
CALL startOneVoice
INIA 0d4
SETD.1 Playing
STA.1
CALL loadStartPatches
RET
startOneVoice:
SETD.0 Empty
DPUP.1 0d2
STD.0.1
INCD.1
INCD.1
INIA 0d1
STA.1 ; A count of one, which runs out on the first tick.
INCD.1
STA.1 ; And live.
RET
; ---- A patch, onto the channel named by A ----
;
; A count, then that many pairs of parameter and value: the format SoundPatch writes and the
; one Lander already plays. B holds what is left, which costs nothing to keep - a CALL saves
; B, so a caller's count is not disturbed by a patch being loaded inside its loop.
;
; The channel is selected FIRST. Every parameter write below lands on whichever channel was
; named last, so a patch loaded without one would be quietly rewriting somebody else's voice.
loadPatch:
OUTA 0x41
LDB.0
INCD.0
loadPatchPair:
LDA.0
OUTA 0x42
INCD.0
LDA.0
OUTA 0x43
INCD.0
DECB
BNB loadPatchPair
RET
#Data
; Six bytes a voice: order cursor, sequence cursor, count, live.
;
; A LABEL WRITTEN HERE COMES OUT AS ITS ADDRESS, two bytes, most significant first - which is
; exactly the shape LDD reads. So a voice starts pointed at its order list and at Empty
; without a line of code: nothing relocates on this machine, so the address the assembler
; wrote is the address it will have.
;
; Empty is a sequence of nothing but its end marker, and the count starts at one. The first
; tick runs the count out, finds the end, and goes to the order list for the real first
; sequence - so the beginning of a piece needs no special case anywhere.
Voice0:
NoOrder Empty 0d1 0d1
Voice1:
NoOrder Empty 0d1 0d1
Voice2:
NoOrder Empty 0d1 0d1
Voice3:
NoOrder Empty 0d1 0d1
Playing:
0d4
ThisChannel:
0x00
; Where this tune's tables are. Pointers rather than the tables themselves, because a tune
; read from a file puts them wherever it was put, and the engine must not care which.
PatchTable:
0x00 0x00
SequenceTable:
0x00 0x00
VoiceStartAt:
0x00 0x00
TuneBase:
0x00 0x00
TunePatches:
0x00
TuneSequences:
0x00
TuneMagic:
0x53 0x42 0x54 0x55 0x01 ; "SBTU" and version one.
; An order list of nothing, which every voice starts pointed at. WHOEVER SUPPLIES THE TUNE
; SETS THE REAL ONE - useTune out of a file's header, or a program with a tune of its own in
; eight lines - so the records below name nothing that a caller has to have defined. They used
; to name Order0 to Order3, which made four order lists part of the contract even for a caller
; whose tune comes from a file and never has any.
NoOrder:
0xFF
; The sequence a voice starts on, so that its first tick goes through the order list like
; every other bar does.
Empty:
0xFF
+50
View File
@@ -0,0 +1,50 @@
{
"osc0_waveform": 4,
"osc0_dutyCycle": 0.200000,
"osc0_detune": 0.000000,
"osc0_gain": 3.500000,
"osc0_active": 1,
"osc0_octave": 0,
"osc0_modRouting0": 0,
"osc0_modRouting1": 0,
"osc0_modRouting2": 0,
"osc0_modDepth0": 0.000000,
"osc0_modDepth1": 0.000000,
"osc0_modDepth2": 0.000000,
"osc1_waveform": 5,
"osc1_dutyCycle": 0.500000,
"osc1_detune": 0.000000,
"osc1_gain": 0.000000,
"osc1_active": 1,
"osc1_octave": 0,
"osc1_modRouting0": 0,
"osc1_modRouting1": 0,
"osc1_modRouting2": 2,
"osc1_modDepth0": 0.000000,
"osc1_modDepth1": 0.000000,
"osc1_modDepth2": 1.500000,
"ampEnv_attack": 0.001000,
"ampEnv_decay": 0.900000,
"ampEnv_sustain": 0.000000,
"ampEnv_release": 0.250000,
"modEnv_attack": 0.000000,
"modEnv_decay": 0.050000,
"modEnv_sustain": 0.000000,
"modEnv_release": 0.025000,
"lfo0_rate": 1.000000,
"lfo0_waveform": 0,
"lfo0_active": 0,
"lfo1_rate": 1.000000,
"lfo1_waveform": 0,
"lfo1_active": 0,
"filter_cutoff": 5000.000000,
"filter_resonance": 0.150000,
"filter_type": 0,
"filter_active": 1,
"filter_modRouting": 1,
"filter_modDepth": 5000.000000,
"filter_resModRouting": 0,
"filter_resModDepth": 0.000000,
"master_volume": 0.750000,
"master_pitchBendRange": 1.000000
}
+50
View File
@@ -0,0 +1,50 @@
{
"osc0_waveform": 4,
"osc0_dutyCycle": 0.250000,
"osc0_detune": -4.000000,
"osc0_gain": 2.800000,
"osc0_active": 1,
"osc0_octave": 0,
"osc0_modRouting0": 3,
"osc0_modRouting1": 0,
"osc0_modRouting2": 0,
"osc0_modDepth0": 0.100000,
"osc0_modDepth1": 0.000000,
"osc0_modDepth2": 0.000000,
"osc1_waveform": 4,
"osc1_dutyCycle": 0.250000,
"osc1_detune": 4.000000,
"osc1_gain": 2.800000,
"osc1_active": 1,
"osc1_octave": 0,
"osc1_modRouting0": 0,
"osc1_modRouting1": 0,
"osc1_modRouting2": 0,
"osc1_modDepth0": 0.000000,
"osc1_modDepth1": 0.000000,
"osc1_modDepth2": 0.000000,
"ampEnv_attack": 0.120000,
"ampEnv_decay": 0.100000,
"ampEnv_sustain": 0.800000,
"ampEnv_release": 0.200000,
"modEnv_attack": 0.001000,
"modEnv_decay": 0.500000,
"modEnv_sustain": 0.000000,
"modEnv_release": 0.200000,
"lfo0_rate": 0.350000,
"lfo0_waveform": 0,
"lfo0_active": 1,
"lfo1_rate": 1.000000,
"lfo1_waveform": 0,
"lfo1_active": 0,
"filter_cutoff": 20000.000000,
"filter_resonance": 0.000000,
"filter_type": 0,
"filter_active": 0,
"filter_modRouting": 0,
"filter_modDepth": 0.000000,
"filter_resModRouting": 0,
"filter_resModDepth": 0.000000,
"master_volume": 0.700000,
"master_pitchBendRange": 1.000000
}
+54
View File
@@ -0,0 +1,54 @@
{
"osc0_waveform": 1,
"osc0_dutyCycle": 0.500000,
"osc0_detune": 0.000000,
"osc0_gain": 2.500000,
"osc0_active": 1,
"osc0_octave": 0,
"osc0_modRouting0": 0,
"osc0_modRouting1": 0,
"osc0_modRouting2": 0,
"osc0_modDepth0": 0.000000,
"osc0_modDepth1": 0.000000,
"osc0_modDepth2": 0.000000,
"osc1_waveform": 0,
"osc1_dutyCycle": 0.500000,
"osc1_detune": 0.000000,
"osc1_gain": 0.300000,
"osc1_active": 1,
"osc1_octave": 2,
"osc1_modRouting0": 0,
"osc1_modRouting1": 0,
"osc1_modRouting2": 2,
"osc1_modDepth0": 0.000000,
"osc1_modDepth1": 0.000000,
"osc1_modDepth2": 2.000000,
"ampEnv_attack": 0.003000,
"ampEnv_decay": 0.080000,
"ampEnv_sustain": 0.800000,
"ampEnv_release": 0.250000,
"modEnv_attack": 0.000000,
"modEnv_decay": 0.090000,
"modEnv_sustain": 0.000000,
"modEnv_release": 0.050000,
"lfo0_rate": 1.000000,
"lfo0_waveform": 0,
"lfo0_active": 0,
"lfo0_mode": 0,
"lfo1_rate": 1.000000,
"lfo1_waveform": 0,
"lfo1_active": 0,
"lfo1_mode": 0,
"filter_cutoff": 5000.000000,
"filter_resonance": 0.150000,
"filter_type": 0,
"filter_active": 1,
"filter_modRouting": 2,
"filter_modDepth": 4500.000000,
"filter_resModRouting": 0,
"filter_resModDepth": 0.000000,
"voice_levelSource": 1,
"voice_gate": 0,
"master_volume": 1.000000,
"master_pitchBendRange": 1.000000
}
+54
View File
@@ -0,0 +1,54 @@
{
"osc0_waveform": 5,
"osc0_dutyCycle": 0.500000,
"osc0_detune": 0.000000,
"osc0_gain": 4.000000,
"osc0_active": 1,
"osc0_octave": 2,
"osc0_modRouting0": 0,
"osc0_modRouting1": 2,
"osc0_modRouting2": 0,
"osc0_modDepth0": 0.000000,
"osc0_modDepth1": 180.000000,
"osc0_modDepth2": 4.000000,
"osc1_waveform": 5,
"osc1_dutyCycle": 0.545000,
"osc1_detune": -15.000000,
"osc1_gain": 4.000000,
"osc1_active": 1,
"osc1_octave": 1,
"osc1_modRouting0": 0,
"osc1_modRouting1": 2,
"osc1_modRouting2": 0,
"osc1_modDepth0": -0.750000,
"osc1_modDepth1": 1104.000000,
"osc1_modDepth2": 4.000000,
"ampEnv_attack": 2.000000,
"ampEnv_decay": 0.100000,
"ampEnv_sustain": 0.000000,
"ampEnv_release": 0.001000,
"modEnv_attack": 0.001000,
"modEnv_decay": 0.603263,
"modEnv_sustain": 0.000000,
"modEnv_release": 2.000000,
"lfo0_rate": 8.796101,
"lfo0_waveform": 0,
"lfo0_active": 0,
"lfo0_mode": 0,
"lfo1_rate": 1.000000,
"lfo1_waveform": 0,
"lfo1_active": 0,
"lfo1_mode": 0,
"filter_cutoff": 13294.700195,
"filter_resonance": 0.574200,
"filter_type": 0,
"filter_active": 1,
"filter_modRouting": 2,
"filter_modDepth": 11600.000000,
"filter_resModRouting": 0,
"filter_resModDepth": 0.000000,
"voice_levelSource": 2,
"voice_gate": 1,
"master_volume": 1.000000,
"master_pitchBendRange": 2.000000
}
+54
View File
@@ -0,0 +1,54 @@
{
"osc0_waveform": 5,
"osc0_dutyCycle": 0.500000,
"osc0_detune": 0.000000,
"osc0_gain": 4.000000,
"osc0_active": 1,
"osc0_octave": 2,
"osc0_modRouting0": 0,
"osc0_modRouting1": 2,
"osc0_modRouting2": 0,
"osc0_modDepth0": 0.000000,
"osc0_modDepth1": 180.000000,
"osc0_modDepth2": 4.000000,
"osc1_waveform": 5,
"osc1_dutyCycle": 0.545000,
"osc1_detune": -15.000000,
"osc1_gain": 4.000000,
"osc1_active": 1,
"osc1_octave": 1,
"osc1_modRouting0": 0,
"osc1_modRouting1": 2,
"osc1_modRouting2": 0,
"osc1_modDepth0": -0.750000,
"osc1_modDepth1": 1104.000000,
"osc1_modDepth2": 4.000000,
"ampEnv_attack": 0.001000,
"ampEnv_decay": 0.100000,
"ampEnv_sustain": 0.000000,
"ampEnv_release": 0.001000,
"modEnv_attack": 0.001000,
"modEnv_decay": 0.288601,
"modEnv_sustain": 0.000000,
"modEnv_release": 2.000000,
"lfo0_rate": 8.796101,
"lfo0_waveform": 0,
"lfo0_active": 0,
"lfo0_mode": 0,
"lfo1_rate": 1.000000,
"lfo1_waveform": 0,
"lfo1_active": 0,
"lfo1_mode": 0,
"filter_cutoff": 389.089539,
"filter_resonance": 0.164624,
"filter_type": 0,
"filter_active": 1,
"filter_modRouting": 0,
"filter_modDepth": 11600.000000,
"filter_resModRouting": 0,
"filter_resModDepth": 0.000000,
"voice_levelSource": 2,
"voice_gate": 1,
"master_volume": 1.000000,
"master_pitchBendRange": 2.000000
}
+50
View File
@@ -0,0 +1,50 @@
{
"osc0_waveform": 4,
"osc0_dutyCycle": 0.700000,
"osc0_detune": 0.000000,
"osc0_gain": 3.500000,
"osc0_active": 1,
"osc0_octave": 0,
"osc0_modRouting0": 0,
"osc0_modRouting1": 0,
"osc0_modRouting2": 0,
"osc0_modDepth0": 0.000000,
"osc0_modDepth1": 0.000000,
"osc0_modDepth2": 0.000000,
"osc1_waveform": 4,
"osc1_dutyCycle": 0.700000,
"osc1_detune": 3.000000,
"osc1_gain": 1.500000,
"osc1_active": 1,
"osc1_octave": 1,
"osc1_modRouting0": 0,
"osc1_modRouting1": 0,
"osc1_modRouting2": 2,
"osc1_modDepth0": 0.000000,
"osc1_modDepth1": 0.000000,
"osc1_modDepth2": 1.000000,
"ampEnv_attack": 0.001000,
"ampEnv_decay": 0.700000,
"ampEnv_sustain": 0.000000,
"ampEnv_release": 0.060000,
"modEnv_attack": 0.000000,
"modEnv_decay": 0.060000,
"modEnv_sustain": 0.000000,
"modEnv_release": 0.030000,
"lfo0_rate": 1.000000,
"lfo0_waveform": 0,
"lfo0_active": 0,
"lfo1_rate": 1.000000,
"lfo1_waveform": 0,
"lfo1_active": 0,
"filter_cutoff": 9000.000000,
"filter_resonance": 0.120000,
"filter_type": 0,
"filter_active": 1,
"filter_modRouting": 2,
"filter_modDepth": 5000.000000,
"filter_resModRouting": 0,
"filter_resModDepth": 0.000000,
"master_volume": 0.750000,
"master_pitchBendRange": 0.000000
}
+50
View File
@@ -0,0 +1,50 @@
{
"osc0_waveform": 0,
"osc0_dutyCycle": 0.500000,
"osc0_detune": 0.000000,
"osc0_gain": 4.000000,
"osc0_active": 1,
"osc0_octave": 1,
"osc0_modRouting0": 0,
"osc0_modRouting1": 0,
"osc0_modRouting2": 0,
"osc0_modDepth0": 0.000000,
"osc0_modDepth1": 0.000000,
"osc0_modDepth2": 0.000000,
"osc1_waveform": 0,
"osc1_dutyCycle": 0.500000,
"osc1_detune": 8.000000,
"osc1_gain": 3.423103,
"osc1_active": 1,
"osc1_octave": 2,
"osc1_modRouting0": 0,
"osc1_modRouting1": 0,
"osc1_modRouting2": 0,
"osc1_modDepth0": 0.000000,
"osc1_modDepth1": 0.000000,
"osc1_modDepth2": 0.000000,
"ampEnv_attack": 0.001000,
"ampEnv_decay": 0.600000,
"ampEnv_sustain": 0.000000,
"ampEnv_release": 0.100000,
"modEnv_attack": 0.001000,
"modEnv_decay": 0.100000,
"modEnv_sustain": 0.000000,
"modEnv_release": 0.020000,
"lfo0_rate": 1.000000,
"lfo0_waveform": 0,
"lfo0_active": 0,
"lfo1_rate": 1.000000,
"lfo1_waveform": 0,
"lfo1_active": 0,
"filter_cutoff": 3000.000000,
"filter_resonance": 0.080000,
"filter_type": 0,
"filter_active": 1,
"filter_modRouting": 0,
"filter_modDepth": 0.000000,
"filter_resModRouting": 0,
"filter_resModDepth": 0.000000,
"master_volume": 0.700000,
"master_pitchBendRange": 2.000000
}
+54
View File
@@ -0,0 +1,54 @@
{
"osc0_waveform": 1,
"osc0_dutyCycle": 0.500000,
"osc0_detune": 0.000000,
"osc0_gain": 4.000000,
"osc0_active": 1,
"osc0_octave": 2,
"osc0_modRouting0": 0,
"osc0_modRouting1": 3,
"osc0_modRouting2": 1,
"osc0_modDepth0": 0.000000,
"osc0_modDepth1": -420.000000,
"osc0_modDepth2": 4.000000,
"osc1_waveform": 1,
"osc1_dutyCycle": 0.500000,
"osc1_detune": 0.000000,
"osc1_gain": 4.000000,
"osc1_active": 1,
"osc1_octave": 0,
"osc1_modRouting0": 0,
"osc1_modRouting1": 3,
"osc1_modRouting2": 1,
"osc1_modDepth0": 0.000000,
"osc1_modDepth1": -420.000000,
"osc1_modDepth2": 4.000000,
"ampEnv_attack": 0.001000,
"ampEnv_decay": 0.577635,
"ampEnv_sustain": 1.000000,
"ampEnv_release": 0.065071,
"modEnv_attack": 0.005000,
"modEnv_decay": 0.500000,
"modEnv_sustain": 0.000000,
"modEnv_release": 0.100000,
"lfo0_rate": 3.398800,
"lfo0_waveform": 2,
"lfo0_active": 1,
"lfo0_mode": 1,
"lfo1_rate": 1.000000,
"lfo1_waveform": 0,
"lfo1_active": 0,
"lfo1_mode": 0,
"filter_cutoff": 8000.000000,
"filter_resonance": 0.000000,
"filter_type": 0,
"filter_active": 0,
"filter_modRouting": 0,
"filter_modDepth": 0.000000,
"filter_resModRouting": 0,
"filter_resModDepth": 0.000000,
"voice_levelSource": 1,
"voice_gate": 1,
"master_volume": 1.000000,
"master_pitchBendRange": 2.000000
}
+50
View File
@@ -0,0 +1,50 @@
{
"osc0_waveform": 4,
"osc0_dutyCycle": 0.500000,
"osc0_detune": -8.000000,
"osc0_gain": 2.500000,
"osc0_active": 1,
"osc0_octave": 0,
"osc0_modRouting0": 3,
"osc0_modRouting1": 0,
"osc0_modRouting2": 0,
"osc0_modDepth0": 0.280000,
"osc0_modDepth1": 0.000000,
"osc0_modDepth2": 0.000000,
"osc1_waveform": 4,
"osc1_dutyCycle": 0.500000,
"osc1_detune": 8.000000,
"osc1_gain": 2.500000,
"osc1_active": 1,
"osc1_octave": 0,
"osc1_modRouting0": 4,
"osc1_modRouting1": 0,
"osc1_modRouting2": 0,
"osc1_modDepth0": 0.280000,
"osc1_modDepth1": 0.000000,
"osc1_modDepth2": 0.000000,
"ampEnv_attack": 0.420000,
"ampEnv_decay": 0.100000,
"ampEnv_sustain": 0.880000,
"ampEnv_release": 1.300000,
"modEnv_attack": 0.000000,
"modEnv_decay": 0.500000,
"modEnv_sustain": 0.000000,
"modEnv_release": 0.400000,
"lfo0_rate": 0.290000,
"lfo0_waveform": 0,
"lfo0_active": 1,
"lfo1_rate": 0.370000,
"lfo1_waveform": 0,
"lfo1_active": 1,
"filter_cutoff": 4200.000000,
"filter_resonance": 0.080000,
"filter_type": 0,
"filter_active": 1,
"filter_modRouting": 1,
"filter_modDepth": 2800.000000,
"filter_resModRouting": 0,
"filter_resModDepth": 0.000000,
"master_volume": 0.720000,
"master_pitchBendRange": 2.000000
}
+50
View File
@@ -0,0 +1,50 @@
{
"osc0_waveform": 4,
"osc0_dutyCycle": 0.250000,
"osc0_detune": 0.000000,
"osc0_gain": 3.000000,
"osc0_active": 1,
"osc0_octave": 0,
"osc0_modRouting0": 0,
"osc0_modRouting1": 3,
"osc0_modRouting2": 0,
"osc0_modDepth0": 0.000000,
"osc0_modDepth1": 5.000000,
"osc0_modDepth2": 0.000000,
"osc1_waveform": 4,
"osc1_dutyCycle": 0.200000,
"osc1_detune": 2.000000,
"osc1_gain": 2.000000,
"osc1_active": 1,
"osc1_octave": 0,
"osc1_modRouting0": 0,
"osc1_modRouting1": 0,
"osc1_modRouting2": 0,
"osc1_modDepth0": 0.000000,
"osc1_modDepth1": 0.000000,
"osc1_modDepth2": 0.000000,
"ampEnv_attack": 0.055000,
"ampEnv_decay": 0.030000,
"ampEnv_sustain": 0.920000,
"ampEnv_release": 0.150000,
"modEnv_attack": 0.000000,
"modEnv_decay": 0.500000,
"modEnv_sustain": 0.000000,
"modEnv_release": 0.200000,
"lfo0_rate": 5.000000,
"lfo0_waveform": 0,
"lfo0_active": 1,
"lfo1_rate": 1.000000,
"lfo1_waveform": 0,
"lfo1_active": 0,
"filter_cutoff": 2500.000000,
"filter_resonance": 0.400000,
"filter_type": 0,
"filter_active": 1,
"filter_modRouting": 0,
"filter_modDepth": 0.000000,
"filter_resModRouting": 0,
"filter_resModDepth": 0.000000,
"master_volume": 0.700000,
"master_pitchBendRange": 2.000000
}
+50
View File
@@ -0,0 +1,50 @@
{
"osc0_waveform": 4,
"osc0_dutyCycle": 0.153236,
"osc0_detune": 0.000000,
"osc0_gain": 4.000000,
"osc0_active": 1,
"osc0_octave": 0,
"osc0_modRouting0": 1,
"osc0_modRouting1": 0,
"osc0_modRouting2": 3,
"osc0_modDepth0": 0.290788,
"osc0_modDepth1": 0.000000,
"osc0_modDepth2": -0.833591,
"osc1_waveform": 1,
"osc1_dutyCycle": 0.500000,
"osc1_detune": 0.000000,
"osc1_gain": 0.000000,
"osc1_active": 0,
"osc1_octave": 0,
"osc1_modRouting0": 0,
"osc1_modRouting1": 0,
"osc1_modRouting2": 0,
"osc1_modDepth0": 0.000000,
"osc1_modDepth1": 0.000000,
"osc1_modDepth2": 0.000000,
"ampEnv_attack": 0.005000,
"ampEnv_decay": 0.100000,
"ampEnv_sustain": 0.700000,
"ampEnv_release": 0.500000,
"modEnv_attack": 0.005000,
"modEnv_decay": 0.500000,
"modEnv_sustain": 0.000000,
"modEnv_release": 0.100000,
"lfo0_rate": 2.598532,
"lfo0_waveform": 0,
"lfo0_active": 1,
"lfo1_rate": 1.000000,
"lfo1_waveform": 0,
"lfo1_active": 0,
"filter_cutoff": 8000.000000,
"filter_resonance": 0.000000,
"filter_type": 0,
"filter_active": 0,
"filter_modRouting": 0,
"filter_modDepth": 0.000000,
"filter_resModRouting": 0,
"filter_resModDepth": 0.000000,
"master_volume": 0.362205,
"master_pitchBendRange": 2.000000
}
+54
View File
@@ -0,0 +1,54 @@
{
"osc0_waveform": 5,
"osc0_dutyCycle": 0.500000,
"osc0_detune": 0.000000,
"osc0_gain": 4.000000,
"osc0_active": 1,
"osc0_octave": 0,
"osc0_modRouting0": 0,
"osc0_modRouting1": 0,
"osc0_modRouting2": 0,
"osc0_modDepth0": 0.000000,
"osc0_modDepth1": 0.000000,
"osc0_modDepth2": 0.000000,
"osc1_waveform": 0,
"osc1_dutyCycle": 0.500000,
"osc1_detune": -29.360291,
"osc1_gain": 2.868294,
"osc1_active": 1,
"osc1_octave": -1,
"osc1_modRouting0": 0,
"osc1_modRouting1": 3,
"osc1_modRouting2": 0,
"osc1_modDepth0": 0.000000,
"osc1_modDepth1": 307.482422,
"osc1_modDepth2": 0.000000,
"ampEnv_attack": 0.076245,
"ampEnv_decay": 0.182086,
"ampEnv_sustain": 1.000000,
"ampEnv_release": 0.001000,
"modEnv_attack": 0.001000,
"modEnv_decay": 0.213054,
"modEnv_sustain": 0.000000,
"modEnv_release": 0.001000,
"lfo0_rate": 19.253021,
"lfo0_waveform": 0,
"lfo0_active": 1,
"lfo0_mode": 1,
"lfo1_rate": 1.000000,
"lfo1_waveform": 0,
"lfo1_active": 0,
"lfo1_mode": 0,
"filter_cutoff": 2081.465332,
"filter_resonance": 0.000000,
"filter_type": 0,
"filter_active": 1,
"filter_modRouting": 2,
"filter_modDepth": -12846.765625,
"filter_resModRouting": 0,
"filter_resModDepth": 0.000000,
"voice_levelSource": 1,
"voice_gate": 0,
"master_volume": 1.000000,
"master_pitchBendRange": 2.000000
}
+54
View File
@@ -0,0 +1,54 @@
{
"osc0_waveform": 5,
"osc0_dutyCycle": 0.500000,
"osc0_detune": 0.000000,
"osc0_gain": 4.000000,
"osc0_active": 1,
"osc0_octave": 2,
"osc0_modRouting0": 0,
"osc0_modRouting1": 2,
"osc0_modRouting2": 0,
"osc0_modDepth0": 0.000000,
"osc0_modDepth1": 180.000000,
"osc0_modDepth2": 4.000000,
"osc1_waveform": 5,
"osc1_dutyCycle": 0.545000,
"osc1_detune": -15.000000,
"osc1_gain": 4.000000,
"osc1_active": 1,
"osc1_octave": 1,
"osc1_modRouting0": 0,
"osc1_modRouting1": 2,
"osc1_modRouting2": 0,
"osc1_modDepth0": -0.750000,
"osc1_modDepth1": 1104.000000,
"osc1_modDepth2": 4.000000,
"ampEnv_attack": 2.000000,
"ampEnv_decay": 0.100000,
"ampEnv_sustain": 0.000000,
"ampEnv_release": 0.001000,
"modEnv_attack": 0.001000,
"modEnv_decay": 0.247558,
"modEnv_sustain": 0.000000,
"modEnv_release": 2.000000,
"lfo0_rate": 8.796101,
"lfo0_waveform": 0,
"lfo0_active": 0,
"lfo0_mode": 0,
"lfo1_rate": 1.000000,
"lfo1_waveform": 0,
"lfo1_active": 0,
"lfo1_mode": 0,
"filter_cutoff": 1242.353027,
"filter_resonance": 0.164624,
"filter_type": 0,
"filter_active": 1,
"filter_modRouting": 0,
"filter_modDepth": 11600.000000,
"filter_resModRouting": 0,
"filter_resModDepth": 0.000000,
"voice_levelSource": 2,
"voice_gate": 1,
"master_volume": 1.000000,
"master_pitchBendRange": 2.000000
}
+58
View File
@@ -0,0 +1,58 @@
; AlarmPatch, converted from Programs/Sounds/LowFuel.json by SoundPatch.
; Designed in soundThing, where it can be heard. Do not edit the numbers
; here: change the patch and convert it again.
#Data
AlarmPatch:
0d50 ; how many pairs follow
0x00 0d1 ; oscillator 0, waveform
0x01 0d255 ; gain
0x02 0d128 ; duty
0x03 0d128 ; detune, in cents
0x04 0d130 ; octave
0x05 0d1 ; on
0x06 0d0 ; what moves its width
0x07 0d128 ; and how far
0x08 0d3 ; what moves its pitch
0x09 0d83 ; and how far
0x0A 0d1 ; what moves its gain
0x0B 0d255 ; and how far
0x10 0d1 ; oscillator 1, waveform
0x11 0d255 ; gain
0x12 0d128 ; duty
0x13 0d128 ; detune, in cents
0x14 0d128 ; octave
0x15 0d1 ; on
0x16 0d0 ; what moves its width
0x17 0d128 ; and how far
0x18 0d3 ; what moves its pitch
0x19 0d83 ; and how far
0x1A 0d1 ; what moves its gain
0x1B 0d255 ; and how far
0x20 0d4 ; amplitude envelope, attack
0x21 0d97 ; decay
0x22 0d255 ; sustain
0x23 0d33 ; release
0x30 0d9 ; modulation envelope, attack
0x31 0d90 ; decay
0x32 0d0 ; sustain
0x33 0d40 ; release
0x40 0d0 ; filter, on
0x41 0d0 ; type: 0 low, 1 high, 2 band
0x42 0d221 ; cutoff, in hertz
0x43 0d0 ; resonance
0x44 0d0 ; what moves the cutoff
0x45 0d128 ; and how far, in hertz
0x46 0d0 ; what moves the resonance
0x47 0d128 ; and how far
0x60 0d1 ; LFO 0, on
0x61 0d2 ; waveform
0x62 0d180 ; rate, in hertz
0x63 0d1 ; 0 free, 1 starts with a voice
0x70 0d0 ; LFO 1, on
0x71 0d0 ; waveform
0x72 0d127 ; rate, in hertz
0x73 0d0 ; 0 free, 1 starts with a voice
0x50 0d1 ; what shapes the level
0x51 0d1 ; 0 gated, 1 triggered
+58
View File
@@ -0,0 +1,58 @@
; BoomPatch, converted from Programs/Sounds/Crash.json by SoundPatch.
; Designed in soundThing, where it can be heard. Do not edit the numbers
; here: change the patch and convert it again.
#Data
BoomPatch:
0d50 ; how many pairs follow
0x00 0d5 ; oscillator 0, waveform
0x01 0d255 ; gain
0x02 0d128 ; duty
0x03 0d128 ; detune, in cents
0x04 0d130 ; octave
0x05 0d1 ; on
0x06 0d0 ; what moves its width
0x07 0d128 ; and how far
0x08 0d2 ; what moves its pitch
0x09 0d147 ; and how far
0x0A 0d0 ; what moves its gain
0x0B 0d255 ; and how far
0x10 0d5 ; oscillator 1, waveform
0x11 0d255 ; gain
0x12 0d140 ; duty
0x13 0d126 ; detune, in cents
0x14 0d129 ; octave
0x15 0d1 ; on
0x16 0d0 ; what moves its width
0x17 0d0 ; and how far
0x18 0d2 ; what moves its pitch
0x19 0d246 ; and how far
0x1A 0d0 ; what moves its gain
0x1B 0d255 ; and how far
0x20 0d180 ; amplitude envelope, attack
0x21 0d40 ; decay
0x22 0d0 ; sustain
0x23 0d4 ; release
0x30 0d4 ; modulation envelope, attack
0x31 0d99 ; decay
0x32 0d0 ; sustain
0x33 0d180 ; release
0x40 0d1 ; filter, on
0x41 0d0 ; type: 0 low, 1 high, 2 band
0x42 0d240 ; cutoff, in hertz
0x43 0d148 ; resonance
0x44 0d2 ; what moves the cutoff
0x45 0d255 ; and how far, in hertz
0x46 0d0 ; what moves the resonance
0x47 0d128 ; and how far
0x60 0d0 ; LFO 0, on
0x61 0d0 ; waveform
0x62 0d220 ; rate, in hertz
0x63 0d0 ; 0 free, 1 starts with a voice
0x70 0d0 ; LFO 1, on
0x71 0d0 ; waveform
0x72 0d127 ; rate, in hertz
0x73 0d0 ; 0 free, 1 starts with a voice
0x50 0d2 ; what shapes the level
0x51 0d1 ; 0 gated, 1 triggered
+55
View File
@@ -0,0 +1,55 @@
; KalimbaPatch, converted from Programs/Sounds/Kalimba.json by SoundPatch.
; Designed in soundThing, where it can be heard. Do not edit the numbers
; here: change the patch and convert it again.
#Data
KalimbaPatch:
0d47 ; how many pairs follow
0x00 0d0 ; oscillator 0, waveform
0x01 0d255 ; gain
0x02 0d128 ; duty
0x03 0d128 ; detune, in cents
0x04 0d129 ; octave
0x05 0d1 ; on
0x06 0d0 ; what moves its width
0x07 0d128 ; and how far
0x08 0d0 ; what moves its pitch
0x09 0d128 ; and how far
0x0A 0d0 ; what moves its gain
0x0B 0d128 ; and how far
0x10 0d0 ; oscillator 1, waveform
0x11 0d218 ; gain
0x12 0d128 ; duty
0x13 0d129 ; detune, in cents
0x14 0d130 ; octave
0x15 0d1 ; on
0x16 0d0 ; what moves its width
0x17 0d128 ; and how far
0x18 0d0 ; what moves its pitch
0x19 0d128 ; and how far
0x1A 0d0 ; what moves its gain
0x1B 0d128 ; and how far
0x20 0d4 ; amplitude envelope, attack
0x21 0d99 ; decay
0x22 0d0 ; sustain
0x23 0d40 ; release
0x30 0d4 ; modulation envelope, attack
0x31 0d40 ; decay
0x32 0d0 ; sustain
0x33 0d18 ; release
0x40 0d1 ; filter, on
0x41 0d0 ; type: 0 low, 1 high, 2 band
0x42 0d185 ; cutoff, in hertz
0x43 0d21 ; resonance
0x44 0d0 ; what moves the cutoff
0x45 0d128 ; and how far, in hertz
0x46 0d0 ; what moves the resonance
0x47 0d128 ; and how far
0x60 0d0 ; LFO 0, on
0x61 0d0 ; waveform
0x62 0d127 ; rate, in hertz
0x70 0d0 ; LFO 1, on
0x71 0d0 ; waveform
0x72 0d127 ; rate, in hertz
0x50 0d1 ; what shapes the level, which this patch predates
+58
View File
@@ -0,0 +1,58 @@
; LatchPatch, converted from Programs/Sounds/DockLatch.json by SoundPatch.
; Designed in soundThing, where it can be heard. Do not edit the numbers
; here: change the patch and convert it again.
#Data
LatchPatch:
0d50 ; how many pairs follow
0x00 0d5 ; oscillator 0, waveform
0x01 0d255 ; gain
0x02 0d128 ; duty
0x03 0d128 ; detune, in cents
0x04 0d130 ; octave
0x05 0d1 ; on
0x06 0d0 ; what moves its width
0x07 0d128 ; and how far
0x08 0d2 ; what moves its pitch
0x09 0d147 ; and how far
0x0A 0d0 ; what moves its gain
0x0B 0d255 ; and how far
0x10 0d5 ; oscillator 1, waveform
0x11 0d255 ; gain
0x12 0d140 ; duty
0x13 0d126 ; detune, in cents
0x14 0d129 ; octave
0x15 0d1 ; on
0x16 0d0 ; what moves its width
0x17 0d0 ; and how far
0x18 0d2 ; what moves its pitch
0x19 0d246 ; and how far
0x1A 0d0 ; what moves its gain
0x1B 0d255 ; and how far
0x20 0d4 ; amplitude envelope, attack
0x21 0d40 ; decay
0x22 0d0 ; sustain
0x23 0d4 ; release
0x30 0d4 ; modulation envelope, attack
0x31 0d68 ; decay
0x32 0d0 ; sustain
0x33 0d180 ; release
0x40 0d1 ; filter, on
0x41 0d0 ; type: 0 low, 1 high, 2 band
0x42 0d110 ; cutoff, in hertz
0x43 0d42 ; resonance
0x44 0d0 ; what moves the cutoff
0x45 0d255 ; and how far, in hertz
0x46 0d0 ; what moves the resonance
0x47 0d128 ; and how far
0x60 0d0 ; LFO 0, on
0x61 0d0 ; waveform
0x62 0d220 ; rate, in hertz
0x63 0d0 ; 0 free, 1 starts with a voice
0x70 0d0 ; LFO 1, on
0x71 0d0 ; waveform
0x72 0d127 ; rate, in hertz
0x73 0d0 ; 0 free, 1 starts with a voice
0x50 0d2 ; what shapes the level
0x51 0d1 ; 0 gated, 1 triggered
+55
View File
@@ -0,0 +1,55 @@
; OboePatch, converted from Programs/Sounds/Oboe.json by SoundPatch.
; Designed in soundThing, where it can be heard. Do not edit the numbers
; here: change the patch and convert it again.
#Data
OboePatch:
0d47 ; how many pairs follow
0x00 0d4 ; oscillator 0, waveform
0x01 0d191 ; gain
0x02 0d57 ; duty
0x03 0d128 ; detune, in cents
0x04 0d128 ; octave
0x05 0d1 ; on
0x06 0d0 ; what moves its width
0x07 0d128 ; and how far
0x08 0d3 ; what moves its pitch
0x09 0d129 ; and how far
0x0A 0d0 ; what moves its gain
0x0B 0d128 ; and how far
0x10 0d4 ; oscillator 1, waveform
0x11 0d128 ; gain
0x12 0d43 ; duty
0x13 0d128 ; detune, in cents
0x14 0d128 ; octave
0x15 0d1 ; on
0x16 0d0 ; what moves its width
0x17 0d128 ; and how far
0x18 0d0 ; what moves its pitch
0x19 0d128 ; and how far
0x1A 0d0 ; what moves its gain
0x1B 0d128 ; and how far
0x20 0d30 ; amplitude envelope, attack
0x21 0d22 ; decay
0x22 0d235 ; sustain
0x23 0d49 ; release
0x30 0d0 ; modulation envelope, attack
0x31 0d90 ; decay
0x32 0d0 ; sustain
0x33 0d57 ; release
0x40 0d1 ; filter, on
0x41 0d0 ; type: 0 low, 1 high, 2 band
0x42 0d178 ; cutoff, in hertz
0x43 0d103 ; resonance
0x44 0d0 ; what moves the cutoff
0x45 0d128 ; and how far, in hertz
0x46 0d0 ; what moves the resonance
0x47 0d128 ; and how far
0x60 0d1 ; LFO 0, on
0x61 0d0 ; waveform
0x62 0d196 ; rate, in hertz
0x70 0d0 ; LFO 1, on
0x71 0d0 ; waveform
0x72 0d127 ; rate, in hertz
0x50 0d1 ; what shapes the level, which this patch predates
+55
View File
@@ -0,0 +1,55 @@
; SquarePatch, converted from Programs/Sounds/SidSquare.json by SoundPatch.
; Designed in soundThing, where it can be heard. Do not edit the numbers
; here: change the patch and convert it again.
#Data
SquarePatch:
0d47 ; how many pairs follow
0x00 0d4 ; oscillator 0, waveform
0x01 0d255 ; gain
0x02 0d29 ; duty
0x03 0d128 ; detune, in cents
0x04 0d128 ; octave
0x05 0d1 ; on
0x06 0d1 ; what moves its width
0x07 0d202 ; and how far
0x08 0d0 ; what moves its pitch
0x09 0d128 ; and how far
0x0A 0d3 ; what moves its gain
0x0B 0d101 ; and how far
0x10 0d1 ; oscillator 1, waveform
0x11 0d0 ; gain
0x12 0d128 ; duty
0x13 0d128 ; detune, in cents
0x14 0d128 ; octave
0x15 0d0 ; on
0x16 0d0 ; what moves its width
0x17 0d128 ; and how far
0x18 0d0 ; what moves its pitch
0x19 0d128 ; and how far
0x1A 0d0 ; what moves its gain
0x1B 0d128 ; and how far
0x20 0d9 ; amplitude envelope, attack
0x21 0d40 ; decay
0x22 0d179 ; sustain
0x23 0d90 ; release
0x30 0d9 ; modulation envelope, attack
0x31 0d90 ; decay
0x32 0d0 ; sustain
0x33 0d40 ; release
0x40 0d0 ; filter, on
0x41 0d0 ; type: 0 low, 1 high, 2 band
0x42 0d221 ; cutoff, in hertz
0x43 0d0 ; resonance
0x44 0d0 ; what moves the cutoff
0x45 0d128 ; and how far, in hertz
0x46 0d0 ; what moves the resonance
0x47 0d128 ; and how far
0x60 0d1 ; LFO 0, on
0x61 0d0 ; waveform
0x62 0d168 ; rate, in hertz
0x70 0d0 ; LFO 1, on
0x71 0d0 ; waveform
0x72 0d127 ; rate, in hertz
0x50 0d1 ; what shapes the level, which this patch predates
+55
View File
@@ -0,0 +1,55 @@
; StringsPatch, converted from Programs/Sounds/LushStrings.json by SoundPatch.
; Designed in soundThing, where it can be heard. Do not edit the numbers
; here: change the patch and convert it again.
#Data
StringsPatch:
0d47 ; how many pairs follow
0x00 0d4 ; oscillator 0, waveform
0x01 0d159 ; gain
0x02 0d128 ; duty
0x03 0d127 ; detune, in cents
0x04 0d128 ; octave
0x05 0d1 ; on
0x06 0d3 ; what moves its width
0x07 0d200 ; and how far
0x08 0d0 ; what moves its pitch
0x09 0d128 ; and how far
0x0A 0d0 ; what moves its gain
0x0B 0d128 ; and how far
0x10 0d4 ; oscillator 1, waveform
0x11 0d159 ; gain
0x12 0d128 ; duty
0x13 0d129 ; detune, in cents
0x14 0d128 ; octave
0x15 0d1 ; on
0x16 0d4 ; what moves its width
0x17 0d200 ; and how far
0x18 0d0 ; what moves its pitch
0x19 0d128 ; and how far
0x1A 0d0 ; what moves its gain
0x1B 0d128 ; and how far
0x20 0d83 ; amplitude envelope, attack
0x21 0d40 ; decay
0x22 0d224 ; sustain
0x23 0d145 ; release
0x30 0d0 ; modulation envelope, attack
0x31 0d90 ; decay
0x32 0d0 ; sustain
0x33 0d81 ; release
0x40 0d1 ; filter, on
0x41 0d0 ; type: 0 low, 1 high, 2 band
0x42 0d197 ; cutoff, in hertz
0x43 0d21 ; resonance
0x44 0d1 ; what moves the cutoff
0x45 0d173 ; and how far, in hertz
0x46 0d0 ; what moves the resonance
0x47 0d128 ; and how far
0x60 0d1 ; LFO 0, on
0x61 0d0 ; waveform
0x62 0d75 ; rate, in hertz
0x70 0d1 ; LFO 1, on
0x71 0d0 ; waveform
0x72 0d85 ; rate, in hertz
0x50 0d1 ; what shapes the level, which this patch predates
+58
View File
@@ -0,0 +1,58 @@
; ThrustHoldPatch, converted from Programs/Sounds/ThrustHold.json by SoundPatch.
; Designed in soundThing, where it can be heard. Do not edit the numbers
; here: change the patch and convert it again.
#Data
ThrustHoldPatch:
0d50 ; how many pairs follow
0x00 0d5 ; oscillator 0, waveform
0x01 0d255 ; gain
0x02 0d128 ; duty
0x03 0d128 ; detune, in cents
0x04 0d128 ; octave
0x05 0d1 ; on
0x06 0d0 ; what moves its width
0x07 0d128 ; and how far
0x08 0d0 ; what moves its pitch
0x09 0d128 ; and how far
0x0A 0d0 ; what moves its gain
0x0B 0d128 ; and how far
0x10 0d0 ; oscillator 1, waveform
0x11 0d183 ; gain
0x12 0d128 ; duty
0x13 0d125 ; detune, in cents
0x14 0d127 ; octave
0x15 0d1 ; on
0x16 0d0 ; what moves its width
0x17 0d128 ; and how far
0x18 0d3 ; what moves its pitch
0x19 0d161 ; and how far
0x1A 0d0 ; what moves its gain
0x1B 0d128 ; and how far
0x20 0d35 ; amplitude envelope, attack
0x21 0d54 ; decay
0x22 0d255 ; sustain
0x23 0d4 ; release
0x30 0d4 ; modulation envelope, attack
0x31 0d59 ; decay
0x32 0d0 ; sustain
0x33 0d4 ; release
0x40 0d1 ; filter, on
0x41 0d0 ; type: 0 low, 1 high, 2 band
0x42 0d171 ; cutoff, in hertz
0x43 0d0 ; resonance
0x44 0d2 ; what moves the cutoff
0x45 0d0 ; and how far, in hertz
0x46 0d0 ; what moves the resonance
0x47 0d128 ; and how far
0x60 0d1 ; LFO 0, on
0x61 0d0 ; waveform
0x62 0d253 ; rate, in hertz
0x63 0d1 ; 0 free, 1 starts with a voice
0x70 0d0 ; LFO 1, on
0x71 0d0 ; waveform
0x72 0d127 ; rate, in hertz
0x73 0d0 ; 0 free, 1 starts with a voice
0x50 0d1 ; what shapes the level
0x51 0d0 ; 0 gated, 1 triggered
+58
View File
@@ -0,0 +1,58 @@
; ThrustOnPatch, converted from Programs/Sounds/ThrustOn.json by SoundPatch.
; Designed in soundThing, where it can be heard. Do not edit the numbers
; here: change the patch and convert it again.
#Data
ThrustOnPatch:
0d50 ; how many pairs follow
0x00 0d5 ; oscillator 0, waveform
0x01 0d255 ; gain
0x02 0d128 ; duty
0x03 0d128 ; detune, in cents
0x04 0d130 ; octave
0x05 0d1 ; on
0x06 0d0 ; what moves its width
0x07 0d128 ; and how far
0x08 0d2 ; what moves its pitch
0x09 0d147 ; and how far
0x0A 0d0 ; what moves its gain
0x0B 0d255 ; and how far
0x10 0d5 ; oscillator 1, waveform
0x11 0d255 ; gain
0x12 0d140 ; duty
0x13 0d126 ; detune, in cents
0x14 0d129 ; octave
0x15 0d1 ; on
0x16 0d0 ; what moves its width
0x17 0d0 ; and how far
0x18 0d2 ; what moves its pitch
0x19 0d246 ; and how far
0x1A 0d0 ; what moves its gain
0x1B 0d255 ; and how far
0x20 0d180 ; amplitude envelope, attack
0x21 0d40 ; decay
0x22 0d0 ; sustain
0x23 0d4 ; release
0x30 0d4 ; modulation envelope, attack
0x31 0d63 ; decay
0x32 0d0 ; sustain
0x33 0d180 ; release
0x40 0d1 ; filter, on
0x41 0d0 ; type: 0 low, 1 high, 2 band
0x42 0d152 ; cutoff, in hertz
0x43 0d42 ; resonance
0x44 0d0 ; what moves the cutoff
0x45 0d255 ; and how far, in hertz
0x46 0d0 ; what moves the resonance
0x47 0d128 ; and how far
0x60 0d0 ; LFO 0, on
0x61 0d0 ; waveform
0x62 0d220 ; rate, in hertz
0x63 0d0 ; 0 free, 1 starts with a voice
0x70 0d0 ; LFO 1, on
0x71 0d0 ; waveform
0x72 0d127 ; rate, in hertz
0x73 0d0 ; 0 free, 1 starts with a voice
0x50 0d2 ; what shapes the level
0x51 0d1 ; 0 gated, 1 triggered
+67
View File
@@ -0,0 +1,67 @@
; Four bars, and the bass is the same one under two of them.
#Tick 0d125000 ; cycles a tick: a sixteenth note at 120 beats a minute
#Patch Oboe oboe.patch
#Voice 0d0 Oboe ; which instrument each voice starts on
#Voice 0d1 Oboe
#Voice 0d2 Oboe
#Voice 0d3 Oboe
#Sequence MotifA
0x30 0d2
0x00 0d2
0x3C 0d2
0x00 0d4
0x48 0d4
0x00 0d4
0x47 0d4
0x00 0d6
; 28 beats
#Sequence MotifB
0x00 0d2
0x35 0d2
0x00 0d2
0x40 0d4
0x00 0d4
0x45 0d4
0x00 0d4
0x43 0d6
#Sequence IntroQuiet
0x00 0d28
#Sequence ChordRoot
0x2C 0x4
0x2E 0x4
0x30 0x4
#Sequence ChordFifth
0x33 0x4
0x35 0x4
0x37 0x4
#Sequence ChordThird
0x3C 0x4
0x3E 0x4
0x40 0x4
#Sequence ChordQuiet
0x00 0d12
#Sequence Bass
0x24 0x8
#Order 0d0
MotifA ChordRoot
#Order 0d1
MotifB ChordFifth
#Order 0d2
IntroQuiet ChordThird
#Order 0d3
IntroQuiet ChordQuiet Bass
+60
View File
@@ -0,0 +1,60 @@
; Four bars, and the bass is the same one under two of them.
#Tick 0d125000 ; cycles a tick: a sixteenth note at 120 beats a minute
#Patch Oboe oboe.patch
#Voice 0d0 Oboe ; which instrument each voice starts on
#Voice 0d1 Oboe
#Voice 0d2 Oboe
#Sequence MotifA
0x30 0d2
0x00 0d2
0x3C 0d2
0x00 0d6
#Sequence MotifB
0x00 0d2
0x35 0d2
0x00 0d2
0x41 0d6
; Truncate Anachronaut motif to three introductory notes.
#Sequence IntroQuiet
0x00 0d12
; new Chords are C-maj, A-maj. "Seeeee-gaaaan"
#Sequence ChordA
; C
0x30 0d6
; A
0x2D 0d6
; One continuous note here makes the chords blend together better.
#Sequence ChordB
; E+
0x34 0d12
#Sequence ChordC
; G+
0x37 0d6
; Cs+
0x3D 0x6
#Sequence ChordQuiet
0x00 0d12
; The extra rests on the end prevent the splash screen from ending before the voices are silent.
#Order 0d0
MotifA ChordA IntroQuiet
#Order 0d1
MotifB ChordB IntroQuiet
#Order 0d2
IntroQuiet ChordC IntroQuiet
+82
View File
@@ -0,0 +1,82 @@
; What the console's attribute register says down a serial line.
;
; The attribute has always meant something to the SCREEN and nothing to a terminal: its low
; nibble picks one of sixteen ink and paper pairs, and only videoPutCell ever read it. So a
; fault printed in red was red in the window and grey down the wire, and every program that
; chose a colour was choosing it for half its audience.
;
; This walks all sixteen schemes and then stops WITH ONE STILL SET, which is the second half
; of what is being checked: the machine has to put the terminal back to plain on its way out,
; and the reset has to come out AFTER the text rather than in front of it. An atexit handler
; runs before stdio flushes what is buffered, so a reset written straight to the file
; descriptor would arrive first and colour nothing.
;
; Written by Anachronaut
#Include print.asm
#Program
start:
; Banks 0 to 7 are ink on black. Nought is plain and is expected to say nothing at all: a
; terminal is assumed to start plain, so a machine that never asks for a colour must be
; silent rather than opening every session with a reset nobody wanted.
RSTA
SETD Scheme
STA
inkNext:
SETD Scheme
LDA
OUTA 0x06
SETD InkText
CALL printString
SETD Scheme
LDA
CALL printDigit
CALL lineFeed
SETD Scheme
LDA
INCA
STA
INIB 0d16
CCF
SUB
BNQ inkNext
; ---- And it stops in colour, on purpose ----
;
; Fifteen is the last of them and nothing puts it back. What follows this in the recording
; is whatever the machine says on its way out, which is the check.
HALT
; A is nought to fifteen, said as ONE HEX CHARACTER. The scheme is a nibble, so a nibble is
; what it should read as - and one character means no scratch byte to hold the second digit
; while the first goes out, which is what the decimal version needed and got wrong.
printDigit:
INIB 0d10
CCF
SUB
BRC digitLow ; Borrowed, so it is below ten.
MVQA ; What is left once the ten is taken off.
INIB 0d65 ; 'A', because the ten has already gone.
CCF
ADD
MVQA
OUTA 0x00
RET
digitLow:
INIB 0d48
CCF
ADD
MVQA
OUTA 0x00
RET
#Data
InkText:
"scheme "
Scheme:
0x00
+179
View File
@@ -0,0 +1,179 @@
; Four voices on one tick, staggered so that each can be heard on its own.
;
; M2 of the music player. What is being checked is that the voices share the tick and NOTHING
; ELSE: each keeps its own place in its own track and its own count of how long the note it is
; holding lasts, so one can rest while another plays.
;
; Twelve ticks of 125,000 cycles, which is a sixteenth note at 120 beats a minute:
;
; ticks 0-3 voice 0 plays middle C voice 1 rests
; ticks 4-7 voice 0 rests voice 1 plays the C above it
; ticks 8-11 voice 0 plays middle C voice 1 plays the C above it
;
; BOTH VOICES ARE ASKED FOR NOTE 60. The octave between them is in their patches, which is
; M3: a patch belongs to its channel, so four of them can be up at once. Under a device where
; it did not, the second patch loaded would own both voices and they would sound in unison.
;
; So the first two windows have one voice in them and can be measured for pitch, and the third
; has both and can be measured for level. Two voices sounding at once cannot be asked their
; pitch - a crossing counter given two notes answers with neither.
;
; THIS IS DELIBERATELY NOT Apps/Play.asm. It shares the design and not the code, and it is
; smaller: no track ever ends here, so there is no live flag and no count of what is still
; playing. What it has is the part worth pinning - a cursor and a countdown per voice, moved
; with LDD and STD.
;
; Written by Anachronaut
#Program
start:
; A patch each, in the format SoundPatch writes: a count and that many pairs. They differ in
; ONE parameter - the octave - and VoiceStart gives the low one to voice 0 and the high one
; to voice 1. If a patch belonged to the device rather than to its channel the second would
; overwrite the first and both voices would sound the same.
SETD.0 MyPatches
SETD.1 PatchTable
STD.0.1
SETD.0 MySequences
SETD.1 SequenceTable
STD.0.1
SETD.0 MyVoiceStart
SETD.1 VoiceStartAt
STD.0.1
SETD.0 Order0
SETD.1 Voice0
STD.0.1
SETD.0 Order1
SETD.1 Voice1
STD.0.1
SETD.0 Order2
SETD.1 Voice2
STD.0.1
SETD.0 Order3
SETD.1 Voice3
STD.0.1
CALL startVoices
INIA 0x60
OUTA 0x46
; 125,000 cycles a tick. The period before the control byte, because writing control with
; the run bit set is what loads it.
INIA 0x01
OUTA 0x52
INIA 0xE8
OUTA 0x53
INIA 0x48
OUTA 0x54
INIA 0x07
OUTA 0x51
SIF
; Sixteen ticks. B survives the calls below, because a CALL saves it.
INIB 0d16
tickLoop:
SETD.1 Voice0
RSTA
CALL stepVoice
SETD.1 Voice1
INIA 0d1
CALL stepVoice
DECB
BRB lastTick
WAIT
BRI tickLoop
lastTick:
; The last step started a note and nothing has waited for it yet. Without this the final
; tick is never rendered, and a window measuring it runs off the end of the samples.
WAIT
done:
CIF
RSTA
OUTA 0x51
HALT
tick:
RETI
#Data
; Middle C, then out of the way, then back - against the octave above doing the opposite.
; ---- The order lists, and what they are for ----
;
; Four bars of four ticks each, and the SAME TWO SEQUENCES do nearly all of it. Beat is used
; twice by voice 0 and twice by voice 1 - four placings of one phrase, written once, and on
; two different voices, because the voice is a property of where a sequence is placed and not
; of the sequence.
;
; ticks 0-3 4-7 8-11 12-15
; voice 0 Beat Quiet Beat Switch
; voice 1 Quiet Beat Beat (its order list ends here)
Order0:
0d0 0d1 0d0 0d2 0d3 0xFF
Order1:
0d1 0d0 0d0 0xFF ; And this one ENDS, so the last bar has voice 1 stopped rather
; than resting. A player that missed the terminator would read
; whatever follows as a sequence index and play it.
Order2:
0d3 0xFF
Order3:
0d3 0xFF
MyPatches:
PatchLow PatchHigh
MySequences:
Beat Quiet Switch Tail ; 0 to 3
MyVoiceStart:
0d0 0d1 0d0 0d0
Beat:
0d60 0d4 ; Note 60 on both voices. The octave between them is their patches.
0xFF
Quiet:
0d00 0d4
0xFF
Switch:
0x80 0d1 ; Voice 0 takes patch one, which is voice 1's, mid-piece.
0d60 0d4 ; The same note number it has played all along.
0xFF
Tail:
0d00 0xFF ; Long enough that nothing runs off the end of its order list.
0xFF
; Seven pairs each, and only the last of them differs: the octave, centred on 128. So the two
; voices below are asked for THE SAME NOTE NUMBER and answer an octave apart, which nothing
; but a patch of their own could do.
PatchLow:
0d8
0x00 0d2 ; Saw
0x01 0xFF ; at full gain
0x05 0d1 ; and switched on.
0x20 0d0 ; No attack
0x21 0d0 ; no decay
0x22 0xFF ; and held at full, so a note lasts exactly its ticks
0x23 0d5 ; and goes quickly when the gate drops, which the windows below
; depend on: a note still fading into the next voice's stretch is
; a second pitch in it, and a crossing counter given two answers
; with neither.
0x04 0d128 ; Octave, centred.
PatchHigh:
0d8
0x00 0d2
0x01 0xFF
0x05 0d1
0x20 0d0
0x21 0d0
0x22 0xFF
0x23 0d5
0x04 0d129 ; And this one an octave up.
#Include player.asm
#Vectors
Boot start
Device 0x50 tick
+70
View File
@@ -0,0 +1,70 @@
; A program that stops while a note is still held down.
;
; The gate is a register on the sound device, and only a program can drop one. This one does
; not: it starts a note whose envelope sustains at full and then hands the machine back, so
; the note is left sounding with nothing left in the world able to end it.
;
; WHAT SHOULD HAPPEN IS THAT COSMOS ENDS IT, the way it puts the screen back and the drive and
; the vectors - and for a stronger reason, since a program that faults cannot tidy up after
; itself either. Without that this note sustains for as long as the machine runs.
;
; Sustain at full and a short release, so the difference between the two answers is loud: a
; gate left up rings for ever and a gate dropped is quiet within a fraction of a second.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x5000
start:
RSTA
OUTA 0x41 ; Channel 0
OUTA 0x42
INIA 0d2
OUTA 0x43 ; Saw
INIA 0x01
OUTA 0x42
INIA 0xFF
OUTA 0x43 ; at full gain
INIA 0x05
OUTA 0x42
INIA 0x01
OUTA 0x43 ; and on.
INIA 0x20
OUTA 0x42
RSTA
OUTA 0x43 ; No attack
INIA 0x21
OUTA 0x42
RSTA
OUTA 0x43 ; no decay
INIA 0x22
OUTA 0x42
INIA 0xFF
OUTA 0x43 ; and held at full, for ever, until a gate says otherwise.
INIA 0x23
OUTA 0x42
INIA 0d5
OUTA 0x43 ; A short release, so a dropped gate is quiet almost at once.
INIA 0xC0
OUTA 0x46
INIA 0d60
OUTA 0x44 ; Middle C, started and never let go of.
SWI osExit
#Data
#Base 0x3000
Unused:
0x00
#Vectors
Boot start
+83
View File
@@ -0,0 +1,83 @@
; The quarter square multiply, against numbers whose products are known.
;
; a * b = qs[a+b] - qs[|a-b|], and the table of quarter squares is built by adding, so nothing
; in the whole arrangement needs a multiply to exist. What this checks is that the arrangement
; actually multiplies: the cases below cover nought, one, the commutation, a square, and the
; largest product two bytes can hold.
;
; Written by Anachronaut
#Include print.asm
#Include math.asm
#Program
start:
CALL mulReady ; The table, once, before anything asks for a product.
RSTA
CALL times ; 0 x 0
INIA 0d7
SETD.0 MulA
STA.0
RSTA
SETD.0 MulB
STA.0
CALL show ; 7 x 0, which is the other way round from the last one.
INIA 0d1
CALL both
CALL show ; 1 x 1
INIA 0d12
CALL both
CALL show ; 12 x 12, a square, which is the case the identity leans on:
; the difference term is nought and the whole answer is one entry.
INIA 0d3
SETD.0 MulA
STA.0
INIA 0d200
SETD.0 MulB
STA.0
CALL show ; 3 x 200
INIA 0d200
SETD.0 MulA
STA.0
INIA 0d3
SETD.0 MulB
STA.0
CALL show ; and 200 x 3, which had better agree.
INIA 0xFF
CALL both
CALL show ; 255 x 255, the largest a byte times a byte can be.
HALT
; A in both operands, for the square cases.
both:
SETD.0 MulA
STA.0
SETD.0 MulB
STA.0
RET
; A in both, then show it. The nought case wants this and nothing else does.
times:
CALL both
CALL show
RET
; The product, high byte then low, which is how a sixteen bit number reads.
show:
CALL mul8
SETD.0 MulHigh
LDA.0
CALL printByteHex
SETD.0 MulLow
LDA.0
CALL printByteHex
CALL lineFeed
RET
+51
View File
@@ -0,0 +1,51 @@
; A device raising its line with nothing installed to answer it, CAUGHT.
;
; The other half of the fault that could not be handed over. A program that asks a device to
; interrupt it and then forgets the handler used to stop the machine dead; now it goes to a
; vector of its own with the port in Q, so the program can say which device it was that
; nobody was listening to.
;
; NOTHING IS TYPED AT THIS. The console raises its line once at the end of input as well as
; for an arriving byte - which is exactly so that a program driven by interrupts is told when
; nothing more is coming - and the test is run with no input at all, so that end is what
; arrives.
;
; Correct output is:
; nobody listening on port 00
; and a clean halt.
#Include console.asm
#Program
start:
INIA 0x02 ; Interrupt when the console has something to say.
OUTA 0x02
SIF
spin:
; Never touches the console. Whatever happens below is something that interrupted this.
BRI spin
; Q holds the port whose entry was empty. The console is port zero.
nobody:
SETD.0 NobodyText
CALL printString
MVQA
CALL printByteHex
CALL newLine
; The console back to how it was found, so nothing else is asked for, and stop. There is
; nothing to resume to: the loop above only exists to be interrupted.
RSTA
OUTA 0x02
HALT
#Data
NobodyText:
"nobody listening on port "
#Vectors
Boot start
NoDevice nobody
+56
View File
@@ -0,0 +1,56 @@
; A software interrupt that names a vector with nothing in it, CAUGHT.
;
; That fault used to be the one the machine had no way of handing over, because the thing
; that would hand it over is the thing that has just found nothing to hand it to. It stopped
; the machine, and no program could do anything about it - which made calling a service the
; system does not implement fatal, and that is an ordinary mistake to make.
;
; It goes to a vector of its own now, with the number of the empty entry in Q. This catches
; it, says which one it was, steps over the SWI and its operand, and carries on - so the
; output says both that the fault arrived and that a program can survive it.
;
; Correct output is:
; empty vector 28
; carried on
; and a clean halt.
#Include console.asm
#Program
start:
SWI 0d40 ; Forty is 0x28, and nothing is installed there.
SETD.0 Carried
CALL printString
CALL newLine
HALT
; Entered because there was nowhere to go. Q holds which entry was empty, and it is the only
; thing on this machine a handler is handed in a register.
missing:
SETD.0 EmptyText
CALL printString
MVQA
CALL printByteHex
CALL newLine
; ---- Carrying on past it ----
;
; The frame holds the address after the SWI and the byte naming its vector, so a bare RETI
; already lands past the instruction that faulted. That is not true of every fault here -
; a refused port or a byte that does not decode both resume ON the thing that failed - so
; it is worth saying out loud which kind this one is.
RETI
#Data
EmptyText:
"empty vector "
Carried:
"carried on"
#Vectors
Boot start
NoHandler missing
+51
View File
@@ -0,0 +1,51 @@
; Asks the machine what its controllers are doing.
;
; A pad reports a LEVEL and not an event: one read gives every button at once, holding is the
; natural thing to say, and reading does not consume anything - so asking twice in a frame
; gives the same answer twice, which this checks by doing exactly that.
;
; The recording behind it is one byte a frame. It presses up, holds it a second frame, adds
; right, lets go of up, lets go of everything, then presses A and B together - which is the
; case a console's key-at-a-time stream cannot express at all.
;
; Written by Anachronaut
#Program
start:
INA 0x64
OUTA 0x00 ; Which pads are there: one bit each, so pad nought alone is 1.
RSTA
SETD.0 Count
STA.0
everyFrame:
INA 0x30
INIB 0x01
AND
BRQ everyFrame ; The frame the recording steps on.
INA 0x60
OUTA 0x00
; And again, without a frame in between. A level does not go away when it is looked at.
INA 0x60
OUTA 0x00
SETD.0 Count
LDA.0
INCA
STA.0
INIB 0d6
CCF
SUB
BNQ everyFrame
; A pad that is not there reads as nothing held, which is honest rather than an error.
INA 0x63
OUTA 0x00
HALT
#Data
Count:
0x00
+47
View File
@@ -0,0 +1,47 @@
; A program that takes a while and makes no sound.
;
; Fixture. Something has to happen after a program that left a note sounding has stopped, or
; there is nothing for the sound device to be measured DURING: the machine halts the moment
; the shell runs out of input, and a note quietened at that instant leaves no samples behind
; to say whether it was quietened at all.
;
; So this burns about two million cycles and exits. Two nested counts and a third around
; them, because a byte only reaches 256 and a fifth of a second is a great deal more than
; that.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x5000
start:
INIA 0d16
outer:
PSHA ; The outer count, which the inner ones need A for.
RSTB
middle:
RSTA
inner:
DECA
BNA inner
DECB
BNB middle
POPA
DECA
BNA outer
SWI osExit
#Data
#Base 0x3000
Unused:
0x00
#Vectors
Boot start
+74 -4
View File
@@ -24,8 +24,11 @@ wrote Asm.sbx: program 7533, data 4099, labels 555
| [`Source/Assembler`](Source/Assembler) | The assembler that runs on a host | | [`Source/Assembler`](Source/Assembler) | The assembler that runs on a host |
| [`Source/DiskTool`](Source/DiskTool) | SplitDisk, which reads and writes SplitBit's filesystem | | [`Source/DiskTool`](Source/DiskTool) | SplitDisk, which reads and writes SplitBit's filesystem |
| [`Source/Linter`](Source/Linter) | SplitLint, which points out needlessly long assembly forms | | [`Source/Linter`](Source/Linter) | SplitLint, which points out needlessly long assembly forms |
| [`Source/Patch`](Source/Patch) | SoundPatch, which turns a soundThing patch into a table the sound device takes |
| [`Source/Tune`](Source/Tune) | TuneC, which turns a written tune into the bytes the music player reads |
| [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life, the colours | | [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life, the colours |
| [`Programs/Libraries`](Programs/Libraries) | Code included by name rather than linked, since there is no linker | | [`Programs/Libraries`](Programs/Libraries) | Code included by name rather than linked, since there is no linker |
| [`Programs/Sounds`](Programs/Sounds) | Patches as soundThing saved them, and the tables SoundPatch made from them, both checked in so the build never needs soundThing |
| [`Programs/Loader`](Programs/Loader) | The standalone loader CosmOS grew out of | | [`Programs/Loader`](Programs/Loader) | The standalone loader CosmOS grew out of |
| [`Programs/CosmOS`](Programs/CosmOS) | The operating system, its applications, and the native assembler | | [`Programs/CosmOS`](Programs/CosmOS) | The operating system, its applications, and the native assembler |
| [`Programs/testPrograms`](Programs/testPrograms) | What the test suite drives | | [`Programs/testPrograms`](Programs/testPrograms) | What the test suite drives |
@@ -114,7 +117,7 @@ Manual.
## Getting Started: ## Getting Started:
Clone it and build the four tools. You need gcc and make, or similar: Clone it and build the six tools. You need gcc and make, or similar:
``` ```
git clone https://github.com/RealBusinessAccount/SplitBit-Emulator.git git clone https://github.com/RealBusinessAccount/SplitBit-Emulator.git
@@ -146,6 +149,11 @@ Both put a second disk in drive 1, at `Disks/personal.img`. It is made once and
rebuilt, cleaned or committed: everything else here can be thrown away and made again from rebuilt, cleaned or committed: everything else here can be thrown away and made again from
source, and that one is where anything made ON the machine lives. source, and that one is where anything made ON the machine lives.
Starting the machine copies it first, three starts back, as `personal.img.1` and so on. Not
one copy: the way a disk is lost is that something goes wrong and the very next thing anybody
does is start the machine again to see how bad it is, which is exactly when a single backup
would be overwritten by the wreckage.
Then `dir` to see what is there, `load Snake.sbx` and `run` to play something, or `load Asm.sbx` and `run cosmos.asm` to watch the machine build itself. Then `dir` to see what is there, `load Snake.sbx` and `run` to play something, or `load Asm.sbx` and `run cosmos.asm` to watch the machine build itself.
Every source in `Programs/` is on that disk, under `/Source`, so anything not shipped as a Every source in `Programs/` is on that disk, under `/Source`, so anything not shipped as a
@@ -299,6 +307,66 @@ this is what turns such a count into a list of routine names.
Without `-o` the output takes the source file's name, in the directory you called the assembler from, with the extension the format asks for: `.bin` for a boot image and `.sbx` for a loadable program. Included files are looked for beside the file that includes them, and then along the directories given with `-I`. Without `-o` the output takes the source file's name, in the directory you called the assembler from, with the extension the format asks for: `.bin` for a boot image and `.sbx` for a loadable program. Included files are looked for beside the file that includes them, and then along the directories given with `-I`.
## Writing A Tune: TuneC
```
./TuneC [-I <dir>] <tune.score> <output.tune>
```
Four voices on one clock. A tune names its instruments, writes sequences of notes and durations, and gives each voice an order list of sequence names - which is where repetition comes from, since a phrase played four times is written once and named four times.
```
; Four bars, and the bass is the same one under two of them.
#Tick 0d125000 ; cycles a tick: a sixteenth note at 120 beats a minute
#Patch Oboe oboe.patch
#Patch Strings strings.patch
#Voice 0d0 Oboe ; which instrument each voice starts on
#Sequence Verse
0d64 0d4 0d67 0d4 0d72 0d8
#Sequence Ending
#Use Strings ; a command, which takes no time at all
0d72 0d16
#Order 0d0
Verse Verse Ending
```
`#` is a directive and `;` is a comment, exactly as in SplitBit assembly and in the shell's scripts, and numbers are written the way the assembler writes them - `0d` and `0x`, and no bare numbers. One rule across the machine rather than a third dialect.
A `.score` is written and a `.tune` is what the machine reads, the same way a `.asm` is written and a `.sbx` is loaded. Patches come from `SoundPatch --blob`, which stays the only thing that understands what a soundThing patch means, and are looked for beside the tune and then on the `-I` path the way an include is.
**What it refuses is everything the player cannot notice for itself.** The machine has no names, so it cannot say a sequence does not exist; it has no lengths, so it cannot say the voices will come apart four bars after the mistake; and a duration of nought is counted down to 255 and held, which sounds like a hang rather than an error. A voice with a part and no instrument is refused too, because by the time a tune is loaded, "never said" and "instrument nought" are the same byte.
## Making A Patch: SoundPatch
```
./SoundPatch <patch.json> <label> [output.asm]
./SoundPatch --blob <patch.json> <name> <output.patch>
```
The sound device is soundThing's voice engine with the editor taken off, so a patch designed
in soundThing makes the same sound here. What differs is how it arrives: soundThing writes
seconds and hertz, and the device takes bytes through a selector. SoundPatch converts one into
the other and writes a table a program can hand to the device.
A byte is not a number of seconds, and that is the whole reason this exists. Times are squared
into four seconds, because the difference between five and fifty milliseconds is the character
of a percussive sound and the difference between three and four seconds is nothing anybody can
hear. Cutoff and LFO rate are exponential, because hearing is logarithmic. Depths and detune
are centred on 128, so half of nothing is no change. The first sound written for a game here
was guessed at directly in bytes: a cutoff of 40 looks small and is 57 Hz, and the bang came
out as a low gurgle.
A field the tool does not recognise stops it rather than being skipped, because a patch format
that has moved on would otherwise produce a table that quietly means something else.
**The build does not depend on it.** soundThing lives in its own repository and is not
required to build anything here. `Programs/Sounds` holds both halves - the patch as soundThing
saved it and the table made from it - so a sound can be regenerated, or read, without going
and finding the editor first. The tool is for when a sound is being changed.
## Checking Assembly: SplitLint ## Checking Assembly: SplitLint
``` ```
@@ -606,7 +674,7 @@ $(BUILD)/%.bin: %.asm
make test make test
``` ```
Builds the four tools, checks they build clean under strict ISO C, and runs nine scripts. Builds the six tools, checks they build clean under strict ISO C, and runs nine scripts.
`Tests/run.sh` assembles and runs every program in `Programs/` and compares the results `Tests/run.sh` assembles and runs every program in `Programs/` and compares the results
against recorded output; six more ask the questions a recorded file cannot answer. Between against recorded output; six more ask the questions a recorded file cannot answer. Between
them they check the two assemblers against each other byte for byte, the two SBFS them they check the two assemblers against each other byte for byte, the two SBFS
@@ -625,7 +693,7 @@ correct.
make sanitize make sanitize
``` ```
Rebuilds all four tools with the address and undefined behaviour sanitizers and runs the Rebuilds all six tools with the address and undefined behaviour sanitizers and runs the
whole suite under them. It catches reads and writes past the end of an array, use after whole suite under them. It catches reads and writes past the end of an array, use after
free, leaks, and undefined arithmetic, takes about twice as long, and puts the ordinary free, leaks, and undefined arithmetic, takes about twice as long, and puts the ordinary
binaries back when it finishes. binaries back when it finishes.
@@ -642,7 +710,7 @@ in the [SplitBit Test Manual](SplitBit%20Test%20Manual.md).
## Documentation: ## Documentation:
Four documents, divided by what they are about rather than by who reads them. Five documents, divided by what they are about rather than by who reads them.
**[SplitBit Programming Manual](SplitBit%20Programming%20Manual.md)** describes **the machine**: the instruction set, the registers, the vector table, interrupts, devices, the memory controller, the console, storage, and faults. Everything here is true of any SplitBit, whatever is running on it. **[SplitBit Programming Manual](SplitBit%20Programming%20Manual.md)** describes **the machine**: the instruction set, the registers, the vector table, interrupts, devices, the memory controller, the console, storage, and faults. Everything here is true of any SplitBit, whatever is running on it.
@@ -650,6 +718,8 @@ Four documents, divided by what they are about rather than by who reads them.
[Programs/CosmOS/README.md](Programs/CosmOS/README.md) describes **the operating system**: its shell, its applications, what a program may ask it for, and the libraries it owns. A different system on the same machine would answer all of that differently, which is why it is documented with the system rather than with the CPU. [Programs/CosmOS/README.md](Programs/CosmOS/README.md) describes **the operating system**: its shell, its applications, what a program may ask it for, and the libraries it owns. A different system on the same machine would answer all of that differently, which is why it is documented with the system rather than with the CPU.
**[SplitBit Tune Manual](SplitBit%20Tune%20Manual.md)** describes **music**: what a tune is, how to write one, what TuneC refuses and why, and the bytes a tune file holds for anything that would rather write one itself.
**[SplitBit Test Manual](SplitBit%20Test%20Manual.md)** describes **the test suite**: what each of its scripts claims, which of those claims are worth the most, how to add to it, and what it is blind to. It is about this repository rather than about the machine, which is why it comes last. **[SplitBit Test Manual](SplitBit%20Test%20Manual.md)** describes **the test suite**: what each of its scripts claims, which of those claims are worth the most, how to add to it, and what it is blind to. It is about this repository rather than about the machine, which is why it comes last.
## License: ## License:
+25 -4
View File
@@ -95,15 +95,28 @@ void assemblerCleanup(intermediateElement *intermediateArray, int arraySize, cha
free(outputFileName); free(outputFileName);
} }
// EVERY OPTION HERE NAMES A FILE THE ASSEMBLER WRITES, and the source is the bare argument
// at the end. A list of bare flags does not say that: "-S <file>" reads just as naturally as
// "dump the symbols of <file>", and asking for it that way hands the source to -S, leaves
// nothing positional behind it, and gets answered with "No source file specified" - which
// describes the symptom and hides the cause. So the long names are printed, every line says
// where the file is written, and an example shows a whole command rather than leaving one to
// be assembled out of the parts.
void printUsage(const char *programName) { void printUsage(const char *programName) {
printf("Usage: %s [OPTIONS] <sourcefile>\n", programName); printf("Usage: %s [OPTIONS] <sourcefile>\n", programName);
printf("\n"); printf("\n");
printf("Assembles one source file. The source is the last argument, on its own; every\n");
printf("<file> below is a path the assembler writes, and every <dir> one it searches.\n");
printf("\n");
printf("Options:\n"); printf("Options:\n");
printf(" -o <file> Write the output to this path instead of alongside the source.\n"); printf(" -o, --output <file> Write the assembled output here, instead of alongside the source.\n");
printf(" -I <dir> Look in this directory for included files. May be given more than once.\n"); printf(" -I, --include <dir> Search this directory for included files. May be given more than once.\n");
printf(" -M <file> Write the source files this output depends on, as a make rule.\n"); printf(" -M, --depend <file> Write a make rule here, naming every source that went into the output.\n");
printf(" -S <file> Write every label and the address it was given, in address order.\n"); printf(" -S, --symbols <file> Write every label here with the address it was given, in address order.\n");
printf(" -h, --help Display this help message.\n"); printf(" -h, --help Display this help message.\n");
printf("\n");
printf("Example:\n");
printf(" %s -I Libraries -S game.sym -o game.sbx game.asm\n", programName);
} }
// Writes a make rule naming every source file that went into the output, so that a // Writes a make rule naming every source file that went into the output, so that a
@@ -207,6 +220,14 @@ int main(int argc, char *argv[]) {
if (optind >= argc) { if (optind >= argc) {
fprintf(stderr, RED "Error: No source file specified.\n" RESET); fprintf(stderr, RED "Error: No source file specified.\n" RESET);
// The commonest way to get here is giving the source to an option that wanted a
// path to write, which consumes it and leaves nothing positional. Saying so is the
// difference between an error that names the mistake and one that names the hole
// the mistake left behind.
if (outputFileName || dependencyFileName || symbolFileName) {
fprintf(stderr, " -o, -M and -S each name a file to WRITE, not one to read.\n"
" The source file is the last argument, on its own.\n");
}
printUsage(argv[0]); printUsage(argv[0]);
return 1; return 1;
} }
+9
View File
@@ -309,6 +309,15 @@ int readToken(intermediateElement *currentElement, FILE *file, int *lineNumber)
// Step 3: Handle string literals // Step 3: Handle string literals
if (c == '"') { if (c == '"') {
while ((c = fgetc(file)) != EOF && c != '"') { while ((c = fgetc(file)) != EOF && c != '"') {
// A STRING MAY HAVE NEWLINES IN IT, and they are as real as any others. Not
// counting them is why every line number after one was short: cosmos.asm has
// thirteen of them and its last label was reported thirteen lines early, which
// makes an error message point at somebody else's code. Counted here, where
// the character is consumed, rather than by scanning the token afterwards -
// the same newline must not be counted twice if this loop ever grows a way out.
if (c == '\n') {
(*lineNumber)++;
}
if (i < (int)(sizeof(buffer) - 1)) { if (i < (int)(sizeof(buffer) - 1)) {
buffer[i++] = c; buffer[i++] = c;
} else { } else {
+25 -1
View File
@@ -66,7 +66,31 @@
#define VECTOR_GUARD_VIOLATION 3 #define VECTOR_GUARD_VIOLATION 3
// A bank was named that has nothing registered in it, or an access ran past its end. // A bank was named that has nothing registered in it, or an access ran past its end.
#define VECTOR_BANK_FAULT 4 #define VECTOR_BANK_FAULT 4
// Vectors 5 to 15 are held back for faults that do not exist yet, so that each cause
// ---- The two faults that used to be uncatchable ----
//
// A vector was dispatched through and had nothing in it. That is discovered by the very
// thing that would dispatch, so for a long time it could only stop the machine: there was
// no way to hand a program a fault about there being nowhere to hand it.
//
// These two are where it goes instead, and they are separate for the same reason every
// other cause is - a missing software vector and a device nobody is listening to are
// different mistakes with different fixes, and a handler should not have to work out which
// it was.
//
// WHICH ENTRY WAS EMPTY ARRIVES IN Q, and that is the only thing on this machine a handler
// is given in a register. It is not a cause register by the back door: the vector still
// says what happened, and Q says which of the 256 entries it happened about, which is a
// parameter rather than a cause. It costs no new state, because the frame already saved the
// Q the interrupted program had and RETI puts it back.
//
// A vector THESE dispatch through and find empty stops the machine, and has to: a machine
// that cannot report a fault about a missing handler by any route has run out of places to
// go, and looping there would be worse than stopping.
#define VECTOR_NO_HANDLER 5
#define VECTOR_NO_DEVICE 6
// Vectors 7 to 15 are held back for faults that do not exist yet, so that each cause
// can have an entry of its own rather than sharing one and needing a cause register to // can have an entry of its own rather than sharing one and needing a cause register to
// tell them apart. Everything from 16 up belongs to programs, in two halves. // tell them apart. Everything from 16 up belongs to programs, in two halves.
// //
+148 -25
View File
@@ -21,25 +21,89 @@ int debugSecondPass = 0;
Label labelArray[MAX_LABELS]; Label labelArray[MAX_LABELS];
int labelCount = 0; int labelCount = 0;
// Beside the labels rather than beside the code that fills it, because the symbol file
// below reads both tables and this one would otherwise not be declared yet.
VectorEntry vectorArray[MAX_VECTORS];
int vectorArrayCount = 0;
// ---- Where everything ended up ---- // ---- Where everything ended up ----
// //
// Every label and the address it was given, in address order. The assembler knows this and // Every label with the memory and address it was given and the place it was written, one
// nothing else does: a program on the disk is bytes, and the machine's own monitor can // per line, fields separated by tabs. The assembler knows all of this and nothing else
// disassemble it but has no idea what any of it is called. // does: a program on the disk is bytes, and the machine's own monitor can disassemble it
// but has no idea what any of it is called or where it came from.
// //
// WHAT IT IS FOR is telling where a program spends its time. Counting which addresses get // WHAT IT IS FOR is two questions. The first is where a program spends its time - counting
// called says a great deal and names nothing, so the answer arrives as a list of numbers // which addresses get called says a great deal and names nothing, so the answer arrives as
// and somebody has to work out by hand which routine each one is inside. With this, a // a list of numbers and somebody has to work out by hand which routine each one is inside.
// tally of call targets becomes a list of routine names. // The second is where a name lives, which matters more the bigger the program gets: CosmOS
// and its libraries define over a thousand names across a dozen files, and finding the one
// definition of a routine means grepping for it and reading past every place it is called.
// The file and line answer that outright.
// //
// Sorted by address rather than by name, because the question asked of it is always "what // TABS, so that the file is a table every ordinary tool already understands - cut -f3, awk
// is at this address", and a label table is small enough that sorting it is free. // -F'\t', sort -k1,2 - and so that a name never has to be quoted. No header line, for the
static int byAddress(const void *left, const void *right) { // same reason: nothing that reads it should have to know to skip one.
const Label *a = left, *b = right; //
// THE KIND IS THE FIRST FIELD BECAUSE THIS IS A HARVARD MACHINE. Program and Data are
// separate address spaces, so 0x3000 is two different places and an address alone does not
// say which. That is invisible in a loadable program, where the two segments are based
// somewhere apart, and immediate in a boot image, where both start at zero and every
// address in the file appears twice.
//
// VECTORS ARE IN HERE TOO, and they are the part nothing else can tell you. A pinned vector
// has its number written in the source that pins it, but an automatic one is handed a number
// by this assembler and that number appears nowhere at all - not in the source, not in the
// binary in any form a reader can find. A vector also costs two hops to follow by hand: the
// name in "SWI osFileRead" is not the name of the routine that implements it, so finding the
// code means grepping for the vector, reading the handler's name off the Vector Segment, and
// grepping again. A vector row gives its number, the handler's name, and the line the two
// were tied together on.
//
// A VECTOR CARRIES BOTH ITS NUMBERS, which is why there is a sixth field. Its number is what
// a program writes and what the machine dispatches on; its slot is where the handler's
// address is stored in Program Memory, base + number * 2, which is what the loader writes
// and what a memory dump shows. Neither can be worked out from the other without knowing
// which table it is in, so the file says both.
//
// THE NUMBER SITS SECOND, beside the kind rather than out at the end, because the two of
// them together are what names the thing: "Vector 26" and "Device 32" are single identifiers
// in the way "Vector" alone is not. Everything after them - where it lives, what it is
// called, where it was written - is something about the thing rather than part of naming it,
// so the line reads as an identity followed by its properties. A label has no number and
// carries a dash in that column, which is the shape of the answer "this kind is not numbered"
// rather than an empty space that could be a missing field.
//
// Sorted by kind and then address rather than by name, because the question asked of it is
// always "what is at this address"; sorting by address alone would interleave separate
// address spaces and make the first column flicker between them. The table is small enough
// that sorting it is free.
// The four kinds, in the order they are listed. Program and Data are the two memories, then
// the two vector tables.
#define ROW_PROGRAM 0
#define ROW_DATA 1
#define ROW_VECTOR 2
#define ROW_DEVICE 3
typedef struct {
int kind;
uint16_t address; // Where it lives: a label's address, or a vector's slot.
const char *name;
const char *fileName;
int lineNumber;
int number; // The vector or port number, or -1 for a label, which has none.
} SymbolRow;
static int byPlace(const void *left, const void *right) {
const SymbolRow *a = left, *b = right;
if (a->kind != b->kind) {
return a->kind < b->kind ? -1 : 1;
}
if (a->address != b->address) { if (a->address != b->address) {
return a->address < b->address ? -1 : 1; return a->address < b->address ? -1 : 1;
} }
return strcmp(a->label, b->label); return strcmp(a->name, b->name);
} }
void writeSymbolFile(const char *path) { void writeSymbolFile(const char *path) {
@@ -48,18 +112,61 @@ void writeSymbolFile(const char *path) {
fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, path); fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, path);
exit(1); exit(1);
} }
Label *sorted = malloc((size_t)labelCount * sizeof(Label)); int rowCount = labelCount + vectorArrayCount;
if (!sorted) { SymbolRow *rows = malloc((size_t)rowCount * sizeof(SymbolRow) + 1);
if (!rows) {
fprintf(stderr, RED "Error: Out of memory writing the symbol file.\n" RESET); fprintf(stderr, RED "Error: Out of memory writing the symbol file.\n" RESET);
fclose(file); fclose(file);
exit(1); exit(1);
} }
memcpy(sorted, labelArray, (size_t)labelCount * sizeof(Label)); int n = 0;
qsort(sorted, (size_t)labelCount, sizeof(Label), byAddress);
for (int i = 0; i < labelCount; i++) { for (int i = 0; i < labelCount; i++) {
fprintf(file, "%04X %s\n", sorted[i].address, sorted[i].label); // A label is put in one of the two segments by populateLabelTable and there is no
// third, but the kind is taken from the type rather than assumed, so a label that
// somehow arrived as neither is not quietly filed under Data.
rows[n].kind = labelArray[i].type == PROGRAM ? ROW_PROGRAM : ROW_DATA;
rows[n].address = labelArray[i].address;
rows[n].name = labelArray[i].label;
rows[n].fileName = labelArray[i].fileName;
rows[n].lineNumber = labelArray[i].lineNumber;
rows[n].number = -1;
n++;
} }
free(sorted); for (int i = 0; i < vectorArrayCount; i++) {
int hardware = vectorArray[i].base == HARDWARE_VECTOR_BASE;
rows[n].kind = hardware ? ROW_DEVICE : ROW_VECTOR;
// The same arithmetic the loader is given, so that what this says a vector's slot is
// and what actually gets written there cannot drift apart.
rows[n].address = vectorArray[i].base
+ (uint16_t)vectorArray[i].index * VECTOR_ENTRY_BYTES;
// A device has no name of its own - it is named by the port it is plugged into - so
// it is listed under its handler, which is the only name it has.
rows[n].name = vectorArray[i].name ? vectorArray[i].name
: vectorArray[i].handlerName ? vectorArray[i].handlerName
: "?";
rows[n].fileName = vectorArray[i].fileName;
rows[n].lineNumber = vectorArray[i].lineNumber;
rows[n].number = vectorArray[i].index;
n++;
}
qsort(rows, (size_t)n, sizeof(SymbolRow), byPlace);
static const char *kindName[4] = { "Program", "Data", "Vector", "Device" };
for (int i = 0; i < n; i++) {
char number[12]; // Wide enough for any int, which is more than a vector needs.
if (rows[i].number < 0) {
// A label has no number, and the field says so rather than being left empty: a
// run of tabs with nothing between them is the one thing a reader of this file,
// human or otherwise, can miscount.
snprintf(number, sizeof(number), "-");
} else {
snprintf(number, sizeof(number), "%d", rows[i].number);
}
fprintf(file, "%s\t%s\t%04X\t%s\t%s\t%d\n",
kindName[rows[i].kind], number, rows[i].address, rows[i].name,
rows[i].fileName ? rows[i].fileName : "?",
rows[i].lineNumber);
}
free(rows);
fclose(file); fclose(file);
} }
@@ -124,6 +231,8 @@ void addLabel(char *labelName, uint16_t address, int type, const char *fileName,
labelArray[labelCount].label = cleanedLabel; labelArray[labelCount].label = cleanedLabel;
labelArray[labelCount].address = address; labelArray[labelCount].address = address;
labelArray[labelCount].type = type; labelArray[labelCount].type = type;
labelArray[labelCount].fileName = fileName;
labelArray[labelCount].lineNumber = lineNumber;
if (debugSecondPass) printf("Added label %s with address %04X\n", labelName, labelArray[labelCount].address); if (debugSecondPass) printf("Added label %s with address %04X\n", labelName, labelArray[labelCount].address);
labelCount++; labelCount++;
} else { } else {
@@ -185,9 +294,6 @@ void populateLabelTable(intermediateElement *intermediateArray, int arraySize) {
int findLabelAddress(const char *labelName); int findLabelAddress(const char *labelName);
VectorEntry vectorArray[MAX_VECTORS];
int vectorArrayCount = 0;
int vectorCount() { int vectorCount() {
return vectorArrayCount; return vectorArrayCount;
} }
@@ -197,6 +303,9 @@ void freeVectorList() {
if (vectorArray[i].name) { if (vectorArray[i].name) {
free(vectorArray[i].name); free(vectorArray[i].name);
} }
if (vectorArray[i].handlerName) {
free(vectorArray[i].handlerName);
}
} }
vectorArrayCount = 0; vectorArrayCount = 0;
} }
@@ -226,6 +335,8 @@ static const struct {
{ "BadOpcode", VECTOR_INVALID_OPCODE }, { "BadOpcode", VECTOR_INVALID_OPCODE },
{ "GuardViolation", VECTOR_GUARD_VIOLATION }, { "GuardViolation", VECTOR_GUARD_VIOLATION },
{ "BankFault", VECTOR_BANK_FAULT }, { "BankFault", VECTOR_BANK_FAULT },
{ "NoHandler", VECTOR_NO_HANDLER },
{ "NoDevice", VECTOR_NO_DEVICE },
}; };
static const int reservedVectorCount = (int)(sizeof(reservedVectors) / sizeof(reservedVectors[0])); static const int reservedVectorCount = (int)(sizeof(reservedVectors) / sizeof(reservedVectors[0]));
@@ -271,7 +382,8 @@ static int findVector(const char *name) {
} }
static void addVector(char *name, uint8_t index, uint16_t base, uint16_t handler, static void addVector(char *name, uint8_t index, uint16_t base, uint16_t handler,
int declaredOnly, intermediateElement *element) { int declaredOnly, intermediateElement *element,
const char *handlerName) {
if (vectorArrayCount >= MAX_VECTORS) { if (vectorArrayCount >= MAX_VECTORS) {
vectorError("Too many vectors defined.", element); vectorError("Too many vectors defined.", element);
} }
@@ -293,6 +405,9 @@ static void addVector(char *name, uint8_t index, uint16_t base, uint16_t handler
vectorArray[vectorArrayCount].base = base; vectorArray[vectorArrayCount].base = base;
vectorArray[vectorArrayCount].handler = handler; vectorArray[vectorArrayCount].handler = handler;
vectorArray[vectorArrayCount].declaredOnly = declaredOnly; vectorArray[vectorArrayCount].declaredOnly = declaredOnly;
vectorArray[vectorArrayCount].handlerName = handlerName ? strdup(handlerName) : NULL;
vectorArray[vectorArrayCount].fileName = element->fileName;
vectorArray[vectorArrayCount].lineNumber = element->lineNumber;
vectorArrayCount++; vectorArrayCount++;
} }
@@ -343,7 +458,8 @@ void populateVectorTable(intermediateElement *intermediateArray, int arraySize)
int handlerToken = nextVectorToken(intermediateArray, arraySize, portToken + 1); int handlerToken = nextVectorToken(intermediateArray, arraySize, portToken + 1);
uint16_t handler = resolveHandler(intermediateArray, handlerToken, "Device"); uint16_t handler = resolveHandler(intermediateArray, handlerToken, "Device");
addVector(NULL, intermediateArray[portToken].byteValue, HARDWARE_VECTOR_BASE, addVector(NULL, intermediateArray[portToken].byteValue, HARDWARE_VECTOR_BASE,
handler, 0, &intermediateArray[i]); handler, 0, &intermediateArray[i],
intermediateArray[handlerToken].token);
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1); i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
continue; continue;
} }
@@ -381,6 +497,12 @@ void populateVectorTable(intermediateElement *intermediateArray, int arraySize)
} }
vectorArray[already].handler = resolveHandler(intermediateArray, handlerToken, token); vectorArray[already].handler = resolveHandler(intermediateArray, handlerToken, token);
vectorArray[already].declaredOnly = 0; vectorArray[already].declaredOnly = 0;
// The declaration said what it is called; this says where it was implemented,
// which is the more useful of the two places to be sent.
free(vectorArray[already].handlerName);
vectorArray[already].handlerName = strdup(intermediateArray[handlerToken].token);
vectorArray[already].fileName = intermediateArray[i].fileName;
vectorArray[already].lineNumber = intermediateArray[i].lineNumber;
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1); i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
continue; continue;
} }
@@ -427,12 +549,13 @@ void populateVectorTable(intermediateElement *intermediateArray, int arraySize)
if (!hasHandler) { if (!hasHandler) {
// Nothing follows it on the line, so this says what the vector is called and // Nothing follows it on the line, so this says what the vector is called and
// what number it has, and leaves implementing it to somebody else. // what number it has, and leaves implementing it to somebody else.
addVector(token, index, SOFTWARE_VECTOR_BASE, 0, 1, &intermediateArray[i]); addVector(token, index, SOFTWARE_VECTOR_BASE, 0, 1, &intermediateArray[i], NULL);
i = handlerToken; i = handlerToken;
continue; continue;
} }
uint16_t handler = resolveHandler(intermediateArray, handlerToken, token); uint16_t handler = resolveHandler(intermediateArray, handlerToken, token);
addVector(token, index, SOFTWARE_VECTOR_BASE, handler, 0, &intermediateArray[i]); addVector(token, index, SOFTWARE_VECTOR_BASE, handler, 0, &intermediateArray[i],
intermediateArray[handlerToken].token);
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1); i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
} }
} }
+23 -3
View File
@@ -15,17 +15,31 @@
// share one namespace: a name may only be defined once across the whole assembly. So this // share one namespace: a name may only be defined once across the whole assembly. So this
// is not the size of one file but the size of a program and its libraries together, and // is not the size of one file but the size of a program and its libraries together, and
// CosmOS with its four libraries went past 256 while still being a small system. // CosmOS with its four libraries went past 256 while still being a small system.
#define MAX_LABELS 1024 //
// AND PAST 1024 the day the shell learned to finish a word somebody had started. Doubled
// rather than nudged: a ceiling reached once is a ceiling that will be reached again, and
// the table is pointers into source that is already in memory - 2048 of them is sixteen
// kilobytes on a host with gigabytes of it.
#define MAX_LABELS 2048
#define MAX_VECTORS 256 #define MAX_VECTORS 256
typedef struct { typedef struct {
char* label; char* label;
uint16_t address; uint16_t address;
int type; int type;
// Where the name was written. Only the symbol file reads these, and it is the source
// of the whole feature: an address tells you a routine exists and a file and line tell
// you where to go and read it.
//
// NEITHER IS OWNED HERE. fileName points at the copy the include list keeps, the same
// storage every intermediateElement points at, and the include list outlives the label
// table - assemblerCleanup frees the labels first. So freeLabelList must not free it.
const char* fileName;
int lineNumber;
} Label; } Label;
// Every label and the address it was given, in address order, so that a tally of // Every label and every vector, with where it lives and where it was written, so that a
// addresses can be turned back into a list of routine names. // tally of addresses can be turned back into a list of routine names.
void writeSymbolFile(const char *path); void writeSymbolFile(const char *path);
// One line of the Vector Segment, once it has been worked out. // One line of the Vector Segment, once it has been worked out.
@@ -35,6 +49,12 @@ typedef struct {
uint16_t base; // Which table: software or hardware. uint16_t base; // Which table: software or hardware.
uint16_t handler; // Where the handler ended up. uint16_t handler; // Where the handler ended up.
int declaredOnly; // Named and numbered, with nobody implementing it here. int declaredOnly; // Named and numbered, with nobody implementing it here.
// For the symbol file, which is the only thing that reads these. A device has no name
// of its own, so the handler's is the only name it can be listed under. fileName is
// borrowed from the include list the way a Label's is; handlerName is owned, like name.
char* handlerName;
const char* fileName;
int lineNumber;
} VectorEntry; } VectorEntry;
void freeLabelList(); void freeLabelList();
+59
View File
@@ -433,6 +433,56 @@ static uint16_t countFree(const Directory *directory, const Superblock *super) {
return (uint16_t)(super->diskBlocks - overhead - used); return (uint16_t)(super->diskBlocks - overhead - used);
} }
// ---- The longest run of free blocks there is ----
//
// The number that says whether a file will fit. Files are laid down contiguously - first fit,
// with the directory itself as the map - so a disk with a thousand blocks free in ten
// scattered pieces refuses a file of two hundred, and the free total gives no hint of it.
//
// A sweep rather than a sort: a candidate walks the disk, each pass looking for the used
// extent nearest at or after it, and anything the candidate is standing inside pushes it to
// the far end and starts the pass again. That is what sbfs.asm does, because that machine has
// no sort - and the two implementations of this format are worth more agreeing than they are
// each being clever separately.
static uint16_t largestRun(const Directory *directory, const Superblock *super) {
const uint32_t firstData = (uint32_t)super->directoryStart + super->directoryBlocks;
uint32_t candidate = firstData;
uint32_t biggest = 0;
for (;;) {
uint32_t next = super->diskBlocks;
int moved = 0;
for (int i = 0; i < directory->entries; i++) {
const uint8_t *entry = entryAt(directory, i);
if (!entryInUse(entry) || entryIsDirectory(entry)) {
continue;
}
const uint32_t start = readWord(entry + SBFS_ENTRY_START);
const uint32_t end = start + entryBlocksUsed(entry);
if (end <= candidate) {
continue;
}
if (start <= candidate) {
candidate = end;
moved = 1;
continue;
}
if (start < next) {
next = start;
}
}
if (moved) {
continue;
}
if (next - candidate > biggest) {
biggest = next - candidate;
}
if (next >= super->diskBlocks) {
return (uint16_t)biggest;
}
candidate = next;
}
}
// ---- Commands ---- // ---- Commands ----
static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks, static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks,
@@ -647,6 +697,15 @@ static int commandList(const char *path, const char *within) {
} }
printf(".\n"); printf(".\n");
// Said only when it is not all of it. On a disk that has only been appended to the two
// are the same number, and printing it twice would be noise on every healthy disk - a
// line that appears only when something is wrong is a line somebody reads.
uint16_t run = largestRun(&directory, &super);
if (run != counted) {
printf("The longest run of free blocks is %u, so nothing bigger than that will"
" fit.\n", run);
}
// A save that stopped between deleting the old entry and naming the new one. The // A save that stopped between deleting the old entry and naming the new one. The
// bytes are all there under the temporary's name and one rename brings them back, // bytes are all there under the temporary's name and one rename brings them back,
// which is the whole of the recovery this format offers - so the thing that matters // which is the whole of the recovery this format offers - so the thing that matters
+30 -1
View File
@@ -69,9 +69,31 @@ static uint16_t readVector(const uint8_t *programMemory, uint16_t base, uint8_t
// Note that a zero entry means "no handler" to everything that dispatches, including the // Note that a zero entry means "no handler" to everything that dispatches, including the
// two entries the CPU treats as start addresses when it reads them at reset. The // two entries the CPU treats as start addresses when it reads them at reset. The
// exemption belongs to that one read, not to the entries themselves. // exemption belongs to that one read, not to the entries themselves.
static uint8_t enterInterrupt(CPURegisters *cpu, uint16_t base, uint8_t index, uint16_t resumeAddress) { static uint8_t dispatch(CPURegisters *cpu, uint16_t base, uint8_t index,
uint16_t resumeAddress, int mayEscalate) {
uint16_t handler = readVector(cpu->Program, base, index); uint16_t handler = readVector(cpu->Program, base, index);
if (handler == 0x0000) { if (handler == 0x0000) {
// ---- Nowhere to go is itself something to report ----
//
// This is the one fault the machine used to have no way of handing over, because
// the thing that would hand it over is the thing that has just found nothing to
// hand it to. So it goes to a vector of its own instead, with the number of the
// empty entry in Q - and a missing software vector and a device nobody is
// listening to are separate entries, because they are separate mistakes.
//
// NOT WHEN ALREADY ESCALATING. If the fault vector for this is itself empty then
// the machine really has run out of places to go, and stopping is the only honest
// answer left.
if (mayEscalate) {
const uint8_t escalation = (base == HARDWARE_VECTOR_BASE)
? VECTOR_NO_DEVICE : VECTOR_NO_HANDLER;
if (!dispatch(cpu, SOFTWARE_VECTOR_BASE, escalation, resumeAddress, 0)) {
// After the frame, so the Q the interrupted program had is safely in it and
// RETI will put it back. What the handler sees is which entry was empty.
cpu->Q = index;
return 0;
}
}
cpu->Fault = FAULT_NO_HANDLER; cpu->Fault = FAULT_NO_HANDLER;
cpu->FaultVector = index; cpu->FaultVector = index;
cpu->Status |= STATUS_FAULT; cpu->Status |= STATUS_FAULT;
@@ -109,6 +131,13 @@ static uint8_t enterInterrupt(CPURegisters *cpu, uint16_t base, uint8_t index, u
return 0; return 0;
} }
// Dispatching the ordinary way: through the vector asked for, and through the fault vector
// for a missing one if that is what it turns out to be.
static uint8_t enterInterrupt(CPURegisters *cpu, uint16_t base, uint8_t index,
uint16_t resumeAddress) {
return dispatch(cpu, base, index, resumeAddress, 1);
}
// A device that refused what it was asked stops the machine where it stands, rather than // A device that refused what it was asked stops the machine where it stands, rather than
// raising a line and letting execution carry on past the mistake. The frame carries the // raising a line and letting execution carry on past the mistake. The frame carries the
// address of the instruction that asked, so a handler can see which one it was, and so a // address of the instruction that asked, so a handler can see which one it was, and so a

Some files were not shown because too many files have changed in this diff Show More