Friday, 19 February 2016

Minigraph

In an idle moment, I went back to the Commodore 64 Advanced User Guide and re-read the section on high resolution bitmapped graphics. To shorten the examples, they include a BASIC DATA dump of a tool called Minigraph, credited to Paul Schatz. It occured to me it would be interesting to work out how these two pages of DATA commands actually do what they claim to do, so I've dissassembled and commented it.

Here's a first draft - it's a work in progress.

// da65 V2.15 - Git f7cdfbf
// Created:    2016-02-19 10:03:56
// Input file: minigraph.bin
// Page:       1

// TURN ON BITMAP -   SYS 50195
// TURN OFF BITMAP -  SYS 50198
// CLEAR BITMAP -     SYS 50207
// COLOUR BITMAP -     SYS 50210,F,B
// PLOT POINT @ X,Y - SYS 50201,X,Y,M
// DRAW LINE TO X,Y - SYS 50204,X,Y,M

// Each 8x8 block of screen is actually a character and there are 40x25 chars
// For a given x e (0,319) and y e (0, 199) pixel
// CH.ROW = INT(Y/8)
// CH.COL = INT(X/8)
// CH.LINE = Y % 8
// BIT within byte = 7 - (X % 8)
// pixel address = ($E000 + CH.ROW*320 + CH.COL*8 + CH.LINE)
// pixel address = ($E000 + INT(Y/8)*320 + INT(X/8)*8 + (Y % 8))
// pixel address = ($E000 + ((Y & $FFF8) * $28) + (X & $F8) + (Y & 7))
// That is, there are 320 bytes per 320x8 pixel stripe (or line)
// Each byte within the stripe is 8 vertical pixels

        .setcpu "6502"

basic_check_for_comma     := $AEFD // BASIC Check for comma
basic_illegal_qty         := $B248 // BASIC routine which emits ?ILLEGAL QUANTITY
basic_evaluate_x          := $B79E // BASIC Evaluate into X register
basic_get_adr_get_byte    := $B7EB // BASIC Get 16-bit address (into $14/$15) and get byte into X

COLOUR_MEMORY     := $C000 // fg/bg colour - up to $C3E7
COLOUR_MEMORY_END := $C3E7
PIXEL_MEMORY      := $E000 // Shadows KERNAL ROM - we write, but must set HIRAM to read
PIXEL_MEMORY_END  := $FF3F

// Free zero-page space
ZP_ADDR_LOW       := $FB
ZP_ADDR_HI        := $FC
FREE_ZEROPAGE_2   := $FD
FREE_ZEROPAGE_3   := $FE

//
// Global variables, from C400 to C412
//
DRAW_MODE         := $C400 // 0 = flip, 1 = draw, 2 = erase
START_XCOORD_LO   := $C401 // current 16-bit X co-ord
START_XCOORD_HI   := $C402 //
START_YCOORD      := $C403 // current 8-bit Y co-ord
UNKNOWN_VALUE_1   := $C404
END_XCOORD_LO     := $C405 // end of line 16-bit X co-ord
END_XCOORD_HI     := $C406 //
END_YCOORD        := $C407 // end of line 8-bot Y co-ord
COLOUR            := $C409 // FG hi-nibble, BG lo-nibble
UNKNOWN_VALUE_2   := $C40A
UNKNOWN_VALUE_3   := $C40B
UNKNOWN_VALUE_4   := $C40C
UNKNOWN_VALUE_5   := $C40D
UNKNOWN_VALUE_6   := $C40E
UNKNOWN_VALUE_7   := $C40F
UNKNOWN_VALUE_8   := $C410
UNKNOWN_VALUE_9   := $C411
UNKNOWN_VALUE_A   := $C412

bitmap_on:
        jmp     fn_bitmap_on

bitmap_off:
        jmp     fn_bitmap_off

plot_point:
        jmp     fn_plot_point

draw_line:
        jmp     fn_draw_line

clear_bmp:
        jmp     fn_clear_bmp

set_colour:
        jmp     fn_set_colour

//
// plot_point routine
// Arguments from from SYS command parameters: ,X,Y,mode
//
fn_plot_point:
        lda     #$00
        sta     UNKNOWN_VALUE_1
        jsr     basic_check_for_comma
        jsr     basic_get_adr_get_byte     // Read 8-bit Y-cordinate into X register and 16-bit X co-ord into $14/$15
        cpx     #$C8                       // Check if Y coord <= 200
        bcc     LC437
