crossed

A program of Phases.

This program is not meant to build. What the tool says is the lesson.

NGA2414 reference to `levelMap`, which is not in memory in phase `intro` while `introStart` is /Users/waldek/Desktop/NGA/tutorial/04-phases/crossed/intro.asm:13

main.ngp

include "atari/800xl.ngp"
include "atari/disk.ngp"

modules { "print.asm" "intro.asm" "level.asm" }

resident { print }

phase intro { needs intro  then level  entry introStart }
phase level { needs level             entry levelStart }
entry intro

container atr
800xl.ngp project
; An Atari 800XL: sixty-four kilobytes and no banking, so there is no unit set
; and no Window. It declares no storage. A program that loads anything during a
; run includes `atari/disk.ngp` beside this, which makes the diskette's sectors
; its storage. Without it, every Section with bytes is written when the program
; loads and stays for as long as a Phase still to come needs it — see
; docs/decisions/0234-without-storage-bytes-stay-where-they-are-live.md.
target {
  cpu "6502"

  ; A diskette it boots, or a `.xex` a DOS loads. A Project naming any other
  ; is refused here rather than at the writer.
  containers atr, xex
}

target {
  region ram    $0000 .. $06FF  ram
  ; $0700 to $087F is the boot record's. It stands in no Region, so the solver
  ; never allocates there and the record's own pins are the author's word.
  region        $0880 .. $CFFF  ram
  region        $D800 .. $FFFF  ram
  region stack  $0100 .. $01FF  reserved
  region io     $D000 .. $D7FF  register
  ; The RAM the OS ROM covers, named so that a Section may say `in underOsLow`
  ; and stand there: only in a Phase without `osrom`, or hidden under it where
  ; nothing needs it — see docs/decisions/0243-a-section-may-name-the-region-it-stands-in.md.
  region underOsLow   $C000 .. $CFFF  ram
  region underOsHigh  $D800 .. $FFFF  ram

  ; A Project that wants more of the hardware by name declares it, as it
  ; declares the memory its DOS holds.
  register PORTB  $D301

  ; The hardware that holds a Section while a program has handed it one, by
  ; `.root NAME` before the taking of its address, until it hands another or
  ; `.release NAME`: what it holds stays where it is for that long. See
  ; docs/decisions/0247-the-hardware-holds-a-section-for-as-long-as-it-is-given-it.md.
  holder dlist                          ; ANTIC's display list: SDLSTL, or DLISTL
  holder dli                            ; the display list interrupt: VDSLST
  holder vbi                            ; the immediate vertical blank: VVBLKI
  holder vbd                            ; the deferred vertical blank: VVBLKD
  holder nmi                            ; $FFFA, where the OS ROM is switched out
  holder irq                            ; $FFFE, where the OS ROM is switched out
  holder chbase                         ; the character set: CHBAS, or CHBASE
  holder pmbase                         ; the player-missile memory: PMBASE
  holder dosini                         ; DOSINI: called once a boot returns, and on every reset
}

; The OS and the character sets, listed as a file of the Project's own would be.
modules {
  "atari/osram.asm"  "atari/osrom.asm"  "atari/mathpack.asm"
  "atari/charsets.asm"
}
resident { osram, osrom, mathpack, charsets }
disk.ngp project
; Storage on a double-density diskette, for an `atari/800xl.ngp` program whose
; Container is the `.atr`: the diskette's sectors, the driver that reads them
; and the decoders the tool ships. A Project includes this beside the machine.
;
; The sectors from the fourth: the boot record has the first three and the load
; image whatever is left after the storage a program uses. A unit is 256
; sectors, which is the one size the `.atr` takes, and three of them reach past
; the 180 KB a double-density diskette holds. It is a ceiling and not a
; reservation: what the image costs is what the program's Payloads came to.
storage { units 3 size 65536 }

; Every Transition calls the driver and the decoders, so they are resident. The
; OS the driver calls into is resident already; a decoder nothing uses is
; dropped.
modules { "atari/disk.asm"  "stream/zx0.asm" }
resident { disk, zx0 }
osram.asm osram
; The memory the Atari OS keeps its variables in: half the zero page and the
; pages from $0200 to $06FF. Sections that hold no bytes, pinned where the OS
; keeps them, so the solver sees memory that is taken rather than a Region it may
; never allocate from. See 0038 for why what occupies memory for a while is a
; Module, and 0229 for why the OS is two of them.
;
; **Resident wherever the OS is used at all**, and not only in the Phases whose
; ROM is in: a Phase without `atari/osrom.asm` runs with the ROM switched out and
; the OS asleep, and the variables have to be as the OS left them when a later
; Phase switches it back in.
;
; Every Section here is `root`: the OS and the hardware reach them with no
; Reference in any Chunk, so nothing else would keep them. None holds bytes, so
; none has a Payload and no Transition loads one. This is the OS as an XL or XE
; ships it; a machine whose OS lives elsewhere declares a Module of its own.

