Thursday, 14 September 2017

Minigraph disassembly complete!

So I've completed pulling apart MINIGRAPH (see my last blog post). Looks like it performs an integer version of Bresenham's Algorithm (what else?) to do the line drawing.

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

// 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
// Total framebuffer memory is 8000 bytes (320 bytes/line x 25 lines)
// We can set the foreground and background colour of each 8x8 cell
// but MINIGRAPH only lets you set a single colour.

        .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
CURRENT_X_LO        := $C401 // current 16-bit X co-ord
CURRENT_X_HI        := $C402 // -------- "" --------
CURRENT_Y_LO        := $C403 // current 16-bit Y co-ord
CURRENT_Y_HI        := $C404 // -------- "" --------
END_X_LO          := $C405 // end of line 16-bit X co-ord
END_X_HI          := $C406 // -------- "" --------
END_Y_LO          := $C407 // end of line Y co-ord
END_Y_HI          := $C408 // -------- "" --------
COLOUR            := $C409 // FG hi-nibble, BG lo-nibble
DEC_BINC_X        := $C40A // False/0 = Increment, True/$FF = Decrement
DEC_BINC_Y        := $C40B // False/0 = Increment, True/$FF = Decrement
DELTA_X_LO        := $C40C // Length of line in X direction
DELTA_X_HI        := $C40D
DELTA_Y_LO        := $C40E // Length of line in Y direction
DELTA_Y_HI        := $C40F
ERROR_ACC_LO  := $C410 // Larger of DELTA_X and DELTA_Y
ERROR_ACC_HI  := $C411
DELTA_Y_LARGER        := $C412 // True if delta-y > delta-x

// ------------------------------------------------------
//
// Entry points:
//
// TURN ON BITMAP -   SYS 50195
// TURN OFF BITMAP -  SYS 50198
// PLOT POINT @ X,Y - SYS 50201,X,Y,M
// DRAW LINE TO X,Y - SYS 50204,X,Y,M
// CLEAR BITMAP -     SYS 50207
// COLOUR BITMAP -    SYS 50210,F,B
//
// ------------------------------------------------------
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     CURRENT_Y_HI            // Zero high byte of CURRENT_Y
        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     CURRENT_Y_LO            // Store y-coordinate in CURRENT_Y_LO
        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     CURRENT_X_HI            // Store x co-ord in CURRENT_X_LO/CURRENT_X_HI
        stx     CURRENT_X_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 CURRENT_X_LO/CURRENT_X_HI
// Y is in CURRENT_Y_LO/CURRENT_Y_HI
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           // CURRENT_X,CURRENT_Y_ => 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           // CURRENT_X,CURRENT_Y_ => 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           // CURRENT_X,CURRENT_Y_ => 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     CURRENT_X_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 CURRENT_X/CURRENT_Y_LO 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     CURRENT_X_LO       // read CURRENT_X_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 + (*CURRENT_X_LO & 0xF8)
        lda     CURRENT_X_HI       //
        adc     ZP_ADDR_HI            //
        sta     ZP_ADDR_HI            // *ZP_ADDR_HI = *ZP_ADDR_HI + (*CURRENT_X_HI)
        lda     CURRENT_Y_LO          //
        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 CURRENT_Y_LO 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 1000 (40 x 25) bytes of colour memory in $C000 to $C3E7
//
// ------------------------------------------------------
fn_set_colour:
        lda     #$C0                     // COLOUR_MEMORY 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 1000 ($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 COLOUR_RAM[$0000..$02FF] (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, to COLOUR_RAM[$0300..$03E7]
        iny
        cpy     #$E8
        bcc     LC56A
        rts
// ------------------------------------------------------
//
// draw_line routine
// Arguments from from SYS command parameters: ,X,Y,mode
//
// ------------------------------------------------------
fn_draw_line:
        lda     #$00
        sta     END_Y_HI                   // END_Y_HI = 0
        sta     DEC_BINC_X                 // DEC_BINC_X = Increment (Assume CURRENT_X < END_X)
        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_Y_LO                   // Store Y co-ord in END_Y_LO
        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_X_HI                   // Store 16-bit X co-ord in END_X
        stx     END_X_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_X_LO
        sec
        sbc     CURRENT_X_LO
        sta     DELTA_X_LO                 // DELTA_X_LO = END_X_LO - CURRENT_X_LO
        lda     END_X_HI
        sbc     CURRENT_X_HI
        sta     DELTA_X_HI                 // DELTA_X_HI = END_X_HI - CURRENT_X_HI
        bpl     LC5D4                      // Unless DELTA_X is positive
        dec     DEC_BINC_X                 //   Set to decrement mode for X
        sec
        lda     #$00
        sbc     DELTA_X_LO                 //   DELTA_X_LO = -DELTA_X_LO
        sta     DELTA_X_LO
        lda     #$00
        sbc     DELTA_X_HI                 //   DELTA_X_HI = -DELTA_X_HI
        sta     DELTA_X_HI