fn_plot_point_err:
        jmp     fn_error
LC437:  stx     START_YCOORD               // Store y-coordinate in START_YCOORD
        ldx     $14                        // get X co-ord
        lda     $15                        //
        beq     LC448                      // If x co-ord < 255 (i.e. high-byte == 0), that's OK
        cmp     #$01                       // if x co-ord >= 512 (i.e. high byte != 1), error
        bne     fn_plot_point_err
        cpx     #$40                       // If x co-ord >= 256, check x co-ord < 320
        bcs     fn_plot_point_err
LC448:  sta     START_XCOORD_HI            // Store x co-ord in START_XCOORD_LO/START_XCOORD_HI
        stx     START_XCOORD_LO
        jsr     basic_check_for_comma      // Now store mode (flip, draw or erase) in X
        jsr     basic_evaluate_x
        cpx     #$03                       // Check mode is < 3
        bcs     fn_plot_point_err
        stx     DRAW_MODE                  // Store mode in DRAW_MODE
// Mode (flip=0, draw=1 or erase=2) must be in DRAW_MODE
// X is in START_XCOORD_LO/START_XCOORD_HI
// Y is in START_YCOORD
set_pixel_mode_switch:
        lda     DRAW_MODE                  // Load mode from DRAW_MODE
        beq     flip_pixel                 // If mode == FLIP, goto flip_pixel
        lsr     a                          // divide mode by 2
        bcc     erase_pixel                // If mode == ERASE (i.e. carry clear), goto erase_pixel
draw_pixel:
        jsr     fn_get_pixel_addr           // START_XCOORD,START_Y_COORD => ZP_ADDR
        jsr     fn_load_pixel_byte          // Load pixel byte from ZP_ADDR and store byte index in X
        ora     data_bit_mirror_table,x     // OR => Set bit in pixel byte
        sta     (ZP_ADDR_LOW),y             // Store modified byte in pixel memory
        rts
erase_pixel:
        jsr     fn_get_pixel_addr           // START_XCOORD,START_Y_COORD => ZP_ADDR
        jsr     fn_load_pixel_byte          // Load pixel byte from ZP_ADDR and store byte index in X
        and     data_nbit_mirror_table,x    // AND => Clear bit in pixel byte
        sta     (ZP_ADDR_LOW),y             // Store modified byte in pixel memory
        rts
flip_pixel:
        jsr     fn_get_pixel_addr           // START_XCOORD,START_Y_COORD => ZP_ADDR
        jsr     fn_load_pixel_byte          // Load pixel byte from ZP_ADDR and store byte index in X
        eor     data_bit_mirror_table,x     // XOR => Flip pixel in pixel byte
        sta     (ZP_ADDR_LOW),y             // Store modified byte in pixel memory
        rts
//
// Load pixel byte and get bit index routine
// Output:
//    X = X co-ord modulo 8 (i.e. selects the bit in the pixel byte, but it needs flipping)
//    A = pixel byte loaded from ZP_ADDR
//
// Note on address $0001:
//    bit 0 : LORAM (0=RAM 1=BASIC_ROM at A000)
//    bit 1 : HIRAM (0=RAM or 1=KERNAL_ROM at E000)
//    bit 2 : CHAREN (0=CHAR_RAM/CHAR_ROM or 1=I/O at D000)
//    the other bits are Datasette related
//
//    So $34 banks all three RAMs in
//       $37 is the BASIC default (BASIC ROM, I/O and KERNAL ROM)
//
// We've stuck our frame buffer at E000 behind KERNAL ROM to save space, hence the bank switch to read it
fn_load_pixel_byte:
        lda     START_XCOORD_LO            // A = X co-ord % 8
        and     #$07                       //
        tax                                // X = A
        sei                                // disable interrupts
        ldy     #$34                       //
        sty     $01                        // write $34 to $0001 - banks RAM into all three regions
        ldy     #$00                       // Read pixel data from pixel memory
        lda     (ZP_ADDR_LOW),y            // A = *ZP_ADDR
        ldy     #$37                       // write $37 to $0001 - back to BASIC/IO/KERNAL
        sty     $01                        //
        cli                                // enable interrupts
        ldy     #$00                       //
        rts                                //

//
// Bit mirror table - gives bit to set in pixel byte for given X % 8
// i.e. value = 1 << (7 - index) for index e 0..7
data_bit_mirror_table:
        .byte   $80     // 1 << 7
        .byte   $40     // 1 << 6
        .byte   $20     // 1 << 5
        .byte   $10     // 1 << 4
        .byte   $08     // 1 << 3
        .byte   $04     // 1 << 2
        .byte   $02     // 1 << 1
        .byte   $01     // 1 << 0