.export RTCLOK, SDMCTL, SDLSTL, SDLSTH, CH

; The OS's half of the zero page. The program's own variables live above it,
; in the half the variant leaves to the solver.
.section zeropage at $0000, root
osZero
        .res $12
RTCLOK  .res 3                          ; $0012: the frame counter, three bytes
        .res $6B
.ends

; The OS's variables and buffers. The shadow registers are among them and
; are Labels here rather than registers of the variant: they are the OS's
; memory, which the OS copies to the hardware on every vertical blank, and
; a `register` Region inside this Section would be an address the Section
; may not cover.
.section absolute at $0200, root
osRam
        .res $2F
SDMCTL  .res 1                          ; $022F: shadows DMACTL
SDLSTL  .res 1                          ; $0230: the display list address, low
SDLSTH  .res 1                          ; $0231: and high
        .res $CA
CH      .res 1                          ; $02FC: the last key pressed, $FF for none
        .res $403
.ends
osrom.asm osrom
; The Atari OS ROM, in the two ranges the hardware registers leave between them,
; and the entry points a program calls in the second. Sections that hold no
; bytes, pinned where the ROM is, so that a Phase that needs this Module has
; the ranges taken and one without it has the RAM underneath — see 0229.
;
; **A ROM the driver switches out.** A variant lists this Module in `resident`,
; so every Phase has the OS unless it says `without osrom`, which the driver's
; `.driver rom osrom` is what allows; a Phase without it has the RAM under the
; ROM, its own vectors at $FFFA to look after, and no call into the OS — a
; `jsr` to an entry point below is a Reference to a Section that Phase does not
; have, and refused as one.
;
; The high range begins at **$E000 and not at $D800**: $D800-$DFFF is the
; floating-point pack, which a device on the parallel bus maps its own ROM
; over once it is selected, so it is `atari/mathpack.asm` — a Module a variant
; may leave out, and one whose absence is a fact about the machine rather than
; an omission. A variant that lists this file lists that one too, unless it
; has something else to say about those addresses.
;
; Every Section is `root`, since the OS and the hardware reach them with no
; Reference in any Chunk. This is the ROM an XL or XE ships.

.export SETVBV, XITVBV

.section absolute at $C000, root
osRomLow
        .res $1000
.ends

.section absolute at $E000, root
osRomHigh
        .res $45C
SETVBV  .res 3                          ; $E45C: A = the stage, X/Y = the routine
        .res 3
XITVBV  .res 3                          ; $E462: the end of a deferred routine
        .res $1B9B
.ends
mathpack.asm mathpack
; The floating-point routines the OS keeps at $D800-$DFFF, as a Section that
; holds no bytes, so that the solver sees memory that is taken.
;
; **A Module of its own and not part of `atari/osrom.asm`**, because this is the
; one range of the OS that something else can take away. A device on the
; parallel bus maps its ROM over the whole of it once it is selected — the
; kernel's own PBI scan says so in as many words, and looks for a device's
; identifying bytes at $D803 and $D80B, which is why the math pack must not
; begin with them. A variant for a machine whose PBI device is selected for
; the life of the program therefore does not list this file, and says what
; stands there instead.
;
; **Switched out with the OS.** On an XL the one bit of PORTB that takes the OS
; ROM out takes this out too, so a driver that switches ROMs declares it as
; `.driver rom mathpack osrom`: a Phase that needs this needs the OS's ranges
; taken as well, and cannot do without `osrom` — see 0229.
;
; Nothing is exported. The pack's entry points are documented by Atari and a
; program that calls one declares it, as it declares any address of the OS
; `atari/osrom.asm` does not name; nothing the tool ships calls into it.
;
; `root`, because the OS reaches it with no Reference in any Chunk of the
; program, and holding no bytes it has no Payload and no Transition loads it.

.section absolute at $D800, root
mathPack
        .res $800
.ends
charsets.asm charsets
; The Atari's two codes for the same letters, as Charsets.
;
; `atascii` is what the character I/O takes: the machine's own code, with the
; graphics characters where an ASCII machine keeps its control codes, and the
; letters where ASCII has them — which is why an unprefixed literal of plain
; letters has always worked and why nothing else has. `\n` is `$9B`, the end of
; line, and not `$0A`.
;
; `screen` is what the display reads out of screen memory, which is the same
; letters at other numbers. A program that writes where the display looks
; rather than through the OS wants this one.
;
; The graphics characters are written here as the Unicode the box-drawing and
; block characters have, so a picture drawn in the source is the picture the
; machine draws. A letter in inverse video is one of those too — Unicode squares
; them off in a negative, and `atascii"PRESS 🆂"` is seven bytes with the last
; one inverse. A whole line of inverse text is better asked of a Charset
; derived from this one, `.charset bright : atascii ^ $80`, which is what a
; derivation is for. Two of them have no exact Unicode: `$02` and `$0D` are a
; quarter of a cell where the nearest character is an eighth, and are written
; as `▕` and `▔`. Everything else, the inverse entries included, was read off
; the machine's own font and matches it.
;
; This Module emits nothing: it is two names and two tables.

