; Fender's 3-sector loader (boot code only)

; A "game DOS" binary file loader. Install it on a disk full of binaries,
; it replaces the DOS boot loader. You don't need DOS.SYS or DUP.SYS on
; such a disk, so there's more room for games. When you boot, the 3-sector
; loader reads the directory and presents you with a menu of up to 20
; files to load with a single keypress.

; presumably written by someone named Fender, in the early 1980s

; I have no idea what the licensing is (shareware, public domain, ???),
; but after all this time, I don't think anyone's going to sue me.

; Disassembled with dis6502
; Commented, labelled, and reformatted for DASM use by B. Watson, 20061105

; Can be assembled to a 384-byte object file with a command like:

;   dasm fenders.dasm -ofenders.obj -f3

; The object code can be installed on an existing single-density ATR
; floppy image on UN*X like so:

;   dd if=fenders.obj of=disk.atr bs=1 count=384 seek=16 conv=notrunc

; (for an XFD image, leave off the "seek=16")

; This is a standalone source file: it doesn't require any "system equates"
; include file.

; Intended for use with DASM, but doesn't use macros or other fancy
; features, so it'll probably assemble on most 6502 assemblers.
; Your assembler may use a slightly different syntax for conditional
; assembly, if so search the file for ".if" and change as needed.
; Also, remove the "processor" line if you aren't using DASM:

   processor 6502

; This disassembly only contains the boot loader code, not the installer.
; The installer is 30 sectors long (10x the size of the loader), and
; pretty straightforward.

; The installer allows you to customize the loader somewhat. It does
; this by changing the boot loader code before writing it. The options
; in the installer are:

; Option                       | Label
; A. Cause COLDSTART on RESET  | COLDSTART_ON_RESET  
; B. Rotate color during load  | SCREEN_OFF    [*]
; C. Screen off after load     | ROTATE_COLOR  [*]
; D. Title                     | (see "screen" label near end of this file)
; F. Change density            | DENSITY (TODO: support this)
; (the other options don't change the boot code)

; [*] B and C options are mislabelled in the installer, selecting
; "Rotate color" actually toggles "Screen off", and vice versa.

; Options A, B, and C cause the installer to modify the in-memory copy
; of the boot code before writing it to disk. I've used conditional
; assembly to simulate this:

COLDSTART_ON_RESET .equ 0 ; 0=false, non-zero=true
ROTATE_COLOR       .equ 0 ; 0=false, non-zero=true
SCREEN_OFF         .equ 0 ; 0=false, non-zero=true

; TODO: support conditional assembly for double density.
; Unlike the first 3 options, double density changes a lot of code.
; The original author doesn't change the code at runtime to support
; double-density; the installer contains complete copies of the
; single and double density boot code.
;DENSITY            .equ 1 ; 1=single, 2=double


;;; OS equates

;; OS ROM entry points

; Question for you Atari SIO and OS experts: why does "Mappping the
; Atari" state that you're supposed to call $E543 for the disk,
; instead of $E459, but the author of this program used $E459 anyway?
SIOV   .equ $e459 ; Serial I/O

COLDSV .equ $e477 ; Cold Start (reboot)
KEYBDV .equ $e420 ; K: handler device table
keyb_get_lo .equ KEYBDV+4 ; pointer to "get byte" routine, minus 1
keyb_get_hi .equ KEYBDV+5 ; (used by get_key)


;; OS zero page
BOOTQ .equ $09 ; Tells OS what to do when RESET is pressed:
                 ; (0 = reboot, 1 = warmstart, JSR through DOSVEC)

;; OS and FMS page 2 RAM variables
SDMCTL .equ $022f ; used to turn off the screen when done
SDLSTL .equ $0230 ; display list start pointer
COLDST .equ $0244 ; "coldstart in progress" flag
RUNAD  .equ $02e0 ; The run address of the loaded file
INITAD .equ $02e2 ; The init address of the loaded file (can be >1 per file)

;; DCB, used for sector I/O parameters by SIOV (called by read_sector)
DDEVIC .equ $0300 ; Device serial bus ID (set to $31 by OS disk boot)
DUNIT  .equ $0301 ; Drive number (we always use 1)
DCOMND .equ $0302 ; SIO command (set to $52, "Read sector", by OS)
DSTATS .equ $0303 ; Data direction register: $40 for "read"
DBUFLO .equ $0304 ; I/O buffer, lo byte
DBUFHI .equ $0305 ; I/O buffer, hi byte
DTIMLO .equ $0306 ; SIO timeout
DBYTLO .equ $0308 ; number of bytes to transfer (AKA sector size), lo byte
DBYTHI .equ $0309 ; ...hi byte
DAUX1  .equ $030a ; For "Read" command, the sector number (lo byte)
DAUX2  .equ $030b ; ...hi byte

;; Hardware registers
COLPF1 .equ $d017 ; GTIA, used by read_sector to rotate the title color

;; Local variables (zero page)
dest_ptr      .equ $43 ; (AKA FMSZPG) 2 bytes, init to load address
end_address   .equ $45 ; 2 bytes
save_pos      .equ $49 ; 1 byte: saves X register while loaded init routine runs
menu_counter  .equ $b0 ; tracks how many entries (filenames) are in the menu
dir_sector_lo .equ $b1 ; lo byte of current directory sector
                       ; (range $69-??, high byte is always 1)
menu_ptr      .equ $b2 ; points to screen RAM, for printing filenames

; Starting sector tables. For each file numbered N on the disk,
; do_dirent saves the low byte of its starting sector at
; start_sector_lo_tbl+N and its high byte at start_sector_hi_tbl+N
; Each table is only 32 bytes, but that's fine as we stop reading the
; directory after 20 files are found (all that will fit on the screen).
start_sector_lo_tbl .equ $c0
start_sector_hi_tbl .equ $e0

;;; Local variables (non zero page)
buffer            .equ $0b00
sector_link_hi    .equ $0b7d ; buffer + $7d
sector_link_lo    .equ $0b7e ; buffer + $7e
sector_byte_count .equ $0b7f ; buffer + $7f

;;; Bootable disk image starts here
   .org $0700

;;; Standard Atari boot disk header (6 bytes)
boot_record:
   .byte $00         ; ignored
   .byte $03         ; number of sectors to read
   .word boot_record ; load address
   .word COLDSV      ; init address, don't think this gets used

;;; Actual code starts here
boot_continuation: ; OS starts running our code here.
OFFSET_COLDST_1 equ *-boot_record+1
OFFSET_COLDST_2 equ *-boot_record+5
 .if COLDSTART_ON_RESET ; installer option A
   LDY #$01 ; tell OS to reboot if RESET pressed
   STY COLDST ; (coldstart in progress = true)
   NOP
 .else
   LDY #$00 ; tell OS not to reboot if RESET pressed
   STY COLDST ; (coldstart in progress = false)
   INY
 .endif

   ; either way, Y is now 1
   STY BOOTQ ; tell OS we booted successfully
   STY DUNIT ; set drive #1 for later SIO use

   DEC DTIMLO ; decrease default disk I/O timeout

   ; set up our custom display list
   LDA #<display_list ; 74 J
   STA SDLSTL
   LDA #>display_list ; 8 .
   STA SDLSTL+1

   ; set up to start reading the directory
   LDA #<menu
   STA menu_ptr
   LDA #>menu
   STA menu_ptr+1
   LDA #$69 ; Start loading directory at sector 361 ($0169)
   STA dir_sector_lo

read_dir_sector:
   LDA dir_sector_lo
   STA DAUX1
   LDA #$01  ; The directory is located at sectors 361-368,
   STA DAUX2 ; so the high byte is always 1
   JSR read_sector
   INC dir_sector_lo
   DEX

   ; X reg is the pointer into the sector buffer, keep that in mind
do_dirent:
   LDA buffer,X ; look at status byte
   BEQ wait_for_input ; if it's 0, there are no more files, so we're done
   BMI next_dirent ; if it's deleted (bit 7 true), skip it
   AND #$01  ; open for write if bit 0 set
   BNE next_dirent ; If it's open for writing, skip it
   INC menu_counter
   LDY menu_counter

   ; grab and save starting sector; we're building a table in RAM that
   ; holds the starting sector for every file on the disk
   LDA buffer+3,X
   STA start_sector_lo_tbl,Y
   LDA buffer+4,X
   STA start_sector_hi_tbl,Y
   TYA

   ; draw "A.", "B.", etc (menu choice letters) in the menu
   ; (menu_ptr) points to column 0 of the current menu line, and gets
   ; 20 added to it each time through the loop (we're using GR.1, which
   ; has 20 bytes per screen line).
   ; Y is the offset into the current line, AKA the column number
   CLC
   ADC #$a0 ; GR.2 offset into color 2 alphabet (128+32=160)
   LDY #$03 ; Menu letter goes in column 3 of the current menu line
   STA (menu_ptr),Y
   INY
   LDA #$8e ; blue period (no, really, a period that's blue)
   STA (menu_ptr),Y ; Store in column 4
   INY

next_char:
   INY
   LDA buffer+5,X ; buffer+5 holds the first ATASCII char of the filename
   INX
   SEC
   SBC #$20 ; subtracting 32 makes them come out in color 1
   STA (menu_ptr),Y
   CPY #$10 ; done with 16-byte dirent?
   BNE next_char ; if not, do next character

   ; Set up pointer for next screen line
   CLC
   LDA menu_ptr
   ADC #$14 ; skip to next GR.1 line ($14 = 20 bytes)
   STA menu_ptr
   BCC skip_ptr_hi
   INC menu_ptr+1 ; inc hi byte if necessary
skip_ptr_hi:
   LDA menu_counter
   CMP #$14 ; see if the screen is full (max entries = 20)
   BEQ wait_for_input ; yep, stop adding entries, or...

next_dirent:
   ; set X to point to the offset of the next dir entry
   ; they occur every 16 bytes, so: X = (X mod 16) + 16
   TXA
   AND #$f0
   CLC
   ADC #$10
   TAX

   ; dirents only take up the first 128 bytes of a dir sector, even
   ; on a double density disk, so:
   ASL ; see if the high bit it set (A >= 128)...
   BCC do_dirent ; no, we're still in the same dir sector, do next entry, or..
   BCS read_dir_sector ; yes, we need to load the next sector

wait_for_input:
   JSR get_key ; get_key waits for the user to press a key and returns its
               ; ATASCII value in A
   SEC
   SBC #$40 ; Convert from ATASCII A-Z to numbers 1-26
   CMP menu_counter ; See if it's one of our valid menu entries

   ; do a "branch if less than or equal" to load_file:
   BEQ load_file ; if equal, branch
   BCS wait_for_input ; else if greater than, ignore the keypress and
                      ; wait for the user to press another key
   ; else if less than, fall through to load_file

load_file: ; A register has the file number (1 indexed; there's no file #0)
   TAX
   INC menu_dlist,X ; change selected entry from GR.1 to GR.2, so the user
                    ; can see which file he'd selected

   ; get starting sector of this file
   LDA start_sector_lo_tbl,X
   STA DAUX1
   LDA start_sector_hi_tbl,X
   STA DAUX2

   JSR try_read ; read the first sector...
   DEX          ; point X at first byte we just read

; main loop of the program (see "Atari Binary Load Format", near end of file)
read_segment:
   ; get the segment load address and save in dest_ptr
   JSR get_next_byte
   STA dest_ptr
   JSR get_next_byte
   STA dest_ptr+1
   AND dest_ptr
   CMP #$ff         ; If we just read two $FF bytes, throw them out
   BEQ read_segment ; ...and read the next 2 bytes instead

   ; get the segment end address and save in end_address
   JSR get_next_byte
   STA end_address
   JSR get_next_byte
   STA end_address+1

load_byte:
   JSR get_next_byte ; get next loaded data byte
   STA (dest_ptr),Y  ; store in destination RAM
   INC dest_ptr      ; bump destination pointer: lo byte
   BNE check_seg_done ; skip the hi byte if the lo byte didn't wrap around
   INC dest_ptr+1    ; bump hi byte, if needed
   BEQ check_for_init ; stop loading this seg if address wraps $FFFF -> $0000
   ; else fall through to check_seg_done

check_seg_done:
   LDA end_address ; double-byte double compare, to see if we've
   CMP dest_ptr    ; finished loading all the data in the segment
   LDA end_address+1
   SBC dest_ptr+1  ; (SBC instead of CMP because we care about the carry here)
   BCS load_byte   ; if we're not done, load the next byte of data

check_for_init:
   LDA INITAD
   ORA INITAD+1
   BEQ read_segment ; if init addr is 0, we don't have one, go do next seg
   STX save_pos     ; else save byte position (X reg) and do the init
   JSR do_init
   LDX save_pos ; init routine returns here, reload byte position
   LDY #$00  ; zero out init address so it doesn't run again next time
   STY INITAD ; (unless of course the next segment(s) load at INITAD)
   STY INITAD+1
   BEQ read_segment ; unconditional branch, go read next segment

   ; JSR do_init simulates indirect JSR
do_init:
   JMP (INITAD)

; WARNING: self-modifying code. read_sector modifies the operand
; of the CPX, setting it to the number of valid data bytes in the
; sector just read. The initial value of $7d represents the number of
; data bytes in a sector that's full of data (125 bytes of actual data).
get_next_byte:
   CPX #$7d ; are we done with all the bytes in this sector?
   BNE return_next_byte ; if not, go do the next one

   LDA DAUX1 ; have we read the last sector?
   ORA DAUX2 ; (if so, the "next sector" stored at DAUX1/2 will be 0)
   BNE try_read ; if not, go read the next sector
OFFSET_SCREENOFF equ *-boot_record
 .if SCREEN_OFF ; installer option C, "turn off screen after loading"
   STA SDMCTL ; turn off the screen (er, why?)...
 .else
   LDA SDMCTL ; don't turn off the screen
 .endif
   JMP (RUNAD) ; ...we're done loading, go run the binary!

read_sector:
   LDA #>buffer
   STA DBUFHI ; set sector buffer (lo byte presumably defaults to 0?)
   LDA #$80 ; sector size for SIO: 128 bytes for single density
   STA DBYTLO

try_read:
   LDA #$40 ; data direction = read (bit 6 set, bit 7 clear)...
   STA DSTATS ; DSTATS is where we store the data direction
   JSR SIOV ; all set up, so call SIO
   BMI try_read ; on error, just retry (forever, if need be)

   ; set up SIO sector number for next time:
   LDA sector_link_hi ; The high 2 bits of the sector link (AKA next sector in
                      ; file) are stored in the LOW 2 bits of sector_link_hi,
                      ; along with the file number in bits 2-7
   AND #$03 ; mask off file number
   STA DAUX2
   LDA sector_link_lo ; sector link, low 8 bits
   STA DAUX1

   ; rotate PF color
OFFSET_ROTCOLOR equ *-boot_record
 .if ROTATE_COLOR ; installer option B
   STA COLPF1
 .else
   LDA COLPF1
 .endif

   ; save number of data bytes in sector
   LDA sector_byte_count
   AND #$7f ; hmm, is this really necessary?
   STA get_next_byte+1 ; WARNING: self-modifying code (see above)

   ; init source and destination indices
   LDY #$00
   LDX #$00

return_next_byte:
   LDA buffer,X
   INX
   RTS

; Simulate JSR through keyb_get_lo. This is one of those Atari
; "entry point minus one" vectors. To call it, you push it onto the
; stack and do an RTS (works because a real JSR saves the PC minus 1 on
; the stack).
; Becauses it gets the vector from ROM instead of hard-coding it in
; the get_key routine, this code will work on all Atari models (400/800,
; XL/XE). It's a compromise between hardcoding the address and doing
; a full-blown IOCB setup and CIO call.
get_key:
   LDA keyb_get_hi
   PHA
   LDA keyb_get_lo
   PHA
   RTS

;;; Data
display_list: ; display list, $084A - $0867
   ; 3 "blank 8 scans", a GR.2 line with LMS, then screen RAM address
   .byte $70,$70,$70,$47,<screen,>screen       ; "pppGh."
menu_dlist:
   ; blank 8 scans, then 20 GR.1 lines, then a jump back to the
   ; start of the display list.
   .byte $70,$06,$06,$06,$06,$06,$06,$06 ; "p......."
   .byte $06,$06,$06,$06,$06,$06,$06,$06 ; "........"
   .byte $06,$06,$06,$06,$06,$41,<display_list,>display_list ; ".....AJ."

   ; screen RAM, $0868 - $087F and beyond
OFFSET_TITLE equ *-boot_record
screen:
   ; $00 is a space, lowercase letters come out as caps (color reg 1)
   .byte $00,$00,$00,$00        ; 4 spaces for centering
   .byte "atari", $00, "arcade" ; "ATARI ARCADE" in green
   .byte $00,$00,$00,$00        ; rest of the GR.2 line (20 bytes total)
menu: ; the filenames start getting stored here, 2nd line of the display

   ; 4 filler bytes, so we come out at an even 3 sectors (384 bytes)
   .byte $00,$00,$00,$00

;;; End of code. Rest of file is boring documentation :)

; Atari Binary Load File Format - A Primer

; The binary load format was first defined for Atari DOS 1.0, and supported
; by all DOSes for the Atari. There's no formal standard that I'm aware of;
; the closest thing is probably the binary load implementation in Atari
; DOS 2.0S or 2.5 (the most widely-used DOSes from Atari), but that's
; copyrighted code, so it couldn't be used as a base to build on or as a
; module for another DOS.

; Sadly, Atari didn't add a CIO SPECIAL (aka XIO from BASIC) command to load
; binary files as part of their DOS's public API... so anyone who wanted to
; write a game-loader or menu that runs under DOS would have to write their
; own loader (for compatibility with many DOSes), or just assume they'll
; be running under DOS 2 and JSR to the unpublished entry point within DOS
; (which changed between DOS 2 and 2.5). Some third-party DOSes (SpartaDOS,
; MyDOS) included a CIO/XIO call for loading and/or running binary files,
; which meant a menu program was easy to write in just a few lines of
; BASIC, but it wouldn't work on standard Atari DOS, and might not even
; be portable from Sparta to MyDOS, if they didn't choose the same CIO
; command for binary loading.

; None of this DOS-related discussion is relevant to Fenders, though.
; The whole point of Fenders is to do away with the overhead of DOS, so:

; - You don't have to keep copies of DOS.SYS (39 sectors) or DUP.SYS (42
; sectors) on your disk. Together these take up over 10% of the available
; storage on a DOS 2 formatted disk. With Fenders you get all 707 sectors
; to use for game storage.

