two-codes

A program of Character sets.

main.ngp

include "atari/130xe.ngp"
modules { "poke.asm" }
container xex
130xe.ngp project
; An Atari 130XE: four extended Banks as a unit set, the Window PORTB brings
; one of them into, the Window over the OS ROM, every Region of the address
; space, and the Modules a program on this machine needs. What PORTB takes to
; show a state is the driver's and not here, and the OS is a Module rather than
; a Region because it occupies memory for a while rather than being a truth
; about addresses.
target {
  cpu "6502"

  ; DOS loads a `.xex`, and a Project naming any other Container is refused
  ; here rather than at the writer.
  containers xex

  units  extension 4
  window ext  $4000 .. $7FFF  views main, extension  base main
  window os   $C000 .. $CFFF, $D800 .. $FFFF  views rom, ram  base rom
}
storage { units extension }

target {
  region ram    $0000 .. $CFFF  ram
  region        $D800 .. $FFFF  ram
  region stack  $0100 .. $01FF  reserved
  region io     $D000 .. $D7FF  register

  ; This machine takes a `.xex` and nothing else, and a `.xex` is loaded by a
  ; DOS, which stays where a DOS of the 2.x family stays. A Project whose DOS
  ; takes more says so with a `reserved` Region of its own, which narrows this
  ; one further.
  region dos    $0700 .. $1FFF  reserved

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

; The OS, the driver for this storage and the decoders the tool ships, listed
; as a file of the Project's own would be. Every Transition calls the driver
; and the decoders, so they are resident; a decoder nothing uses is dropped.
modules { "atari/os.asm"  "atari/charsets.asm"  "atari/portb.asm"  "stream/zx0.asm" }
resident { os, charsets, portb, zx0 }
os.asm os
; The Atari OS as a Module: what it occupies while it is in memory, and the
; names a program reaches it by. Sections that hold no bytes, pinned where
; the OS lives, so the solver sees memory that is taken rather than a Region
; it may never allocate from — which is what lets a Phase that switches the
; ROM out have those addresses back, once there is a way to say so. See
; 0038 for why what occupies memory for a while is a Module, and 0051 for
; what this first cut settles and what it leaves.
;
; 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; the variant is what chooses.

.export RTCLOK, SDMCTL, SDLSTL, SDLSTH, CH, SETVBV, XITVBV

; 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

; The ROM, in the two ranges the hardware registers leave between them, and
; the entry points a program calls in the second. The RAM underneath is what
; a Phase without the OS would be given.
.section absolute at $C000, root
osRomLow
        .res $1000
.ends

.section absolute at $D800, root
osRomHigh
        .res $C5C
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
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
portb.asm portb
; The storage driver for extended memory behind PORTB: the units are the
; Banks of the variant's `extension`, which a value written to PORTB brings
; into the $4000-$7FFF window — `ext` in the variant — and the stream is a
; pointer into that window that walks on to the next unit at its end. Each
; role is a macro declared with `.driver`, which the routine, the loader's
; glue, the decoders and the tool's own Procs expand where they use it as
; `nga.open`, `nga.read`, `nga.show` and `nga.showAt`. What the hardware
; calls each state of a Window is this Module's alone: `portbValues` holds
; a PORTB value per state of `ext` — base RAM first, then one per Bank, as
; many as the variant's `extension` counts — and the OS Window is bit 0.
; `PORTB` is the variant's register.
;
; Resident, and outside the window — the variant lists this Module in
; `resident`, and the tool holds it outside the window the stream reads
; through.

portbWindow    = $4000
portbWindowEnd = portbWindow + $4000    ; one past the window: the high byte of the first address outside it
portbUnitPages = $40                    ; a unit, in pages: what an offset carries by
portbBase      = $FF                    ; base RAM in, the OS ROM in, BASIC and the self-test out

.driver open    portbOpen
.driver read    portbRead
.driver stream  ext
.driver show    ext portbShow
.driver showAt  ext portbShowAt
.driver show    os  portbShowOs
.driver showAt  os  portbShowOsAt

.transform copy portbCopy

.macro portbOpen
        jsr portbOpenStream
.endm

.macro portbRead
        jsr portbReadByte
.endm

; The state to show, as the tool numbers ext's states: 0 is base RAM, and
; Bank n is n + 1.
.macro portbShow state
        ldx #state
        jsr portbSelect
.endm

; X = the state to show.
.macro portbShowAt
        jsr portbSelect
.endm

; The OS Window is bit 0 of PORTB: set for the ROM, state 0, and clear for
; the RAM beneath it, state 1. The bank bits are left as they are.
.macro portbShowOs state
        lda PORTB
        and #$FE
        ora #1 - state
        sta PORTB
.endm

; X = the state to show, 0 or 1.
.macro portbShowOsAt
        lda PORTB
        ora #1
        cpx #0
        beq @store
        and #$FE
@store
        sta PORTB
.endm

; PORTB per state of ext: base RAM, then one value per Bank. The list runs
; to the 130XE's four and the variant's count trims it, so that a variant
; with fewer Banks takes a prefix; one with more lists its values here.
; Every value has bit 0 set — the OS ROM in — which is what lets
; portbSelect keep the OS Window's bit with one `and` and no scratch byte.
.macro portbBankValues n, values...
.if n > 0
.match values
.case first, rest...
        .byte first
        portbBankValues n - 1, rest...
.endmatch
.endif
.endm

; The table portbSelect indexes, in a Section of its own: what has an
; address stands in one, and a Proc holds code alone.
.section
portbValues
        .byte portbBase
        portbBankValues extension, $E3, $E7, $EB, $EF
.ends

; The stream: where the next byte is, and which unit is in. A plain zero-page
; Section rather than Temporaries, because the value has to survive between
; one call and the next while nothing here is running.
.section zeropage
portbPtr        .res 2
portbUnit       .res 1
.ends

; X = the state of ext to show: its PORTB value, with the OS bit left as it
; is, since the OS Window is the same byte. Writes no memory, so that a
; loader running it leaves nothing behind but the register.
.proc portbSelect
        lda PORTB
        ora #$FE
        and portbValues,x
        sta PORTB
        rts
.endp

; A = the unit, X/Y = the offset in it: the stream stands there. An offset
; past the unit's end carries into the units after, since the routine
; counts on in a Frame without knowing where a unit ends: a unit is $4000
; bytes, so the offset's top two bits are units.
.proc portbOpenStream
        sta portbUnit
        stx portbPtr
        tya
@carry
        cmp #portbUnitPages
        bcc @within
        sbc #portbUnitPages
        inc portbUnit
        jmp @carry
@within
        clc
        adc #>portbWindow
        sta portbPtr+1
        ldx portbUnit
        inx                             ; Bank n is state n + 1
        jsr portbSelect
        rts
.endp

; A = the next byte of the stream. X and Y are not preserved.
.proc portbReadByte
        ldy #0
        lda (portbPtr),y
        inc portbPtr
        bne @done
        inc portbPtr+1
        ldy portbPtr+1
        cpy #>portbWindowEnd
        bne @done
        pha
        jsr portbNextUnit
        pla
@done
        rts
.endp

; The window's end: the next unit in, and the pointer back at its start.
.proc portbNextUnit
        inc portbUnit
        ldx portbUnit
        inx
        jsr portbSelect
        lda #0
        sta portbPtr
        lda #>portbWindow
        sta portbPtr+1
        rts
.endp

;  The decoder of `copy`: X/Y = the destination, the stream at the stored
; size and then the bytes. Reads the window through the pointer rather than
; through portbReadByte, which is what a driver's own decoder is for: a run at a
; time, where a run ends at the source's page end or at the size, so that
; the inner loop is `(zp),y` down to zero — the window's end is a page end,
; so a unit is never crossed inside a run.
portbDst  .ztemp 2
portbSize .ztemp 2
portbRun  .ztemp 1                      ; bytes in the run, zero for 256

.proc portbCopy
        stx portbDst
        sty portbDst+1
        jsr portbReadByte
        sta portbSize
        jsr portbReadByte
        sta portbSize+1
@run
        lda portbSize
        ora portbSize+1
        beq @done
        lda portbPtr                    ; to the end of the source's page
        eor #$FF
        clc
        adc #1
        sta portbRun
        lda portbSize+1
        bne @copy                       ; at least a page left: the run stands
        lda portbRun
        beq @cap                        ; a whole page, and less than one left
        cmp portbSize
        bcc @copy
@cap
        lda portbSize
        sta portbRun
@copy
        ldy portbRun
@byte
        dey
        lda (portbPtr),y
        sta (portbDst),y
        tya
        bne @byte
        ldx portbRun                    ; the run, as 256 where it is zero
        bne @counted
        inc portbPtr+1
        inc portbDst+1
        dec portbSize+1
        jmp @crossed
@counted
        txa
        clc
        adc portbPtr
        sta portbPtr
        bcc @source
        inc portbPtr+1
@source
        txa
        clc
        adc portbDst
        sta portbDst
        bcc @destination
        inc portbDst+1
@destination
        sec
        lda portbSize
        stx portbRun
        sbc portbRun
        sta portbSize
        bcs @crossed
        dec portbSize+1
@crossed
        lda portbPtr+1
        cmp #>portbWindowEnd
        bne @run
        jsr portbNextUnit               ; the window's end: the next unit in
        jmp @run
@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
        rol                     ; the carry the asl left is the sentinel
        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

poke.asm

SAVMSC = $58                            ; where the OS put screen memory

; tag word
; Written where the display looks rather than through the character I/O, so
; these are the display's numbers and not the machine's.
.section
word
        .byte screen"NGA"
wordEnd
.ends
; end word

; tag assert
; The same letter has a number in each, and nothing has to be remembered: the
; tool is held to both, here, at the line that says so.
.assert atascii'A'  == $41
.assert screen'A'   == $21
.assert atascii'\n' == $9B
; end assert

.proc entry
        ldy #wordEnd - word - 1
@copy   lda word,y
        sta (SAVMSC),y
        dey
        bpl @copy
@stop   jmp @stop
.endp

Memory map

  1. phase0128 zero page, 15632 bytes
AddressBytesSectionModuleKindPhasesWaits
$0000–$007F128osZeroossection0
$0200–$06FF1280osRamossection0
$2000–$20023wordpokesection0
$2003–$200F13entrypokeproc0
$C000–$CFFF4096osRomLowossection0
$D800–$FFFF10240osRomHighossection0

The xex

28 bytes, 2 segments, 0 unaccounted.

02container header
$2000–$200F 16 bytes at 2
$20003poke.wordpoke.asm
$20032poke.entrypoke.asm
$20053poke.entrypoke.asm
$20082poke.entrypoke.asm
$200A1poke.entrypoke.asm
$200B2poke.entrypoke.asm
$200D3poke.entrypoke.asm
$02E0–$02E1 2 bytes at 22
$02E02runad