An IFFL system experiment by Cadaver & Soci
-------------------------------------------

0. Introduction

IFFL (Interflexible File Loader or Integrated File Flexible Loader) is a
system where all data files needed by a program are combined into one big
file. This is typically seen in some newer game cracks. This rant
describes my personal experiment of building such loadersystem.

Use this IFFL system at your own risk! It's only meant for educational use
though it might work fairly well. Loading of packed files (whatever packer you
might need) is left as an exercise, by default it only loads unpacked files.

IFFL sector scanning code inspired by works of Triad (Quorthon?) &
Nostalgia (Fungus / Zagon / GRG / DocBacardi). Metal Warrior 3 and Myth
IFFL cracks used for reference.

IFFL system disk image and source

The IFFL tools have been updated to incorporate fixes by Luigi Di Fraia.


1. IFFL theory of operation

The easiest way to do IFFL can be used even without any drive-programming at
all. To read multiple files from a single file, we simply open the file with
DOS calls, and start to read, throwing away bytes until we arrive at the
desired file. This is quite slow.

To speed things up, we must be able to skip directly into the beginning of any
file within the IFFL datafile. The DOS doesn't have random access capabilities
for standard files, so we must resort to direct track/sector reading. Good IFFL
systems also pack the files tightly, so that a file can start right after the
previous file in the middle of a sector. Therefore, we also need to know the
start offset within sector for each file.

Again, an easy but bad way is to note each file's start track/sector/offset when
the IFFL datafile is being created. We store this information somewhere, and
use it to get to the files. But what happens if the IFFL datafile gets file-
copied to another place on another disk? All track/sector info is now useless.

Therefore, good IFFL systems "scan" the datafile when first starting up,
using custom drive code that follows the track/sector chain until the end, and
makes note of each file's startpoint. You usually see this in IFFL-cracks: the
diskdrive "sweeps" over the entire disk in a matter of a few seconds.

After the scanning is done, it's easy for the drivecode to use the file track/
sector/offset information to find each file. Transmitting data to the C64 is
a bit more complicated than in standard fastloaders: instead of just sending
full sectors, we must be careful to see where the next file is coming up (might
be in the middle of a sector) so that any extra bytes don't get transmitted.


2. IFFL fileformat

I doubt there is any standard for how IFFL files are formatted, so I invented
my own. I decided to have 127 files maximum, each with a maximum length of 65535
bytes. To make the scanning possible, one approach is to store the lengths of
all files in the datafile, and that's what I did.

The first sector of the IFFL datafile contains the length info. First 127 bytes
are the length LSB's, and the next 127 are the MSB's. Files will be referred to
with numbers, so that the file first added is $00, the next $01, then $02 etc.

After this first sector, raw file data follows. There are no additional info
bytes anymore, just all the files concatenated in order.

In the source package there are 2 simple crossdevelopment utilities: MAKEIFFL
and ADDIFFL. The first creates an empty IFFL file, with just the 254-byte length
table, and the second adds a file to it.


3. The operation of the drivecode: scanning

To understand how the IFFL system works on the code-level, familiarity with
standard 1541 fastloaders is required. There's the usual drivecode uploading
routine, 2-bit data transfer with badline handling, PAL/NTSC speed adjustment
and Restore protection, and the use of escape bytes to mark "load error" or
"loading ended OK".

The drivecode starts from the label drv_init. First it has to seek the IFFL
datafile from the disk directory. Jobcodes are used for sector reading, nothing
special there. If there is an error at any point, the error code byte will be
transmitted to the C64, and the drivecode will exit.

When the IFFL datafile is found, its first sector (the file lengths) is read.
The file length tables are copied from the sector buffer to safety, and then we
get to the interesting part, the scanning.

We set up buffer 2 track/sector ($0a/$0b) with the next sector of the IFFL file,
and use the jobcode $e0. This causes the drive to seek the track in question,
and then execute the code at buffer 2 of the disk drive RAM ($0500).

This transfers control to the drv_scan label. Now the idea is to get the
information needed for scanning as fast as possible, so that the user doesn't
get bored. We don't need full data from the sectors, just the track/sector links
so that we can follow the IFFL file.

I first tested using the ROM routines to do the following:

- Read the sector (stored in $0b) header & wait for sync (ROM)
- Read & decode 5 GCR bytes from the sector start
- Check for any files starting on this sector, then follow the T/S link
- Repeat until end of the IFFL file

But this was quite slow. To speed it up, we must abandon going through the
sectors in order. Instead we can get the T/S links from all the sectors on the
track in a single revolution, and afterwards we can "virtually" follow them.
The code thus becomes:

- Wait for sync (ROM)
- Read 5 GCR bytes (sector header) to $0400
- Check that it is a valid header ($52 is the first GCR byte)
- Wait for sync (ROM)
- Read 5 GCR bytes (start of sector data) to $0405
- Decode the header to get the current sector number (ROM)
- Decode the sector data start (ROM), and using the sector number as index,
  store the T/S link
- Repeat for all sectors of the track
- Now, go through each sector "virtually", checking for files starting on each
  sector

When the T/S link indicates that a different track is needed, the drv_scan
routine has to write the new track to $0a and exit the job returning an OK
errorcode. Now the main program knows we are not yet finished and starts the
$e0 job again, unless the track link points to 0, which signals end of the IFFL
file.

How the "check for files starting on this sector" part is accomplished? I
did the following: When the scanning first starts, a 16-bit counter is
initialized with the value 0.

For each sector, the counter value is checked. If it's less than 254, the next
file starts on this sector and we store the counter's value as the "file offset
within sector". We also store the current track/sector as the file's start
track/sector. Then the file's length is added to the counter. We check if the
counter is still below 254 (many short files can begin on the same sector), and
if so, repeat the process. Finally, 254 is subtracted from the counter for each
sector as we traverse to the next.

A file length 0 is used in the length table to mark "no more files". However,
we allow the scanning routine to fill the track/sector/offset table completely
(127+1 files, the last as a definite endmark). This is a bit ugly but shortens
the drivecode a bit, and allows you to give any valid but nonexistent file
number to the loader without it crashing :)

Finally, it must be noted that the file track/sector tables overwrite the file
length tables. This saves drive memory, as the file length is no longer needed
when its start position has been determined.

When the scanning is done without errors, a $00 byte is transmitted to the C64
to signal success. The INITLOADER routine on C64 receives this byte and sets
the carry flag accordingly (C=0 for no error)


4. The operation of the drivecode: loading

We receive a byte from the C64, in the range 0-126. This is the file number
to load. The receiving happens using an asynchronous protocol, and if ATN is
pulled low instead (Kernal I/O), we know to exit the drivecode immediately.

The file number can be used as an index to the track/sector/offset table.
For each sector the process is following:

- If at the first sector, the send startoffset is the file start offset.
  Otherwise 0.
- Check if we are at the next file's first sector. If so, the send endoffset is
  the next file's start offset, otherwise 254 (full sector transfer).
- If send endoffset is equal to startoffset, we are right at the EOF and can
  transfer nothing, so loading is finished. Send escape byte and 0 for OK.
- Read the sector using jobcodes.
- Send the sector databytes between startoffset and endoffset. If an escape
  byte needs to be transmitted as data, send the escape byte twice.
- Follow the track/sector link to the next sector. If track is 0, the whole
  IFFL file ends, so loading is finished. Send escape byte and 0 for OK.

If there's an error in reading the sector, the escape byte and the drive
controller error code ($02-$0f) are sent.

Finally, we loop back to wait for a file number.

Negative file numbers are used to rescan IFFL, for example disk side changes.
This is demonstrated in the example by wiping the picture memory, rescanning,
and reloading the picture. After a rescan request (sent directly with SENDBYTE), 
the C64 side must receive a byte (GETBYTE) and check that it's zero.


5. The sourcecode

;-------------------------------------------------------------------------------
; IFFL-system example with 2-bit transfer by Cadaver 9/2004
; Unitialized Y register fixed in 3/2015
; Error send fix & possibility to rescan added 10/2022
;
; Use at own risk!
;-------------------------------------------------------------------------------

                processor 6502
                org $0801

                dc.b $0b,$08            ;Address of next BASIC instruction
                dc.w 2004               ;Line number
                dc.b $9e                ;SYS-token
                dc.b $32,$30,$36,$31    ;2061 in ASCII
                dc.b $00,$00,$00        ;BASIC program end