; - You don't have to wait for a full-blown DOS boot. It takes a while
; to load DOS.SYS and DUP.SYS (81 sectors, taken together), which are a
; rather complete file management system and menu interface that you don't
; need for playing games. With Fenders, you get your menu almost instantly.

; - Once DOS and DUP are loaded, you have to type "A Return Return" to
; get a disk directory, then "L Return FILENAME Return" to load a file.
; With Fenders, you type one letter.

; There is no support in the OS ROM for the binary load format, or for
; the disk device at all at the CIO level (the ROM does have SIO routines
; for reading/writing raw sectors, which is what Fenders uses). There are
; many binary loader implementations on the Atari. Each third-party DOS
; generally had to roll its own, and there were numerous "No DOS" and
; "Game DOS" utilities (like Fenders) that had to implement read-only
; support for the DOS 2 filesystem along with a binary loader. Fenders
; does this in 330 bytes of code (including the user interface).

; An Atari binary load file consists of one or more segments, each with
; its own load address and end address.

; A segment is:

; FFFF leader (bytes 0 and 1) - A two-byte leader: $FF, $FF
;                               Required for 1st segment, optional for others
; Load address (bytes 2 and 3 in standard 6502 LSB/MSB)
; End address (bytes 4 and 5)
; (Load address - End address + 1) bytes of data to be loaded

