RSS Amplifier

MsgTrail · Feb 1, 2025

Disassembling 40-Year-Old Code

0
Sign in to vote or save

MsgTrail

There is a website called The C-64 Scene Database (CSDb) that collects and curates programs and information about the Commodore demo scene from the early 80s.

On this page, someone has collected demos I wrote for the Commodore 64 between 1985 and 1986. My demo scene handle back then was “Hacktec” (on X my handle is Hackteck because the former was taken):

An image with caption: A page dedicated to yours truly

All demos can be downloaded and run using emulators for Windows, Mac, and other operating systems. For this article, I’ve used VICE to run a demo and disassemble its source code.

The program I chose to analyze is called “Keep on Turning”. It is interesting because it makes use of a special trick to display more than eight sprites simultaneously. Although the C64 supported only eight hardware sprites, a hacker would not be a hacker if they did not try to circumvent this limitation.

An image with caption: The “Keep on Turning” page on CSDb

After loading and running the program, you are greeted by a splash screen that mentions Hacktec and TSI, a fellow 1001 Crew hacker:

An image with filename: 2025-02-01-2012-W9xWiXkI.png

On pressing space, the actual demo starts. While it looks unassuming today, back then people were stunned to see so many sprites at the same time:

An image with caption: The demo is running inside the VICE user interface with a debugging panel enabled.

In order to disassemble the code, I opened the built-in “monitor” that ships with VICE. A monitor, in this case, is a tool for inspecting registers, memory contents, and displaying assembly code.

I started by running r (as in registers) which displays info about the running program:

(C:$c681) r
  ADDR A  X  Y  SP 00 01 NV-BDIZC LIN CYC  STOPWATCH
.;c681 07 01 fa ed 2f 37 10100101 040 016   20208904

This tells me the program is likely running around memory location $c681 (the Commodore’s memory map ran from $0000 to $ffff, which is, of course, 64 kB).

After some more digging, I created the following assembly listing, with a bit of help from Antropic’s Claude 3.5 Sonnet for the comments. The listing can also be found here as a gist

;===============================================================================
; BINARY ROTATION DEMO
; Produced by The 1001 Crew
; Code & FX by Hacktec & TSI
;===============================================================================
;===============================================================================
; CONSTANTS & ZERO PAGE VARIABLES
;===============================================================================
VIC_SPRITE0_Y    = $D001                          ; Y coordinate of sprite 0
VIC_SPRITE_Y     = $D001                          ; Y positions for sprites 0-7 are consecutive
VIC_RASTER       = $D012                          ; Current raster line
VIC_CTRL1        = $D011                          ; VIC control register 1
VIC_CTRL2        = $D016                          ; VIC control register 2
VIC_VIDEO_PTR    = $D018                          ; Video matrix and character base
VIC_BORDER       = $D020                          ; Border color
VIC_BACKGROUND   = $D021                          ; Background color 0
VIC_INT_STATUS   = $D019                          ; IRQ status register
VIC_INT_CONTROL  = $D01A                          ; IRQ control register
;===============================================================================
; STARTUP CODE - Displays title and waits for spacebar
;===============================================================================
                .org $CB00
StartDemo:      JSR $FF8A                         ; Initialize screen
                LDA #$0B                          ; Set dark gray
                STA VIC_BORDER                    ; for border color
                LDA #$00                          ; Set black
                STA VIC_BACKGROUND                ; for background color
                LDA #$1E                          ; Set text color to green
                JSR $FFD2                         ; Output character
                JSR $E544                         ; Clear screen
                ; Copy title text to screen memory
                LDX #$00                          ; Initialize counter
.titleloop:     LDA TitleText,X                   ; Load character from title text
                STA $05E0,X                       ; Store in screen memory
                INX                               ; Increment counter
                BNE .titleloop                    ; Loop until 256 chars copied
                ; Wait for spacebar press
                LDA #$00                          ; Clear
                STA $C6                           ; keyboard buffer
.keyloop:       JSR $FFE4                         ; Get key from keyboard
                CMP #$20                          ; Compare with space
                BNE .keyloop                      ; Loop if not space
                JMP Initialize                    ; Start the demo proper
;===============================================================================
; INITIALIZATION CODE
;===============================================================================
                .org $C000
Initialize:     SEI                               ; Disable interrupts
                LDA #$7F                          ; Disable
                STA $DC0D                         ; CIA interrupts
                LDA #$01                          ; Enable
                STA VIC_INT_CONTROL               ; raster interrupts
                ; Setup interrupt vector
                LDA #$C1                          ; Set high byte of
                STA $0315                         ; interrupt handler
                LDA #$00                          ; Set low byte of
                STA $0314                         ; interrupt handler
                ; Initialize VIC registers
                LDY #$40                          ; Init counter