start:          jsr initloader
                bcs error
                lda #$00
                jsr loadfile            ;Load file $00 - music
                bcs error
                jsr initmusicplayback
                lda #$10                ;Try to load some nonexistent file -
                jsr loadfile            ;should be harmless as long file number
reload:         lda #$01                ;< MAXFILES
                jsr loadfile            ;Load file $01 - picture
                bcs error
                jsr showpicture
                lda #$20                ;Clear picture memory area
                sta clearsta+2
                ldx #$00
                txa
clearsta:       sta $2000,x
                inx
                bne clearsta
delay:          ldy $d011
                bpl delay
delay2:         ldy $d011
                bmi delay2
                inc clearsta+2
                ldy clearsta+2
                cpy #$60
                bcc clearsta
                lda #$80                ;Perform IFFL reinit (negative filenumber)
                jsr sendbyte
                jsr getbyte             ;Then read byte to see it initialized OK
                cmp #$00
                beq reload

error:          sta $d020               ;If any error, store errorcode to border and
                jmp error               ;loop endlessly

initmusicplayback:
                sei
                lda #<raster
                sta $0314
                lda #>raster
                sta $0315
                lda #50                 ;Set low bits of raster
                sta $d012               ;position
                lda $d011
                and #$7f                ;Set high bit of raster
                sta $d011               ;position (0)
                lda #$7f                ;Set timer interrupt off
                sta $dc0d
                lda #$01                ;Set raster interrupt on
                sta $d01a
                lda $dc0d               ;Acknowledge timer interrupt
                lda #$00
                jsr $1000
                cli
                rts

raster:         jsr $1003
                dec $d019
                jmp $ea31

showpicture:    lda #$02                ;Show the picture we just loaded
                sta $dd00               ;Set videobank
                lda #59
                sta $d011               ;Set Y-scrolling / bitmap mode
                lda #$18
                sta $d016               ;Set multicolor mode
                lda #$80
                sta $d018               ;Set screen pointer
                lda #$00
                sta $d020               ;Set correct background colors
                lda #$01
                sta $d021
                ldx #$00
copycolors:     lda $6400,x             ;Copy the colorscreen
                sta $d800,x
                lda $6500,x
                sta $d900,x
                lda $6600,x
                sta $da00,x
                lda $6700,x
                sta $db00,x
                inx
                bne copycolors
                rts

;-------------------------------------------------------------------------------
; IFFL system
;
; Call INITLOADER first to scan the IFFL file. Returncode:C=0 OK, C=1 error
; The IFFL filename is compiled in at the end of the drivecode, so change it
; as necessary.
;
; Call LOADFILE with filenumber in A to load. Returncode:C=0 OK, C=1 error
; Valid filenumbers are $00-$7e.
;
; To rescan IFFL, call SENDBYTE with negative value. After that, you must call
; GETBYTE and see that the returned value is zero to denote successful rescan.
;
; Use any Kernal file I/O to detach the IFFL drivecode.
;
; 2-bit transfer is used so sprites must be off and IRQs can get delayed by a
; couple of rasterlines.
;-------------------------------------------------------------------------------

        ;Loader defines

RETRIES         = $20           ;Retries when reading a sector
MAXFILES        = $7f           ;Max.127 files in the IFFL file
MW_DATA_LENGTH  = $20           ;Bytes in one M/W command

        ;C64 defines

loadtempreg     = $02
status          = $90           ;Kernal zeropage variables
messages        = $9d
fa              = $ba

ciout           = $ffa8         ;Kernal routines
listen          = $ffb1
second          = $ff93
unlsn           = $ffae
acptr           = $ffa5
chkin           = $ffc6
chkout          = $ffc9
chrin           = $ffcf
chrout          = $ffd2
close           = $ffc3
open            = $ffc0
setmsg          = $ff90
setnam          = $ffbd
setlfs          = $ffba
clrchn          = $ffcc
getin           = $ffe4
load            = $ffd5
save            = $ffd8

        ;1541 defines

drvlentbllo     = $0300         ;File length lobytes
drvlentblhi     = $0380         ;File length hibytes
drvbuf          = $0400         ;Sector data buffer

drvtrklinktbl   = $0420         ;Track link table for fast scanning
drvsctlinktbl   = $0440         ;Sector link table for fast scanning