; Fenders actually doesn't require the FFFF leader even for the first
; segment, and could deal with multiple FFFF leaders if present: it
; just skips over them. I haven't checked to see if Atari DOS does
; the same thing or not.

; A well-formed binary load file must have an end address higher than the
; start address for every segment. It's undefined what happens otherwise
; (Fenders quits loading if the current address ever wraps around from
; $FFFF to 0000, I don't know what it does if the end address is the same
; as the start address).

; There's no rule against having overlapping segments, but it's kind
; of an odd thing to do.

; After each segment is loaded, a loader must check INITAD (a 16-bit
; vector at location $02E2) to see if the segment loaded anything at
; that address. If so, a JSR through the vector is performed, with the
; expectation that the loaded program's initialization routine will run
; and then do an RTS to return control to the loader. This is often used
; (or abused) by crackers to load a screen with their name/logo.  In a
; real DOS, IOCB #1 is left open, so theoretically the init routine could
; read data from the file, located between segments as it were. Fenders
; doesn't do this, as it doesn't implement a CIO handler at all. Any program
; that relies on this behavior won't work with Fenders, which isn't much
; of a limitation (I doubt this capability has ever been used in the
; entire history of the Atari. It might be useful for something like an
; auto-relocating load?). Use of the init vector is optional. Since it's
; checked after every segment, there can be more than one init routine
; run this way.