LC5D4:  lda     #$00                       // DEC_BINC_X = Increment (Assume CURRENT_Y < END_Y)
        sta     DEC_BINC_Y
        lda     END_Y_LO
        sec                                // Calculate deltas (as per X above)
        sbc     CURRENT_Y_LO
        sta     DELTA_Y_LO
        lda     END_Y_HI
        sbc     CURRENT_Y_HI
        sta     DELTA_Y_HI
        bpl     LC602                      // Unless DELTA_Y is positive
        dec     DEC_BINC_Y                 //   Switch to decrement mode
        sec                                //   Negate Delta Y
        lda     #$00
        sbc     DELTA_Y_LO
        sta     DELTA_Y_LO
        lda     #$00
        sbc     DELTA_Y_HI
        sta     DELTA_Y_HI
LC602:  lda     #$00
        sta     DELTA_Y_LARGER            // DELTA_Y_LARGER = False
        lda     DELTA_Y_LO
        sec
        sbc     DELTA_X_LO                // Check if DELTA_X or DELTA_Y is larger
        lda     DELTA_Y_HI                // By performing DELTA_Y - DELTA_X
        sbc     DELTA_X_HI
        bcc     LC631                     // Unless DELTA_X > DELTA_Y:
        ldx     DELTA_Y_LO                //   swap DELTA_X and DELTA_Y (because DELTA_X <= DELTA_Y)
        lda     DELTA_X_LO
        sta     DELTA_Y_LO
        stx     DELTA_X_LO
        ldx     DELTA_Y_HI
        lda     DELTA_X_HI
        sta     DELTA_Y_HI
        stx     DELTA_X_HI
        dec     DELTA_Y_LARGER            //   Set DELTA_Y_LARGER to True
LC631:  lda     DELTA_X_LO                // Pre-load ERROR_ACC with the larger delta
        sta     ERROR_ACC_LO
        lda     DELTA_X_HI
        sta     ERROR_ACC_HI

// Line drawing loop
// At this point DELTA_X is now the larger of the actual DELTA_X/DELTA_Y
// DELTA_Y is the smaller of the actual DELTA_X/DELTA_Y. But we can ignore
// that and assume we're in the first octant. This routine now performs an
// integer version of Bresenham's Algorithm. Note that instead of comparing
// with 0.5, things are doubled so we add or subtract each delta twice
//
//        \    |    /
//          \  |  /
//            \|/ HERE
//       ------o------
//            /|\
//          /  |  \
//        /    |    \
//
        jsr     set_pixel_mode_switch     // Write pixel at CURRENT_X/CURRENT_Y
finish_check:
// Check if we're at the end of the line yet
// If DELTA_Y_LARGER, check Y co-ord, else check X co-ord
        lda     DELTA_Y_LARGER            // If NOT(DELTA_Y_LARGER)
        bne     LC657
        lda     CURRENT_X_LO              //   Compare CURRENT_X and END_X (the longer axis)
        cmp     END_X_LO
        bne     do_maths                  //   If not equal, draw some more
        lda     CURRENT_X_HI
        cmp     END_X_HI
        bne     do_maths                  //   If not equal, draw some more
        beq     exit_fn                   //   They are equal, so terminate line draw

LC657:  lda     CURRENT_Y_LO              // Else, compare CURRENT_Y and END_Y (the longer axis)
        cmp     END_Y_LO
        bne     do_maths                  //   If not equal, draw some more
        lda     CURRENT_Y_HI
        cmp     END_Y_HI
        bne     do_maths                  //   If not equal, draw some more
exit_fn:
        rts                               // At end of line - leave
do_maths:
        lda     DELTA_Y_LARGER            // If NOT(DELTA_Y_LARGER)
        bne     LC673
        jsr     fn_inc_dec_x              //     Move along X (long axis)
        jmp     LC676                     // Else
LC673:  jsr     fn_inc_dec_y              //     Move along Y (long axis)

LC676:  jsr     fn_subtract_delta_y       // ERROR_ACC -= 2 * SMALLER_DELTA
        jsr     fn_subtract_delta_y

        bpl     LC692                     // If NOT(ERROR_ACC is +ve)

        lda     DELTA_Y_LARGER            //     If NOT(DELTA_Y_LARGER)
        bne     LC689
        jsr     fn_inc_dec_y              //         Move along Y (short axis)
        jmp     LC68C                     //     Else
LC689:  jsr     fn_inc_dec_x              //         Move along X (short axis)

LC68C:  jsr     fn_add_delta_x            //     ERROR_ACC += 2 * LARGER_DELTA
        jsr     fn_add_delta_x

LC692:  jsr     set_pixel_mode_switch     // Set pixel

        jmp     finish_check              // Repeat this loop