drvtrktbl       = $0300         ;Start track of files (overwrites lentbl)
drvscttbl       = $0380         ;Start sector of files (overwrites lentbl)
drvoffstbl      = $0780         ;Start offset of files

buf1cmd         = $01           ;Buffer 1 command
buf1trk         = $08           ;Buffer 1 track
buf1sct         = $09           ;Buffer 1 sector
buf2cmd         = $02           ;Buffer 2 command
buf2trk         = $0a           ;Buffer 2 track
buf2sct         = $0b           ;Buffer 2 sector
drvtemp         = $0c           ;Temp variable
drvtemp2        = $0d           ;Temp variable, IFFL file number
drvcntl         = $0e           ;IFFL byte counter for scanning
drvcnth         = $0f
iddrv0          = $12           ;Disk drive ID
id              = $16           ;Disk ID
buflo           = $30           ;Buffer address lowbyte
bufhi           = $31           ;Buffer address highbyte
sectors         = $43           ;Amount of sectors on current track

returnok        = $f505         ;Return from job exec with OK code
waitsync        = $f556         ;Wait for SYNC
decode          = $f7e8         ;Decode 5 GCR bytes, bufferindex in Y
initialize      = $d005         ;Initialize routine (for return to diskdrive OS)

;-------------------------------------------------------------------------------
; LOADFILE
;
; Loads an unpacked file from the IFFL-file. INITLOADER must have been called
; first.
;
; Parameters: A:File number ($00-$7e)
; Returns: C=0 File loaded OK
;          C=1 Error
; Modifies: A,X,Y
;-------------------------------------------------------------------------------

loadfile:       sta $d07a                       ;SCPU to 1MHz mode
                jsr sendbyte                    ;Send file number
                lda #$00
                sta bufferstatus                ;Reset amount of bytes in buffer
                jsr load_getbyte                ;Get startaddress low
                sta load_sta+1
                jsr load_getbyte                ;Startaddress high
                sta load_sta+2
load_loop:      jsr load_getbyte                ;Get data byte from file
load_sta:       sta $1000                       ;and store it
                inc load_sta+1
                bne load_loop
                inc load_sta+2
                jmp load_loop

load_getbyte:   ldx bufferstatus                ;Bytes still in buffer?
                beq load_fillbuffer
                lda loadbuffer-1,x
                dex
                stx bufferstatus 
                inc $d020                       ;Oldskool effect
                dec $d020
                rts

load_fillbuffer:
                jsr getbyte             ;Get number of bytes to transfer
                beq load_end            ;$00 means load end (either OK or error)
                sta bufferstatus
                ldx #$00
load_fillbufferloop:
                jsr getbyte             ;Fill the buffer in a loop
                sta loadbuffer,x
                inx
                cpx bufferstatus
                bcc load_fillbufferloop
                bcs load_getbyte
load_end:       jsr getbyte             ;Get reasoncode ($00 = OK, higher = error)
                cmp #$01
                pla
                pla
                rts

;-------------------------------------------------------------------------------
; INITLOADER
;
; Uploads the IFFL drivecode to disk drive memory and starts it.
;
; Parameters: -
; Returns: C=0 IFFL initialized OK
;          C=1 Error
; Modifies: A,X,Y
;-------------------------------------------------------------------------------

initloader:     sei
                lda #$00
il_detectntsc:  ldx $d011              ;Get the biggest rasterline in the
                bmi il_detectntsc      ;area >= 256 to detect NTSC/PAL
il_detectntsc2: ldx $d011
                bpl il_detectntsc2
il_detectntsc3: cmp $d012
                bcs il_detectntsc4
                lda $d012
il_detectntsc4: ldx $d011
                bmi il_detectntsc3
                cli
                cmp #$20
                bcc il_isntsc
                lda #$30                ;Adjust 2-bit fastload transfer
                sta getbyte_delay       ;delay for PAL
