mandel-6502/mandel-core.s
Brooke Vibber 65563b0e79 Decent speedup in XE mode
Noticed that the separate addition for the low 1 bit case was
doing some dupe memory loads. Ended up making separate code
paths for even and odd values so the even saves two cycles
(from 52 down to 50 cycles) and the odd saves 14 cycles (from
69 down to 55 cycles). nice!

This gets the XE runtime on default view down from 3m38s to 3m33s,
a 5 second runtime improvement
2026-08-22 16:45:09 -07:00

2214 lines
41 KiB
ArmAsm

.zeropage
ox: .res 4 ; fixed6.26: center point x
oy: .res 4 ; fixed6.26: center point y
cx: .res 4 ; fixed6.26: c_x
cy: .res 4 ; fixed6.26: c_y
zx: .res 4 ; fixed6.26: z_x
zy: .res 4 ; fixed6.26: z_y
zx_2: .res 4 ; fixed6.26: z_x^2
zy_2: .res 4 ; fixed6.26: z_y^2
zx_zy: .res 4 ; fixed6.26: z_x * z_y
dist: .res 4 ; fixed6.26: z_x^2 + z_y^2
z_buffer_active: .res 1 ; boolean: 1 if we triggered the lake, 0 if not
z_buffer_start: .res 1 ; u8: index into z_buffer
z_buffer_end: .res 1 ; u8: index into z_buffer
iter: .res 1 ; u8: iteration count
ptr: .res 2 ; u16
pixel_ptr: .res 2 ; u16
temp: .res 2 ; u16
temp2: .res 2 ; u16
.data
; can move to .data
sx: .res 2 ; i16: screen pixel x
sy: .res 2 ; i16: screen pixel y
zoom: .res 1 ; u8: zoom shift level
fill_level: .res 1 ; u8
pixel_color: .res 1 ; u8
pixel_mask: .res 1 ; u8
pixel_shift: .res 1 ; u8
pixel_offset: .res 1 ; u8
palette_offset: .res 1 ; u8
chroma_offset: .res 1 ; u8
palette_ticks: .res 1 ; u8
chroma_ticks: .res 1 ; u8
count_frames: .res 1 ; u8
count_iters: .res 2 ; u16
text_col: .res 1 ; u8
text_row: .res 1 ; u8
palette_delay = 23
chroma_delay = 137
; FP registers in zero page
FR0 = $d4 ; float48
FRE = $da
FR1 = $e0 ; float48
FR2 = $e6 ; float48
CIX = $f2 ; u8 - index into INBUFF
INBUFF = $f3 ; u16 - pointer to ascii
FLPTR = $fc ; u16 - pointer to user buffer float48
CH1 = $02f2 ; previous character read from keyboard
CH = $02fc ; current character read from keyboard
LBUFF = $0580 ; result buffer for FASC routine
; FP ROM routine vectors
FASC = $D8E6 ; FLOATING POINT TO ASCII (output in INBUFF, last char has high bit set)
IFP = $D9AA ; INTEGER TO FLOATING POINT CONVERSION (FR0:u16 -> FR0:float48)
FPI = $D9D2 ; floating point to integer
FADD = $DA66 ; ADDITION (FR0 += FR1)
FSUB = $DA60 ; SUBTRACTION (FR0 -= FR1)
FMUL = $DADB ; MULTIPLICATION (FR0 *= FR1)
FDIV = $DB28 ; DIVISION (FR0 /= FR1)
ZFR0 = $DA44 ; clear FR0
ZF1 = $DA46 ; CLEAR ZERO PAGE FLOATING POINT NUMBER (XX)
FLD0R = $DD89 ; LOAD FR0 WITH FLOATING POINT NUMBER (YYXX)
FLD1R = $DD98 ; LOAD FR1 WITH FLOATING POINT NUMBER (YYXX)
FST0R = $DDA7 ; STORE FR0 IN USER BUFFER (YYXX)
FMOVE = $DDB6 ; MOVE FR0 TO FR1
; High data
framebuffer_top = $a000
textbuffer = $af00
framebuffer_bottom = $b000
display_list = $bf00
framebuffer_end = $c000
height = 176
half_height = height >> 1
width = 160
half_width = width >> 1
stride = width >> 2
EXTENDED_RAM = $4000 ; 16KiB bank on the XE
PORTB = $D301 ; memory & bank-switch for XL/XE
DMACTL = $D400
DLISTL = $D402
DLISTH = $D403
WSYNC = $D40A
; OS shadow registers
SDLSTL = $230
SDLSTH = $231
; interrupt stuff
SYSVBV = $E45F
XITVBV = $E462
SETVBV = $E45C
COLOR0 = $2C4
COLOR1 = $2C5
COLOR2 = $2C6
COLOR3 = $2C7
COLOR4 = $2C8
; Keycodes!
KEY_PLUS = $06
KEY_MINUS = $0e
KEY_UP = $8e
KEY_DOWN = $8f
KEY_LEFT = $86
KEY_RIGHT = $87
KEY_1 = $1f
KEY_2 = $1e
KEY_3 = $1a
KEY_4 = 24
KEY_5 = 29
KEY_6 = 27
KEY_7 = 51
KEY_8 = 53
KEY_9 = 48
KEY_0 = 50
KEY_PERIOD = 34
KEY_E = 42
KEY_X = 22
KEY_Y = 43
.data
.struct float48
exponent .byte
mantissa .byte 5
.endstruct
.import mul_lobyte256
.import mul_hibyte256
.import mul_hibyte512
.import sqr_lobyte
.import sqr_hibyte
strings:
str_self:
.byte "MANDEL-6502"
str_self_end:
.byte 0
str_speed:
.byte "us/iter: "
str_speed_end:
.byte 0
str_run:
.byte " RUN"
str_run_end:
.byte 0
str_done:
.byte "DONE"
str_done_end:
.byte 0
str_padding:
.byte " "
str_padding_end:
.byte 0
str_space:
.byte " "
.byte 0
str_h:
.byte "h"
.byte 0
str_m:
.byte "m"
.byte 0
str_s:
.byte "s"
.byte 0
str_speed_len = str_speed_end - str_speed
str_run_len = str_run_end - str_run
str_done_len = str_done_end - str_done
str_padding_len = str_padding_end - str_padding
; "3h59m59s"
str_elapsed_spacer = 8
speed_start = 40 - str_done_len - str_speed_len - str_padding_len - str_elapsed_spacer - 1
col_x = 1
str_x:
.byte "X:"
.byte 0
str_x_len = 2
str_x_space = 12
str_x_padding = 2
col_y = col_x + str_x_len + str_x_space + str_x_padding
str_y:
.byte "Y:"
.byte 0
str_y_len = 2
str_y_space = 12
str_y_padding = 2
col_zoom = col_y + str_y_len + str_y_space + str_y_padding
str_zoom:
.byte "ZOOM:"
.byte 0
str_zoom_len = 5
char_map:
; Map ATASCII string values to framebuffer font entries
; Sighhhhh
.repeat 32, i
.byte i + 64
.endrepeat
.repeat 64, i
.byte i
.endrepeat
.repeat 32, i
.byte 96 + i
.endrepeat
hex_chars:
digits_zero:
.byte "0123456789abcdef"
digits_space:
.byte " 123456789abcdef"
aspect:
; aspect ratio!
; pixels at 320w are 5:6 (narrow)
; pixels at 160w are 5:3 (wide)
;
; cy = (sy << (8 - zoom)) * (96 / 128 = 3 / 4)
; cx = (sx << (8 - zoom)) * ((3 / 4) * (5 / 3) = 5 / 4)
;
; so vertical range -92 .. 91.9 is -2.15625 .. 2.15624
; &horizontal range -80 .. 79.9 is -3.125 .. 3.124
;
; 184h is the equiv of 220.8h at square pixels
; 320 / 220.8 = 1.45 display aspect ratio
aspect_x: ; fixed3.13 5/4
.word 5 << (13 - 2)
aspect_y: ; fixed3.13 3/4
.word 3 << (13 - 2)
fixed3_13_as_float: ; float48
; 1 << 13
; 8192
; 81 92 . 00 00 00
.byte 65 ; exponent/sign - +1 byte
.byte $81
.byte $92
.byte $00
.byte $00
.byte $00
sec_per_frame: ; float48 00 . 01 66 66 66 67
.byte 63 ; exponent/sign - -1 bytes
.byte $01 ; BCD digits
.byte $66
.byte $66
.byte $66
.byte $67
us_per_sec: ; float48 1e9 01 00 0,0 00 . 00
.byte 67 ; exponent/sign +3 bytes
.byte $01 ; BCD digits
.byte $00
.byte $00
.byte $00
.byte $00
total_iters: ; float48
.repeat 6
.byte 0
.endrepeat
total_sec: ; float48
.repeat 6
.byte 0
.endrepeat
display_list_start:
; 24 lines overscan
.repeat 3
.byte $70 ; 8 blank lines
.endrep
; 8 scan lines, 1 row of 40-column text
.byte $42
.addr textbuffer
; 184 lines graphics
; ANTIC mode e (160px 2bpp, 1 scan line per line)
.byte $4e
.addr framebuffer_top
.repeat half_height - 1
.byte $0e
.endrep
.byte $4e
.addr framebuffer_bottom
.repeat half_height - 1
.byte $0e
.endrep
; 8 scan lines, 1 row of 40-column text
.byte $42
.addr textbuffer + 40
.byte $41 ; jump and blank
.addr display_list
display_list_end:
display_list_len = display_list_end - display_list_start
color_map:
.byte 0
.repeat 85
.byte %01010101
.byte %10101010
.byte %11111111
.endrepeat
palette_start:
.byte $0e
.byte $08
.byte $04
palette_repeat:
.byte $0e
.byte $08
palette_entries = 3
palette_chroma:
.repeat 15, i
.byte (i + 1) << 4
.endrepeat
.repeat 2, i
.byte (i + 1) << 4
.endrepeat
palette_chroma_entries = 15
.code
;z_buffer_len = 16 ; 10.863 ms/px
;z_buffer_len = 12 ; 10.619 ms/px
z_buffer_len = 8 ; 10.612 ms/px
;z_buffer_len = 4 ; 12.395 ms/px
z_buffer_mask = z_buffer_len - 1
z_buffer:
; the last N zx/zy values
.repeat z_buffer_len
.word 0
.word 0
.endrepeat
.export _mandel_start
;max_fill_level = 6
max_fill_level = 3
fill_masks:
; .byte %00011111
; .byte %00001111
; .byte %00000111
.byte %00000011
.byte %00000001
.byte %00000000
pixel_masks:
.byte %11111111
.byte %11110000
.byte %11000000
viewport_zoom:
.byte 0
.byte 5
.byte 7
.byte 5
.byte 7
.byte 7
viewport_ox:
.dword ($00000000 & $3fffffff) << 2
.dword ($ff110000 & $3fffffff) << 2
.dword ($ff110000 & $3fffffff) << 2
.dword ($fe400000 & $3fffffff) << 2
.dword ($fe3b0000 & $3fffffff) << 2
.dword $fd220000
viewport_oy:
.dword ($00000000 & $3fffffff) << 2
.dword ($ffb60000 & $3fffffff) << 2
.dword ($ffbe0000 & $3fffffff) << 2
.dword ($00000000 & $3fffffff) << 2
.dword ($fffe0000 & $3fffffff) << 2
.dword $ff000000
elapsed_work:
.dword 0
elapsed_digit:
.byte 0
input_col:
.byte 0
input_row:
.byte 0
input_max:
.byte 0
; 2 + 9 * byte cycles
.macro add bytes, dest, arg1, arg2
clc ; 2 cyc
.repeat bytes, byte ; 9 * byte cycles
lda arg1 + byte
adc arg2 + byte
sta dest + byte
.endrepeat
.endmacro
; 20 cycles
.macro add16 dest, arg1, arg2
add 2, dest, arg1, arg2
.endmacro
; 38 cycles
.macro add32 dest, arg1, arg2
add 4, dest, arg1, arg2
.endmacro
; 2-7 cycles
.macro add_carry dest
.scope
bcc after ; 2 cyc
inc dest ; 5 cyc
after:
.endscope
.endmacro
; 2 + 9 * byte cycles
.macro sub bytes, dest, arg1, arg2
sec ; 2 cyc
.repeat bytes, byte ; 9 * byte cycles
lda arg1 + byte
sbc arg2 + byte
sta dest + byte
.endrepeat
.endmacro
; 20 cycles
.macro sub16 dest, arg1, arg2
sub 2, dest, arg1, arg2
.endmacro
; 38 cycles
.macro sub32 dest, arg1, arg2
sub 4, dest, arg1, arg2
.endmacro
; 3 + 5 * (bytes - 1) cycles
.macro shl bytes, arg
asl arg ; 3 cyc
.repeat bytes-1, i
rol arg + 1 + i ; 5 cyc
.endrepeat
.endmacro
; 8 cycles
.macro shl16 arg
shl 2, arg
.endmacro
; 13 cycles
.macro shl24 arg
shl 3, arg
.endmacro
; 18 cycles
.macro shl32 arg
shl 4, arg
.endmacro
; 6 * bytes cycles
; 4 * bytes bytes
.macro copy bytes, dest, arg
.repeat bytes, byte ; 6 * bytes cycles
lda arg + byte ; 3 cyc
sta dest + byte ; 3 cyc
.endrepeat
.endmacro
; 12 cycles
; 8 bytes
.macro copy16 dest, arg
copy 2, dest, arg
.endmacro
; 24 cycles
.macro copy32 dest, arg
copy 4, dest, arg
.endmacro
; 36 cycles
.macro copyfloat dest, arg
copy 6, dest, arg
.endmacro
; 2 + 8 * byte cycles
.macro neg bytes, arg
sec ; 2 cyc
.repeat bytes, byte ; 8 * byte cycles
lda #00 ; 2 cyc
sbc arg + byte ; 3 cyc
sta arg + byte ; 3 cyc
.endrepeat
.endmacro
; 18 cycles
.macro neg16 arg
neg 2, arg
.endmacro
; 34 cycles
.macro neg32 arg
neg 4, arg
.endmacro
; 11-27 + 18 * shift cycles
; 65-81 cycles for shift=3
.macro shift_round_16 arg, shift
.repeat shift
shl32 arg ; 18 cycles
.endrepeat
round16 arg ; 11-27 cycles
.endmacro
; input: arg1, arg2 as fixed4.12
; output: dest as fixed8.24
; patch point jsr at 16 bytes in
imul16_patch_offset = 16
.macro imul16 dest, arg1, arg2
copy16 FR0, arg1 ; 12 cyc
copy16 FR1, arg2 ; 12 cyc
jsr imul16_func ; ? cyc
copy32 dest, FR2 ; 24 cyc
.endmacro
; input: arg as fixed4.12
; output: dest as fixed8.24
; patch point jsr at 8 bytes in
sqr16_patch_offset = 8
.macro sqr16 dest, arg
copy16 FR0, arg ; 12 cyc
jsr sqr16_func ; ? cyc
copy32 dest, FR2 ; 24 cyc
.endmacro
; input: arg as u8
; output: dest as u16
; clobbers a, x
; 19 cyc
.macro sqr8 dest, arg
ldx arg ; 3 cyc
lda sqr_lobyte,x ; 5 cyc
sta dest ; 3 cyc
lda sqr_hibyte,x ; 5 cyc
sta dest + 1 ; 3 cyc
.endmacro
.segment "TABLES"
; lookup table for top byte -> PORTB value for bank-switch
.align 256
bank_switch_table:
.repeat 256, i
.byte ((i & $c0) >> 4) | $e3
.endrepeat
.code
.macro bank_switch bank
lda #((bank << 2) | $e3)
sta PORTB
.endmacro
.macro imul8 dest, arg1, arg2, xe
.if xe
; using 64KB lookup table
; 50-55 cycles
; min: 11 + 9 + 9 + 21 = 50
; max: 11 + 9 + 9 + 26 = 55
; clobbers x, y, dest, ptr
.scope
output = dest
; top 2 bits are the table bank selector
; note we keep arg2 in X after this lookup as we need it later!
ldx arg2 ; 3 cyc
lda bank_switch_table,x ; 4 cyc
sta PORTB ; 4 cyc
; bottom 14 bits except the LSB are the per-bank table index
; add $4000 for the bank pointer
; note the low byte of ptr is assumed to remain at 0! do not clobber it!
txa ; 2 cyc
and #$3f ; 2 cyc
ora #$40 ; 2 cyc
sta ptr + 1 ; 3 cyc
; mask off the low bit for the index, as the table contains
; entries for every other arg1 value...
lda arg1 ; 3 cyc
tay ; 2 cyc
and #1 ; 2 cyc
bne odd ; 2 cyc
even:
; copy the entry into output
lda (ptr),y ; 5 cyc
sta output ; 3 cyc
iny ; 2 cyc
lda (ptr),y ; 5 cyc
sta output + 1 ; 3 cyc
jmp done ; 3 cyc
odd:
; skip the LSB for the index
dey ; 2 cyc
; add arg2 one last time for the skipped bit
txa ; 2 cyc
clc ; 2 cyc
adc (ptr), y ; 5 cyc
sta output ; 3 cyc
lda #0 ; 2 cyc
iny ; 2 cyc
adc (ptr),y ; 5 cyc
sta output + 1 ; 3 cyc
; note: we are not restoring memory to save 6 cycles!
; this means those 16kb have to be switched back to base RAM
; if we need to use them anywhere else
;;; restore memory
;;lda #$81 ; 2 cyc - disabled
;;sta PORTB ; 4 cyc - disabled
done:
.endscope
.else
; Using base 48k RAM compatibility mode
; Small table of half squares
; Adapted from https://everything2.com/title/Fast+6502+multiplication
; 81-92 cycles
.scope
mul_factor_a = arg1
mul_factor_x = arg2
mul_product_lo = dest
mul_product_hi = dest + 1
lda mul_factor_a ; 3 cyc
; (a + x)^2/2
clc ; 2 cyc
adc mul_factor_x ; 3 cyc
tax ; 2 cyc
bcc under256 ; 2 cyc
lda mul_hibyte512,x ; 4 cyc
bcs next ; 2 cyc
under256:
lda mul_hibyte256,x ; 4 cyc
sec ; 2 cyc
next:
sta mul_product_hi ; 3 cyc
lda mul_lobyte256,x ; 4 cyc
; - a^2/2
ldx mul_factor_a ; 3 cyc
sbc mul_lobyte256,x ; 4 cyc
sta mul_product_lo ; 3 cyc
lda mul_product_hi ; 3 cyc
sbc mul_hibyte256,x ; 4 cyc
sta mul_product_hi ; 3 cyc
; + x & a & 1:
; (this is a kludge to correct a
; roundoff error that makes odd * odd too low)
ldx mul_factor_x ; 3 cyc
txa ; 2 cyc
and mul_factor_a ; 3 cyc
and #1 ; 2 cyc
clc ; 2 cyc
adc mul_product_lo ; 3 cyc
bcc small_product ; 2 cyc
inc mul_product_hi ; 5 cyc
; - x^2/2
small_product:
sec ; 2 cyc
sbc mul_lobyte256,x ; 4 cyc
sta mul_product_lo ; 3 cyc
lda mul_product_hi ; 3 cyc
sbc mul_hibyte256,x ; 4 cyc
sta mul_product_hi ; 3 cyc
.endscope
.endif
.endmacro
; Initialize a 16 KB chunk of the table
; input: multipliers in temp
; output: new multipliers in temp
; clobbers: temp, temp2
.proc imul8xe_init_section
arg1 = FR1
arg2 = FR2
result = FR0
ptr = temp2
lda #$00
sta ptr
lda #$40
sta ptr + 1
ldy #0
; outer loop: $00 -> $3f
outer_loop:
; reset result to 0
lda #0
sta result
sta result + 1
; inner loop: $00 -> $ff
inner_loop:
; copy result to data set
lda result
sta (ptr),y
lda result + 1
iny
sta (ptr),y
dey
; result += 2 * arg2
clc
lda arg2
adc result
sta result
lda #0
adc result + 1
sta result + 1
clc
lda arg2
adc result
sta result
lda #0
adc result + 1
sta result + 1
; inner loop check
inc arg1
inc arg1
inc ptr
inc ptr
bne inner_loop
; outer loop check
inc arg2
inc ptr + 1
lda ptr + 1
cmp #$80
bne outer_loop
rts
.endproc
; min base mem: 81 * 4 + 22 * 2 + 10 + 6 = 384 cyc
; max base mem: 92 * 4 + 27 * 2 + 50 + 6 = 478 cyc
; min ext-mem: 50 * 4 + 22 * 2 + 10 + 6 = 260 cyc
; max ext-mem: 55 * 4 + 27 * 2 + 50 + 6 = 330 cyc
.macro imul16_impl xe
.local arg1
.local arg2
.local result
.local inter
.local arg1_pos
.local arg2_pos
arg1 = FR0 ; 16-bit arg (clobbered)
arg2 = FR1 ; 16-bit arg (clobbered)
result = FR2 ; 32-bit result
inter = temp2
; h1l1 * h2l2
; (h1*256 + l1) * (h2*256 + l2)
; h1*256*(h2*256 + l2) + l1*(h2*256 + l2)
; h1*h2*256*256 + h1*l2*256 + h2*l1*256 + l1*l2
; l1 * l2
imul8 result, arg1, arg2, xe ; 81-92 or 50-55
; 256 * 256 * h1 * h2
imul8 result + 2, arg1 + 1, arg2 + 1, xe ; 81-92 or 50-55
; 256 * h1 * l1
imul8 inter, arg1 + 1, arg2, xe ; 81-92 or 50-55
add16 result + 1, result + 1, inter ; 20 cyc
add_carry result + 3 ; 2-7 cyc
; 256 * l1 * h2
imul8 inter, arg1, arg2 + 1, xe ; 81-92 or 50-55
add16 result + 1, result + 1, inter ; 20 cyc
add_carry result + 3 ; 2-7 cyc
; In case of negative inputs, adjust high word
; https://stackoverflow.com/a/28827013
; 10-50 cycles
lda arg1 + 1 ; 3 cyc
bpl arg1_pos ; 2 cyc
sub16 result + 2, result + 2, arg2 ; 20 cyc
arg1_pos:
lda arg2 + 1 ; 3 cyc
bpl arg2_pos ; 2 cyc
sub16 result + 2, result + 2, arg1 ; 20 cyc
arg2_pos:
rts ; 6 cyc
.endmacro
; min base ram: 5 + 19 * 2 + 81 + 22 * 2 + 6 = 174
; max base ram: 23 + 19 * 2 + 92 + 27 * 2 + 6 = 213
; min ext ram: 5 + 19 * 2 + 50 + 22 * 2 + 6 = 143
; max ext ram: 23 + 19 * 2 + 55 + 27 * 2 + 6 = 176
.macro sqr16_impl xe
.scope
arg = FR0 ; 16-bit arg (clobbered)
result = FR2 ; 32-bit result
;inter = temp2
inter = FR1
; 5-23 cycles
lda arg + 1 ; 3 cyc
bpl arg_pos ; 2 cyc
neg16 arg ; 18 cyc
arg_pos:
; hl * hl
; (h*256 + l) * (h*256 + l)
; h*256*(h*256 + l) + l*(h*256 + l)
; h*h*256*256 + h*l*256 + h*l*256 + l*l
; l * l
sqr8 result, arg ; 19 cyc
; 256 * 256 * hl * hl
sqr8 result + 2, arg + 1 ; 19 cyc
; 2 * h * l * 256
imul8 inter, arg + 1, arg, xe ; 81-92 / 50-55 cyc
add16 result + 1, result + 1, inter ; 20 cyc
add_carry result + 3 ; 2-7 cyc
add16 result + 1, result + 1, inter ; 20 cyc
add_carry result + 3 ; 2-7 cyc
rts ; 6 cyc
.endscope
.endmacro
.proc imul16_func
imul16_impl 0
.endproc
.proc imul16xe_func
imul16_impl 1
.endproc
.proc sqr16_func
sqr16_impl 0
.endproc
.proc sqr16xe_func
sqr16_impl 1
.endproc
; 11-27 cycles
.macro round16 arg
; Round top 16 bits of 32-bit fixed-point number in-place
.local increment
.local high_half
.local check_sign
.local next
; low word > $8000: round up
; = $8000: round up if positive
; round down if negative
; < $8000: round down
; $8000 17
; $8001 27
; $8100 21
; $7fff 11
lda arg + 1 ; 3 cyc
cmp #$80 ; 2 cyc
beq high_half ; 2 cyc
bpl increment ; 2 cyc
bmi next ; 2 cyc
high_half:
lda arg ; 3 cyc
beq check_sign ; 2 cyc
jmp increment ; 3 cyc
check_sign:
lda arg + 3 ; 3 cyc
bmi next ; 2 cyc
increment: ; 5-10 cyc
inc arg + 2 ; 5 cyc
bne next ; 2 cyc
inc arg + 3 ; 5 cyc
next:
.endmacro
; input in FR0, 16 bits signed 3.13 fixed
; output in FR0, Atari float
; clobbers a, x, y, FR0, FR1
.proc fixed3_13_to_float
ldx #.lobyte(fixed3_13_as_float)
ldy #.hibyte(fixed3_13_as_float)
jsr FLD1R
; check sign bit! conversion routine is for unsigned
lda FR0 + 1
bpl positive
negative:
neg16 FR0
jsr IFP
; set float sign bit
lda FR0
ora #$80
sta FR0
jmp common
positive:
jsr IFP
common:
jsr FDIV
rts
.endproc
; rounds to 16-bit first!
; input in FR0, 32 bits signed 6.26 fixed
; output in FR0, Atari float
; clobbers a, x, y, FR0, FR1
.proc fixed6_26_to_float
shift_round_16 FR0, 3
copy16 FR0, FR0 + 2
jsr fixed3_13_to_float
rts
.endproc
; input in FR0, Atari float
; output in FR0, 16 bits signed 3.13 fixed
; clobbers a, x, y, FR0, FR1
.proc float_to_fixed3_13
ldx #.lobyte(fixed3_13_as_float)
ldy #.hibyte(fixed3_13_as_float)
jsr FLD1R
jsr FMUL
; check sign bit! conversion routine is for unsigned
lda FR0
bcc positive
negative:
; clearfloat sign bit
lda FR0
eor #$80
sta FR0
jsr FPI
neg16 FR0
jmp common
positive:
jsr FPI
common:
rts
.endproc
.proc mandelbrot
; input:
; cx: position scaled to 6.26 fixed point - -32..+31.9
; cy: position scaled to 6.26
;
; output:
; iter: iteration count at escape or 0
; zx = 0
; zy = 0
; zx_2 = 0
; zy_2 = 0
; zx_zy = 0
; dist = 0
; iter = 0
; lda #00
; ldx #(iter - zx + 1)
;initloop:
; sta zx - 1,x
; dex
; bne initloop
; sta z_buffer_start
; sta z_buffer_end
lda #00
sta zx
sta zx + 1
sta zx + 2
sta zx + 3
sta zy
sta zy + 1
sta zy + 2
sta zy + 3
sta zx_2
sta zx_2 + 1
sta zx_2 + 2
sta zx_2 + 3
sta zy_2
sta zy_2 + 1
sta zy_2 + 2
sta zy_2 + 3
sta zx_zy
sta zx_zy + 1
sta zx_zy + 2
sta zx_zy + 3
sta dist
sta dist + 1
sta dist + 2
sta dist + 3
sta iter
sta z_buffer_start
sta z_buffer_end
loop:
inc count_iters
bne low_iters
inc count_iters + 1
low_iters:
; iter++ & max-iters break
inc iter
bne keep_going
jmp exit_path
keep_going:
.macro quick_exit arg, max
; arg: fixed6.26
; max: integer
.local positive
.local negative
.local nope_out
.local first_equal
.local all_done
; check sign bit
lda arg + 3
bmi negative
positive:
cmp #(max << 2)
bmi all_done ; 'less than'
jmp exit_path
negative:
cmp #(256 - (max << 2))
beq first_equal ; 'equal' on first byte
bpl all_done ; 'greater than'
nope_out:
jmp exit_path
first_equal:
; following bytes all 0 shows it's really 'equal'
lda arg + 2
bne all_done
lda arg + 1
bne all_done
lda arg
bne all_done
jmp exit_path
all_done:
.endmacro
; 6.26: (-32 .. 31.9)
; zx = zx_2 - zy_2 + cx
sub32 zx, zx_2, zy_2
add32 zx, zx, cx
quick_exit zx, 2
; zy = zx_zy + zx_zy + cy
add32 zy, zx_zy, zx_zy
add32 zy, zy, cy
quick_exit zy, 2
; convert 6.26 -> 3.13: (-4 .. +3.9)
shift_round_16 zx, 3
shift_round_16 zy, 3
; zx_2 = zx * zx
fixup_sqr16_1:
sqr16 zx_2, zx + 2
; zy_2 = zy * zy
fixup_sqr16_2:
sqr16 zy_2, zy + 2
; zx_zy = zx * zy
fixup_imul16_1:
imul16 zx_zy, zx + 2, zy + 2
; dist = zx_2 + zy_2
add32 dist, zx_2, zy_2
quick_exit dist, 4
; if may be in the lake, look for looping output with a small buffer
; as an optimization vs running to max iters
lda z_buffer_active
beq skip_z_buffer
ldx z_buffer_start
cpx z_buffer_end
beq z_nothing_to_read
z_buffer_loop:
.macro z_compare arg
.local compare_no_match
lda z_buffer,x
inx
cmp arg
bne compare_no_match
iny
compare_no_match:
.endmacro
.macro z_advance
.local skip_reset_x
cpx #(z_buffer_len * 4)
bmi skip_reset_x
ldx #0
skip_reset_x:
.endmacro
.macro z_store arg
lda arg
sta z_buffer,x
inx
.endmacro
; Compare the previously stored z values
ldy #0
z_compare zx + 2
z_compare zx + 3
z_compare zy + 2
z_compare zy + 3
cpy #4
bne z_no_matches
jmp z_exit
z_no_matches:
z_advance
cpx z_buffer_end
bne z_buffer_loop
z_nothing_to_read:
; Store and expand
z_store zx + 2
z_store zx + 3
z_store zy + 2
z_store zy + 3
z_advance
stx z_buffer_end
; Increment the start roller if necessary (limit size)
lda iter
cmp #(z_buffer_len * 4)
bmi skip_inc_start
lda z_buffer_start
clc
adc #4
tax
z_advance
stx z_buffer_start
skip_inc_start:
skip_z_buffer:
jmp loop
z_exit:
lda #0
sta iter
exit_path:
ldx #0
lda iter
bne next
inx
next:
stx z_buffer_active
rts
.endproc
.macro scale_zoom dest
; clobbers X, flags
.local cont
.local enough
; cx = (sx << (8 - zoom))
ldx zoom
cont:
cpx #8
beq enough
shl16 dest
inx
jmp cont
enough:
.endmacro
.macro zoom_factor dest, src, aspect
; output: dest: fixed6.26
; input: src: fixed3.13
; aspect: fixed3.13
; clobbers A, X, flags, etc
copy16 dest, src
scale_zoom dest
; cy = cy * (3 / 4)
; cx = cx * (5 / 4)
imul16 dest, dest, aspect
.endmacro
.proc pset
; screen coords in signed sx,sy
; iter holds the target to use
; @todo implement
; iter -> color
ldx iter
lda color_map,x
ldx fill_level
and pixel_masks,x
sta pixel_color
lda pixel_masks,x
eor #$ff
sta pixel_mask
; sy -> line base address in temp
lda sy
bpl positive
negative:
; temp1 = top half
lda #.lobyte(framebuffer_top + stride * half_height)
sta pixel_ptr
lda #.hibyte(framebuffer_top + stride * half_height)
sta pixel_ptr + 1
jmp point
positive:
lda #.lobyte(framebuffer_bottom)
sta pixel_ptr
lda #.hibyte(framebuffer_bottom)
sta pixel_ptr + 1
point:
; pixel_ptr += sy * stride
; temp * 40
; = temp * 32 + temp * 8
; = (temp << 5) + (temp << 3)
copy16 temp, sy
shl16 temp
shl16 temp
shl16 temp
add16 pixel_ptr, pixel_ptr, temp
shl16 temp
shl16 temp
add16 pixel_ptr, pixel_ptr, temp
; Ok so temp1 points to the start of the line, which is 40 bytes.
; Get the byte and bit offsets
lda sx
clc
adc #half_width
sta temp
; pixel_shift = temp & 3
; pixel_color <<= pixel_shift (shifting in zeros)
; pixel_mask <<= pixel_shift (shifting in ones)
and #3
sta pixel_shift
tax
shift_loop:
beq shift_done
lsr pixel_color
lsr pixel_color
sec
ror pixel_mask
sec
ror pixel_mask
dex
jmp shift_loop
shift_done:
ldy fill_level
ldx fill_masks,y
inx
; pixel_offset = temp >> 2
lda temp
lsr a
lsr a
sta pixel_offset
tay
draw_pixel:
; read, mask, or, write
lda (pixel_ptr),y
and pixel_mask
ora pixel_color
sta (pixel_ptr),y
dex
beq done
clc
lda #40
adc pixel_ptr
sta pixel_ptr
lda #0
adc pixel_ptr + 1
sta pixel_ptr + 1
jmp draw_pixel
done:
rts
.endproc
; in/out: column in text_col
; in: row in text_row
; in: pointer to string in INBUFF
; clobbers x/y/a/temp
.proc draw_string
drawptr = temp
strptr = INBUFF
clc
lda #.lobyte(textbuffer)
adc text_col
sta temp
lda #.hibyte(textbuffer)
adc #0
sta temp + 1
ldx text_row
beq done_rows
continue_rows:
clc
lda temp
adc #40
sta temp
lda temp + 1
adc #0
sta temp + 1
dex
bne continue_rows
done_rows:
ldy #0
loop:
lda (strptr),y
; if char's null, terminate c-style
beq done
; save the char for terminator check
pha
; strip the high bit (terminator)
and #$7f
tax
lda char_map,x
sta (drawptr),y
iny
pla
; _last_ char has high bit set in atari rom routines
bmi done
jmp loop
done:
; move the text column pointer
tya
clc
adc text_col
sta text_col
rts
.endproc
.macro draw_string_const str
lda #.lobyte(str)
sta INBUFF
lda #.hibyte(str)
sta INBUFF + 1
jsr draw_string
.endmacro
.proc vblank_handler
inc count_frames
inc chroma_ticks
lda chroma_ticks
cmp #(chroma_delay)
bne skip_chroma
lda #0
sta chroma_ticks
inc chroma_offset
lda chroma_offset
cmp #(palette_chroma_entries)
bne skip_chroma
lda #0
sta chroma_offset
skip_chroma:
inc palette_ticks
lda palette_ticks
cmp #(palette_delay)
bne skip_luma
lda #0
sta palette_ticks
inc palette_offset
lda palette_offset
cmp #(palette_entries)
bne skip_luma
lda #0
sta palette_offset
skip_luma:
jsr update_palette
jmp XITVBV
.endproc
.proc update_palette
lda #0
sta COLOR4
ldx chroma_offset
ldy palette_offset
lda palette_chroma,x
ora palette_start,y
sta COLOR2
;inx
iny
lda palette_chroma,x
ora palette_start,y
sta COLOR1
;inx
iny
lda palette_chroma,x
ora palette_start,y
sta COLOR0
rts
.endproc
.proc update_speed
; convert frames (u16) to fp
; add to frames_total
; convert pixels (u16) to fp
; add to pixels_total
; (frames_total * 16.66666667) / pixels_total
; convert to ATASCII
; draw text
.endproc
.proc keycheck
; clobbers all
; returns 255 in A if state change or 0 if no change
; check keyboard buffer
lda CH
cmp #$ff
beq skip_char
; Clear the keyboard buffer and re-enable interrupts
ldx #$ff
stx CH
tay
lda zoom
cpy #KEY_PLUS
beq plus
cpy #KEY_MINUS
beq minus
; temp+temp2 = $00010000 << (8 - zoom)
lda #$00
sta temp
sta temp + 1
lda #$01
sta temp + 2
lda #$00
sta temp + 3
scale_zoom temp + 2
cpy #KEY_UP
beq up
cpy #KEY_DOWN
beq down
cpy #KEY_LEFT
beq left
cpy #KEY_RIGHT
beq right
jmp number_keys
skip_char:
lda #0
rts
plus:
lda zoom
cmp #7
bpl skip_char
inc zoom
jmp done
minus:
lda zoom
cmp #1
bmi skip_char
dec zoom
jmp done
up:
add32 oy, oy, temp
jsr display_coords
jmp done
down:
sub32 oy, oy, temp
jsr display_coords
jmp done
left:
sub32 ox, ox, temp
jsr display_coords
jmp done
right:
add32 ox, ox, temp
jsr display_coords
jmp done
number_keys:
cpy #KEY_1
beq one
cpy #KEY_2
beq two
cpy #KEY_3
beq three
cpy #KEY_4
beq four
cpy #KEY_5
beq five
cpy #KEY_6
beq six
jmp letter_keys
one:
ldx #0
jmp load_key_viewport
two:
ldx #1
jmp load_key_viewport
three:
ldx #2
jmp load_key_viewport
four:
ldx #3
jmp load_key_viewport
five:
ldx #4
jmp load_key_viewport
six:
ldx #5
jmp load_key_viewport
letter_keys:
cpy #KEY_X
bne not_x
jsr input_x
jmp done
not_x:
cpy #KEY_Y
bne not_y
jsr input_y
jmp done
not_y:
jmp skip_char
load_key_viewport:
jsr load_viewport
; fall through
done:
lda #255
rts
.endproc
.proc input_x
ldx #col_x
ldy #1
jsr input_number
rts
.endproc
.proc input_y
rts
.endproc
.proc input_number
rts
.endproc
.proc clear_screen
; zero the range from framebuffer_top to display_list
lda #.lobyte(framebuffer_top)
sta temp
lda #.hibyte(framebuffer_top)
sta temp + 1
zero_page_loop:
lda #0
ldy #0
zero_byte_loop:
sta (temp),y
iny
bne zero_byte_loop
inc temp + 1
lda temp + 1
cmp #.hibyte(display_list)
bne zero_page_loop
rts
.endproc
.proc status_bar
; Status bar
lda #0
sta text_col
lda #0
sta text_row
draw_string_const str_self
lda #(40 - str_run_len)
sta text_col
draw_string_const str_run
rts
.endproc
.proc display_coords
lda #1
sta text_row
lda #col_x
sta text_col
draw_string_const str_x
copy32 FR0, ox
jsr fixed6_26_to_float
jsr FASC
jsr draw_string
lda #col_y
sta text_col
draw_string_const str_y
copy32 FR0, oy
jsr fixed6_26_to_float
jsr FASC
jsr draw_string
lda #col_zoom
sta text_col
draw_string_const str_zoom
lda zoom
clc
adc #0
sta FR0
lda #0
sta FR0 + 1
jsr IFP
jsr FASC
jsr draw_string
rts
.endproc
; input: viewport selector in x
; clobbers: a, x
.proc load_viewport
lda viewport_zoom,x
sta zoom
txa
asl a
asl a
tax
lda viewport_ox,x
sta ox
lda viewport_oy,x
sta oy
inx
lda viewport_ox,x
sta ox + 1
lda viewport_oy,x
sta oy + 1
inx
lda viewport_ox,x
sta ox + 2
lda viewport_oy,x
sta oy + 2
inx
lda viewport_ox,x
sta ox + 3
lda viewport_oy,x
sta oy + 3
rts
.endproc
.proc _mandel_start
jsr imul8xe_init
; initialize viewport
ldx #0 ; overview
jsr load_viewport
; Disable display DMA
lda #0
sta DMACTL
jsr clear_screen
jsr display_coords
; Copy the display list into properly aligned memory
; Can't cross 1024-byte boundaries :D
ldx #0
copy_byte_loop:
lda display_list_start,x
sta display_list,x
inx
cpx #display_list_len
bne copy_byte_loop
; Set up the display list
lda #.lobyte(display_list)
sta DLISTL ; actual register
sta SDLSTL ; shadow register the OS will copy in
lda #.hibyte(display_list)
sta DLISTH ; actual register
sta SDLSTH ; shadow register the OS will copy in
; Re-enable display DMA
lda #$22
sta DMACTL
; Initialize the palette
lda #0
sta palette_offset
sta palette_delay
sta chroma_offset
sta chroma_delay
jsr update_palette
; install the vblank handler
lda #7 ; deferred
ldx #.hibyte(vblank_handler)
ldy #.lobyte(vblank_handler)
jsr SETVBV
main_loop:
; count_frames = 0; count_iters = 0
lda #0
sta count_frames
sta count_iters
sta count_iters + 1
; total_sec = 0.0; total_iters = 0.0
jsr ZFR0
ldx #.lobyte(total_sec)
ldy #.hibyte(total_sec)
jsr FST0R
ldx #.lobyte(total_iters)
ldy #.hibyte(total_iters)
jsr FST0R
jsr clear_screen
jsr status_bar
jsr display_coords
lda #0
sta fill_level
fill_loop:
; sy = -92 .. 91
lda #(256-half_height)
sta sy
lda #(256-1)
sta sy + 1
loop_sy:
; sx = -80 .. 79
lda #(256-half_width)
sta sx
lda #(256-1)
sta sx + 1
loop_sx:
; check the fill mask
ldy #0
loop_skip_level:
cpy fill_level
beq current_level
lda fill_masks,y
and sx
bne not_skipped_mask1
lda fill_masks,y
and sy
beq skipped_mask
not_skipped_mask1:
iny
jmp loop_skip_level
current_level:
lda fill_masks,y
and sx
bne skipped_mask
lda fill_masks,y
and sy
beq not_skipped_mask
skipped_mask:
jmp skipped
not_skipped_mask:
; run the fractal!
zoom_factor cx, sx, aspect_x
add32 cx, cx, ox
zoom_factor cy, sy, aspect_y
neg32 cy
add32 cy, cy, oy
jsr mandelbrot
jsr pset
jsr keycheck
beq no_key
; @fixme clear the pixel stats
jmp main_loop
no_key:
; check if we should update the counters
; count_frames >= 120? update!
lda count_frames
cmp #120 ; >= 2 seconds
bpl update_status
jmp skipped
update_status:
; FR0 = (float)count_iters & clear count_iters
copy16 FR0, count_iters
jsr IFP
lda #0
sta count_iters
sta count_iters + 1
; FR1 = total_iters
ldx #.lobyte(total_iters)
ldy #.hibyte(total_iters)
jsr FLD1R
; FR0 += FR1
jsr FADD
; total_iters = FR0
ldx #.lobyte(total_iters)
ldy #.hibyte(total_iters)
jsr FST0R
; FR0 = (float)count_frames & clear count_frames
; warning: this should really disable interrupts @TODO
lda count_frames
sta FR0
lda #0
sta FR0 + 1
sta count_frames
jsr IFP
; FR0 *= sec_per_frame
ldx #.lobyte(sec_per_frame)
ldy #.hibyte(sec_per_frame)
jsr FLD1R
jsr FMUL
; FR0 += total_sec
ldx #.lobyte(total_sec)
ldy #.hibyte(total_sec)
jsr FLD1R
jsr FADD
; total_sec = FR0
ldx #.lobyte(total_sec)
ldy #.hibyte(total_sec)
jsr FST0R
; FR0 /= total_iters
ldx #.lobyte(total_iters)
ldy #.hibyte(total_iters)
jsr FLD1R
jsr FDIV
; FR0 *= us_per_sec
ldx #.lobyte(us_per_sec)
ldy #.hibyte(us_per_sec)
jsr FLD1R
jsr FMUL
; round (down) to integer
jsr FPI
clc
jsr IFP
lda #speed_start
sta text_col
lda #0
sta text_row
draw_string_const str_speed
lda text_col
pha
draw_string_const str_padding
pla
sta text_col
; convert to ASCII in INBUFF and print
jsr FASC
jsr draw_string
; elapsed time
; FR0 = total_sec
ldx #.lobyte(total_sec)
ldy #.hibyte(total_sec)
jsr FLD0R
; FR0 -> integer -> elapsed_work
jsr FPI
lda FR0
sta elapsed_work
lda FR0 + 1
sta elapsed_work + 1
draw_string_const str_space
.macro do_countdown divisor, digits
ldx #.lobyte(divisor)
ldy #.hibyte(divisor)
lda #.lobyte(digits)
sta INBUFF
lda #.hibyte(digits)
sta INBUFF + 1
jsr countdown
.endmacro
do_countdown 36000, digits_space
do_countdown 3600, digits_zero
draw_string_const str_h
do_countdown 600, digits_zero
do_countdown 60, digits_zero
draw_string_const str_m
do_countdown 10, digits_zero
do_countdown 1, digits_zero
draw_string_const str_s
skipped:
; sx += fill_level[fill_masks] + 1
ldx fill_level
lda fill_masks,x
clc
adc #1 ; will never carry
adc sx
sta sx
lda #0
adc sx + 1
sta sx + 1
lda sx
cmp #half_width
beq loop_sx_done
jmp loop_sx
loop_sx_done:
; sy += fill_level[fill_masks] + 1
ldx fill_level
lda fill_masks,x
clc
adc #1 ; will never carry
adc sy
sta sy
lda #0
adc sy + 1
sta sy + 1
lda sy
cmp #half_height
beq loop_sy_done
jmp loop_sy
loop_sy_done:
fill_loop_done:
inc fill_level
lda fill_level
cmp #max_fill_level
beq loop
jmp fill_loop
loop:
; finished
lda #(40 - str_done_len)
sta text_col
lda #0
sta text_row
draw_string_const str_done
jsr keycheck
beq loop
jmp main_loop
.endproc
; digit string in INBUFF
; divisor X/Y
; clobbers temp, calls draw_string
.proc countdown
divisor = temp
stx divisor
sty divisor + 1
; count the hours
ldy #0
countdown_loop:
lda elapsed_work + 1
cmp divisor + 1
beq countdown_lobyte
bcc countdown_done
bcs countdown_inc
countdown_lobyte:
lda elapsed_work
cmp divisor
bcc countdown_done
countdown_inc:
sec
lda elapsed_work
sbc divisor
sta elapsed_work
lda elapsed_work + 1
sbc divisor + 1
sta elapsed_work + 1
iny
jmp countdown_loop
countdown_done:
lda (INBUFF),y
eor #$80
sta elapsed_digit
lda #.lobyte(elapsed_digit)
sta INBUFF
lda #.hibyte(elapsed_digit)
sta INBUFF + 1
jsr draw_string
rts
.endproc
.proc imul8xe_init
bank_switch 0
lda #0
sta EXTENDED_RAM
bank_switch 1
lda #1
sta EXTENDED_RAM
bank_switch 0
lda EXTENDED_RAM
beq init
; no bank switching available, we just overwrite the value in base ram
rts
init:
; patch imul16_func into a forwarding thunk to imul16xe_func
lda #$4c ; 'jmp' opcode
sta imul16_func
lda #.lobyte(imul16xe_func)
sta imul16_func + 1
sta mandelbrot::fixup_imul16_1 + imul16_patch_offset + 1
lda #.hibyte(imul16xe_func)
sta imul16_func + 2
sta mandelbrot::fixup_imul16_1 + imul16_patch_offset + 2
; ditto for sqr16_func -> sqr16xe_func
lda #$4c ; 'jmp' opcode
sta sqr16_func
lda #.lobyte(sqr16xe_func)
sta sqr16_func + 1
sta mandelbrot::fixup_sqr16_1 + sqr16_patch_offset + 1
sta mandelbrot::fixup_sqr16_2 + sqr16_patch_offset + 1
lda #.hibyte(sqr16xe_func)
sta sqr16_func + 2
sta mandelbrot::fixup_sqr16_1 + sqr16_patch_offset + 2
sta mandelbrot::fixup_sqr16_2 + sqr16_patch_offset + 2
; create the lookup table
; go through the input set, in four 16KB chunks
arg1 = FR1
arg2 = FR2
result = FR0
lda #$00
sta arg1
sta arg2
sta ptr
lda #$40
sta ptr + 1
; $00 * $00 -> $3f * $ff
bank_switch 0
jsr imul8xe_init_section
; $40 * $00 -> $7f * $ff
bank_switch 1
jsr imul8xe_init_section
; $80 * $00 -> $bf * $ff
bank_switch 2
jsr imul8xe_init_section
; $c0 * $00 -> $ff * $ff
bank_switch 3
jsr imul8xe_init_section
rts
.endproc