├── .gitignore ├── README.md ├── defs.asm ├── irq.asm ├── level.asm ├── macros.asm ├── main.asm └── screen.asm /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Smooth scrolling screen on Commodore 64 2 | 3 | Here is some sample code to implement horizontal smooth scrolling on a C64. 4 | 5 | ``` 6 | The algorithm in pseudo-code is: 7 | 8 | when_graphics_chip_is_at_line_3() { 9 | if (xscroll == 0) { 10 | shift_upper_half_of_color_ram(); 11 | } 12 | } 13 | 14 | when_graphics_chip_is_at_vblank() { 15 | xscroll--; 16 | if (xscroll == 4) { 17 | shift_upper_half_of_screen_ram_to_back_buffer(); 18 | } 19 | else if (xscroll == 2) { 20 | shift_lower_half_of_screen_ram_to_back_buffer(); 21 | } 22 | else if (xscroll < 0) { 23 | swap_screen_buffer(); 24 | shift_lower_half_of_color_ram(); 25 | draw_next_column_to_screen_and_color_ram() 26 | } 27 | } 28 | ``` 29 | 30 | See http://1amstudios.com/2014-12-07-c64-smooth-scrolling for more details 31 | 32 | [DustLayer](http://www.dustlayer.com) is a fanstastic site devoted to C64 internals with great descriptions and tutorials. Highly recommended! 33 | -------------------------------------------------------------------------------- /defs.asm: -------------------------------------------------------------------------------- 1 | 2 | * = $0800 3 | screen_back_buffer !fill 1000, $21 ; reserve 1000 bytes for our screen back buffer at 0x800 -------------------------------------------------------------------------------- /irq.asm: -------------------------------------------------------------------------------- 1 | ; consts 2 | irq_delay_default = 0 3 | FIRST_VIS_LINE = 50 4 | START_COPYING_UPPER_COLOR_RAM_LINE = 65 5 | BEGIN_VBLANK_LINE = 245 6 | SYSTEM_IRQ_HANDLER = $ea81 7 | 8 | 9 | ; vars 10 | xscroll !byte 7 11 | 12 | 13 | irq_setup sei ; disable interrupts 14 | ldy #$7f ; 01111111 15 | sty $dc0d ; turn off CIA timer interrupt 16 | lda $dc0d ; cancel any pending IRQs 17 | lda #$01 18 | sta $d01a ; enable VIC-II Raster Beam IRQ 19 | lda $d011 ; bit 7 of $d011 is the 9th bit of the raster line counter. 20 | and #$7f ; make sure it is set to 0 21 | sta $d011 22 | +set_raster_interrupt START_COPYING_UPPER_COLOR_RAM_LINE, irq_line_65 23 | cli ; enable interupts 24 | rts 25 | 26 | ; ----------------------------------------------------------------------- 27 | irq_line_65 28 | dec $d019 ; set interrupt handled flag 29 | lda xscroll 30 | bne *+5 ; if xscroll != 0, skip next line 31 | ; if we're on the last screen draw before swapping, 32 | ; copy top half of color RAM behind the beam 33 | jsr color_shift_upper 34 | 35 | +set_raster_interrupt BEGIN_VBLANK_LINE, irq_begin_vblank 36 | jmp SYSTEM_IRQ_HANDLER ; call system IRQ handler 37 | 38 | ; ----------------------------------------------------------------------- 39 | irq_begin_vblank 40 | dec $d019 ; set interrupt handled flag 41 | dec xscroll ; decrement xscroll 42 | 43 | bmi irq_swap_screens ; if xscroll wraps back past 0, we need to swap screen buffers 44 | ; and shift color RAM 45 | +update_x_scroll xscroll ; set softscroll register 46 | 47 | lda xscroll 48 | cmp #4 ; if x==4, copy to back buffer now 49 | bne *+8 ; otherwise, skip next 2 lines 50 | jsr screen_shift_upper ; copy top half of screen 51 | jmp irq_handler_exit 52 | 53 | cmp #2 ; if x==2, copy to back buffer 54 | bne irq_handler_exit 55 | jsr screen_shift_lower ; copy lower half of screen 56 | jmp irq_handler_exit 57 | 58 | irq_swap_screens 59 | lda #7 ; reset xscroll 60 | sta xscroll 61 | +update_x_scroll xscroll 62 | 63 | jsr screen_swap ; swap screen to back buffer 64 | jsr color_shift_lower ; shift lower color ram. 65 | jsr level_render_next_col ; draw new column at right edge of screen 66 | 67 | irq_handler_exit 68 | +set_raster_interrupt START_COPYING_UPPER_COLOR_RAM_LINE, irq_line_65 69 | jmp SYSTEM_IRQ_HANDLER ; call system IRQ handler 70 | 71 | -------------------------------------------------------------------------------- /level.asm: -------------------------------------------------------------------------------- 1 | ; Constants 2 | 3 | screen_ptr = $04 4 | color_ptr = $06 5 | screen_col = $08 6 | screen_ptr_dest = $0a 7 | color_ptr_dest = $0c 8 | 9 | COLOR_CHANGE_DELAY = 10 10 | 11 | ; Variables 12 | ceil_height !byte 0 13 | floor_height !byte 0 14 | message_index !byte 0 15 | current_color !byte 1 16 | current_color_change_delay !byte COLOR_CHANGE_DELAY 17 | level_data !byte 1,2,3,4,5,6,7,8,7,6,5,4,3,2,1 18 | level_index !byte 0 19 | 20 | str_message !scr "1amstudios.com ", 0 21 | 22 | 23 | level_init 24 | ldy #0 25 | +set16im 0, screen_col 26 | 27 | level_render_next_col 28 | +set16 screen_base, screen_ptr 29 | +set16im $d800, color_ptr 30 | +add16 screen_ptr, screen_col, screen_ptr ;increment screen row 31 | +add16 color_ptr, screen_col, color_ptr ;increment screen row 32 | inc screen_col 33 | 34 | dec current_color_change_delay 35 | bne level_render_col 36 | lda #COLOR_CHANGE_DELAY 37 | sta current_color_change_delay 38 | inc current_color 39 | 40 | level_render_col 41 | ldx level_index 42 | lda level_data, x ; load ceiling height 43 | sta ceil_height 44 | tax ; x now holds ceiling height 45 | 46 | level_render_ceil 47 | lda #$A0 48 | sta (screen_ptr), y ; output to screen 49 | lda current_color 50 | sta (color_ptr), y ; and color ram 51 | 52 | +add16im screen_ptr, 40, screen_ptr ; increment screen row 53 | +add16im color_ptr, 40, color_ptr ; increment color row 54 | 55 | dex ; decrement ceiling counter 56 | bne level_render_ceil ; if we're not at the bottom of the ceiling keep rendering the colum 57 | level_skip_ceil 58 | ldx level_index 59 | lda level_data, x ; load floor height 60 | sta floor_height 61 | inx ; increment level data index 62 | cpx #10 63 | bne *+4 ; skip next line if we don't want to wrap to start of level data again 64 | ; +4 (2 for bne, 2 for ldx) 65 | ldx #0 ; wrap around level data 66 | stx level_index ; store updated level data index 67 | 68 | lda #23 ; 69 | sec 70 | sbc ceil_height 71 | sbc floor_height 72 | tax ; x now holds the number of rows between end of ceiling and start of floor 73 | 74 | 75 | level_skip_to_floor 76 | lda #$20 77 | sta (screen_ptr), y ; output blank char to screen 78 | +add16im screen_ptr, 40, screen_ptr ; increment screen row 79 | +add16im color_ptr, 40, color_ptr ; increment color row 80 | dex 81 | bne level_skip_to_floor ; keep skipping rows until x == 0 82 | ldx floor_height ; X now holds height of floor 83 | level_render_floor 84 | lda #$A0 85 | sta (screen_ptr), y ; output char to screen 86 | lda current_color 87 | sta (color_ptr), y ; and color ram 88 | +add16im screen_ptr, 40, screen_ptr ; increment screen row 89 | +add16im color_ptr, 40, color_ptr ; increment color row 90 | dex ; decrement floor counter 91 | bne level_render_floor ; if we're not at the bottom of the floor keep rendering the column 92 | 93 | ; print message at bottom of screen 94 | ldx message_index 95 | lda str_message, x ; output char to screen 96 | sta (screen_ptr), y 97 | lda #1 98 | sta (color_ptr), y ; and color ram 99 | inx 100 | cpx #16 101 | bne *+4 ; jump over next 2 lines if we're not at 16 yet 102 | ldx #0 103 | stx message_index 104 | 105 | lda screen_col 106 | cmp #40 ; if we've rendered 40 columns, we're finished 107 | beq level_render_done 108 | 109 | jmp level_render_next_col ; and we haven't hit column 30 yet, render the next column 110 | 111 | level_render_done 112 | rts ; return from last jsr 113 | 114 | level_render_last_col ; this is used to render the rightmost column of the screen when we need 115 | ; new data to scroll in 116 | ldy #0 117 | +set16im 39, screen_col 118 | jsr level_render_next_col 119 | rts 120 | 121 | -------------------------------------------------------------------------------- /macros.asm: -------------------------------------------------------------------------------- 1 | ; Constants 2 | SYSTEM_IRQ_VECTOR = $314 3 | 4 | 5 | !macro set16im .value, .dest { ; store a 16bit constant to a memory location 6 | lda #<.value 7 | sta .dest 8 | lda #>.value 9 | sta .dest+1 10 | } 11 | 12 | !macro set16 .value, .dest { ; copy a 16bit memory location to dest 13 | lda .value 14 | sta .dest 15 | lda .value+1 16 | sta .dest+1 17 | } 18 | 19 | !macro add16im .n1, .n2, .result { ; add a 16bit constant to a memory location, store in result 20 | clc ; ensure carry is clear 21 | lda .n1+0 ; add the two least significant bytes 22 | adc #<.n2 23 | sta .result+0 24 | lda .n1+1 ; add the two most significant bytes 25 | adc #>.n2 26 | sta .result+1 27 | } 28 | 29 | !macro add16 .n1, .n2, .result { ; add 2 16bit memory locations, store in result 30 | clc 31 | lda .n1 32 | adc .n2 33 | sta .result+0 34 | lda .n1+1 35 | adc .n2+1 36 | sta .result+1 37 | } 38 | 39 | !macro set_raster_interrupt .line, .handler { 40 | sei ; disable interrupts 41 | lda #.line 42 | sta $d012 ; this is the raster line register 43 | +set16im .handler, SYSTEM_IRQ_VECTOR ; set system IRQ vector to our handler 44 | cli ; enable interrupts 45 | } 46 | 47 | !macro disable_x_scroll { ; set horizontal softscroll value to 0 48 | lda $d016 49 | and #$F8 50 | sta $d016 51 | } 52 | 53 | !macro update_x_scroll .xvalue { ; set horizontal softscroll value to xvalue 54 | lda $d016 55 | and #$F8 56 | clc 57 | adc .xvalue 58 | sta $d016 59 | } 60 | 61 | !macro debug_print .value, .column { ; put a char in the bottom line of the screen 62 | lda .value 63 | clc 64 | adc #$30 65 | sta $0400+40*24+.column 66 | sta $0800+40*24+.column 67 | lda #1 68 | sta $d800+40*24+.column 69 | } -------------------------------------------------------------------------------- /main.asm: -------------------------------------------------------------------------------- 1 | !cpu 6510 2 | !to "./build/test.prg",cbm 3 | 4 | !source "defs.asm" 5 | !source "macros.asm" 6 | 7 | * = $0801 ; BASIC start address (#2049) 8 | !byte $0d,$08,$dc,$07,$9e,$20,$34,$39 ; BASIC loader to start at $c000... 9 | !byte $31,$35,$32,$00,$00,$00 ; BASIC op-codes to execute 'SYS 49152' (49152 = 0xc000) 10 | 11 | 12 | * = $c000 ; start address for 6502 code 13 | 14 | init 15 | jsr screen_clear ; clear and initialize screen 16 | jsr screen_init 17 | jsr level_init ; draw initial level to screen 18 | 19 | jsr irq_setup ; initialize our IRQ 20 | 21 | jmp * ; loop while waiting for IRQs 22 | 23 | 24 | !source "screen.asm" 25 | !source "level.asm" 26 | !source "irq.asm" 27 | 28 | -------------------------------------------------------------------------------- /screen.asm: -------------------------------------------------------------------------------- 1 | ;vars 2 | screen_buffer_nbr !byte 0 3 | screen_base !word 0 ; plus 1 row 4 | screen_back_buffer_base !word 0 5 | raster_1 !byte 0 6 | raster_2 !byte 0 7 | 8 | ROWS_PER_COPY = 12 9 | 10 | ; --------- screen_clear was taken from https://gist.github.com/actraiser/8e335033b2622409bd96 ----- 11 | 12 | screen_clear ldx #$00 ; set X to zero (black color code) 13 | stx $d021 ; set background color 14 | stx $d020 ; set border color 15 | +set16im $0400, screen_base ; init screen pointers 16 | +set16im $0800, screen_back_buffer_base 17 | 18 | 19 | _screen_clear_loop 20 | lda #$20 ; #$20 is a blank char 21 | sta $0400,x ; fill four areas with 256 spacebar characters 22 | sta $0500,x 23 | sta $0600,x 24 | sta $06e8,x 25 | lda #$00 ; set foreground to black in Color Ram 26 | sta $d800,x 27 | sta $d900,x 28 | sta $da00,x 29 | sta $dae8,x 30 | inx 31 | bne _screen_clear_loop ; did X overflow to zero yet? 32 | rts ; return from this subroutine 33 | 34 | screen_init 35 | lda $d016 ; d016 is VIC-II control register. 36 | and #%11110111 ; un-set bit 3 to enable 38 column mode 37 | sta $d016 38 | rts 39 | 40 | screen_swap lda screen_buffer_nbr ; toggle screen_buffer_number between 0 and 1 41 | eor #$01 42 | sta screen_buffer_nbr 43 | bne screen_swap_to_1 44 | ;set screen ptr to screen 0 45 | lda $d018 ; top 4 bits of d018 holds the screen location in RAM 46 | and #$0f ; mask upper 4 bits 47 | ora #$10 ; set upper 4 bits to '1' 48 | sta $d018 49 | +set16im $0400, screen_base 50 | +set16im $0800, screen_back_buffer_base 51 | rts 52 | 53 | screen_swap_to_1 ;set screen ptr to screen 1 54 | lda $d018 55 | and #$0f 56 | ora #$20 57 | sta $d018 58 | +set16im $0800, screen_base 59 | +set16im $0400, screen_back_buffer_base 60 | rts 61 | 62 | screen_shift_lower ; copy the lower half of screen to back buffer 63 | +set16 screen_base, screen_ptr 64 | inc screen_ptr ; shift columns over by 1 while copying 65 | +set16 screen_back_buffer_base, screen_ptr_dest 66 | +add16im screen_ptr, 40*ROWS_PER_COPY, screen_ptr 67 | +add16im screen_ptr_dest, 40*ROWS_PER_COPY, screen_ptr_dest 68 | ldy #0 ; y is the current column 69 | ldx #ROWS_PER_COPY ; x is the nbr of rows to copy 70 | jmp video_ram_copy_line 71 | 72 | screen_shift_upper 73 | +set16 screen_base, screen_ptr 74 | inc screen_ptr ; shift columns over by 1 at the same as copying to back buffer 75 | +set16 screen_back_buffer_base, screen_ptr_dest 76 | ldy #0 ; y is the current column 77 | ldx #ROWS_PER_COPY ; x is the nbr of rows to copy 78 | 79 | video_ram_copy_line ; copy a line of screen data to back buffer 80 | ; we unroll the loop for better performance 81 | lda (screen_ptr), y 82 | sta (screen_ptr_dest), y 83 | iny 84 | lda (screen_ptr), y 85 | sta (screen_ptr_dest), y 86 | iny 87 | lda (screen_ptr), y 88 | sta (screen_ptr_dest), y 89 | iny 90 | lda (screen_ptr), y 91 | sta (screen_ptr_dest), y 92 | iny 93 | lda (screen_ptr), y 94 | sta (screen_ptr_dest), y 95 | iny 96 | lda (screen_ptr), y 97 | sta (screen_ptr_dest), y 98 | iny 99 | lda (screen_ptr), y 100 | sta (screen_ptr_dest), y 101 | iny 102 | lda (screen_ptr), y 103 | sta (screen_ptr_dest), y 104 | iny 105 | lda (screen_ptr), y 106 | sta (screen_ptr_dest), y 107 | iny 108 | 109 | cpy #36 110 | bne video_ram_copy_line 111 | 112 | ; now copy the last 3 bytes 113 | lda (screen_ptr), y 114 | sta (screen_ptr_dest), y 115 | iny 116 | lda (screen_ptr), y 117 | sta (screen_ptr_dest), y 118 | iny 119 | lda (screen_ptr), y 120 | sta (screen_ptr_dest), y 121 | 122 | dex ; have we copied all the rows? 123 | beq video_ram_copy_done 124 | 125 | ldy #0 ; reset column number 126 | +add16im screen_ptr, 40, screen_ptr 127 | +add16im screen_ptr_dest, 40, screen_ptr_dest 128 | jmp video_ram_copy_line ; copy another line 129 | 130 | video_ram_copy_done 131 | rts 132 | 133 | color_shift_upper ; copy color RAM. Uses same code as screen_shift 134 | +set16im $d801, screen_ptr 135 | +set16im $d800, screen_ptr_dest 136 | ldy #0 ; y is the current column 137 | ldx #ROWS_PER_COPY ; nbr of rows to copy 138 | jmp video_ram_copy_line 139 | 140 | color_shift_lower 141 | +set16im $d801+40*ROWS_PER_COPY, screen_ptr 142 | +set16im $d800+40*ROWS_PER_COPY, screen_ptr_dest 143 | ldy #0 ; y is the current column 144 | ldx #ROWS_PER_COPY ; nbr of rows to copy 145 | jmp video_ram_copy_line --------------------------------------------------------------------------------