il_isntsc:      lda #<il_nmi            ;Set NMI vector (let NMI trigger once
                sta $0318               ;so RESTORE keypresses won't interfere
                sta $fffa               ;with loading)
                lda #>il_nmi
                sta $0319
                sta $fffb
                lda #$81
                sta $dd0d               ;Timer A interrupt source
                lda #$01                ;Timer A count ($0001)
                sta $dd04
                lda #$00
                sta $dd05
                lda #%00011001          ;Run Timer A in one-shot mode
                sta $dd0e
                lda #>drivecode_drv
                sta ifl_mwstring+1
                lda #>drivecode_c64
                sta ifl_senddata+2
                lda #(drivecodeend_drv-drivecode_drv+MW_DATA_LENGTH-1)/MW_DATA_LENGTH
                sta loadtempreg         ;Number of "packets" to send
                ldy #$00
                beq ifl_nextpacket
ifl_sendmw:     lda ifl_mwstring,x      ;Send M-W command (backwards)
                jsr ciout
                dex
                bpl ifl_sendmw
                ldx #MW_DATA_LENGTH
ifl_senddata:   lda drivecode_c64,y     ;Send one byte of drivecode
                jsr ciout
                iny
                bne ifl_notover
                inc ifl_senddata+2
ifl_notover:    inc ifl_mwstring+2      ;Also, move the M-W pointer forward
                bne ifl_notover2
                inc ifl_mwstring+1
ifl_notover2:   dex
                bne ifl_senddata
                jsr unlsn               ;Unlisten to perform the command
ifl_nextpacket: lda fa                  ;Set drive to listen
                jsr listen
                lda #$6f
                jsr second
                ldx #$05
                dec loadtempreg         ;All "packets" sent?
                bpl ifl_sendmw
                dex
ifl_sendme:     lda ifl_mestring,x      ;Send M-E command (backwards)
                jsr ciout
                dex
                bpl ifl_sendme
                jsr unlsn               ;Start drivecode
                jsr getbyte             ;Get a byte from the drive
                cmp #$02                ;Error or OK?
                rts

il_nmi:         rti

;-------------------------------------------------------------------------------
; GETBYTE
;
; Gets one byte from the diskdrive with 2-bit protocol. Sprites must be off.
;
; Parameters: -
; Returns: A:Byte
; Modifies: A
;-------------------------------------------------------------------------------

getbyte:        lda $dd00               ;CLK low
                ora #$10
                sta $dd00
getbyte_wait:   bit $dd00               ;Wait for 1541 to signal data ready by
                bmi getbyte_wait        ;setting DATA low
                sei
getbyte_waitbadline:
                lda $d011
                clc                     ;Wait until a badline won't distract
                sbc $d012               ;the timing
                and #$07
                beq getbyte_waitbadline
                lda $dd00
                and #$03
                sta $dd00               ;Set CLK high
getbyte_delay:  bpl getbyte_delay2
getbyte_delay2: nop
                nop
                nop
                nop
                sta getbyte_eor+1
                lda $dd00
                lsr
                lsr
                eor $dd00
                lsr
                lsr
                eor $dd00
                lsr
                lsr
getbyte_eor:    eor #$00
                eor $dd00
                cli
                rts

;-------------------------------------------------------------------------------
; SENDBYTE
;
; Sends one byte to the diskdrive with asynchronous protocol (no timing
; requirements on C64 or 1541 side)
;
; Parameters: A:Byte
; Returns: -
; Modifies: A,Y
;-------------------------------------------------------------------------------

sendbyte:       sta loadtempreg
                ldy #$08                ;Bit counter
sendbyte_bitloop:
                lsr loadtempreg         ;Rotate byte to be sent
                lda $dd00
                and #$ff-$30
                ora #$10
                bcc sendbyte_zerobit
                eor #$30
sendbyte_zerobit:
                sta $dd00
                lda #$c0                ;Wait for CLK & DATA low
sendbyte_ack:   bit $dd00
                bne sendbyte_ack
                lda $dd00
                and #$ff-$30            ;Set DATA and CLK high
                sta $dd00
sendbyte_ack2:  bit $dd00               ;Wait for CLK & DATA high
                bvc sendbyte_ack2
                bpl sendbyte_ack2
                dey
                bne sendbyte_bitloop
sendbyte_endloop:
                rts

;-------------------------------------------------------------------------------
; M-W and M-E command strings
;-------------------------------------------------------------------------------

ifl_mwstring:   dc.b MW_DATA_LENGTH,$00,$00,"W-M"

ifl_mestring:   dc.b >drv_init, <drv_init, "E-M"

;-------------------------------------------------------------------------------
; Drivecode
;-------------------------------------------------------------------------------

