From 4bac47a4fdad2fd56f9023eaae9a4be83fe9fced Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Mon, 23 Jun 2025 00:31:53 -0700 Subject: [PATCH 01/27] fix at 256 seconds --- mandel.s | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mandel.s b/mandel.s index 6837e00..714f6b6 100644 --- a/mandel.s +++ b/mandel.s @@ -2002,10 +2002,14 @@ update_status: countdown_loop: lda elapsed_work + 1 cmp #.hibyte(divisor) + beq countdown_lobyte bcc countdown_done + bcs countdown_inc + countdown_lobyte: lda elapsed_work cmp #.lobyte(divisor) bcc countdown_done + countdown_inc: sec lda elapsed_work sbc #.lobyte(divisor) From fd954da47e75a99b0033ec11f0dd145e1d8ab544 Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Mon, 23 Jun 2025 08:17:39 -0700 Subject: [PATCH 02/27] Create map file for convenience export a symbol and it'll appear in mandel.map --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index bd14c7d..711adcd 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ all : mandel.xex mandel.xex : mandel.o tables.o atari-asm-xex.cfg - ld65 -C ./atari-asm-xex.cfg -o $@ mandel.o tables.o + ld65 -C ./atari-asm-xex.cfg --mapfile mandel.map -o $@ mandel.o tables.o %.o : %.s ca65 -o $@ $< @@ -15,4 +15,6 @@ clean : rm -f tables.s rm -f *.o rm -f *.xex + rm -f mandel.map + From fab2760394b9b74f07488bc345997fbd9e566389 Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sat, 28 Jun 2025 13:43:43 -0700 Subject: [PATCH 03/27] refactor countdown as a procedure call --- mandel.s | 104 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 58 insertions(+), 46 deletions(-) diff --git a/mandel.s b/mandel.s index 714f6b6..cad4e5e 100644 --- a/mandel.s +++ b/mandel.s @@ -1991,55 +1991,25 @@ update_status: lda FR0 + 1 sta elapsed_work + 1 - ;jsr IFP - ;jsr FASC - ;jsr draw_string - - .macro countdown divisor, digits - .scope - ; count the hours - ldx #0 - countdown_loop: - lda elapsed_work + 1 - cmp #.hibyte(divisor) - beq countdown_lobyte - bcc countdown_done - bcs countdown_inc - countdown_lobyte: - lda elapsed_work - cmp #.lobyte(divisor) - bcc countdown_done - countdown_inc: - sec - lda elapsed_work - sbc #.lobyte(divisor) - sta elapsed_work - lda elapsed_work + 1 - sbc #.hibyte(divisor) - sta elapsed_work + 1 - inx - jmp countdown_loop - countdown_done: - lda digits,x - eor #$80 - sta elapsed_digit - lda #.lobyte(elapsed_digit) - sta INBUFF - lda #.hibyte(elapsed_digit) - sta INBUFF + 1 - jsr draw_string - .endscope - .endmacro - draw_string_const str_space - countdown 36000, digits_space - countdown 3600, digits_zero + + .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 - countdown 600, digits_zero - countdown 60, digits_zero + do_countdown 600, digits_zero + do_countdown 60, digits_zero draw_string_const str_m - countdown 10, digits_zero - countdown 1, digits_zero + do_countdown 10, digits_zero + do_countdown 1, digits_zero draw_string_const str_s skipped: @@ -2101,3 +2071,45 @@ 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 From 96e0356e578f7c94dd2c5764ecbf59f70dbe29d9 Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Thu, 3 Jul 2025 18:41:24 -0700 Subject: [PATCH 04/27] WIP input handling for coords experimental output via 32-bits mult, looses precision in conversion --- mandel.s | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 128 insertions(+), 8 deletions(-) diff --git a/mandel.s b/mandel.s index cad4e5e..4cc0f39 100644 --- a/mandel.s +++ b/mandel.s @@ -126,6 +126,10 @@ KEY_7 = 51 KEY_8 = 53 KEY_9 = 48 KEY_0 = 50 +KEY_PERIOD = 34 +KEY_E = 42 +KEY_X = 22 +KEY_Y = 43 .struct float48 exponent .byte @@ -257,6 +261,28 @@ fixed3_13_as_float: ; float48 .byte $00 .byte $00 +u65536_as_float: ; float48 + ; 1 << 16 + ; 65536 + ; 06 55 36 . 00 00 + .byte 66 ; exponent/sign - +2 bytes + .byte $06 + .byte $55 + .byte $36 + .byte $00 + .byte $00 + +fixed6_26_as_float: ; float48 + ; 1 << 26 + ; 67108864 + ; 67 10 88 64 . 00 + .byte 67 ; exponent/sign - +3 bytes + .byte $67 + .byte $10 + .byte $88 + .byte $64 + .byte $00 + sec_per_frame: ; float48 00 . 01 66 66 66 67 .byte 63 ; exponent/sign - -1 bytes .byte $01 ; BCD digits @@ -403,6 +429,13 @@ elapsed_work: 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 @@ -983,6 +1016,66 @@ common: .endproc +; 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 + ; check sign bit! conversion routine is for unsigned + lda FR0 + 3 + and #$80 + sta temp + + beq positive + neg32 FR0 +positive: + + ; save low word + lda FR0 + pha + lda FR0 + 1 + pha + + ; convert high word + sta FR0 + 2 + sta FR1 + lda FR0 + 3 + sta FR0 + 1 + jsr IFP + + lda temp + beq positive2 + ; set float sign bit + lda FR0 + ora #$80 + sta FR0 +positive2: + + ; high word to FR1 + ldx #.lobyte(u65536_as_float) + ldy #.hibyte(u65536_as_float) + jsr FLD1R + jsr FMUL + jsr FMOVE + + ; convert low word + pla + lda temp + 1 + pla + lda temp + jsr IFP + + ; combine + jsr FADD + + ; scale + ldx #.lobyte(fixed6_26_as_float) + ldy #.hibyte(fixed6_26_as_float) + jsr FLD1R + jsr FDIV + + rts +.endproc + ; input in FR0, Atari float ; output in FR0, 16 bits signed 3.13 fixed ; clobbers a, x, y, FR0, FR1 @@ -1603,7 +1696,7 @@ number_keys: beq five cpy #KEY_6 beq six - jmp skip_char + jmp letter_keys one: ldx #0 @@ -1622,7 +1715,21 @@ five: jmp load_key_viewport six: ldx #5 - ; fall through + jmp load_key_viewport + +letter_keys: + cpy #KEY_X + bne not_x + jsr input_x + jmp load_key_viewport +not_x: + cpy #KEY_Y + bne not_y + jsr input_y + jmp load_key_viewport +not_y: + jmp skip_char + load_key_viewport: jsr load_viewport ; fall through @@ -1632,6 +1739,23 @@ done: .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) @@ -1679,9 +1803,7 @@ zero_byte_loop: draw_string_const str_x copy32 FR0, ox - shift_round_16 FR0, 3 - copy16 FR0, FR0 + 2 - jsr fixed3_13_to_float + jsr fixed6_26_to_float jsr FASC jsr draw_string @@ -1690,9 +1812,7 @@ zero_byte_loop: draw_string_const str_y copy32 FR0, oy - shift_round_16 FR0, 3 - copy16 FR0, FR0 + 2 - jsr fixed3_13_to_float + jsr fixed6_26_to_float jsr FASC jsr draw_string From f2a6af0995512835bae7e6ecd094e15ef5115fc3 Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Thu, 3 Jul 2025 18:43:10 -0700 Subject: [PATCH 05/27] Replace the not-enough-precision 32 bit to float impl keep the proc though to encapsulate it but uses the older logic of rounding down to 3.13 first --- mandel.s | 79 +++----------------------------------------------------- 1 file changed, 4 insertions(+), 75 deletions(-) diff --git a/mandel.s b/mandel.s index 4cc0f39..a97d6e3 100644 --- a/mandel.s +++ b/mandel.s @@ -261,28 +261,6 @@ fixed3_13_as_float: ; float48 .byte $00 .byte $00 -u65536_as_float: ; float48 - ; 1 << 16 - ; 65536 - ; 06 55 36 . 00 00 - .byte 66 ; exponent/sign - +2 bytes - .byte $06 - .byte $55 - .byte $36 - .byte $00 - .byte $00 - -fixed6_26_as_float: ; float48 - ; 1 << 26 - ; 67108864 - ; 67 10 88 64 . 00 - .byte 67 ; exponent/sign - +3 bytes - .byte $67 - .byte $10 - .byte $88 - .byte $64 - .byte $00 - sec_per_frame: ; float48 00 . 01 66 66 66 67 .byte 63 ; exponent/sign - -1 bytes .byte $01 ; BCD digits @@ -1016,63 +994,14 @@ common: .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 - ; check sign bit! conversion routine is for unsigned - lda FR0 + 3 - and #$80 - sta temp - - beq positive - neg32 FR0 -positive: - - ; save low word - lda FR0 - pha - lda FR0 + 1 - pha - - ; convert high word - sta FR0 + 2 - sta FR1 - lda FR0 + 3 - sta FR0 + 1 - jsr IFP - - lda temp - beq positive2 - ; set float sign bit - lda FR0 - ora #$80 - sta FR0 -positive2: - - ; high word to FR1 - ldx #.lobyte(u65536_as_float) - ldy #.hibyte(u65536_as_float) - jsr FLD1R - jsr FMUL - jsr FMOVE - - ; convert low word - pla - lda temp + 1 - pla - lda temp - jsr IFP - - ; combine - jsr FADD - - ; scale - ldx #.lobyte(fixed6_26_as_float) - ldy #.hibyte(fixed6_26_as_float) - jsr FLD1R - jsr FDIV - + shift_round_16 FR0, 3 + copy16 FR0, FR0 + 2 + jsr fixed3_13_to_float rts .endproc From b46e6fb343e425985f53f868f1dbe4e3f6c0e384 Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Mon, 1 Sep 2025 12:28:33 -0700 Subject: [PATCH 06/27] fix typo on stub x/y inputs was accidentally falling through to the load a viewport from a keypress thingy which was not needed here --- mandel.s | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mandel.s b/mandel.s index a97d6e3..5edcbce 100644 --- a/mandel.s +++ b/mandel.s @@ -1650,12 +1650,12 @@ letter_keys: cpy #KEY_X bne not_x jsr input_x - jmp load_key_viewport + jmp done not_x: cpy #KEY_Y bne not_y jsr input_y - jmp load_key_viewport + jmp done not_y: jmp skip_char From 29cd3d968fbff768c23959fab2f6addcaff8ccfe Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sat, 6 Sep 2025 19:53:25 -0700 Subject: [PATCH 07/27] Shaves 3 seconds off initial view runtime on XE :D Instead of relying solely on the JMP thunks added to imul16_func and sqr16_func, three call sites within the mandelbrot iteration function are patched directly to jsr to the XE versions, saving like 15 cycles per iter Ok so it's not a lot, but every seconds counts. ;) with XE code disabled: 1539 us/iter 5m13s with old XE code: 1417 us/iter 4m48s with new XE code: 1406 us/iter 4m45s --- mandel.s | 147 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 82 insertions(+), 65 deletions(-) diff --git a/mandel.s b/mandel.s index 5edcbce..b0f9c28 100644 --- a/mandel.s +++ b/mandel.s @@ -485,6 +485,7 @@ input_max: .endmacro ; 6 * bytes cycles +; 4 * bytes bytes .macro copy bytes, dest, arg .repeat bytes, byte ; 6 * bytes cycles lda arg + byte ; 3 cyc @@ -493,6 +494,7 @@ input_max: .endmacro ; 12 cycles +; 8 bytes .macro copy16 dest, arg copy 2, dest, arg .endmacro @@ -538,6 +540,8 @@ input_max: ; 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 @@ -547,6 +551,8 @@ input_max: ; 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 @@ -692,71 +698,6 @@ bank_switch_table: .endif .endmacro -.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 - lda #.hibyte(imul16xe_func) - sta imul16_func + 2 - - ; ditto for sqr16_func -> sqr16xe_func - lda #$4c ; 'jmp' opcode - sta sqr16_func - lda #.lobyte(sqr16xe_func) - sta sqr16_func + 1 - lda #.hibyte(sqr16xe_func) - sta sqr16_func + 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 ; Initialize a 16 KB chunk of the table ; input: multipliers in temp @@ -1156,12 +1097,15 @@ keep_going: 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 @@ -2162,3 +2106,76 @@ countdown_done: 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 From 6479cf530c1c584f33b96f2b19885d02415863bb Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Tue, 16 Sep 2025 21:29:40 -0700 Subject: [PATCH 08/27] update some timings --- mandel.s | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mandel.s b/mandel.s index b0f9c28..b52f24a 100644 --- a/mandel.s +++ b/mandel.s @@ -461,7 +461,7 @@ input_max: sub 4, dest, arg1, arg2 .endmacro -; 3 + 5 * bytes cycles +; 3 + 5 * (bytes - 1) cycles .macro shl bytes, arg asl arg ; 3 cyc .repeat bytes-1, i @@ -469,17 +469,17 @@ input_max: .endrepeat .endmacro -; 13 cycles +; 8 cycles .macro shl16 arg shl 2, arg .endmacro -; 18 cycles +; 13 cycles .macro shl24 arg shl 3, arg .endmacro -; 23 cycles +; 18 cycles .macro shl32 arg shl 4, arg .endmacro @@ -529,11 +529,11 @@ input_max: neg 4, arg .endmacro -; 11-27 + 23 * shift cycles -; 103-119 cycles for shift=4 +; 11-27 + 18 * shift cycles +; 65-81 cycles for shift=3 .macro shift_round_16 arg, shift .repeat shift - shl32 arg ; 23 cycles + shl32 arg ; 18 cycles .endrepeat round16 arg ; 11-27 cycles .endmacro From b27be3c1592c26609a26b6d0f82dcaf88aad5763 Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sun, 28 Dec 2025 09:23:38 -0800 Subject: [PATCH 09/27] Add a C shell, which currently just passes through This is a first step toward moving the UI to C and adding file and network I/O in C. The fractal core will remain in assembler as well as the multiplier. --- Makefile | 10 ++++--- atari-xex.cfg | 62 +++++++++++++++++++++++++++++++++++++++ mandel.s => mandel-core.s | 4 +-- mandel.c | 15 ++++++++++ mandel.h | 4 +++ 5 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 atari-xex.cfg rename mandel.s => mandel-core.s (99%) create mode 100644 mandel.c create mode 100644 mandel.h diff --git a/Makefile b/Makefile index 711adcd..c94074b 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,11 @@ all : mandel.xex -mandel.xex : mandel.o tables.o atari-asm-xex.cfg - ld65 -C ./atari-asm-xex.cfg --mapfile mandel.map -o $@ mandel.o tables.o +mandel.xex : mandel.o mandel-core.o tables.o atari-xex.cfg + ld65 -C ./atari-xex.cfg --mapfile mandel.map -o $@ mandel.o mandel-core.o tables.o atari.lib + +mandel.s : mandel.c mandel.h + cc65 -o $@ mandel.c %.o : %.s ca65 -o $@ $< @@ -13,8 +16,7 @@ tables.s : tables.js clean : rm -f tables.s + rm -f mandel.s rm -f *.o rm -f *.xex rm -f mandel.map - - diff --git a/atari-xex.cfg b/atari-xex.cfg new file mode 100644 index 0000000..ee41c4c --- /dev/null +++ b/atari-xex.cfg @@ -0,0 +1,62 @@ +# Sample linker configuration for C programs using the Atari binary file support. +# Use with: cl65 -tatari -Catari-xex.cfg prog.c -o prog.xex +FEATURES { + STARTADDRESS: default = $2000; +} +SYMBOLS { + __SYSTEM_CHECK__: type = import; # force inclusion of "system check" load chunk + __STACKSIZE__: type = weak, value = $0800; # 2k stack + __STARTADDRESS__: type = export, value = %S; + __RESERVED_MEMORY__: type = weak, value = $0000; + __SYSCHKHDR__: type = export, value = 0; # Disable system check header + __SYSCHKTRL__: type = export, value = 0; # Disable system check trailer +} +MEMORY { + ZP: file = "", define = yes, start = $0082, size = $007E; +# "system check" load chunk + SYSCHKCHNK: file = %O, start = $2E00, size = $0300; +# "main program" load chunk +# Note we reserve $4000-7fff for the bank-switch window. + #MAIN: file = %O, define = yes, start = %S, size = $BC20 - __STACKSIZE__ - __RESERVED_MEMORY__ - %S; + MAIN: file = %O, define = yes, start = %S, size = $4000 - __STACKSIZE__ - __RESERVED_MEMORY__ - %S; + +# Note $a000-$bfff is against the BASIC cartridge, may require booting with OPTION. + TABLES: file = %O, define = yes, start = $8000, size = $a000 - $8000; +} +FILES { + %O: format = atari; +} +FORMATS { + atari: runad = start, + initad = SYSCHKCHNK: __SYSTEM_CHECK__; +} +SEGMENTS { + ZEROPAGE: load = ZP, type = zp; + EXTZP: load = ZP, type = zp, optional = yes; + SYSCHK: load = SYSCHKCHNK, type = rw, define = yes, optional = yes; + STARTUP: load = MAIN, type = ro, define = yes; + LOWBSS: load = MAIN, type = rw, optional = yes; # not zero initialized + LOWCODE: load = MAIN, type = ro, define = yes, optional = yes; + ONCE: load = MAIN, type = ro, optional = yes; + CODE: load = MAIN, type = ro, define = yes; + RODATA: load = MAIN, type = ro; + DATA: load = MAIN, type = rw; + INIT: load = MAIN, type = rw, optional = yes; + BSS: load = MAIN, type = bss, define = yes; + TABLES: load = TABLES, type = ro, optional = yes, align = 256; +} +FEATURES { + CONDES: type = constructor, + label = __CONSTRUCTOR_TABLE__, + count = __CONSTRUCTOR_COUNT__, + segment = ONCE; + CONDES: type = destructor, + label = __DESTRUCTOR_TABLE__, + count = __DESTRUCTOR_COUNT__, + segment = RODATA; + CONDES: type = interruptor, + label = __INTERRUPTOR_TABLE__, + count = __INTERRUPTOR_COUNT__, + segment = RODATA, + import = __CALLIRQ__; +} diff --git a/mandel.s b/mandel-core.s similarity index 99% rename from mandel.s rename to mandel-core.s index b52f24a..6ebb089 100644 --- a/mandel.s +++ b/mandel-core.s @@ -361,7 +361,7 @@ z_buffer: .word 0 .endrepeat -.export start +.export _mandel_start ;max_fill_level = 6 max_fill_level = 3 @@ -1745,7 +1745,7 @@ zero_byte_loop: rts .endproc -.proc start +.proc _mandel_start jsr imul8xe_init diff --git a/mandel.c b/mandel.c new file mode 100644 index 0000000..f287fa3 --- /dev/null +++ b/mandel.c @@ -0,0 +1,15 @@ +/** + * The UI and I/O wrapper for the Mandelbrot runner, in C. + * + * For the moment *all* logic is in mandel-core.s, I'm just + * trying to get this to run within a cc65 environment. + * Eventually just the inner loop fun will live in there. + */ + +#include +#include +#include "mandel.h" + +void main(void) { + mandel_start(); +} \ No newline at end of file diff --git a/mandel.h b/mandel.h new file mode 100644 index 0000000..e43fad7 --- /dev/null +++ b/mandel.h @@ -0,0 +1,4 @@ +#include + +// From mandel-core.s: +extern void mandel_start(void); From 97fdc12565c9f2e6b853b0e97688179195ee5281 Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sun, 28 Dec 2025 12:32:57 -0800 Subject: [PATCH 10/27] Put the tables before the main code, and shrink the segment Leaves more room for code and dynamic data/stack --- atari-xex.cfg | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/atari-xex.cfg b/atari-xex.cfg index ee41c4c..e9090b4 100644 --- a/atari-xex.cfg +++ b/atari-xex.cfg @@ -10,18 +10,19 @@ SYMBOLS { __RESERVED_MEMORY__: type = weak, value = $0000; __SYSCHKHDR__: type = export, value = 0; # Disable system check header __SYSCHKTRL__: type = export, value = 0; # Disable system check trailer + __TABLESEG_SIZE__: type = weak, value = 6 * $100; + __FRAMEBUFFER_START__: type = weak, value = $a000; } MEMORY { ZP: file = "", define = yes, start = $0082, size = $007E; # "system check" load chunk SYSCHKCHNK: file = %O, start = $2E00, size = $0300; -# "main program" load chunk # Note we reserve $4000-7fff for the bank-switch window. - #MAIN: file = %O, define = yes, start = %S, size = $BC20 - __STACKSIZE__ - __RESERVED_MEMORY__ - %S; - MAIN: file = %O, define = yes, start = %S, size = $4000 - __STACKSIZE__ - __RESERVED_MEMORY__ - %S; - # Note $a000-$bfff is against the BASIC cartridge, may require booting with OPTION. - TABLES: file = %O, define = yes, start = $8000, size = $a000 - $8000; + TABLES: file = %O, define = yes, start = %S, size = __TABLESEG_SIZE__; +# "main program" load chunk + MAIN: file = %O, define = yes, start = %S + __TABLESEG_SIZE__, size = __FRAMEBUFFER_START__ - __STACKSIZE__ - __RESERVED_MEMORY__ - __TABLESEG_SIZE__ - %S; + } FILES { %O: format = atari; @@ -34,6 +35,7 @@ SEGMENTS { ZEROPAGE: load = ZP, type = zp; EXTZP: load = ZP, type = zp, optional = yes; SYSCHK: load = SYSCHKCHNK, type = rw, define = yes, optional = yes; + TABLES: load = TABLES, type = ro, optional = yes, align = 256; STARTUP: load = MAIN, type = ro, define = yes; LOWBSS: load = MAIN, type = rw, optional = yes; # not zero initialized LOWCODE: load = MAIN, type = ro, define = yes, optional = yes; @@ -43,7 +45,6 @@ SEGMENTS { DATA: load = MAIN, type = rw; INIT: load = MAIN, type = rw, optional = yes; BSS: load = MAIN, type = bss, define = yes; - TABLES: load = TABLES, type = ro, optional = yes, align = 256; } FEATURES { CONDES: type = constructor, From a93dd00e3697f9af47bb09a118722678b597a4cf Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sun, 28 Dec 2025 12:55:08 -0800 Subject: [PATCH 11/27] Rearrange the segments a bit * put TABLES in the low memory, before the bank switch window * reserve bank switch window * put rest of the code after that and before the framebuffer so TABLES lives just before $4000 and MAIN lives in $8000-$bfff could split some more code and/or data into low mem and/or move the tables not used in extended memory mode into the bank switch window so they take no address space on XE or expanded memory machines --- atari-xex.cfg | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/atari-xex.cfg b/atari-xex.cfg index e9090b4..467d9d4 100644 --- a/atari-xex.cfg +++ b/atari-xex.cfg @@ -1,7 +1,7 @@ # Sample linker configuration for C programs using the Atari binary file support. # Use with: cl65 -tatari -Catari-xex.cfg prog.c -o prog.xex FEATURES { - STARTADDRESS: default = $2000; + STARTADDRESS: default = $8000; } SYMBOLS { __SYSTEM_CHECK__: type = import; # force inclusion of "system check" load chunk @@ -10,19 +10,24 @@ SYMBOLS { __RESERVED_MEMORY__: type = weak, value = $0000; __SYSCHKHDR__: type = export, value = 0; # Disable system check header __SYSCHKTRL__: type = export, value = 0; # Disable system check trailer + __TABLESEG_START__: type = weak, value = $2E00 + $0300; __TABLESEG_SIZE__: type = weak, value = 6 * $100; - __FRAMEBUFFER_START__: type = weak, value = $a000; + __BANKSY_START__: type = weak, value = $4000; + __BANKSY_SIZE__: type = weak, value = $4000; + __FRAMEBUFFER_START__: type = weak, value = $A000; } MEMORY { +# Note -- $80 and $81 (LOMEM) appear to be reserved in ZP. ZP: file = "", define = yes, start = $0082, size = $007E; # "system check" load chunk SYSCHKCHNK: file = %O, start = $2E00, size = $0300; -# Note we reserve $4000-7fff for the bank-switch window. # Note $a000-$bfff is against the BASIC cartridge, may require booting with OPTION. - TABLES: file = %O, define = yes, start = %S, size = __TABLESEG_SIZE__; + TABLES: file = %O, define = yes, start = __TABLESEG_START__, size = __TABLESEG_SIZE__; +# We reserve $4000-7fff for the bank-switch window. +# In theory we could keep data and code here that we only use on 48k/64k systems. + BANKSWITCH: file = "", define = yes, start = __BANKSY_START__, size = __BANKSY_SIZE__; # "main program" load chunk - MAIN: file = %O, define = yes, start = %S + __TABLESEG_SIZE__, size = __FRAMEBUFFER_START__ - __STACKSIZE__ - __RESERVED_MEMORY__ - __TABLESEG_SIZE__ - %S; - + MAIN: file = %O, define = yes, start = %S, size = __FRAMEBUFFER_START__ - __STACKSIZE__ - __RESERVED_MEMORY__ - %S; } FILES { %O: format = atari; @@ -36,6 +41,7 @@ SEGMENTS { EXTZP: load = ZP, type = zp, optional = yes; SYSCHK: load = SYSCHKCHNK, type = rw, define = yes, optional = yes; TABLES: load = TABLES, type = ro, optional = yes, align = 256; + BANKSWICH: load = BANKSWITCH, type = ro, optional = yes; STARTUP: load = MAIN, type = ro, define = yes; LOWBSS: load = MAIN, type = rw, optional = yes; # not zero initialized LOWCODE: load = MAIN, type = ro, define = yes, optional = yes; From 25c37a1188b8726f95cee5615fc94cbb34765b0c Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Wed, 8 Apr 2026 19:58:27 -0700 Subject: [PATCH 12/27] zeropage tweaks * switched zero-page from hardcoded assignments to symbols * moved most non-hotpath stuff out to .data * merged ptr and pixel_ptr Slight slowdown in Atari800MacX from 5m13s to 5m15s --- mandel-core.s | 103 +++++++++++++++++++++++++------------------------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/mandel-core.s b/mandel-core.s index 6ebb089..34dff8b 100644 --- a/mandel-core.s +++ b/mandel-core.s @@ -1,44 +1,44 @@ -; Our zero-page vars -ox = $80 ; fixed6.26: center point x -oy = $84 ; fixed6.26: center point y -cx = $88 ; fixed6.26: c_x -cy = $8c ; fixed6.26: c_y +.zeropage -zx = $90 ; fixed6.26: z_x -zy = $94 ; fixed6.26: z_y -zx_2 = $98 ; fixed6.26: z_x^2 -zy_2 = $9c ; fixed6.26: z_y^2 +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_zy = $a0 ; fixed6.26: z_x * z_y -dist = $a4 ; fixed6.26: z_x^2 + z_y^2 -sx = $a8 ; i16: screen pixel x -sy = $aa ; i16: screen pixel y -z_buffer_active = $ac ; boolean: 1 if we triggered the lake, 0 if not -z_buffer_start = $ad ; u8: index into z_buffer -z_buffer_end = $ae ; u8: index into z_buffer -iter = $af ; u8: iteration count +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 -ptr = $b0 ; u16 -pixel_ptr = $b2 ; u16 -zoom = $b4 ; u8: zoom shift level -fill_level = $b5 ; u8 -pixel_color = $b6 ; u8 -pixel_mask = $b7 ; u8 -pixel_shift = $b8 ; u8 -pixel_offset = $b9 ; u8 -palette_offset = $ba ; u8 -chroma_offset = $bb ; u8 -palette_ticks = $bc ; u8 -chroma_ticks = $bd ; u8 -count_frames = $be ; u8 -; free space $bf +zx_zy: .res 4 ; fixed6.26: z_x * z_y +dist: .res 4 ; fixed6.26: z_x^2 + z_y^2 -count_iters = $c0 ; u16 -text_col = $c2 ; u8 -text_row = $c3 ; u8 -; free space c4-cb -temp = $cc ; u16 -temp2 = $ce ; u16 +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 +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 @@ -131,6 +131,8 @@ KEY_E = 42 KEY_X = 22 KEY_Y = 43 +.data + .struct float48 exponent .byte mantissa .byte 5 @@ -142,7 +144,6 @@ KEY_Y = 43 .import sqr_lobyte .import sqr_hibyte -.data strings: str_self: @@ -1253,21 +1254,21 @@ enough: negative: ; temp1 = top half lda #.lobyte(framebuffer_top + stride * half_height) - sta pixel_ptr + sta ptr lda #.hibyte(framebuffer_top + stride * half_height) - sta pixel_ptr + 1 + sta ptr + 1 jmp point positive: lda #.lobyte(framebuffer_bottom) - sta pixel_ptr + sta ptr lda #.hibyte(framebuffer_bottom) - sta pixel_ptr + 1 + sta ptr + 1 point: - ; pixel_ptr += sy * stride + ; ptr += sy * stride ; temp * 40 ; = temp * 32 + temp * 8 ; = (temp << 5) + (temp << 3) @@ -1275,10 +1276,10 @@ point: shl16 temp shl16 temp shl16 temp - add16 pixel_ptr, pixel_ptr, temp + add16 ptr, ptr, temp shl16 temp shl16 temp - add16 pixel_ptr, pixel_ptr, temp + add16 ptr, ptr, temp ; Ok so temp1 points to the start of the line, which is 40 bytes. ; Get the byte and bit offsets @@ -1318,20 +1319,20 @@ shift_done: draw_pixel: ; read, mask, or, write - lda (pixel_ptr),y + lda (ptr),y and pixel_mask ora pixel_color - sta (pixel_ptr),y + sta (ptr),y dex beq done clc lda #40 - adc pixel_ptr - sta pixel_ptr + adc ptr + sta ptr lda #0 - adc pixel_ptr + 1 - sta pixel_ptr + 1 + adc ptr + 1 + sta ptr + 1 jmp draw_pixel done: From 3ce4c1e580bfa6d186aa89f4e2b199cff7b7e5d7 Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sat, 22 Aug 2026 13:01:23 -0700 Subject: [PATCH 13/27] Add some timing notes --- mandel-core.s | 68 +++++++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/mandel-core.s b/mandel-core.s index 34dff8b..5080d7e 100644 --- a/mandel-core.s +++ b/mandel-core.s @@ -563,12 +563,13 @@ sqr16_patch_offset = 8 ; input: arg as u8 ; output: dest as u16 ; clobbers a, x +; 19 cyc .macro sqr8 dest, arg - ldx arg - lda sqr_lobyte,x - sta dest - lda sqr_hibyte,x - sta dest + 1 + 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" @@ -770,6 +771,10 @@ inner_loop: .endproc +; min low-mem: 81 * 4 + 28 * 2 + 10 + 6 = 396 cyc +; max low-mem: 92 * 4 + 28 * 2 + 50 + 6 = 480 cyc +; min ext-mem: 51 * 4 + 28 * 2 + 10 + 6 = 276 cyc +; max low-mem: 70 * 4 + 28 * 2 + 50 + 6 = 392 cyc .macro imul16_impl xe .local arg1 .local arg2 @@ -787,32 +792,37 @@ inner_loop: ; h1*256*(h2*256 + l2) + l1*(h2*256 + l2) ; h1*h2*256*256 + h1*l2*256 + h2*l1*256 + l1*l2 - imul8 result, arg1, arg2, xe + imul8 result, arg1, arg2, xe ; 81-92 or 51-70 - imul8 result + 2, arg1 + 1, arg2 + 1, xe + imul8 result + 2, arg1 + 1, arg2 + 1, xe ; 81-92 or 51-70 - imul8 inter, arg1 + 1, arg2, xe - add16 result + 1, result + 1, inter - add_carry result + 3 + imul8 inter, arg1 + 1, arg2, xe ; 81-92 or 51-70 + add16 result + 1, result + 1, inter ; 20 cyc + add_carry result + 3 ; 8 cyc - imul8 inter, arg1, arg2 + 1, xe - add16 result + 1, result + 1, inter - add_carry result + 3 + imul8 inter, arg1, arg2 + 1, xe ; 81-92 or 51-70 + add16 result + 1, result + 1, inter ; 20 cyc + add_carry result + 3 ; 8 cyc ; In case of negative inputs, adjust high word ; https://stackoverflow.com/a/28827013 - lda arg1 + 1 - bpl arg1_pos - sub16 result + 2, result + 2, arg2 + ; 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 - bpl arg2_pos - sub16 result + 2, result + 2, arg1 + 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: 19 * 2 + 81 + 28 * 2 + 6 = 181 +; max base ram: 19 * 2 + 92 + 28 * 2 + 6 = 192 +; min ext ram: 19 * 2 + 51 + 28 * 2 + 6 = 151 +; max ext ram: 19 * 2 + 70 + 28 * 2 + 6 = 170 .macro sqr16_impl xe .scope arg = FR0 ; 16-bit arg (clobbered) @@ -820,9 +830,9 @@ arg2_pos: ;inter = temp2 inter = FR1 - lda arg + 1 - bpl arg_pos - neg16 arg + lda arg + 1 ; 3 cyc + bpl arg_pos ; 2 cyc + neg16 arg ; 18 cyc arg_pos: ; hl * hl @@ -830,15 +840,15 @@ arg2_pos: ; h*256*(h*256 + l) + l*(h*256 + l) ; h*h*256*256 + h*l*256 + h*l*256 + l*l - sqr8 result, arg + sqr8 result, arg ; 19 cyc - sqr8 result + 2, arg + 1 + sqr8 result + 2, arg + 1 ; 19 cyc - imul8 inter, arg + 1, arg, xe - add16 result + 1, result + 1, inter - add_carry result + 3 - add16 result + 1, result + 1, inter - add_carry result + 3 + imul8 inter, arg + 1, arg, xe ; 81-92 / 51-70 cyc + add16 result + 1, result + 1, inter ; 20 cyc + add_carry result + 3 ; 8 cyc + add16 result + 1, result + 1, inter ; 20 cyc + add_carry result + 3 ; 8 cyc rts ; 6 cyc .endscope From 173901e49a24922700c8d54825d822d0f62fd2d3 Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sat, 22 Aug 2026 13:01:56 -0700 Subject: [PATCH 14/27] Add a copy of the old bit-shift multiplier code --- bitmul.s | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 bitmul.s diff --git a/bitmul.s b/bitmul.s new file mode 100644 index 0000000..df84704 --- /dev/null +++ b/bitmul.s @@ -0,0 +1,124 @@ +; the old 16-bit bit-and-shift multiplier +; copied from old code, won't compile as-is + +; inner loop for imul16 +; bitnum < 8: 25 or 41 cycles +; bitnum >= 8: 30 or 46 cycles +.macro bitmul16 arg1, arg2, result, bitnum + .local zero + .local one + .local next + + ; does 16-bit adds + ; arg1 and arg2 are treated as unsigned + ; negative signed inputs must be flipped first + + ; 7 cycles up to the branch + + ; check if arg1 has 0 or 1 bit in this place + ; 5 cycles either way + .if bitnum < 8 + lda arg1 ; 3 cyc + and #(1 << (bitnum)) ; 2 cyc + .else + lda arg1 + 1 ; 3 cyc + and #(1 << ((bitnum) - 8)) ; 2 cyc + .endif + bne one ; 2 cyc + +zero: ; 18 cyc, 23 cyc + lsr result + 3 ; 5 cyc + jmp next ; 3 cyc + +one: ; 32 cyc, 37 cyc + ; 16-bit add on the top bits + clc ; 2 cyc + lda result + 2 ; 3 cyc + adc arg2 ; 3 cyc + sta result + 2 ; 3 cyc + lda result + 3 ; 3 cyc + adc arg2 + 1 ; 3 cyc + ror a ; 2 cyc - get a jump on the shift + sta result + 3 ; 3 cyc +next: + ror result + 2 ; 5 cyc + ror result + 1 ; 5 cyc + .if bitnum >= 8 + ; we can save 5 cycles * 8 bits = 40 cycles total by skipping this byte + ; when it's all uninitialized data + ror result ; 5 cyc + .endif + +.endmacro + +; 5 to 25 cycles +.macro check_sign arg + ; Check sign bit and flip argument to postive, + ; keeping a count of sign bits in the X register. + .local positive + lda arg + 1 ; 3 cyc + bpl positive ; 2 cyc + neg16 arg ; 18 cyc + inx ; 2 cyc +positive: +.endmacro + +; 518 - 828 cyc +.macro imul16 dest, arg1, arg2 + copy16 FR0, arg1 ; 12 cyc + copy16 FR1, arg2 ; 12 cyc + jsr imul16_func ; 470-780 cyc + copy32 dest, FR2 ; 24 cyc +.endmacro + + +.macro shift_round_16 arg, shift + .repeat shift + shl32 arg + .endrepeat + round16 arg +.endmacro + +.macro imul16_round dest, arg1, arg2, shift + copy16 FR0, arg1 ; 12 cyc + copy16 FR1, arg2 ; 12 cyc + jsr imul16_func ; 470-780 cyc + shift_round_16 FR2, shift + copy16 dest, FR2 + 2 ; 12 cyc +.endmacro + +; min 470 cycles +; max 780 cycles +.proc imul16_func + arg1 = FR0 ; 16-bit arg (clobbered) + arg2 = FR1 ; 16-bit arg (clobbered) + result = FR2 ; 32-bit result + + ldx #0 ; 2 cyc + ; counts the number of sign bits in X + check_sign arg1 ; 5 to 25 cyc + check_sign arg2 ; 5 to 25 cyc + + ; zero out the 32-bit temp's top 16 bits + lda #0 ; 2 cyc + sta result + 2 ; 3 cyc + sta result + 3 ; 3 cyc + ; the bottom two bytes will get cleared by the shifts + + ; unrolled loop for maximum speed, at the cost + ; of a larger routine + ; 440 to 696 cycles + .repeat 16, bitnum + ; bitnum < 8: 25 or 41 cycles + ; bitnum >= 8: 30 or 46 cycles + bitmul16 arg1, arg2, result, bitnum + .endrepeat + + ; In case of mixed input signs, return a negative result. + cpx #1 ; 2 cyc + bne positive_result ; 2 cyc + neg32 result ; 34 cyc +positive_result: + + rts ; 6 cyc +.endproc From d9f7ce3e356102f62af03f51f73eeea73c762562 Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sat, 22 Aug 2026 14:27:52 -0700 Subject: [PATCH 15/27] whoops missed a bit --- mandel-core.s | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mandel-core.s b/mandel-core.s index 5080d7e..eefd557 100644 --- a/mandel-core.s +++ b/mandel-core.s @@ -819,10 +819,10 @@ arg2_pos: rts ; 6 cyc .endmacro -; min base ram: 19 * 2 + 81 + 28 * 2 + 6 = 181 -; max base ram: 19 * 2 + 92 + 28 * 2 + 6 = 192 -; min ext ram: 19 * 2 + 51 + 28 * 2 + 6 = 151 -; max ext ram: 19 * 2 + 70 + 28 * 2 + 6 = 170 +; min base ram: 5 + 19 * 2 + 81 + 28 * 2 + 6 = 186 +; max base ram: 23 + 19 * 2 + 92 + 28 * 2 + 6 = 215 +; min ext ram: 5 + 19 * 2 + 51 + 28 * 2 + 6 = 156 +; max ext ram: 23 + 19 * 2 + 70 + 28 * 2 + 6 = 193 .macro sqr16_impl xe .scope arg = FR0 ; 16-bit arg (clobbered) @@ -830,6 +830,7 @@ arg2_pos: ;inter = temp2 inter = FR1 + ; 5-23 cycles lda arg + 1 ; 3 cyc bpl arg_pos ; 2 cyc neg16 arg ; 18 cyc From b4cb773cf1fb128246a461c869f277e9ea5fa806 Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sat, 22 Aug 2026 14:34:53 -0700 Subject: [PATCH 16/27] tweak comment --- mandel-core.s | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mandel-core.s b/mandel-core.s index eefd557..5e56e87 100644 --- a/mandel-core.s +++ b/mandel-core.s @@ -771,10 +771,10 @@ inner_loop: .endproc -; min low-mem: 81 * 4 + 28 * 2 + 10 + 6 = 396 cyc -; max low-mem: 92 * 4 + 28 * 2 + 50 + 6 = 480 cyc +; min base mem: 81 * 4 + 28 * 2 + 10 + 6 = 396 cyc +; max base mem: 92 * 4 + 28 * 2 + 50 + 6 = 480 cyc ; min ext-mem: 51 * 4 + 28 * 2 + 10 + 6 = 276 cyc -; max low-mem: 70 * 4 + 28 * 2 + 50 + 6 = 392 cyc +; max ext-mem: 70 * 4 + 28 * 2 + 50 + 6 = 392 cyc .macro imul16_impl xe .local arg1 .local arg2 From 829f46755ad73438828440a23ed082e46e7a6d8d Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sat, 22 Aug 2026 14:43:17 -0700 Subject: [PATCH 17/27] Save another few cycles per multiple add_carry took a fixed 8 cycles with a lda/adc/sta pattern we can instead use bcc to handle the carry-not-set case in just 2 cycles, skipping over the inc which takes 5 cycles on the carry-is-set case. Result is 2-7 cycles instead of 8, saving 1-6 cycles twice per 16-bit multiplication or square. Neat! --- mandel-core.s | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/mandel-core.s b/mandel-core.s index 5e56e87..a153ff5 100644 --- a/mandel-core.s +++ b/mandel-core.s @@ -435,11 +435,13 @@ input_max: add 4, dest, arg1, arg2 .endmacro -; 8 cycles +; 2-7 cycles .macro add_carry dest - lda dest ; 3 cyc - adc #0 ; 2 cyc - sta dest ; 3 cyc + .scope + bcc after ; 2 cyc + inc dest ; 5 cyc + after: + .endscope .endmacro ; 2 + 9 * byte cycles @@ -771,10 +773,10 @@ inner_loop: .endproc -; min base mem: 81 * 4 + 28 * 2 + 10 + 6 = 396 cyc -; max base mem: 92 * 4 + 28 * 2 + 50 + 6 = 480 cyc -; min ext-mem: 51 * 4 + 28 * 2 + 10 + 6 = 276 cyc -; max ext-mem: 70 * 4 + 28 * 2 + 50 + 6 = 392 cyc +; 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: 51 * 4 + 22 * 2 + 10 + 6 = 264 cyc +; max ext-mem: 70 * 4 + 27 * 2 + 50 + 6 = 390 cyc .macro imul16_impl xe .local arg1 .local arg2 @@ -798,11 +800,11 @@ inner_loop: imul8 inter, arg1 + 1, arg2, xe ; 81-92 or 51-70 add16 result + 1, result + 1, inter ; 20 cyc - add_carry result + 3 ; 8 cyc + add_carry result + 3 ; 2-7 cyc imul8 inter, arg1, arg2 + 1, xe ; 81-92 or 51-70 add16 result + 1, result + 1, inter ; 20 cyc - add_carry result + 3 ; 8 cyc + add_carry result + 3 ; 2-7 cyc ; In case of negative inputs, adjust high word ; https://stackoverflow.com/a/28827013 @@ -819,10 +821,10 @@ arg2_pos: rts ; 6 cyc .endmacro -; min base ram: 5 + 19 * 2 + 81 + 28 * 2 + 6 = 186 -; max base ram: 23 + 19 * 2 + 92 + 28 * 2 + 6 = 215 -; min ext ram: 5 + 19 * 2 + 51 + 28 * 2 + 6 = 156 -; max ext ram: 23 + 19 * 2 + 70 + 28 * 2 + 6 = 193 +; 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 + 51 + 22 * 2 + 6 = 144 +; max ext ram: 23 + 19 * 2 + 70 + 27 * 2 + 6 = 191 .macro sqr16_impl xe .scope arg = FR0 ; 16-bit arg (clobbered) @@ -847,9 +849,9 @@ arg2_pos: imul8 inter, arg + 1, arg, xe ; 81-92 / 51-70 cyc add16 result + 1, result + 1, inter ; 20 cyc - add_carry result + 3 ; 8 cyc + add_carry result + 3 ; 2-7 cyc add16 result + 1, result + 1, inter ; 20 cyc - add_carry result + 3 ; 8 cyc + add_carry result + 3 ; 2-7 cyc rts ; 6 cyc .endscope From 27995007c5da6b259069bf7147d0ce19c25125a9 Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sat, 22 Aug 2026 15:25:58 -0700 Subject: [PATCH 18/27] experiment --- mandel-core.s | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/mandel-core.s b/mandel-core.s index a153ff5..69980b8 100644 --- a/mandel-core.s +++ b/mandel-core.s @@ -592,7 +592,7 @@ bank_switch_table: .macro imul8 dest, arg1, arg2, xe .if xe ; using 64KB lookup table - ; 51-70 cycles + ; 52-69 cycles ; clobbers x, y, dest, ptr .scope output = dest @@ -636,9 +636,7 @@ bank_switch_table: txa ; 2 cyc adc output ; 3 cyc sta output ; 3 cyc - lda #0 ; 2 cyc - adc output+1 ; 3 cyc - sta output+1 ; 3 cyc + add_carry output + 1 ; 2-7 cyc done: .endscope @@ -775,8 +773,8 @@ inner_loop: ; 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: 51 * 4 + 22 * 2 + 10 + 6 = 264 cyc -; max ext-mem: 70 * 4 + 27 * 2 + 50 + 6 = 390 cyc +; min ext-mem: 52 * 4 + 22 * 2 + 10 + 6 = 268 cyc +; max ext-mem: 69 * 4 + 27 * 2 + 50 + 6 = 386 cyc .macro imul16_impl xe .local arg1 .local arg2 @@ -794,15 +792,15 @@ inner_loop: ; h1*256*(h2*256 + l2) + l1*(h2*256 + l2) ; h1*h2*256*256 + h1*l2*256 + h2*l1*256 + l1*l2 - imul8 result, arg1, arg2, xe ; 81-92 or 51-70 + imul8 result, arg1, arg2, xe ; 81-92 or 52-69 - imul8 result + 2, arg1 + 1, arg2 + 1, xe ; 81-92 or 51-70 + imul8 result + 2, arg1 + 1, arg2 + 1, xe ; 81-92 or 52-69 - imul8 inter, arg1 + 1, arg2, xe ; 81-92 or 51-70 + imul8 inter, arg1 + 1, arg2, xe ; 81-92 or 52-69 add16 result + 1, result + 1, inter ; 20 cyc add_carry result + 3 ; 2-7 cyc - imul8 inter, arg1, arg2 + 1, xe ; 81-92 or 51-70 + imul8 inter, arg1, arg2 + 1, xe ; 81-92 or 52-69 add16 result + 1, result + 1, inter ; 20 cyc add_carry result + 3 ; 2-7 cyc @@ -823,8 +821,8 @@ arg2_pos: ; 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 + 51 + 22 * 2 + 6 = 144 -; max ext ram: 23 + 19 * 2 + 70 + 27 * 2 + 6 = 191 +; min ext ram: 5 + 19 * 2 + 52 + 22 * 2 + 6 = 145 +; max ext ram: 23 + 19 * 2 + 69 + 27 * 2 + 6 = 190 .macro sqr16_impl xe .scope arg = FR0 ; 16-bit arg (clobbered) @@ -847,7 +845,7 @@ arg2_pos: sqr8 result + 2, arg + 1 ; 19 cyc - imul8 inter, arg + 1, arg, xe ; 81-92 / 51-70 cyc + imul8 inter, arg + 1, arg, xe ; 81-92 / 52-69 cyc add16 result + 1, result + 1, inter ; 20 cyc add_carry result + 3 ; 2-7 cyc add16 result + 1, result + 1, inter ; 20 cyc From e284587edc743cce7961c61c0c0ccacce9ce9edd Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sat, 22 Aug 2026 15:35:03 -0700 Subject: [PATCH 19/27] Fix regression from merging ptr and pixel_ptr the XE fast-path assumed low byte of ptr never changes to save a couple cycles --- mandel-core.s | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/mandel-core.s b/mandel-core.s index 34dff8b..348e072 100644 --- a/mandel-core.s +++ b/mandel-core.s @@ -18,6 +18,7 @@ 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 @@ -1254,21 +1255,21 @@ enough: negative: ; temp1 = top half lda #.lobyte(framebuffer_top + stride * half_height) - sta ptr + sta pixel_ptr lda #.hibyte(framebuffer_top + stride * half_height) - sta ptr + 1 + sta pixel_ptr + 1 jmp point positive: lda #.lobyte(framebuffer_bottom) - sta ptr + sta pixel_ptr lda #.hibyte(framebuffer_bottom) - sta ptr + 1 + sta pixel_ptr + 1 point: - ; ptr += sy * stride + ; pixel_ptr += sy * stride ; temp * 40 ; = temp * 32 + temp * 8 ; = (temp << 5) + (temp << 3) @@ -1276,10 +1277,10 @@ point: shl16 temp shl16 temp shl16 temp - add16 ptr, ptr, temp + add16 pixel_ptr, pixel_ptr, temp shl16 temp shl16 temp - add16 ptr, ptr, 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 @@ -1319,20 +1320,20 @@ shift_done: draw_pixel: ; read, mask, or, write - lda (ptr),y + lda (pixel_ptr),y and pixel_mask ora pixel_color - sta (ptr),y + sta (pixel_ptr),y dex beq done clc lda #40 - adc ptr - sta ptr + adc pixel_ptr + sta pixel_ptr lda #0 - adc ptr + 1 - sta ptr + 1 + adc pixel_ptr + 1 + sta pixel_ptr + 1 jmp draw_pixel done: From f6489670b117f8c4a2463a81c62c7af4b135852e Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sat, 22 Aug 2026 15:58:17 -0700 Subject: [PATCH 20/27] comments --- mandel-core.s | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mandel-core.s b/mandel-core.s index f021749..ddcd7fc 100644 --- a/mandel-core.s +++ b/mandel-core.s @@ -793,14 +793,18 @@ inner_loop: ; 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 52-69 + ; 256 * 256 * h1 * h2 imul8 result + 2, arg1 + 1, arg2 + 1, xe ; 81-92 or 52-69 + ; 256 * h1 * l1 imul8 inter, arg1 + 1, arg2, xe ; 81-92 or 52-69 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 52-69 add16 result + 1, result + 1, inter ; 20 cyc add_carry result + 3 ; 2-7 cyc @@ -842,10 +846,13 @@ arg2_pos: ; 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 / 52-69 cyc add16 result + 1, result + 1, inter ; 20 cyc add_carry result + 3 ; 2-7 cyc From 23fa002f3393563da4195b6248a03632ee35872a Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sat, 22 Aug 2026 16:30:41 -0700 Subject: [PATCH 21/27] comments --- mandel-core.s | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mandel-core.s b/mandel-core.s index ddcd7fc..1f7234b 100644 --- a/mandel-core.s +++ b/mandel-core.s @@ -599,12 +599,14 @@ bank_switch_table: 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 From 65563b0e79b0b17f8de4a72795f82c36fdd66fbc Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sat, 22 Aug 2026 16:45:09 -0700 Subject: [PATCH 22/27] 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 --- mandel-core.s | 69 +++++++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/mandel-core.s b/mandel-core.s index 1f7234b..5159e2f 100644 --- a/mandel-core.s +++ b/mandel-core.s @@ -593,7 +593,9 @@ bank_switch_table: .macro imul8 dest, arg1, arg2, xe .if xe ; using 64KB lookup table - ; 52-69 cycles + ; 50-55 cycles + ; min: 11 + 9 + 9 + 21 = 50 + ; max: 11 + 9 + 9 + 26 = 55 ; clobbers x, y, dest, ptr .scope output = dest @@ -612,15 +614,36 @@ bank_switch_table: ora #$40 ; 2 cyc sta ptr + 1 ; 3 cyc - ; copy the entry into output + ; mask off the low bit for the index, as the table contains + ; entries for every other arg1 value... lda arg1 ; 3 cyc - and #$fe ; 2 cyc tay ; 2 cyc - lda (ptr),y ; 5 cyc - sta output ; 3 cyc - iny ; 2 cyc - lda (ptr),y ; 5 cyc - sta output+1 ; 3 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 @@ -629,18 +652,6 @@ bank_switch_table: ;;lda #$81 ; 2 cyc - disabled ;;sta PORTB ; 4 cyc - disabled - ; check that 1 bit we skipped to fit into space - lda arg1 ; 3 cyc - and #1 ; 2 cyc - beq done ; 2 cyc - - ; add arg2 one last time for the skipped bit - clc ; 2 cyc - txa ; 2 cyc - adc output ; 3 cyc - sta output ; 3 cyc - add_carry output + 1 ; 2-7 cyc - done: .endscope .else @@ -776,8 +787,8 @@ inner_loop: ; 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: 52 * 4 + 22 * 2 + 10 + 6 = 268 cyc -; max ext-mem: 69 * 4 + 27 * 2 + 50 + 6 = 386 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 @@ -796,18 +807,18 @@ inner_loop: ; h1*h2*256*256 + h1*l2*256 + h2*l1*256 + l1*l2 ; l1 * l2 - imul8 result, arg1, arg2, xe ; 81-92 or 52-69 + 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 52-69 + 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 52-69 + 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 52-69 + 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 @@ -828,8 +839,8 @@ arg2_pos: ; 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 + 52 + 22 * 2 + 6 = 145 -; max ext ram: 23 + 19 * 2 + 69 + 27 * 2 + 6 = 190 +; 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) @@ -855,7 +866,7 @@ arg2_pos: sqr8 result + 2, arg + 1 ; 19 cyc ; 2 * h * l * 256 - imul8 inter, arg + 1, arg, xe ; 81-92 / 52-69 cyc + 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 From e029213c51da3bf1ff19dc13578c9220184099fa Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sat, 22 Aug 2026 17:53:32 -0700 Subject: [PATCH 23/27] comment fix --- mandel-core.s | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mandel-core.s b/mandel-core.s index 5159e2f..2777bda 100644 --- a/mandel-core.s +++ b/mandel-core.s @@ -812,7 +812,7 @@ inner_loop: ; 256 * 256 * h1 * h2 imul8 result + 2, arg1 + 1, arg2 + 1, xe ; 81-92 or 50-55 - ; 256 * h1 * l1 + ; 256 * h1 * l2 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 From f3a92708d23d7822e4433f685313970a42047c9b Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sat, 22 Aug 2026 18:48:05 -0700 Subject: [PATCH 24/27] unclobber arg in sqr16 this'll make it easier to use direct targets bypassing function args costs 2 cycles on negatives in this version for now but it should save us later --- mandel-core.s | 137 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 82 insertions(+), 55 deletions(-) diff --git a/mandel-core.s b/mandel-core.s index 2777bda..64c5540 100644 --- a/mandel-core.s +++ b/mandel-core.s @@ -533,6 +533,20 @@ input_max: neg 4, arg .endmacro +.macro copy_neg bytes, dest, arg + sec ; 2 cyc + .repeat bytes, byte ; 8 * byte cycles + lda #00 ; 2 cyc + sbc arg + byte ; 3 cyc + sta dest + byte ; 3 cyc + .endrepeat +.endmacro + +; 18 cycles +.macro copy_neg16 dest, arg + copy_neg 2, dest, arg +.endmacro + ; 11-27 + 18 * shift cycles ; 65-81 cycles for shift=3 .macro shift_round_16 arg, shift @@ -790,70 +804,58 @@ inner_loop: ; 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 + .scope + arg1 = FR0 ; 16-bit arg + arg2 = FR1 ; 16-bit arg + result = FR2 ; 32-bit result (output) + inter = temp2 ; 16-bit temporary (clobbered) - ; 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 + ; 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 + ; 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 * 256 * h1 * h2 + imul8 result + 2, arg1 + 1, arg2 + 1, xe ; 81-92 or 50-55 - ; 256 * h1 * l2 - 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 * h1 * l2 + 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 + ; 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: + ; 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: - rts ; 6 cyc + lda arg2 + 1 ; 3 cyc + bpl arg2_pos ; 2 cyc + sub16 result + 2, result + 2, arg1 ; 20 cyc + arg2_pos: + + rts ; 6 cyc + .endscope .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 + +; min base: 19 * 2 + 81 + 22 * 2 = 163 +; max base: 19 * 2 + 92 + 27 * 2 = 184 +; min ext: 19 * 2 + 50 + 22 * 2 = 132 +; max ext: 19 * 2 + 55 + 27 * 2 = 147 +.macro sqr16_impl_inner result, arg, 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: - + inter = temp2 ; hl * hl ; (h*256 + l) * (h*256 + l) ; h*256*(h*256 + l) + l*(h*256 + l) @@ -871,8 +873,33 @@ arg2_pos: add_carry result + 3 ; 2-7 cyc add16 result + 1, result + 1, inter ; 20 cyc add_carry result + 3 ; 2-7 cyc + .endscope +.endmacro - rts ; 6 cyc + +; min base ram: 5 + 163 + 6 = 174 +; max base ram: 7 + 184 + 18 + 6 = 215 +; min ext ram: 5 + 132 + 6 = 143 +; max ext ram: 7 + 147 + 18 + 6 = 178 +.macro sqr16_impl xe + .scope + arg = FR0 ; 16-bit arg + negated = FR1 + result = FR2 ; 32-bit result + + ; 5-7 cycles + lda arg + 1 ; 3 cyc + bpl arg_pos ; 2 cyc + jmp arg_neg ; 3 cyc + + arg_pos: + sqr16_impl_inner result, arg, xe ; 163-184 / 132-147 cyc + rts ; 6 cyc + + arg_neg: + copy_neg16 negated, arg ; 18 cyc + sqr16_impl_inner result, negated, xe ; 163-184 / 132-147 cyc + rts ; 6 cyc .endscope .endmacro From d93dd2e6a6b6c9adedd9cb718c5227f0bff62558 Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sun, 23 Aug 2026 15:18:25 -0700 Subject: [PATCH 25/27] step 1: sqr16_impl and imul16_impl take argument addresses first step to removing function call overhead --- mandel-core.s | 49 ++++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/mandel-core.s b/mandel-core.s index 64c5540..2e1c8b2 100644 --- a/mandel-core.s +++ b/mandel-core.s @@ -799,15 +799,15 @@ inner_loop: .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 +; min base mem: 81 * 4 + 22 * 2 + 10 = 378 cyc +; max base mem: 92 * 4 + 27 * 2 + 50 = 472 cyc +; min ext-mem: 50 * 4 + 22 * 2 + 10 = 254 cyc +; max ext-mem: 55 * 4 + 27 * 2 + 50 = 324 cyc +.macro imul16_impl result, arg1, arg2, xe .scope - arg1 = FR0 ; 16-bit arg - arg2 = FR1 ; 16-bit arg - result = FR2 ; 32-bit result (output) + ;arg1 ; 16-bit arg + ;arg2 ; 16-bit arg + ;result ; 32-bit result (output) inter = temp2 ; 16-bit temporary (clobbered) ; h1l1 * h2l2 @@ -843,8 +843,6 @@ inner_loop: bpl arg2_pos ; 2 cyc sub16 result + 2, result + 2, arg1 ; 20 cyc arg2_pos: - - rts ; 6 cyc .endscope .endmacro @@ -877,15 +875,15 @@ inner_loop: .endmacro -; min base ram: 5 + 163 + 6 = 174 -; max base ram: 7 + 184 + 18 + 6 = 215 -; min ext ram: 5 + 132 + 6 = 143 -; max ext ram: 7 + 147 + 18 + 6 = 178 -.macro sqr16_impl xe +; min base ram: 5 + 163 + 3 = 171 +; max base ram: 7 + 184 + 18 = 209 +; min ext ram: 5 + 132 + 3 = 140 +; max ext ram: 7 + 147 + 18 = 172 +.macro sqr16_impl result, arg, xe .scope - arg = FR0 ; 16-bit arg + ; arg ; 16-bit arg negated = FR1 - result = FR2 ; 32-bit result + ; result ; 32-bit result ; 5-7 cycles lda arg + 1 ; 3 cyc @@ -894,29 +892,34 @@ inner_loop: arg_pos: sqr16_impl_inner result, arg, xe ; 163-184 / 132-147 cyc - rts ; 6 cyc + jmp done ; 3 cyc arg_neg: copy_neg16 negated, arg ; 18 cyc sqr16_impl_inner result, negated, xe ; 163-184 / 132-147 cyc - rts ; 6 cyc + + done: .endscope .endmacro .proc imul16_func - imul16_impl 0 + imul16_impl FR2, FR0, FR1, 0 + rts .endproc .proc imul16xe_func - imul16_impl 1 + imul16_impl FR2, FR0, FR1, 1 + rts .endproc .proc sqr16_func - sqr16_impl 0 + sqr16_impl FR2, FR0, 0 + rts .endproc .proc sqr16xe_func - sqr16_impl 1 + sqr16_impl FR2, FR0, 1 + rts .endproc ; 11-27 cycles From e58df379e72ced940342df9a34b8aeee53480355 Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sun, 23 Aug 2026 15:48:48 -0700 Subject: [PATCH 26/27] step 2: break out the multiplication hotspot mandelbrot_hotspot_impl macro calls through to the sqr16_impl and imul16_impl macros with the xe mode this is backed by two realized functions, as mandelbrot_hotspot and mandelbrot_hotspot_xe these are also called via direct jmp instead of jsr/rts because there's only one call site so we can save 6 cycles per iteration by jmp/jmp could save 6 more cycles per iter by specializing all of the mandelbrot proc but there isn't room in ram right now sqr16_func is removed as it is unused and we ran out of code space adding the hotspot's extra implementations imul16_func is kept, as it's called via zoom_factor in a couple of places. this forwards to the xe version at a cost of 3 cycles, as the call sites aren't patched --- mandel-core.s | 85 +++++++++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/mandel-core.s b/mandel-core.s index 2e1c8b2..17ecfc4 100644 --- a/mandel-core.s +++ b/mandel-core.s @@ -558,8 +558,6 @@ input_max: ; 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 @@ -569,13 +567,11 @@ imul16_patch_offset = 16 ; 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 +;.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 @@ -912,15 +908,15 @@ inner_loop: rts .endproc -.proc sqr16_func - sqr16_impl FR2, FR0, 0 - rts -.endproc +;.proc sqr16_func +; sqr16_impl FR2, FR0, 0 +; rts +;.endproc -.proc sqr16xe_func - sqr16_impl FR2, FR0, 1 - rts -.endproc +;.proc sqr16xe_func +; sqr16_impl FR2, FR0, 1 +; rts +;.endproc ; 11-27 cycles .macro round16 arg @@ -1160,16 +1156,11 @@ keep_going: 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 +fixup_mandelbrot_hotspot: + jmp mandelbrot_hotspot +after_mandelbrot_hotspot: ; dist = zx_2 + zy_2 add32 dist, zx_2, zy_2 @@ -1265,6 +1256,27 @@ next: .endproc +.macro mandelbrot_hotspot_impl xe + ; zx_2 = zx * zx + sqr16_impl zx_2, zx + 2, xe + + ; zy_2 = zy * zy + sqr16_impl zy_2, zy + 2, xe + + ; zx_zy = zx * zy + imul16_impl zx_zy, zx + 2, zy + 2, xe + + jmp mandelbrot::after_mandelbrot_hotspot +.endmacro + +.proc mandelbrot_hotspot + mandelbrot_hotspot_impl 0 +.endproc + +.proc mandelbrot_hotspot_xe + mandelbrot_hotspot_impl 1 +.endproc + .macro scale_zoom dest ; clobbers X, flags .local cont @@ -2192,22 +2204,21 @@ init: 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 + ;lda #$4c ; 'jmp' opcode + ;sta sqr16_func + ;lda #.lobyte(sqr16xe_func) + ;sta sqr16_func + 1 + ;lda #.hibyte(sqr16xe_func) + ;sta sqr16_func + 2 + + lda #.lobyte(mandelbrot_hotspot_xe) + sta mandelbrot::fixup_mandelbrot_hotspot + 1 + lda #.hibyte(mandelbrot_hotspot_xe) + sta mandelbrot::fixup_mandelbrot_hotspot + 2 ; create the lookup table From 2ed28e5590e47c24f9a166272d95a08f1ddec8d2 Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Sun, 23 Aug 2026 16:08:18 -0700 Subject: [PATCH 27/27] drop some old commented out code --- mandel-core.s | 8 -------- 1 file changed, 8 deletions(-) diff --git a/mandel-core.s b/mandel-core.s index 17ecfc4..a43894d 100644 --- a/mandel-core.s +++ b/mandel-core.s @@ -1051,14 +1051,6 @@ common: ; 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