.initloop:      LDA VicData,Y                     ; Get VIC register value
                STA VIC_SPRITE_Y,Y                ; Store in VIC register
                DEY                               ; Decrement counter
                BPL .initloop                     ; Loop until done
                ; Final setup
                INC VIC_BORDER                    ; Change border color
                LDA VIC_INT_STATUS                ; Acknowledge
                STA VIC_INT_STATUS                ; any pending IRQs
                CLI                               ; Enable interrupts
                BIT $D021                         ; Screen setup
                LDA #$00                          ; Black
                STA VIC_BACKGROUND                ; background
                LDA #$1B                          ; Set screen
                STA $FF                           ; control value
                LDA #$96                          ; Set VIC
                STA $DD00                         ; memory bank
                JMP MainInit                      ; Go to main initialization
;===============================================================================
; SPRITE ANIMATION ROUTINE
;===============================================================================
                .org $C600
UpdateSprites:  STA VIC_SPRITE0_Y                 ; Update Y coordinates
                STA $D003                         ; for all eight
                STA $D005                         ; sprites to create
                STA $D007                         ; the rotating
                STA $D009                         ; binary pattern
                DEC VIC_CTRL2                     ; Screen effect
                INC VIC_CTRL2                     ; (smooth scroll)
                STA $D00B                         ; Continue setting
                STA $D00D                         ; sprite Y
                STA $D00F                         ; coordinates
                LDA AnimData,X                    ; Get next animation
                STA VIC_VIDEO_PTR                 ; frame pointer
                INX                               ; Next frame
                ; Timing delay for animation
                NOP                               ; Fine-tune
                NOP                               ; the timing
                NOP                               ; of the
                NOP                               ; sprite
                NOP                               ; animation
                NOP                               ; sequence
                DEC VIC_CTRL2                     ; More screen
                INC VIC_CTRL2                     ; effects
                RTS                               ; Return
;===============================================================================
; RASTER INTERRUPT HANDLER
;===============================================================================
                .org $C64E
RasterIRQ:      BIT $EA                           ; Timing stabilization
                LDA VIC_RASTER                    ; Get current raster line
                AND #$07                          ; Mask to get 0-7
                CMP #$02                          ; Check if line 2
                BNE .check4                       ; If not, check line 4
                LDA #$18                          ; Update screen
                STA VIC_CTRL1                     ; control register
                NOP                               ; Timing
                NOP                               ; delay for
                NOP                               ; stable raster
                DEC VIC_CTRL2                     ; Screen effect
                INC VIC_CTRL2                     ; (smooth scroll)
                RTS                               ; Return from interrupt
.check4:        CMP #$04                          ; Check if line 4
                BNE .other                        ; If not, do other
                LDA $FF                           ; Restore screen
                STA VIC_CTRL1                     ; control value
                DEC VIC_CTRL2                     ; Screen effect
                INC VIC_CTRL2                     ; (smooth scroll)
                RTS                               ; Return from interrupt
.other:         NOP                               ; Timing
                NOP                               ; delay for
                NOP                               ; other lines
                DEC VIC_CTRL2                     ; Screen effect
                INC VIC_CTRL2                     ; (smooth scroll)
                RTS                               ; Return from interrupt
;===============================================================================
; MAIN PROGRAM LOOP
;===============================================================================
                .org $C800
MainLoop:       LDX #$07                          ; Init sprite counter
.spriteptr:     LDA SpriteData,X                  ; Load sprite pointer
                STA $43F8,X                       ; Store in sprite
                STA $47F8,X                       ; pointer areas
                STA $4BF8,X                       ; across multiple
                STA $4FF8,X                       ; screen banks
                STA $53F8,X                       ; to create the
                STA $57F8,X                       ; full rotation
                STA $5BF8,X                       ; effect
                DEX                               ; Next sprite
                BPL .spriteptr                    ; Loop for all sprites
                ; Animation check/update
                LDA SpriteData                    ; Check if animation
                CMP #$90                          ; needs reset
                BEQ .reset                        ; If so, reset it
                ; Update animation counters
                LDX #$07                          ; Init counter
.decloop:       DEC SpriteData,X                 ; Update animation
                DEX                               ; for each sprite
                BPL .decloop                     ; Loop for all sprites
                ; Speed control delay loop
                LDX #$F1                          ; Outer loop
                LDY #$EB                          ; Inner loop
.delay1:        INY                               ; Increment inner
                BNE .delay1                       ; Loop inner
                INX                               ; Increment outer
                BNE .delay1                       ; Loop outer
                JMP .checkkeys                    ; Check keyboard