drivecode_c64:
                rorg $500
drivecode_drv:

;-------------------------------------------------------------------------------
; Subroutine: scan the IFFL file (executed via jobcode $e0)
;-------------------------------------------------------------------------------

drv_scan:       lda #>drvbuf
                sta bufhi
                lda sectors
                sta drvtemp
drv_scansector: lda #<drvbuf            ;Read sector header to $0400
                jsr drv_scan5bytes
                lda drvbuf              ;Check for correct header
                cmp #$52
                bne drv_scansector
                lda #<drvbuf+5          ;Read data beginning to $0405
                jsr drv_scan5bytes
                ldy #$00
                sty buflo
                jsr decode              ;Decode the header
                lda $54                 ;Take sectornumber
                pha
                ldy #$05
                jsr decode              ;Decode the data beginning
                pla
                tax
                lda $53                 ;Store T/S link to our linktable
                sta drvtrklinktbl,x     ;so that we can "virtually" loop
                lda $54                 ;through the sectors later
                sta drvsctlinktbl,x
                dec drvtemp             ;Loop until all sectors scanned
                bne drv_scansector

drv_scanfile:   ldy drvtemp2            ;Current file number
                lda drvcnth             ;See if the byte counter is less than
                bne drv_scannext        ;254, otherwise go to next sector
                lda drvcntl
                cmp #254
                bcs drv_scannext