data_nbit_mirror_table:
        .byte   $7F    // ~(1 << 7)
        .byte   $BF    // ~(1 << 6)
        .byte   $DF    // ~(1 << 5)
        .byte   $EF    // ~(1 << 4)
        .byte   $F7    // ~(1 << 3)
        .byte   $FB    // ~(1 << 2)
        .byte   $FD    // ~(1 << 1)
        .byte   $FE    // ~(1 << 0)

//
// fn_get_pixel_addr routine
// Turns START_XCOORD/START_YCOORD into a pixel memory address
// Result comes back in ZP_ADDR
//
fn_get_pixel_addr:
        lda     #$00                  //
        sta     ZP_ADDR_LOW           // ZP_ADDR = $E000 (start of pixel memory)
        lda     #$E0                  //
        sta     ZP_ADDR_HI            //
        lda     START_XCOORD_LO       // read START_XCOORD_LO
        and     #$F8                  // get (INT(X/8) * 8)
        clc                           // clear carry flag
        adc     ZP_ADDR_LOW           // add memory to accumulator with carry
        sta     ZP_ADDR_LOW           // *ZP_ADDR_LOW = *ZP_ADDR_LOW + (*START_XCOORD_LO & 0xF8)
        lda     START_XCOORD_HI       //
        adc     ZP_ADDR_HI            //
        sta     ZP_ADDR_HI            // *ZP_ADDR_HI = *ZP_ADDR_HI + (*START_XCOORD_HI)
        lda     START_YCOORD          //
        pha                           // Keep original Y-coord
        and     #$07                  // get Y co-ord % 8
        clc                           //
        adc     ZP_ADDR_LOW           // Add to *ZP_ADDR_LOW
        sta     ZP_ADDR_LOW           //
        bcc     LC4D6                 // If wrapped, increment *ZP_ADDR_HI
        inc     ZP_ADDR_HI            //
LC4D6:  pla                           // Get original (unmasked) value of START_YCOORD back
        lsr     a                     // Divide it by 8 to give the character row we need
        lsr     a                     //
        lsr     a                     //
        // Now we need to multiply A by 320
        asl     a                     // Double it, because data_mult_32 is a table of 16-bit values
        tax                           //
        lda     data_mult_320_low,x   // Get the low byte from the table
        clc                           //
        adc     ZP_ADDR_LOW           // Add it to *ZP_ADDR_LOW
        sta     ZP_ADDR_LOW           //
        lda     data_mult_320_hi,x    // Get the high byte from the table
        adc     ZP_ADDR_HI            // Add it to *ZP_ADDR_HI
        sta     ZP_ADDR_HI            //
        rts                           //

// This is the 320 times table, as 16-bit values
// It's used for converting a character row (in a 40x25 screen) into a byte offset
// as there are 320 bytes per character row
// $0000 $0140 $0280 $03C0 etc
data_mult_320_low:  .byte $00
data_mult_320_hi:  .byte $00
        .byte $40
        .byte $01
        .byte $80
        .byte $02
        .byte $c0
        .byte $03
        .byte $00
        .byte $05
        .byte $40
        .byte $06
        .byte $80
        .byte $07
        .byte $c0
        .byte $08
        .byte $00
        .byte $0a
        .byte $40
        .byte $0b
        .byte $80
        .byte $0c
        .byte $c0
        .byte $0d
        .byte $00
        .byte $0f
        .byte $40
        .byte $10
        .byte $80
        .byte $11
        .byte $c0
        .byte $12
        .byte $00
        .byte $14
        .byte $40
        .byte $15
        .byte $80
        .byte $16
        .byte $c0
        .byte $17
        .byte $00
        .byte $19
        .byte $40
        .byte $1a
        .byte $80
        .byte $1b
        .byte $c0
        .byte $1c
        .byte $00
        .byte $1e

//
// clear_bmp routine
// No arguments
//
// Writes $00 to $E000..$FFFF
// Writes $100 bytes (y) $20 times (x)
//
fn_clear_bmp:
        ldx     #$20
        lda     #$E0
        sta     ZP_ADDR_HI
        lda     #$00
        sta     ZP_ADDR_LOW     // write $E000 to ZP_ADDR_LOW/ZP_ADDR_HI
        tay