// ------------------------------------------------------
// ERROR_ACC -= DELTA_Y
// ------------------------------------------------------
fn_subtract_delta_y:
        lda     ERROR_ACC_LO
        sec
        sbc     DELTA_Y_LO
        sta     ERROR_ACC_LO   // ERROR_ACC_LO -= DELTA_Y_LO
        lda     ERROR_ACC_HI
        sbc     DELTA_Y_HI
        sta     ERROR_ACC_HI   // ERROR_ACC_HI -= DELTA_Y_HI
        rts

// ------------------------------------------------------
// ERROR_ACC += DELTA_X
// ------------------------------------------------------
fn_add_delta_x:
        lda     ERROR_ACC_LO
        clc
        adc     DELTA_X_LO
        sta     ERROR_ACC_LO    // ERROR_ACC_LO += DELTA_X_LO
        lda     ERROR_ACC_HI
        adc     DELTA_X_HI
        sta     ERROR_ACC_HI    // ERROR_ACC_HI += DELTA_X_HI
        rts

// ------------------------------------------------------
// Inc/Dec X Co-ordinate
//
// If DEC_BINC_X is True, decrement
// If DEC_BINC_X is False, increment
// ------------------------------------------------------
fn_inc_dec_x:
        lda     DEC_BINC_X
        bne     LC6CE
        inc     CURRENT_X_LO
        bne     LC6CD
        inc     CURRENT_X_HI
LC6CD:  rts
LC6CE:  lda     CURRENT_X_LO
        bne     LC6D6
        dec     CURRENT_X_HI
LC6D6:  dec     CURRENT_X_LO
        rts

// ------------------------------------------------------
// Inc/Dec Y Co-ordinate
//
// If DEC_BINC_Y is True, decrement
// If DEC_BINC_Y is False, increment
// ------------------------------------------------------
fn_inc_dec_y:
        lda     DEC_BINC_Y
        bne     LC6E8
        inc     CURRENT_Y_LO      // Inc low byte
        bne     LC6E7           // If it wraps...
        inc     CURRENT_Y_HI      // Inc high byte
LC6E7:  rts
LC6E8:  lda     CURRENT_Y_LO      // Check low byte
        bne     LC6F0           // If zero..
        dec     CURRENT_Y_HI      // Dec high byte too
LC6F0:  dec     CURRENT_Y_LO      // Either way, dec low byte
        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

// ------------------------------------------------------
// End of file
// ------------------------------------------------------

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

Sunday, 20 March 2011

Showing the contents of CHAR ROM

While reading the Commodore 64 Programmer's Reference Guide (PRG), page 109 talked about reading Character ROM. I felt inspired, and wrote this:


1 rem prints char rom contents as
2 rem pretty strings
3 rem by thejpster 2011/03/20 
10 print "enter letter or space to quit"
20 get a$:if a$="" then 6
30 if a$=" " then end
39 turn petscii into screen code
40 z%=asc(a$) : gosub 2000
49 rem disable interrupts
50 poke 56334,peek(56334)and254
59 rem map char rom into $d000
60 poke 1,peek(1)and251
69 rem read char data
70 for i=0 to 7
80 a%(i)=peek(53248+(z%*8)+i)
90 next
99 rem put $d000 back to i/o
100 poke 1,peek(1)or4
109 rem re-enable interrupts
110 poke 56334,peek(56334)or1
120 for i=0 to 7
130 z%=a%(i) : gosub 1000
140 next
150 goto 10
1000 rem prints a byte in z% as *s
1010 p$="" : v%=256
1020 for b=7 to 0 step -1
1030 v%=v% / 2
1040 if z% and v% then 1060
1050 p$=p$+" ": goto 1070
1060 p$=p$+"*"
1070 next
1080 print p$
1090 return
2000 rem PETSCII to screen code
2010 if z% <= 31 then z%=z%+128:return
2020 if z% <= 63 then return
2030 if z% <= 95 then z%=z%-64:return
2040 if z% <= 127 then z%=z%-32:return
2050 if z% <= 159 then z%=z%+64:return
2060 if z% <= 191 then z%=z%-64:return
2070 if z% <= 223 then z%=z%-128:return
2080 return


The whole thing was a useful exercise in reminding myself of C64 basic and getting files in and out of VICE (a C64 emulator), using petcat and c1541.

It looks like this:



Some useful commands I've learned:
jonathan@laptop$ x64 # start vice, create new disc, save code
jonathan@laptop$ c1541
c1541 #8> attach my_disk.d64
c1541 #8> dir
0 "                "    2a
1     "hello           "  prg 
4     "big letters     "  prg 
659 blocks free.
c1541 #8> read "big letters" "big letters.prg"
c1541 #8> exit
jonathan@laptop$ petcat -o big_letters.txt -- "big letters.prg"