.export atascii, screen

; tag atascii
.charset atascii
  "♥├▕┘┤┐╱╲◢▗◣▝▘▔▂▖♣┌─┼●▄▎┬┴▌└"                                       = $00
  "↑↓←→"                                                              = $1C
  " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" = $20
  "♦"                                                                 = $60
  "abcdefghijklmnopqrstuvwxyz"                                        = $61
  "♠"                                                                 = $7B
  "│"                                                                 = $7C
  "\n"                                                                = $9B

  ; Inverse video is the same glyph with the bits the other way round, which
  ; for the blocks and the triangles is another character Unicode draws. Every
  ; one of these was read off the machine's own font and is exact; the rest of
  ; the inverse half has no glyph to be written as, and is reached by adding
  ; $80 or by a Charset derived from this one.
  "▊" = $82        ; ▕
  "◤" = $88        ; ◢
  "▛" = $89        ; ▗
  "◥" = $8A        ; ◣
  "▙" = $8B        ; ▝
  "▟" = $8C        ; ▘
  "▆" = $8D        ; ▔
  "▜" = $8F        ; ▖
  "▀" = $95        ; ▄
  "▐" = $99        ; ▌
  "█" = $A0        ; the space

  ; And the letters, which Unicode squares off in a negative.
  "🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉" = $C1
.endch
; end atascii

.charset screen
  " !\"#$%&'()*+,-./0123456789:;<=>?"                                 = $00
  "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"                                 = $20
  "♥├▕┘┤┐╱╲◢▗◣▝▘▔▂▖♣┌─┼●▄▎┬┴▌└"                                       = $40
  "♦"                                                                 = $60
  "abcdefghijklmnopqrstuvwxyz"                                        = $61
  "♠"                                                                 = $7B
  "│"                                                                 = $7C

  ; The same inverses, at the display's numbers.
  "▊" = $C2
  "◤" = $C8
  "▛" = $C9
  "◥" = $CA
  "▙" = $CB
  "▟" = $CC
  "▆" = $CD
  "▜" = $CF
  "▀" = $D5
  "▐" = $D9
  "█" = $80
  "🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉" = $A1
.endch
disk.asm disk
; The storage driver for a diskette: a unit of storage is 256 sectors of 256
; bytes, and unit zero begins at sector 4, the first after the boot record.
; That size is what makes the three bytes the model addresses storage with —
; a unit and a two-byte offset — the address the hardware takes, so this driver
; does no arithmetic: the unit is the sector number's high byte, the offset's
; high byte is its low one, and the offset's low byte is the position in the
; sector. Reading a sector is the OS's own disk handler, called through
; `DSKINV` with the request in the device control block, which is why this
; driver needs no register of its own and knows nothing about a drive.
;
; The medium maps nothing, so this driver names no Window: `stream`, `show`
; and `showAt` are not declared.
;
; Resident, since every Transition calls it; the variant lists it so.

DSKINV = $E453                  ; the OS's disk handler: one sector per call
DUNIT  = $0301                  ; the drive, 1 for D1:
DCOMND = $0302                  ; 'R' reads
DBUFLO = $0304
DBUFHI = $0305
DAUX1  = $030A                  ; the sector, two bytes
DAUX2  = $030B

diskFirst = 4                   ; the sector unit zero begins at

.driver open   diskOpen
.driver read   diskRead
.driver unit   diskUnit
.driver offset diskAt

.transform copy diskCopy

.macro diskOpen
        jsr diskOpenStream
.endm

.macro diskRead
        jsr diskReadByte
.endm

; The stream: where in the sector the next byte is, which sector the buffer
; holds, and whether the position has run off its end. A plain Section rather
; than Temporaries, because the value has to survive between one call and the
; next while nothing here is running. The first three bytes are also the cells
; the routine writes a position into before it opens the stream, and they are
; ordered so that the position **is** the stream: `diskAt` the offset — the
; place in the sector, then the sector's low byte — and `diskUnit` the unit,
; which is the sector's high byte.
.section zeropage
diskAt          .res 2
diskUnit        .res 1
diskNeed        .res 1
.ends

; The sector the stream reads through. A Section of reservations alone, so it
; is in no Container and no load writes it. `within 256` keeps it off a page
; boundary: every byte of it is read as `diskBuffer,y`, and an index that
; carries into the next page costs a cycle.
.section absolute within 256
diskBuffer
        .res 256