LC529:  sta     (ZP_ADDR_LOW),y // loop y 0..ff
        iny
        bne     LC529
        inc     ZP_ADDR_HI
        dex             // loop x 20..1
        bne     LC529
        rts
//
// load_colour sub-routine
// Read a value from the SYS command
// into X and check it's less than 16
//
fn_load_colour_param:
        jsr     basic_check_for_comma
        jsr     basic_evaluate_x
        cpx     #$10
        bcc     LC541
        jmp     fn_error
LC541:  rts

//
// set_colour routine
// Arguments from from SYS command parameters: ,FG,BG
// Modifies colour memory in $C200 to $C3E7
//
sfn_set_colour:
        lda     #$C0                     // $C000 in ZP_ADDR_LOW,ZP_ADDR_HI
        sta     ZP_ADDR_HI
        lda     #$00
        sta     ZP_ADDR_LOW
        jsr     fn_load_colour_param     // Read fg colour
        txa                              // into A
        asl     a
        asl     a
        asl     a
        asl     a                        // Multiply it by 16 (move it to high nibble)
        sta     COLOUR                   // Write to C409
        jsr     fn_load_colour_param     // Read bg colour
        txa                              // into A
        ora     COLOUR                   // Add to fg colour
// There are $3E8 colour cells (each represents an 8x8) in the
// hi-res image. Write the same colour to all of them. 4-bits background/4-bits foreground.
        ldx     #$02
        ldy     #$00
LC560:  sta     (ZP_ADDR_LOW),y          // Write colour to $C000..$C2FF (x=0..2, y=0..255)
        iny
        bne     LC560
        inc     ZP_ADDR_HI
        dex
        bpl     LC560
LC56A:  sta     (ZP_ADDR_LOW),y          // Write another $E8 bytes, up to $C3E7
        iny
        cpy     #$E8
        bcc     LC56A
        rts
//
// draw_line routine
// Arguments from from SYS command parameters: ,X,Y,mode
//
fn_draw_line:
        lda     #$00
        sta     $C408                      // Store 0 in $C408
        sta     UNKNOWN_VALUE_2            // Store 0 in UNKNOWN_VALUE_2
        jsr     basic_check_for_comma
        jsr     basic_get_adr_get_byte     // Read 8-bit Y-cordinate into X register and 16-bit X co-ord into $14/$15
        cpx     #$C8                       // Check Y co-ord is < 200
        bcc     LC587
LC584:  jmp     fn_error
LC587:  stx     END_YCOORD                 // Store Y co-ord in END_YCOORD
        ldx     $14                        // Get X co-ord
        lda     $15                        // Check X co-ord is < 256, or >= 256 and < 320
        beq     LC598                      // --""--
        cmp     #$01                       // --""--
        bne     LC584                      // --""--
        cpx     #$40                       // --""--
        bcs     LC584                      // --""--
LC598:  sta     END_XCOORD_HI              // Store 16-bit X co-ord in END_XCOORD
        stx     END_XCOORD_LO
        jsr     basic_check_for_comma
        jsr     basic_evaluate_x
        cmp     #$03                       // Load mode into X and check < 3
        bcs     LC584
        stx     DRAW_MODE                  // Store draw mode
        lda     END_XCOORD_LO
        sec
        sbc     START_XCOORD_LO
        sta     UNKNOWN_VALUE_4
        lda     END_XCOORD_HI
        sbc     START_XCOORD_HI
        sta     UNKNOWN_VALUE_5
        bpl     LC5D4
        dec     UNKNOWN_VALUE_2
        sec
        lda     #$00
        sbc     UNKNOWN_VALUE_4
        sta     UNKNOWN_VALUE_4
        lda     #$00
        sbc     UNKNOWN_VALUE_5
        sta     UNKNOWN_VALUE_5
LC5D4:  lda     #$00
        sta     UNKNOWN_VALUE_3
        lda     END_YCOORD
        sec
        sbc     START_YCOORD
        sta     UNKNOWN_VALUE_6
        lda     $C408
        sbc     UNKNOWN_VALUE_1
        sta     UNKNOWN_VALUE_7
        bpl     LC602
        dec     UNKNOWN_VALUE_3
        sec
        lda     #$00
        sbc     UNKNOWN_VALUE_6
        sta     UNKNOWN_VALUE_6
        lda     #$00
        sbc     UNKNOWN_VALUE_7
        sta     UNKNOWN_VALUE_7