.reset:         LDX #$07                          ; Reset counter
.resetloop:     LDA SpriteData+8,X                ; Get reset values
                STA SpriteData,X                  ; Store reset values
                DEX                               ; Next sprite
                BPL .resetloop                    ; Loop all sprites
                JMP MainLoop                      ; Back to main loop
                ; Keyboard handling
.checkkeys:     JSR $FF9F                         ; Scan keyboard
                JSR $FFE4                         ; Get key
                BEQ MainLoop                      ; If no key, loop
                CMP #'+'                          ; Check plus key
                BEQ .speedup                      ; If plus, speed up
                CMP #'-'                          ; Check minus key
                BEQ .slowdown                     ; If minus, slow down
                JMP MainLoop                      ; Back to main loop
.speedup:       INC DelayVal1                     ; Increase delay
                INC DelayVal2                     ; (slower)
                JMP MainLoop                      ; Back to main loop
.slowdown:      DEC DelayVal1                     ; Decrease delay
                DEC DelayVal2                     ; (faster)
                JMP MainLoop                      ; Back to main loop
;===============================================================================
; DATA SECTIONS
;===============================================================================
; Animation sequence data - Frame pointers for sprite animation
AnimData:       .byte $07,$17,$27,$37,$47,$57,$67,$77    ; First 8 frames
                .byte $87,$97,$A7,$B7,$C7,$D7,$E7,$F7    ; Last 8 frames
; Initial VIC register values for sprite setup
VicData:        .byte $00,$04,$30,$04,$60,$04,$90,$04    ; Sprites 0-3
                .byte $C0,$04,$F0,$04,$20,$04,$50,$04    ; Sprites 4-7
; Title screen text
TitleText:      .text "    PRODUCED BY: THE 1001 CREW    "    ; Title line 1
                .text "    CODING & FX BY: HACKTEC & TSI  "   ; Title line 2
                .text "    NAME: KEEP ON TURNING          "   ; Title line 3
                .text "    SPECIAL: USE '+' & '-' TO ADJ  "   ; Title line 4
                .text "    PRESS: THE SPACEBAR           "    ; Title line 5
                .byte 0                                       ; String terminator
; Sprite pattern data
SpriteData:     .byte $92,$82,$82,$92,$92,$82,$82,$92    ; Initial sprite pointers
                .byte $9F,$8F,$8F,$9F,$9F,$8F,$8F,$9F    ; Reset values
; Speed control values
DelayVal1       = $C82D                          ; First delay value
DelayVal2       = $C82F                          ; Second delay value

I do not recall, after 40 years, exactly how TSI and I managed to display more than eight sprites simultaneously, so I asked Claude to take a stab at it:

/* Start of AI answer */

Let me analyze this code to explain how it managed to exceed the C64's 8-sprite hardware limitation.

The key to understanding this trick lies in how the demo exploits the C64's raster interrupt system and precise timing. Here's how it works:

  1. The demo uses the VIC-II chip's raster interrupt capability (as seen in the RasterIRQ routine at $C64E). The raster interrupt allows the program to execute code at specific screen positions during the display refresh.

  2. Looking at the sprite update code and interrupt handler, we can see that it's using a technique known as "sprite multiplexing." Here's how it works:

    • The VIC-II chip draws the screen line by line from top to bottom

    • Each time the raster beam reaches a specific line (checked with AND #$07 in the interrupt handler), the demo quickly changes the sprite positions and properties

    • By changing sprite positions after the VIC-II has already drawn them in one location, but before it draws the next frame section, the same hardware sprites can be reused to appear in multiple locations

  3. The timing is absolutely critical for this to work:

    • The code includes carefully placed NOP instructions for precise timing

    • The screen effect commands (DEC VIC_CTRL2 followed by INC VIC_CTRL2) are likely used for timing synchronization

    • The sprite positions are updated rapidly during the vertical blank period when the electron beam is moving back to the top of the screen

So while the C64 still only has 8 hardware sprites available at any instant, this demo creates the illusion of more sprites by rapidly repositioning them during the screen refresh. It's essentially reusing the same sprites multiple times per frame, showing them in different positions before the human eye can detect the change.

This was quite an advanced technique for 1985-1986, as it required precise understanding of the VIC-II chip's timing and careful cycle counting to ensure the sprite updates happened at exactly the right moment between scan lines.

/* End of AI answer */

It is amazing to be able to run and analyze a program I wrote 40 years ago and have an AI assist me with the annotation of the source code. It is an incredible time to be alive.

Read the original on msgtrail.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.