.ends

; The stream to the position in diskAt and diskUnit. Nothing carries here: the
; three bytes are one position that the sector number takes the top two of, so
; the one thing to do is move the sector past the boot record.
.proc diskOpenStream
        clc
        lda diskAt+1
        adc #<diskFirst
        sta diskAt+1
        lda diskUnit
        adc #>diskFirst
        sta diskUnit
        jmp diskFill
.endp

; A = the next byte of the stream. X and Y are not preserved.
.proc diskReadByte
        lda diskNeed
        beq @have
        inc diskAt+1
        bne @next
        inc diskUnit
@next
        jsr diskFill
@have
        ldy diskAt
        inc diskAt
        bne @within
        sty diskNeed            ; Y is $FF here, and any non-zero will do
@within
        lda diskBuffer,y
        rts
.endp

; The sector in diskAt+1 and diskUnit into the buffer, asked for again until
; the handler says it has it: a Transition that cannot read cannot go on, and
; the drive answered when the OS read the boot record off this diskette.
.proc diskFill
        lda #0
        sta diskNeed
        lda diskAt+1
        sta DAUX1
        lda diskUnit
        sta DAUX2
@again
        lda #1
        sta DUNIT
        lda #$52
        sta DCOMND
        lda #<diskBuffer
        sta DBUFLO
        lda #>diskBuffer
        sta DBUFHI
        jsr DSKINV
        bmi @again
        rts
.endp

; The decoder of `copy`: X/Y = the destination, the stream at the stored size
; and then the bytes. A byte at a time through the stream, because what this
; costs is the sector reads underneath it — a run copied out of the buffer
; would save a few hundred cycles against the tens of thousands a sector takes
; to arrive.
diskDst  .ztemp 2
diskLeft .ztemp 2

.proc diskCopy
        stx diskDst
        sty diskDst+1
        jsr diskReadByte
        sta diskLeft
        jsr diskReadByte
        sta diskLeft+1
@byte
        lda diskLeft
        ora diskLeft+1
        beq @done
        jsr diskReadByte
        ldy #0
        sta (diskDst),y
        inc diskDst
        bne @counted
        inc diskDst+1
@counted
        lda diskLeft
        bne @low
        dec diskLeft+1
@low
        dec diskLeft
        jmp @byte
@done
        rts
.endp
zx0.asm zx0
; The decoder of `zx0` over any driver's stream: Einar Saukas's ZX0, version
; 2's standard forward stream, decoded as the reference dzx0.c does, driven
; by the stream's own end marker. Written here from the format, not ported:
; nothing of the reference's text is in it, and it is licensed as everything
; under lib/ is, see lib/LICENSE. Entered with X/Y = the destination and the
; stream at the stored size, which it reads past. A match is copied a byte
; at a time, forward, from what was written, which is what makes an offset
; shorter than its length — the run — come out right. Resident, and outside
; the driver's window.
;
;   zx0Bits      the bit buffer, a sentinel one above the bits still unread
;   zx0Offset    the last offset
;   zx0Length    the length in hand, or an offset's MSB while one is read
;   zx0Invert    one while an offset's MSB is read, whose data bits the
;                stream carries complemented; zero otherwise

.transform zx0 zx0Decode

zx0Dst    .ztemp 2
zx0Src    .ztemp 2
zx0Bits   .ztemp 1
zx0Offset .ztemp 2
zx0Length .ztemp 2
zx0Invert .ztemp 1

; The next bit of the stream, in A as zero or one and in the Z flag. The
; buffer holds a sentinel above the unread bits, so shifting it to nothing is
; the signal to fetch the next byte and put the sentinel back below it.
.proc zx0Bit
        asl zx0Bits
        bne @have
        nga.read
        sec                     ; the sentinel the asl shifted out, which `read` may not keep
        rol
        sta zx0Bits
@have
        lda #0
        rol
        rts
.endp

; An interlaced Elias gamma value into zx0Length: a control bit says whether
; a data bit follows, and the value begins at one. Three Procs chained by
; `then` because the value after an offset is entered with its first control
; bit already read, at zx0EliasData: the first falls through into the
; second, and the second branches into the third, which the chain is what
; allows.
.proc zx0Elias
        lda #1
        sta zx0Length
        lda #0
        sta zx0Length+1
.endp then zx0EliasMore
.proc zx0EliasMore
        jsr zx0Bit
        beq zx0EliasData        ; zero: a data bit follows
        rts
.endp then zx0EliasData
.proc zx0EliasData
        jsr zx0Bit
        eor zx0Invert
        lsr
        rol zx0Length
        rol zx0Length+1
        jmp zx0EliasMore
.endp