LC602:  lda     #$00
        sta     UNKNOWN_VALUE_A
        lda     UNKNOWN_VALUE_6
        sec
        sbc     UNKNOWN_VALUE_4
        lda     UNKNOWN_VALUE_7
        sbc     UNKNOWN_VALUE_5
        bcc     LC631
        ldx     UNKNOWN_VALUE_6
        lda     UNKNOWN_VALUE_4
        sta     UNKNOWN_VALUE_6
        stx     UNKNOWN_VALUE_4
        ldx     UNKNOWN_VALUE_7
        lda     UNKNOWN_VALUE_5
        sta     UNKNOWN_VALUE_7
        stx     UNKNOWN_VALUE_5
        dec     UNKNOWN_VALUE_A
LC631:  lda     UNKNOWN_VALUE_4
        sta     UNKNOWN_VALUE_8
        lda     UNKNOWN_VALUE_5
        sta     UNKNOWN_VALUE_9
        jsr     set_pixel_mode_switch
LC640:  lda     UNKNOWN_VALUE_A
        bne     LC657
        lda     START_XCOORD_LO
        cmp     END_XCOORD_LO
        bne     LC668
        lda     START_XCOORD_HI
        cmp     END_XCOORD_HI
        bne     LC668
        beq     LC667
LC657:  lda     START_YCOORD
        cmp     END_YCOORD
        bne     LC668
        lda     UNKNOWN_VALUE_1
        cmp     $C408
        bne     LC668
LC667:  rts

LC668:  lda     UNKNOWN_VALUE_A
        bne     LC673
        jsr     LC6C0
        jmp     LC676

LC673:  jsr     LC6DA
LC676:  jsr     LC698
        jsr     LC698
        bpl     LC692
        lda     UNKNOWN_VALUE_A
        bne     LC689
        jsr     LC6DA
        jmp     LC68C

LC689:  jsr     LC6C0
LC68C:  jsr     LC6AC
        jsr     LC6AC
LC692:  jsr     set_pixel_mode_switch
        jmp     LC640

LC698:  lda     UNKNOWN_VALUE_8
        sec
        sbc     UNKNOWN_VALUE_6
        sta     UNKNOWN_VALUE_8
        lda     UNKNOWN_VALUE_9
        sbc     UNKNOWN_VALUE_7
        sta     UNKNOWN_VALUE_9
        rts

LC6AC:  lda     UNKNOWN_VALUE_8
        clc
        adc     UNKNOWN_VALUE_4
        sta     UNKNOWN_VALUE_8
        lda     UNKNOWN_VALUE_9
        adc     UNKNOWN_VALUE_5
        sta     UNKNOWN_VALUE_9
        rts

LC6C0:  lda     UNKNOWN_VALUE_2
        bne     LC6CE
        inc     START_XCOORD_LO
        bne     LC6CD
        inc     START_XCOORD_HI
LC6CD:  rts

LC6CE:  lda     START_XCOORD_LO
        bne     LC6D6
        dec     START_XCOORD_HI
LC6D6:  dec     START_XCOORD_LO
        rts

LC6DA:  lda     UNKNOWN_VALUE_3
        bne     LC6E8
        inc     START_YCOORD
        bne     LC6E7
        inc     UNKNOWN_VALUE_1
LC6E7:  rts

LC6E8:  lda     START_YCOORD
        bne     LC6F0
        dec     UNKNOWN_VALUE_1
LC6F0:  dec     START_YCOORD
        rts
//
// bitmap_on routine
// No arguments
//
fn_bitmap_on:
        lda     $DD00 // Clear bottom two bits
        and     #ZP_ADDR_HI  // of
        sta     $DD00 // cia2_data_port_a
        lda     #$3B  // Write $3B to
        sta     $D011 // vic_control_reg_1
        lda     #$08  // Write $08 to
        sta     $D018 // vic_memory_control_reg
        rts
//
// bitmap_off routine
// No arguments
//
fn_bitmap_off:
        lda     $DD00 // Set bottom two bits
        ora     #$03  // of
        sta     $DD00 // cia2_data_port_a
        lda     #$1B  // Write $1B to
        sta     $D011 // vic_control_reg_1
        lda     #$15  // Write $15 to
        sta     $D018 // vic_memory_control_reg
        rts

//
// Error handler.
// No arguments.
// Turns bitmap off and prints
// ?ILLEGAL QUANTITY
//
fn_error:
        jsr     fn_bitmap_off
        jmp     basic_illegal_qty