From fa0de6dc776a875ed97f74c4261d91629ee58fb7 Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Tue, 16 Sep 2025 21:29:40 -0700 Subject: [PATCH 01/21] WIP savings of half a cycle per imul8_xe Uses X to cache arg1, which is always used, instead of arg2, which is only used on odds. Should save half a cycle per imul8_xe, untested --- mandel.s | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/mandel.s b/mandel.s index b0f9c28..ec9f17f 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 @@ -588,7 +588,7 @@ bank_switch_table: .macro imul8 dest, arg1, arg2, xe .if xe ; using 64KB lookup table - ; 51-70 cycles + ; 50-70 cycles ; clobbers x, y, dest, ptr .scope output = dest @@ -600,13 +600,13 @@ bank_switch_table: ; bottom 14 bits except the LSB are the per-bank table index ; add $4000 for the bank pointer - txa ; 2 cyc and #$3f ; 2 cyc ora #$40 ; 2 cyc sta ptr + 1 ; 3 cyc ; copy the entry into output lda arg1 ; 3 cyc + tax ; 2 cyc and #$fe ; 2 cyc tay ; 2 cyc lda (ptr),y ; 5 cyc @@ -623,13 +623,13 @@ bank_switch_table: ;;sta PORTB ; 4 cyc - disabled ; check that 1 bit we skipped to fit into space - lda arg1 ; 3 cyc + txa ; 2 cyc and #1 ; 2 cyc beq done ; 2 cyc ; add arg2 one last time for the skipped bit clc ; 2 cyc - txa ; 2 cyc + lda arg1 ; 3 cyc adc output ; 3 cyc sta output ; 3 cyc lda #0 ; 2 cyc From 6479cf530c1c584f33b96f2b19885d02415863bb Mon Sep 17 00:00:00 2001 From: Brooke Vibber Date: Tue, 16 Sep 2025 21:29:40 -0700 Subject: [PATCH 02/21] 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 03/21] 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 04/21] 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 05/21] 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 06/21] 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 07/21] 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 08/21] 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 09/21] 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 10/21] 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 11/21] 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 12/21] 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 13/21] 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 14/21] 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 15/21] 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 16/21] 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 17/21] 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 18/21] 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 19/21] 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 20/21] 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 21/21] 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