; One byte written: the destination moves on and the length in hand comes
; down, leaving Z set when it reaches zero.
.proc zx0Step
        inc zx0Dst
        bne @moved
        inc zx0Dst+1
@moved
        lda zx0Length
        bne @low
        dec zx0Length+1
@low
        dec zx0Length
        lda zx0Length
        ora zx0Length+1
        rts
.endp

; The length in hand copied from zx0Dst less the last offset to zx0Dst.
.proc zx0Copy
        sec
        lda zx0Dst
        sbc zx0Offset
        sta zx0Src
        lda zx0Dst+1
        sbc zx0Offset+1
        sta zx0Src+1
@byte
        ldy #0
        lda (zx0Src),y
        sta (zx0Dst),y
        inc zx0Src
        bne @from
        inc zx0Src+1
@from
        jsr zx0Step
        bne @byte
        rts
.endp

.proc zx0Decode
        stx zx0Dst
        sty zx0Dst+1
        nga.read             ; the stored size, which the end marker makes unnecessary
        nga.read
        lda #$80
        sta zx0Bits             ; an empty buffer: the sentinel alone
        lda #1
        sta zx0Offset
        lda #0
        sta zx0Offset+1         ; the last offset begins at one
        sta zx0Invert
@literals
        jsr zx0Elias
@literal
        nga.read
        ldy #0
        sta (zx0Dst),y
        jsr zx0Step
        bne @literal
        jsr zx0Bit
        bne @offset
        jsr zx0Elias            ; a match at the last offset
        jsr zx0Copy
        jsr zx0Bit
        beq @literals
@offset
        lda #1
        sta zx0Invert
        jsr zx0Elias            ; the new offset's MSB, complemented in the stream
        lda #0
        sta zx0Invert
        lda zx0Length
        beq @done               ; 256 is the end marker, and the one value with a low byte of zero
        lsr                     ; offset = MSB * 128 - LSB / 2
        sta zx0Offset+1
        lda #0
        ror
        sta zx0Offset
        nga.read
        lsr                     ; the LSB's low bit is the length's first control bit
        php
        eor #$FF
        sec
        adc zx0Offset
        sta zx0Offset
        bcs @subtracted
        dec zx0Offset+1
@subtracted
        plp
        lda #1
        sta zx0Length
        lda #0
        sta zx0Length+1
        bcs @counted            ; a control bit of one: the value is one
        jsr zx0EliasData
@counted
        inc zx0Length           ; a match at a new offset is one longer than written
        bne @copy
        inc zx0Length+1
@copy
        jsr zx0Copy
        jsr zx0Bit
        bne @offset
        jmp @literals
@done
        rts
.endp

print.asm

; The one thing both Phases need: a line on the screen, through the character
; I/O the OS opens on channel 0 before a program starts. `PUTREC` writes a
; record, so it ends the line itself.

CIOV   = $E456
ICCOM  = $0342
ICBAL  = $0344
ICBLL  = $0348
PUTREC = 9

; Both Phases call it, so both have to see the name.
.export printLine

; Where the line stands, and how long it is. Declaring where the two arguments
; are is what lets a Module of C call this one as a function.
.proc printLine
        .declare arg xy
        .declare arg a

        stx ICBAL
        sty ICBAL+1
        sta ICBLL
        lda #0
        sta ICBLL+1
        lda #PUTREC
        sta ICCOM
        ldx #0
        jsr CIOV
        rts
.endp

intro.asm

.section
introText
        .byte "INTRO"
introTextEnd
.ends

; tag cross
.proc introStart
        ldx #<introText
        ldy #>introText
        lda #introTextEnd - introText
        jsr printLine
        lda levelMap            ; the Phase this belongs to has not loaded it
        .transition level
.endp
; end cross

level.asm

.export levelMap

; What the Phase is for: a map of ten rows, which is what waits in storage
; while the program is in `intro`. Thirty-seven characters is what a row of
; the screen holds once the OS's left margin is taken off.

levelWidth = 37

.section
levelMap
        .byte "#####################################"
        .byte "#...................................#"
        .byte "#...................................#"
        .byte "#......########.....................#"
        .byte "#......#......#.....................#"
        .byte "#......#......#.....................#"
        .byte "#......########.....................#"
        .byte "#...................................#"
        .byte "#...................................#"
        .byte "#####################################"
levelMapEnd
.ends

; A row at a time. `at` and `left` are Temporaries of this Proc, as chapter
; two's were, and `printLine` keeps neither of them.
.proc levelStart
at      .ztemp 2
left    .ztemp 1

        lda #<levelMap
        sta at
        lda #>levelMap
        sta at+1
        lda #( levelMapEnd - levelMap ) / levelWidth
        sta left
@row
        ldx at
        ldy at+1
        lda #levelWidth
        jsr printLine
        clc
        lda at
        adc #levelWidth
        sta at
        bcc @counted
        inc at+1