; After all segments have been loaded, a loader must check RUNAD (16-bit
; vector located at $2E0) to see if any segment loaded anything at that
; address. If so, a JSR through the vector is performed to run the main
; routine of the just-loaded program, which may or may not do an RTS to
; return control to the caller. In a real DOS, the RTS will generally redraw
; the DOS menu or print a new command prompt (whatever makes sense). Fenders
; does a JMP (RUNAD), not expecting the loaded program to return (most
; games just run forever).

; If RUNAD never gets loaded, a real DOS will return to its menu or prompt
; without trying to run anything. You could use this to just load some
; data (a font, maybe) into memory. Fenders will crash if RUNAD never gets
; loaded, since it's set to 0 by the OS (a JMP (0) happens, transferring
; control off to never-never-land). Again, not much of a limitation, since
; it's intended for running games, not general-purpose data-file loading.

; There is ABSOLUTELY NO protection against a binary file trying to
; load on top of Fenders while it's running (or the memory-mapped
; I/O registers, or zero page, or anywhere it wants to). There is also
; no protection against a loaded init routine messing up the zero page
; pointers or disk buffer. In practice, this doesn't happen often
; (and a "rude" file that did this would probably have problems loading
; under a standard DOS, too).

; This information comes from various sources. I probably gleaned most of
; it from "Mapping the Atari" or the Compute! magazine "Insight: Atari"
; column, way back when. Both of these are available on the web now:

; Mapping the Atari, De Re Atari, and a lot of other books can be
; found at http://www.atariarchives.org

; Most of the old Compute! and Antic magazines have been scanned and
; archived at http://www.atarimagazines.com/

; There's also a "Digital ANALOG Project" that's got a lot of Analog
; magazine issues archived: http://www.cyberroach.com/analog/

; For a compact reference to the binary load format, see:
; http://www.atarimax.com/jindroush.atari.org/afmtexe.html

; TODO: I want to add high speed I/O to Fenders, for use with the SIO2PC
; and AtariSIO (or APE).