drv_scanfileok: sta drvoffstbl,y        ;Store file offset
                adc drvlentbllo,y       ;Now add this file's length to the
                sta drvcntl             ;byte counter
                lda drvcnth
                adc drvlentblhi,y
                sta drvcnth
                lda buf2trk             ;Store file track/sector
                sta drvtrktbl,y         ;(file length gets overwritten but
                lda buf2sct             ;it's no longer needed at this point)
                sta drvscttbl,y
                inc drvtemp2            ;Increment file counter
                bpl drv_scanfile        ;Fill up the table, then exit
                lda #$00
                beq drv_scandone

drv_scannext:   lda drvcntl             ;Now subtract 254 bytes from the counter
                sec                     ;as we go to the next sector
                sbc #254
                sta drvcntl
                bcs drv_scannextok
                dec drvcnth
drv_scannextok: ldx buf2sct
                lda drvsctlinktbl,x     ;Get next sector from our linktable
                sta buf2sct
                lda drvtrklinktbl,x     ;Get next track from our linktable
                cmp buf2trk
                beq drv_scanfile        ;If same track, go back to loop for
drv_scandone:   sta buf2trk             ;files
                jmp $f505               ;Otherwise, have to return from the
                                        ;job execution, and execute again
drv_scan5bytes: sta buflo
                jsr waitsync            ;Wait for SYNC (clears Y)
drv_5byteloop:  bvc drv_5byteloop       ;Wait for data from the R/W head
                clv
                lda $1c01
                sta (buflo),y
                iny
                cpy #$05                ;Read 5 GCR bytes
                bne drv_5byteloop
                rts

;-------------------------------------------------------------------------------
; Drive main code
;-------------------------------------------------------------------------------

drv_init:       lda #18                 ;Read first dir sector
                ldx #1
drv_dirsctloop: jsr drv_readsector
                bcs drv_initfail
                ldy #$02
drv_fileloop:   lda drvbuf,y            ;File type must be PRG
                and #$83
                cmp #$82
                bne drv_nextfile
                sty drv_namecmp+1
                ldx #$03
                lda #$a0                ;Make an endmark at the 16th letter
                sta drvbuf+19,y
drv_namecmp:    lda drvbuf,x
                cmp drv_filename-3,x    ;Check against each letter of filename
                bne drv_namedone        ;until at the endzero
                inx
                bne drv_namecmp
drv_namedone:   cmp #$a0                ;If got to a $a0, name correct
                beq drv_found
drv_nextfile:   tya
                clc
                adc #$20                ;Go to next file
                tay
                bcc drv_fileloop
drv_nextdirsct: ldx drvbuf+1
                lda drvbuf              ;Any more dir sectors?
                bne drv_dirsctloop      ;Errorcode $10 not used by 1541
                lda #$10                ;so use it as "IFFL file not found"
drv_initfail:   jsr drv_sendbyte        ;Send error code & exit through
                jmp initialize          ;INITIALIZE

drv_found:      lda drvbuf+1,y          ;IFFL datafile found, get its start
                ldx drvbuf+2,y          ;track & sector
                jsr drv_readsector
                bcs drv_initfail
                ldy #MAXFILES-1
drv_copylentbl: lda drvbuf+2,y          ;First sector contains the file lengths.
                sta drvlentbllo,y       ;Copy them to the length tables
                lda drvbuf+2+MAXFILES,y
                sta drvlentblhi,y
                dey
                bpl drv_copylentbl
                lda #$00                ;Clear the length of the last file
                sta drvlentbllo+MAXFILES ;in case we have full 127 files
                sta drvlentblhi+MAXFILES
                sta drvtemp2            ;Clear the scanning file counter
                sta drvcntl             ;Clear the 16bit IFFL byte counter
                lda drvbuf              ;Now get the next T/S (where actual
                sta buf2trk             ;data starts) and perform the scanning
                lda drvbuf+1            ;with the seek/execute jobcode
                sta buf2sct             ;(drv_scan at $500 gets executed)
drv_scanloop:   lda #$e0
                sta buf2cmd
                cli
drv_scanwait:   lda buf2cmd
                bmi drv_scanwait
                sei
                cmp #$02                ;If error, retry
                bcs drv_initfail
drv_scanok:     lda buf2trk             ;Keep calling the job until the file is
                bne drv_scanloop        ;at an end
                jsr drv_sendbyte        ;Now A=0, send the byte so that C64
                                        ;knows the file was scanned successfully

drv_mainloop:   cli                     ;Allow interrupts so drive may stop
                jsr drv_getbyte         ;Get file number from C64
                tay                     ;(file number also now in drvtemp2)
                bmi drv_reinit          ;If negative filenumber, rescan IFFL file
                lda drvoffstbl,y        ;Get file start offset
                sta drv_mainsendstart+1
                lda drvtrktbl,y         ;Get file start track & sector
                sta buf1trk
                lda drvscttbl,y
                sta buf1sct

drv_mainsctloop:ldy drvtemp2            ;Get the file number back
                ldx #$fe                ;Assume we'll send a full sector (254b.)
                lda buf1trk             ;If we're on the startsector of the
                cmp drvtrktbl+1,y       ;next file, we can only send up to the
                bne drv_mainnotlast     ;next file's startoffset
                lda buf1sct
                cmp drvscttbl+1,y
                bne drv_mainnotlast
                ldx drvoffstbl+1,y      ;If endoffset = startoffset, we're
                cpx drv_mainsendstart+1 ;already on the next file and can't
                beq drv_mainfiledone    ;send anything
drv_mainnotlast:stx drv_mainsendend+1
                jsr drv_readsector2     ;Read sector, abort if failed
                bcs drv_mainfilefail

drv_mainsendend:ldy #$00
                cpy #$fe
                php
                tya
                sec
                sbc drv_mainsendstart+1 ;Get amount of bytes to send
                jsr drv_sendbyte
drv_mainsendloop:
                lda drvbuf+1,y          ;Send buffer backwards
                jsr drv_sendbyte
                dey
drv_mainsendstart:
                cpy #$00
                bne drv_mainsendloop
                plp                     ;See if it was a partial sector
                bne drv_mainfiledone    ;(last one)

drv_mainnextsct:lda #$00
                sta drv_mainsendstart+1 ;Startoffset for next sector is 0
                lda drvbuf+1            ;Follow the T/S link
                sta buf1sct
                lda drvbuf
                sta buf1trk             ;Go back to send the next sector,
                bne drv_mainsctloop     ;unless IFFL file end encountered
drv_mainfiledone:
                lda #$00                ;Errorcode 0: all OK
drv_mainfilefail:
                pha                     ;File end: send $00 and errorcode
                lda #$00
                jsr drv_sendbyte
                pla
                jsr drv_sendbyte
                jmp drv_mainloop        ;Then go back to main to wait for
                                        ;file number

drv_reinit:     jmp drv_init

;-------------------------------------------------------------------------------
; Subroutine: send byte in A to C64 using 2-bit protocol (no IRQs allowed)
;-------------------------------------------------------------------------------

drv_sendbyte:   sta drvtemp
                lsr
                lsr
                lsr
                lsr
                tax
                lda #$04
drv_sendwait:   bit $1800               ;Wait for CLK==low
                beq drv_sendwait
                lsr                     ;Set DATA=low
                sta $1800
                lda drv_sendtbl,x       ;Get the CLK,DATA pairs for low nybble
                pha
                lda drvtemp
                and #$0f
                tax
                lda #$04
drv_sendwait2:  bit $1800               ;Wait for CLK==high (start of high speed transfer)
                bne drv_sendwait2
                lda drv_sendtbl,x       ;Get the CLK,DATA pairs for high nybble
                sta $1800
                asl
                and #$0f
                sta $1800
                pla
                sta $1800
                asl
                and #$0f
                sta $1800
                nop
                nop
                lda #$00                ;CLK=DATA=high
                sta $1800
                rts

;-------------------------------------------------------------------------------
; 2-bit send table
;-------------------------------------------------------------------------------

drv_sendtbl:    dc.b $0f,$07,$0d,$05
                dc.b $0b,$03,$09,$01
                dc.b $0e,$06,$0c,$04
                dc.b $0a,$02,$08,$00

;-------------------------------------------------------------------------------
; Subroutine: get byte from C64 in A
;-------------------------------------------------------------------------------

drv_getbyte:    ldy #$08                ;Counter: receive 8 bits
drv_recvbit:    lda #$85
                and $1800               ;Wait for CLK==low || DATA==low
                bmi drv_gotatn          ;Quit if ATN
                beq drv_recvbit
                lsr                     ;Read the data bit
                lda #2                  ;Prepare for CLK=high, DATA=low
                bcc drv_rskip
                lda #8                  ;Prepare for CLK=low, DATA=high
drv_rskip:      sta $1800               ;Acknowledge the bit received
                ror drvtemp2            ;and store it
drv_rwait:      lda $1800               ;Wait for CLK==high || DATA==high
                and #5
                eor #5
                beq drv_rwait
                lda #0
                sta $1800               ;Set CLK=DATA=high
                dey
                bne drv_recvbit         ;Loop until all bits have been received
                lda drvtemp2            ;Return the data to A
                rts
drv_gotatn:     pla                     ;If ATN goes low, exit to the operating
                pla                     ;system. Discard the return address and
                jmp $d00e               ;jump to the INITIALIZE routine

;-------------------------------------------------------------------------------
; Subroutine: read sector
;-------------------------------------------------------------------------------

drv_readsector: sta buf1trk
                stx buf1sct
drv_readsector2:ldy #RETRIES            ;Retry counter
drv_readsectorretry:
                lda #$80
                sta buf1cmd
                cli
drv_readsectorpoll:
                lda buf1cmd
                bmi drv_readsectorpoll
                sei
                cmp #$02                ;Errorcode
                bcc drv_readsectorok
                ldx id                  ;Handle possible disk ID change
                stx iddrv0
                ldx id+1
                stx iddrv0+1
                dey                     ;Decrement retry counter and try again
                bne drv_readsectorretry
drv_readsectorok:
                rts

;-------------------------------------------------------------------------------
; IFFL filename
;-------------------------------------------------------------------------------

drv_filename:   dc.b "IFFLDATA",0

drivecodeend_drv:
                rend
drivecodeend_c64:

;-------------------------------------------------------------------------------
; Load buffer
;-------------------------------------------------------------------------------

loadbuffer:     ds.b 254,0
bufferstatus:   dc.b 0



6. Conclusion and further references

Though the code was only meant as an example of the IFFL concept, it still ended
up somewhat complex. Personally, I'm unlikely to use IFFL in own productions,
due to difficulty in retaining support for any devices.

For IDE64, IFFL support is possible due to the random access seek mechanism, see
this modified version of the IFFL code provided by Soci / Singular:

IFFL system modified for IDE64 by Soci

But for general Kernal-only loading compatibility without random access support,
the IFFL file scanning / seeking could end up slow due to having to load the
whole file byte by byte until the desired position is reached.

Finally, Luigi Di Fraia has a GitHub project where this IFFL system is improved 
further, to be better ready for actual production use, including compression and
multiple drive support. See here:

iffl-system by Luigi Di Fraia

As always, have fun!


                                                  Lasse Öörni
                                                  loorni@gmail.com