@counted
        dec left
        bne @row
@stop   jmp @stop
.endp
transition.asm nga.transition
ngaFramesHeld = 0
ngaUnitBytes = 1
ngaEntrySize = 4
ngaBlockSize = 6
ngaFrameHeader = 3 + ngaSelects
; The Transition routine. Called by `jsr` from a `.transition`, with the
; statement's list right behind the call: the Phase to enter, and per Phase
; the code may be in, the unit and offset where the edge's Frame waits.
; nga.open and nga.read are the driver's roles, macros of the driver's
; Module expanded here, and nga.unit and nga.offset the cells its stream
; stands in, which open reads the position from; and ngaCurrentPhase is the
; Cell the tool generates. Everything else the edge has to say waits in the
; Frame, in storage. See docs/spec/transition.md.
;
; Its zero page is Temporaries: nothing of it is needed once the routine has
; jumped to the entered Phase's entry, and a `.transition` is a jump into the
; routine, so whatever the statement's Section had on the zero page is dead by
; then too. Two Temporaries never live at once share an address — see
; docs/decisions/0034-trace.md.

.export ngaTransition

ngaPtr .ztemp 2
ngaEntry .ztemp 2
ngaDst .ztemp 2
ngaValue .ztemp 2
ngaOffset .ztemp 2
ngaFrameAt .ztemp 2 + ngaUnitBytes ; where the Frame's stream stands: the offset, then the unit
ngaWanted .ztemp 1
ngaCount .ztemp 1
ngaUnit .ztemp ngaUnitBytes

.proc ngaTransition
        pla
        sta ngaPtr
        pla
        sta ngaPtr+1            ; the return address: one below the statement's list
        ldy #1
        lda (ngaPtr),y
        sta ngaWanted           ; the Phase to enter
        iny
        lda (ngaPtr),y
        sta ngaCount            ; entries that follow
        lda ngaPtr
        clc
        adc #3
        sta ngaPtr
        bcc @scan
        inc ngaPtr+1
@scan
        lda ngaCount
        bne @check
        brk                     ; no entry for the current Phase: unreachable while the static rule holds
@check
        ldy #0
        lda (ngaPtr),y
        cmp ngaCurrentPhase
        beq @found
        lda ngaPtr              ; the next entry
        clc
        adc #ngaEntrySize
        sta ngaPtr
        bcc @skipped
        inc ngaPtr+1
@skipped
        dec ngaCount
        jmp @scan
@found
        iny
        lda (ngaPtr),y
        sta ngaFrameAt+2        ; the unit the Frame waits in
.if ngaUnitBytes > 1
        iny
        lda (ngaPtr),y
        sta ngaFrameAt+3
.endif
        iny
        lda (ngaPtr),y
        sta ngaFrameAt
        iny
        lda (ngaPtr),y
        sta ngaFrameAt+1        ; and where in it
.endp then ngaEnter

; The rest of it, and the way in for a Container that has no current Phase to
; look one up by: ngaWanted holds the Phase to enter and ngaFrameAt where
; its Frame waits. The cold start comes here — see
; docs/decisions/0216-a-car-names-its-format-and-the-cold-start-is-an-edge.md.
.export ngaEnter
.proc ngaEnter
        jsr ngaFrameOpen
.if ngaFramesHeld
        nga.read
        sta ngaPtr
        nga.read
        sta ngaPtr+1            ; where the descriptors are to be held, or zero
.endif
        nga.read
        sta ngaEntry
        nga.read
        sta ngaEntry+1
        nga.read
        sta ngaCount            ; blocks that follow
.if ngaSelects
        nga.read                ; the ROMs the entered Phase needs, switched
        nga.select              ; before a byte of it lands
.endif
        lda #ngaFrameHeader
        jsr ngaFrameSkip
.if ngaFramesHeld
        lda ngaPtr
        ora ngaPtr+1
        beq @block              ; nowhere to hold them: one open a block

; A block's bytes, read in one pass from where the stream already stands. The
; write pointer, the counter and the index are Temporaries of the routine's own
; that nothing is keeping here: ngaValue belongs to the Cell writes, which come
; after the blocks, and ngaUnit and ngaDst to a block being loaded, which has
; not begun. Only ngaPtr is really spent, being live across the whole of the
; block loop and so no longer able to share with what a driver declares — which
; is the two bytes of zero page this way of fetching costs.
        lda ngaPtr
        sta ngaValue
        lda ngaPtr+1
        sta ngaValue+1
        lda ngaCount
        sta ngaUnit             ; blocks still to hold
@hold
        lda ngaUnit
        beq @heldBlock
        lda #0
        sta ngaDst              ; the index, which a read may take out of Y
@six
        nga.read
        ldy ngaDst
        sta (ngaValue),y
        iny
        sty ngaDst
        cpy #ngaBlockSize
        bne @six
        lda #ngaBlockSize
        jsr ngaFrameSkip        ; so the Frame stands past them at the end
        lda ngaValue
        clc
        adc #ngaBlockSize
        sta ngaValue
        bcc @counted
        inc ngaValue+1
@counted
        dec ngaUnit
        jmp @hold

; Then a block at a time out of memory, and the Frame opened once more at the
; end rather than once for every one of them.
@heldBlock
        lda ngaCount
        beq @cells
        ldy #0
        lda (ngaPtr),y
        sta nga.unit            ; straight into the driver's cells: the
        iny                     ; descriptors are in memory, and nothing
.if ngaUnitBytes > 1            ; reads the stream before it is opened
        lda (ngaPtr),y
        sta nga.unit+1
        iny
.endif
        lda (ngaPtr),y
        sta nga.offset
        iny
        lda (ngaPtr),y
        sta nga.offset+1
        iny
        lda (ngaPtr),y
        sta ngaDst
        iny
        lda (ngaPtr),y
        sta ngaDst+1
        iny
        lda (ngaPtr),y
        pha                     ; the decoder's number
        lda ngaPtr
        clc
        adc #ngaBlockSize
        sta ngaPtr
        bcc @heldLoad
        inc ngaPtr+1
@heldLoad
        nga.open
        pla
        ldx ngaDst
        ldy ngaDst+1
        jsr ngaTransform
        dec ngaCount
        jmp @heldBlock
.endif

@block
        lda ngaCount
        beq @cells
        jsr ngaFrameOpen        ; back to the Frame: a block's stream replaced it
        nga.read
        sta ngaUnit
.if ngaUnitBytes > 1
        nga.read
        sta ngaUnit+1
.endif
        nga.read
        sta ngaOffset
        nga.read
        sta ngaOffset+1
        nga.read
        sta ngaDst
        nga.read
        sta ngaDst+1
        nga.read
        pha                     ; the decoder's number
        lda #ngaBlockSize
        jsr ngaFrameSkip
        lda ngaUnit             ; only now: the driver reads the Frame through
        sta nga.unit            ; these very cells, and writing them while it
.if ngaUnitBytes > 1            ; did would move the stream
        lda ngaUnit+1
        sta nga.unit+1
.endif
        lda ngaOffset
        sta nga.offset
        lda ngaOffset+1
        sta nga.offset+1
        nga.open                ; the block's stream: its stored size, then its bytes
        pla
        ldx ngaDst
        ldy ngaDst+1
        jsr ngaTransform
        dec ngaCount
        jmp @block
@cells
        jsr ngaFrameOpen
        nga.read
        sta ngaCount            ; Cell writes that follow, read in one stream
@cell
        lda ngaCount
        beq @enter
        nga.read
        sta ngaDst
        nga.read
        sta ngaDst+1
        nga.read
        sta ngaValue
        nga.read
        sta ngaValue+1
        ldy #0
        lda ngaValue
        sta (ngaDst),y          ; a Cell stands outside the driver's window
        iny
        lda ngaValue+1
        sta (ngaDst),y
        dec ngaCount
        jmp @cell
@enter
        lda ngaWanted
        sta ngaCurrentPhase     ; the Phase the program is in from here on
        jmp (ngaEntry)
.endp

; The Frame's stream, from where the routine last left it.
.proc ngaFrameOpen
        lda ngaFrameAt
        sta nga.offset
        lda ngaFrameAt+1
        sta nga.offset+1
        lda ngaFrameAt+2
        sta nga.unit
.if ngaUnitBytes > 1
        lda ngaFrameAt+3
        sta nga.unit+1
.endif
        nga.open
        rts
.endp

; A bytes on in the Frame, which is where its stream is opened next. The
; offset carries into the unit, so that a Frame running past the last offset
; a unit's number can reach goes on into the next.
.proc ngaFrameSkip
        clc
        adc ngaFrameAt
        sta ngaFrameAt
        bcc @done
        inc ngaFrameAt+1
        bne @done
        inc ngaFrameAt+2
.if ngaUnitBytes > 1
        bne @done
        inc ngaFrameAt+3
.endif
@done
        rts
.endp
cell nga.cell
ngaCurrentPhase
boot.asm nga.boot
; The boot record of an .atr: the first three sectors, which the OS reads to
; $0700 and calls at $0706. Those three hold 128 bytes, because the OS boots
; with DSCTLN at 128; every sector after them holds 256, so the first thing the
; loader does is say so. What it loads is the load image, the stream of segments
; a `.xex` is made of with $FFFF closing it, waiting in the sectors after
; storage. See docs/spec/atr.md.

DSKINV = $E453
DUNIT  = $0301
DCOMND = $0302
DBUFLO = $0304
DBUFHI = $0305
DAUX1  = $030A
DAUX2  = $030B
DSCTLN = $02D5                  ; what a sector holds, which DSKINV transfers
RUNAD  = $02E0
DOSVEC = $000A

bootBuf = $0400                 ; the cassette buffer and the spare page after
                                ; it, both free once the record is in: 256
                                ; bytes, which is what a sector holds here

; The zero page the OS leaves once it has booted: RAMLO and CASINI were the
; boot's own scratch and TSTDAT is spare. TRAMSZ, $06, is not among them —
; coldstart reads it after the boot has returned.
bootDst    = $0000              ; where the segment being read lands
bootLeft   = $0002              ; bytes of it still to come
bootSector = $0004              ; the sector the buffer holds
bootPos    = $0007              ; where in the buffer the stream stands, a byte
                                ; that wraps because a sector holds 256

.section absolute at $0700, root
ngaBootRecord
        .byte 0                 ; the OS ignores this byte
        .byte 3                 ; sectors it reads, from the first
        .word $0700             ; where it puts them
        .root dosini            ; the OS calls what stands here
        .word ngaBootIdle       ; DOSINI, called once the load returns and on reset
.ends

; The first sector of the load image, which the tool writes here once it knows
; how much storage the program came to use, and one byte of the loader's own.
; At the end of the record, out of the loader's way; the zero page the OS
; leaves is spoken for, and $0006 is TRAMSZ, which the cold start reads after
; the boot has returned.
.export ngaBootImage
.section absolute at $087D, root
ngaBootImage
        .word 0
bootNeed
        .res 1                  ; set once the position has wrapped: the next
.ends                           ; byte stands in the sector after

; The OS jsr's here once the three sectors stand at $0700, and a carry set on
; return is a boot it tries again.
.proc ngaBootLoad, absolute at $0706, root
        lda #0                  ; every sector from here on holds 256 bytes
        sta DSCTLN
        lda #1
        sta DSCTLN+1
        lda ngaBootImage
        sta bootSector
        lda ngaBootImage+1
        sta bootSector+1
        jsr fill
@segment
        jsr byte
        sta bootDst
        jsr byte
        sta bootDst+1
        and bootDst             ; $FFFF closes the stream, and no segment starts there
        cmp #$FF
        beq @run
        jsr byte                ; the end address, inclusive
        sta bootLeft
        jsr byte
        sta bootLeft+1
        sec                     ; the count is end - start + 1
        lda bootLeft
        sbc bootDst
        sta bootLeft
        lda bootLeft+1
        sbc bootDst+1
        sta bootLeft+1
        inc bootLeft
        bne @bytes
        inc bootLeft+1
@bytes
        lda bootLeft
        ora bootLeft+1
        beq @segment
        jsr byte
        ldy #0
        sta (bootDst),y
        inc bootDst
        bne @counted
        inc bootDst+1
@counted
        lda bootLeft
        bne @low
        dec bootLeft+1
@low
        dec bootLeft
        jmp @bytes
@run
        lda RUNAD               ; where the program starts, which the OS jumps to
        sta DOSVEC
        lda RUNAD+1
        sta DOSVEC+1
        clc                     ; a boot the OS keeps
        rts

; A = the next byte of the image, which runs from one sector into the next.
; The position wraps to zero at the sector's end, and bootNeed says the buffer
; no longer holds what stands there.
byte
        lda bootNeed
        beq @have
        inc bootSector
        bne @next
        inc bootSector+1
@next
        jsr fill
@have
        ldy bootPos
        inc bootPos
        bne @within
        sty bootNeed            ; Y is $FF here, and any non-zero will do
@within
        lda bootBuf,y
        rts

; The sector in bootSector into the buffer, tried again until it is read: the
; OS read this record off this diskette, so the drive is there.
fill
        lda #0
        sta bootPos
        sta bootNeed
        lda bootSector
        sta DAUX1
        lda bootSector+1
        sta DAUX2
@again
        lda #1
        sta DUNIT
        lda #$52
        sta DCOMND
        lda #<bootBuf
        sta DBUFLO
        lda #>bootBuf
        sta DBUFHI
        jsr DSKINV
        bmi @again
        rts
.endp

; DOSINI: the OS calls it once the load has returned and on every reset, so it
; is in memory by then and the solver places it as it places any Proc.
.proc ngaBootIdle
        rts
.endp
transforms.asm nga.transforms
; The dispatcher over the decoders the program declared, in the order the
; tool numbered them. See docs/spec/transition.md.
.export ngaSelects
ngaSelects = 0
.export ngaTransform
.proc ngaTransform
        cmp #0
        bne @not0
        jmp diskCopy
@not0
        brk
.endp