Saturday, September 14, 2013

Conway's Game Of Life on a Commodore 64.

I remember when I learned for the 1st time about Conway's Game Of Life – a Cellular Automata model in a Polish equivalent of Popular Mechanics for youth: “Mlody Technik” (Young Technician) in 1984. I have been at the time a technical high school freshman. I immediately recognized how great a simulation platform a computer would be for this model. Unfortunately I did not have a computer or even an access to one. I spent long hours simulating colonies on a piece of a graph paper.
In the early 2000-s when I was learning C#/.NET I did a C# implementation as an exercise. The GUI is a bit awkward, but simulation is fairly fast and can emulate quite big arrays of the cells. My son loves to play with this program even though I never had time to refine it or get rid of few bugs. It has edit/load/save abilities, user can set, unset cells with a mouse click, zoom in and out and enable grid lines. My 9-year old has no problem with that, so I guess everybody could get used to it.

Modern GUI C# implementation of Game of Life.
C# Game of Life - program setup.
C# Game of Life - with elevated zoom and grid lines enabled for easy editing.
In the meantime I made numerous console based ports in C language for DOS and Linux. It is always fun. Now, while this is quite easy with modern computers and programming languages, it is not the same on some old slow CPU and limited memory platform using low level machine language or slow interpreted language like BASIC for example.
Recently I realized that I have never did a C-64 port. Being a Commodore aficionado, I found this a great oversight, so I got to work.
My first tries using BASIC and Power-C for C-64 and C-128 were disappointing. My algorithm was very slow. I realize that with some clever coding I could do a fast simulation in higher level language. Perhaps one day I will try to invent some clever GOL simulation algorithm in C which will be fast enough for practical application. Even though I like to work directly on a retro hardware, (emulator does not emulate look and feel), this time I wanted to try the cross-platform IDE called C64 Studio (I love the demo helicopter game that comes with it). I installed it, made sure it points to my installation of Vice emulator and got to work. I researched some GOL implementations for CBM platform on the internet and have been impressed by this great piece of 6502/C64 code by 'Ruk':

Although Ruk's goal was minimum size and maximum speed, so the program is not really packed with features, it is still very impressive what you can do with this ancient by today's standard hardware. He does some pretty clever encoding of the colony so the calculating of the next generation is real fast and code is extremely compact. It fits in less than 256 bytes with full screen emulation/presentation and sound.
I did not match Ruk's implementation of GOL speed and tiny size, however these were not my goals (well, maybe the speed was, but I lack the skill at the moment). Instead my program is packed with practical features and makes up for slower speed and (much) bigger code size with numerous functions like: built-in demo colonies, random colony generator, colony full screen screen editor and disk I/O. Although program takes 16 blocks on a disk, the colony files are only one block in size (binary encoded, 40*25/8 = 125 bytes is all it takes to encode 1000-cell colony) and save and load pretty fast. The simulation speed is also satisfactory with about 2 ¼ generations per second.

Conway's Game Of Life – the rules.

The world of Game of Life (GOL) consists of a 2-dimensional array of cells, which can have a dot (live cell) or be empty. The next generation of dots is calculated by counting the live neighbors on adjacent cells. E.g: please refer to illustration below, where the central cell is live (has a dot) and has 8 adjacent cells, which contain a total of 3 live neighbors.

A cell with adjacent cells.
The rules of death and life are as follows:

- Death -
  • Each dot that has 1 or no live neighbors dies out of loneliness.
  • Each dot that has 4 or more live neighbors, dies out of the overcrowding.
- Life -
  • On each empty cell that has exactly 3 live neighbors, a new dot is born.
  • Each dot that has 2 or 3 live neighbors, remains in its live status.


Computer algorithm.

On today's fast hardware, the directly implemented algorithm causes no speed concerns. The cells can be encoded in bit fields or directly in a BOOLEAN or character array. The cells of the arrays are iterated sequentially and the neighbors number is calculated for each cell separately and the next generation of the colony can be generated to the separate memory buffer and then presented in whatever form to the user.
However working with the 1 MHz 8-bit CPU with limited RAM is different. There are memory and performance considerations. We want to avoid too much of the data copying operations (internal colony representation vs. screen presentation, copying data between buffers etc.) and really huge arrays. In my implementation I decided to compute the next generation directly in the text screen memory (the world is a 1000 cells array, 25 rows 40 columns each). To avoid artifacts on the screen while the next generation is calculated, double buffering technique could be used. However I chose a technique to encode the screen codes for dying and born cells of the next generation to be as closely represented on the screen to the live and empty cells of a current generation as possible. For example the dot (live cell) is presented with screen code $51, which is a big filled oval dot. The dying cell is represented with code $57, which is a very similar semi-graphical character to the one represented by code $51, except it just has a small not filled with color hole inside (donut like) while remaining the same size. Therefore when the next generation calculating algorithm is marking the cells for dying, their screen presentation do not change significantly. Just a little hole appears briefly in the middle of each dying cell, which has a nice side effect for the presentation function anyway. Even better screen code match was found for a new-born cell. Note that it is not desired to have new born dots appear on the screen before the whole array is calculated. The screen code $60 is represented on the screen as empty space, just as the empty cell is represented by code $20, also resulting with an empty space on the screen. For the computer algorithm these are 2 different codes (desirable feature). However for the user, there is no change on the screen when the algorithm marks cells where a new dot is born in the next generation. Thanks to that I avoided necessity of performing double buffering and switching screen memory banks. After next generation is calculated, quick routine converts the died and new born screen codes to the empty and dot codes (fully filled) and the next generation is again calculated in the endless loop. The end-user presentation in effect is quite pleasant and appears as a seamless generation after generation display.

Apparently the assembly routine calculating the next generation of the colony was the most challenging for me. The performance of the simulation depends on it the most. I used direct GOL rules implementation with few tricks and optimization techniques. The resulting binary image is under 4 kB in size. Considering the amount of features packed, I consider this a moderate achievement. Thanks to the speed of the machine language, I achieved satisfactory performance without affecting the readability of the code or necessity to use advanced colony encoding techniques. Although it is my desire in the future to do just that and make the code blazing fast, for now I will settle for 2 generations per second speed as I need some more 6502 assembly language practice to achieve better results in code.
Zero page indexing addressing mode and various counters are used by the algorithm to cycle through screen memory cells from left upper corner to right-lower corner of the screen (YIND – column counter is used to iterate through cells in a row and then ZPG vector is increased by 40 and YIND zeroed at the beginning of each next row).

Here is the routine which is the heart of the GOL simulation algorithm:

; calculate the next generation    
; Theory of operation:
; The screen memory is scanned from start to end.
; Each row is iterated with pointer ZPGA1 and index Y.
; Addressing mode (ZPGA1),Y is used. Register Y is
; reloaded with column counter yind, which is incremented
; accordingly at the end of the loop,
; then ZPGA1 is increased by 40 at the start of the next row.
; The number of neighbors is calculated for each cell.
; Depending on the # of neighbors, the cell is encoded
; to either die (DCELL), be born (BCELL) or remain unchanged.
; After this procedure, the 'disp' routine should be called
; to convert died cells to empty spaces and born cells (BCELL)
; to live cells (LCELL).
next
    ldx #0
    stx xind  ; initialize row index
    stx yind  ; initialize column index
    stx nbct  ; initialize neighbor cells counter
    lda #<MSCR  ; initialize ZPG address buffer
    sta ZPGA1   ; and current cell address counter
    sta smct
    lda #>MSCR
    sta ZPGA1+1
    sta smct+1
nl0 ; left neighbor check    
    ldy yind
    beq nl1         ; skip left neighbor check if column=0
    dey             ; decrement ZPGA1 pointer (left side neighbor)
    lda (ZPGA1),y   ; load cell's value
    cmp #LCELL      ; if not a live cell
    beq nl000
    cmp #DCELL      ; or not a 'died' cell (in this iteration)
    bne nl00        ; jump to nl00
nl000    
    inc nbct        ; increment neighbors count
nl00    
    ldy yind
nl1 ; right neighbor check
    cpy #MAXCOL     ; if cell at the far right side
    beq nl2         ; then skip right neighbor check
    iny             ; increment ZPGA1 pointer (right side neighbor)
    lda (ZPGA1),y   ; load cell's value
    cmp #LCELL      ; if not a live cell
    beq nl111
    cmp #DCELL      ; or not a 'died' cell (in this iteration)
    bne nl2        ; jump to nl2
nl111    
    inc nbct        ; increment neighbors count
nl2 ; north neighbors check
    lda xind
    beq nl4         ; skip north neighbors check if row=0
    jsr sb40zpga1   ; subtract 40 from ZPGA1 (north neighbor)
    ldy yind
    lda (ZPGA1),y   ; load cell's value
    cmp #LCELL      ; if not a live cell
    beq nl222
    cmp #DCELL      ; or not a 'died' cell (in this iteration)
    bne nl22        ; jump to nl22
nl222    
    inc nbct        ; increment neighbors count
nl22  ; north-west (NW) neighbor check
    ldy yind
    beq nl3         ; skip NW neighbor check if column=0
    dey             ; decrement ZPGA1 pointer (NW side neighbor)
    lda (ZPGA1),y   ; load cell's value
    cmp #LCELL      ; if not a live cell
    beq nl2220
    cmp #DCELL      ; or not a 'died' cell (in this iteration)
    bne nl220       ; jump to nl220
nl2220    
    inc nbct        ; increment neighbors count
nl220
    iny             ; return ZPGA1 pointer to north neighbor's cell
nl3
    cpy #MAXCOL     ; if cell at the far right side
    beq nl33        ; then skip the right neighbor check
    iny             ; increment ZPGA1 (right side neighbor)
    lda (ZPGA1),y   ; load cell's value
    cmp #LCELL      ; if not a live cell
    beq nl333
    cmp #DCELL      ; or not a 'died' cell (in this iteration)
    bne nl33        ; jump to nl33
nl333    
    inc nbct        ; increment neighbors count
nl33
    jsr rldsm2zpga1 ; reload smct to ZPGA1
nl4 ; south neighbor check
    lda xind
    cmp #MAXROW     ; if row at maximum,
    beq nl6         ; skip south neighbors check
    jsr add40zpga1  ; add 40 to ZPGA1 (now ZPGA1+Y -> south neighbor's cell)
    ldy yind
    lda (ZPGA1),y   ; load cell's value
    cmp #LCELL      ; if not a live cell
    beq nl444
    cmp #DCELL      ; or not a 'died' cell (in this iteration)
    bne nl44        ; jump to nl44
nl444    
    inc nbct        ; increment neighbors count
nl44  ; south-west (SW) neighbor check
    ldy yind
    beq nl5         ; skip SW neighbor check if column=0
    dey             ; decrement ZPGA1 (SW side neighbor)
    lda (ZPGA1),y   ; load cell's value
    cmp #LCELL      ; if not a live cell
    beq nl4440
    cmp #DCELL      ; or not a 'died' cell (in this iteration)
    bne nl440       ; jump to nl440
nl4440    
    inc nbct        ; increment neighbors count
nl440
    iny             ; return ZPGA1 pointer to south neighbor's cell
nl5
    cpy #MAXCOL     ; if cell at the far right side
    beq nl6         ; then skip the right neighbor check
    iny             ; increment ZPGA1 (SE side neighbor)
    lda (ZPGA1),y   ; load cell's value
    cmp #LCELL      ; if not a live cell
    beq nl55
    cmp #DCELL      ; or not a 'died' cell (in this iteration)
    bne nl6         ; jump to nl6
nl55    
    inc nbct        ; increment neighbors count    
nl6
    jsr rldsm2zpga1 ; reload smct to ZPGA1 (restore address of the row)
    lda yind        ; store old column# before incrementing
    sta yold        ; for further calculations
    inc yind        ; increment column#
    lda yind
    cmp #(MAXCOL+1)
    bne nl66
    lda #0
    sta yind
    jsr add40smct   ; increment smct (point to the next row of cells)
    inc xind        ; increment row#
nl66  ; now mark the cells to live/die/be born
    ldy yold
    lda (ZPGA1),y   ; load current cell
    cmp #LCELL      ; check if a live one
    beq nl666
    cmp #DCELL
    bne nl7         ; not a live one, jump to nl7
nl666 ; live cell, determine its fate
    lda nbct        ; load neighbors counter
    cmp #0          ; if nbct<2, cell dies
    beq nldie
    cmp #1
    beq nldie
    cmp #2
    beq nlnext
    cmp #3          ; if nbct==3, cell does not change
    beq nlnext      ; if nbct>3, cell dies
nldie
    lda #DCELL
    ldy yold
    sta (ZPGA1),y   ; update cell with 'died' code
    clc
    bcc nlnext      ; prepare for next cell
nl7 ; not a live cell, determine its fate
    lda nbct        ; load neighbors counter
    cmp #3          ; only if nbct==3, a new one is born
    bne nlnext
    lda #BCELL
    ldy yold
    sta (ZPGA1),y   ; update cell with 'born' status.
nlnext
    lda xind        ; check if the end
    cmp #(MAXROW+1)
    beq nlend       ; yes, jump to nlend
    jsr rldsm2zpga1 ; reload smct to ZPGA1
    lda #0          ; initialize neighbors counter
    sta nbct
    jmp nl0         ; do the next cell
nlend

    rts

Before saving, colony's editor screen is encoded into binary form (so each byte holds information about 8 cells) into buffer with this routine:

; convert screen (editor's format) into binary format
; store in colb buffer
encodeed2colb
    lda #<MSCR
    sta ZPGA1
    lda #>MSCR
    sta ZPGA1+1
    lda #0
    sta gpct        ; gpct - encoded byte
    sta gpct+1      ; index to colb buffer (encoded colony)
    lda #8
    sta gpbyt       ; gpbyt - bit counter
ee2cloop000
    ldy #0
    lda (ZPGA1),y   ; load byte from screen memory
    cmp #'*'        ; is it live cell?
    bne ee2cloop002 ; no, branch to ee2cloop002
    lda gpct        ; load encoded byte to Acc
    ldx gpbyt       ; load bit counter to X
    ora encodearr,x ; set the bit in Acc masked by encodearr,X
    sta gpct        ; store updated encoded byte
ee2cloop002    
    dec gpbyt       ; decrement bit counter
    lda gpbyt
    beq ee2cloop001 ; if byte counter is 0, branch to ee2cloop001
ee2cloop000a    
    jsr SystemZone.inczpga1 ; increment ZPG1 (screen memory)
    lda ZPGA1       ; check if end of screen memory
    cmp #<MESCR
    bne ee2cloop000
    lda ZPGA1+1
    cmp #>MESCR
    beq ee2cfinished
    clc
    bcc ee2cloop000 ; do next bit
ee2cloop001         ; single byte encoding finished
    ldx gpct+1      ; load encoded colony buffer index
    lda gpct        ; load encoded byte
    sta colb,x      ; store byte in the encoded colony buffer
    inc gpct+1      ; increment index to encoded colony buffer
    lda #8          ; reload bit counter
    sta gpbyt
    lda #0          ; reset encoded byte
    sta gpct
    clc
    bcc ee2cloop000a  ; do the next bit
ee2cfinished

    rts

Here is how the encoded binary format colony look like, this example is a built in demo colony:

; shooter colony, binary encoded    
dcshootenc    

    !byte 100    ; first byte is the number of encoded bytes that follow
    ; encoded colony
    !text B00000000,B00000000,B00000000,B00000000,B00000000
    !text B00000000,B00000000,B00000000,B00000000,B00000000
    !text B00000000,B00000000,B00000000,B00000000,B00000000
    !text B00000000,B00000000,B00000000,B00000000,B00000000
    !text B00000000,B00000000,B00000000,B00000000,B00000000
    !text B00000000,B00000000,B00000000,B11000000,B00011000
    !text B00000000,B00000000,B00000001,B01000000,B00011000
    !text B01100000,B00110000,B00000001,B10000000,B00000000
    !text B01100000,B01010000,B00000000,B00000000,B00000000
    !text B00000000,B01100000,B01100000,B00000000,B00000000
    !text B00000000,B00000000,B01010000,B00000000,B00000000
    !text B00000000,B00000000,B01000000,B00000000,B00000000
    !text B00000000,B00000000,B00000000,B00000000,B00001100
    !text B00000000,B00000000,B00000000,B00000000,B00001010
    !text B00000000,B00000000,B00000000,B00000000,B00001000
    !text B00000000,B00000000,B00000000,B00000000,B00000000
    !text B00000000,B00000000,B00000000,B00000000,B00000000
    !text B00000000,B00000000,B00000000,B01110000,B00000000
    !text B00000000,B00000000,B00000000,B01000000,B00000000

    !text B00000000,B00000000,B00000000,B00100000,B00000000


Features and presentation.

After the welcome screen, user presses a key and the help screen is displayed showing keyboard shortcuts to the functions of the program. Due to the presentation environment being text based, that information is not present on the screen during colony editing or simulation, therefore it is a good idea to read and memorize this screen or print it before proceeding.

Game of Life simulator - help screen.
In the next step, user is presented with the program's start menu. Possible choices consist of two template/demo colonies, ability to start with an empty editor or editor filled with random colony. There is also a load from file option available and option to terminate the application (confirmation is required).

Game of Life - start menu.
Depending on the user's choice, the editor starts filled with the colony or empty. Editor encodes the empty and life cells differently than the simulator. The empty cells are represented by minus '-' signs, while the life cells are represented with asterisks '*'. This approach has 2 advantages:

  1. User can recognize immediately in what mode the application is currently working (remember – there is no extra text information, the whole screen is taken by colony data presentation).
  2. The empty cells done with minus sign present a grid which makes it easier for user to design/modify the colony.
Game of Life - editor screen filled with random colony.
In editor, user can position the cursor (visible as a reverse color rectangle) using cursor keys on the keyboard and alter the cell status with space key. Other possible functions are 'H' to display help screen, 'C' to clear all the cells (requires confirmation), 'R' to fill cells with random values (requires confirmation) and 'Q' to exit the editor and start simulation. At the exit, user is prompted to save the colony to file, which can be skipped.

Game of Life - exit editor, save to file prompt.
If user chooses to save the colony to file, a prompt appears to enter the colony file name.

Game of Life - save colony, file name prompt.
The simple file name editor allows to enter alpha-numeric string of up to 12 characters in length. User can press DEL key to remove incorrectly entered characters. If the entered file name is empty (RETURN key pressed with the cursor positioned at the 1st character of the file name), the Save operation is considered as canceled and program proceeds to simulation. Otherwise, the program attempts to save the colony to file. In case of I/O error, there is no action. Extension “.COL” is added to the file name automatically.

Simulator starts in paused mode. User can immediately see that program entered simulation mode because the cells are now presented differently on the screen (Illustration below).

Game of Life – paused simulator.
Pressing any key starts the simulation. Please note the donut style cells (see Illustration below), which are the cells marked to die in the next generation by the algorithm. Newborn dots are not visible until the next generation is fully created and the screen codes converted to the full dots or spaces accordingly.
(NOTE: on the screen shot below, not all the cells that are supposed to die in the next generation are yet marked like donut, because the screen shot captured the screen in the middle of the process)

Game of Life - running simulation.
At any moment during simulation user can choose to quit application (requires confirmation), pause it, go to editor and alter/save the colony, display the help screen or go back to start menu (requires confirmation) using the keyboard shortcuts presented on the help screen at the program start up.

Game of Life - quitting the application.
Game of Life - the fun is over.

Here is the link to source code. Look for a ZIP archive named ConwaysLife.
Thank you for visiting.

Marek Karcz
9/15/2013.

Sunday, February 3, 2013

IRQ sprite clock for Commodore-64.

Today I'd like to share with Commodore community this little piece I made back in the 1990-s - an IRQ sprite clock for Commodore-64.


Clock works in IRQ interrupt. Presentation is made with sprites (clock digits are mapped to sprite objects and displayed on screen). It is possible to use C-64 while this clock works. Oddly enough, this clock seems to be interfering with I/O while running on real hardware (unable to use disk drive or tape, will hung) but works fine in emulator. If you find the bug, please let me know!


; SPRITE IRQ CLOCK (C) Marek Karcz
; Code below compiles with CC65 assembler:
; cl65 --verbose --listing spriteclock.asm
; Use HEX editor to insert "00 C0" at the beginning
; of the executable to be able to run it on C64 or
; in emulator.
; Usage:
; LOAD"SPRITECLOCK",8,1
; CLR:NEW
; SYS49152,"HHMMSS"
; Where:
;     HH - hours, MM - minutes, SS - seconds
; To turn the clock off:
; SYS49152

VIC      = 53248 ;VIC Base
WSKSP0   = 2040  ;PTR. SPRITE 0
WSKSP1   = 2041  ;PTR. SPRITE 1
WSKSP2   = 2042  ;PTR. SPRITE 2
BLOK13   = 832   ;PTR. BEG. OF BLOCK 13
BLOK14   = 896   ;PTR. BEG. OF BLOCK 14
BLOK15   = 960   ;SEE ABOVE - BLOCK 15
IRQWKT   = 788   ;IRQ VECT.
POBZNA   = 121   ;READ CURR. BYTE
PRZECT   = $AEFD ;CHECK COMMA
ANAWYR   = 44446 ;PARSE EXPR.
POBTXT   = 46978 ;GET STRING

         .ORG $C000

         JMP STEROW
LICZNIK:  .BYTE 60                   ;COUNTER
WZORCE:   .BYTE 2,3,5,9,5,9          ;TEMPLATES
CZAS:     .BYTE 1,2,0,0,0,0          ;TIME
LICZPOZ:  .BYTE 1                    ;POS. COUNTER
CYF0:     .BYTE 0,252,204,204        ;DIGIT 0
          .BYTE 204,204,204,252
CYF1:     .BYTE 0,12,12,12           ;DIGIT 1
          .BYTE 12,12,12,12
CYF2:     .BYTE 0,252,12,12          ;DIGIT 2, etc...
          .BYTE 252,192,192,252
CYF3:     .BYTE 0,252,12,12
          .BYTE 60,12,12,252
CYF4:     .BYTE 0,204,204,204
          .BYTE 252,12,12,12
CYF5:     .BYTE 0,252,192,252
          .BYTE 12,12,12,252
CYF6:     .BYTE 0,252,192,192
          .BYTE 252,204,204,252
CYF7:     .BYTE 0,252,12,24
          .BYTE 48,96,192,192
CYF8:     .BYTE 0,252,204,204
          .BYTE 252,204,204,252
CYF9:     .BYTE 0,252,204,204
          .BYTE 252,12,12,252
DWUKR:    .BYTE 0,0,48,48            ;COLON
          .BYTE 0,48,48,0
COPY:    LDX #$00
         LDY #$00
LOOP1:   LDA CYF0,X
BLKNR:   STA BLOK13,Y
         CPX #7
         BEQ POZ
         INY
         INY
         INY
         INX
         JMP LOOP1
POZ:     RTS
IRQROM:  JMP $EA31
MAIN:    DEC LICZNIK
         BNE IRQROM
         LDA #60
         STA LICZNIK
         LDA CZAS
         LDX #3
         CMP #2
         BEQ NOC
         LDX #9
NOC:     STX WZORCE+1
         LDX #5
NXTCYF:  LDA CZAS,X
         INC CZAS,X
         CMP WZORCE,X
         BNE KOP1
         LDA #$00
         STA CZAS,X
         DEX
         BPL NXTCYF
KOP1:    LDA #$00
         STA LICZPOZ
         LDX #<CZAS
         LDY #>CZAS
         STX KOPIUJ+1
         STY KOPIUJ+3
         LDX #<BLOK13
         LDY #>BLOK13
         STX POS0B+1
         STY POS0C+1
KOPIUJ:  LDX #<CZAS
         LDY #>CZAS
         STX POS0+1
         STY POS0+2
POS0:    LDA CZAS
         CMP #$00
         BNE NIE00
         LDX #<CYF0
         LDY #>CYF0
POS0A:   STX LOOP1+1
         STY LOOP1+2
POS0B:   LDX #<BLOK13
POS0C:   LDY #>BLOK13
         STX BLKNR+1
         STY BLKNR+2
         JSR COPY
         JMP POS1
NIE00:   CMP #$01
         BNE NIE01
         LDX #<CYF1
         LDY #>CYF1
         JMP POS0A
NIE01:   CMP #$02
         BNE NIE02
         LDX #<CYF2
         LDY #>CYF2
         JMP POS0A
NIE02:   CMP #$03
         BNE NIE03
         LDX #<CYF3
         LDY #>CYF3
         JMP POS0A
NIE03:   CMP #$04
         BNE NIE04
         LDX #<CYF4
         LDY #>CYF4
         JMP POS0A
NIE04:   CMP #$05
         BNE NIE05
         LDX #<CYF5
         LDY #>CYF5
         JMP POS0A
NIE05:   CMP #$06
         BNE NIE06
         LDX #<CYF6
         LDY #>CYF6
         JMP POS0A
NIE06:   CMP #$07
         BNE NIE07
         LDX #<CYF7
         LDY #>CYF7
         JMP POS0A
NIE07:   CMP #$08
         BNE NIE08
         LDX #<CYF8
         LDY #>CYF8
         JMP POS0A
NIE08:   LDX #<CYF9
         LDY #>CYF9
         JMP POS0A
POS1:    INC KOPIUJ+1
         LDA KOPIUJ+1
         CMP #$00
         BNE POS1A
         INC KOPIUJ+3
POS1A:   INC LICZPOZ
         LDA LICZPOZ
         CMP #1
         BNE POS1B
         LDX #<(BLOK13+1)
         LDY #>(BLOK13+1)
POS1A1:  STX POS0B+1
         STY POS0C+1
         JMP KOPIUJ
POS1B:   CMP #2
         BNE POS1C
         LDX #<BLOK14
         LDY #>BLOK14
         JMP POS1A1
POS1C:   CMP #3
         BNE POS1D
         LDX #<(BLOK14+1)
         LDY #>(BLOK14+1)
         JMP POS1A1
POS1D:   CMP #4
         BNE POS1E
         LDX #<BLOK15
         LDY #>BLOK15
         JMP POS1A1
POS1E:   CMP #5
         BNE POS1F
         LDX #<(BLOK15+1)
         LDY #>(BLOK15+1)
         JMP POS1A1
POS1F:   JMP $EA31
START:   LDX #$00
         LDA #$00
STLOOP:  STA BLOK13,X
         STA BLOK14,X
         STA BLOK15,X
         INX
         CPX #63
         BEQ ST1
         JMP STLOOP
ST1:     LDX #$00
         LDY #$00
ST1LOOP: LDA DWUKR,X
         STA BLOK13+2,Y
         STA BLOK14+2,Y
         INX
         CPX #8
         BEQ ST2
         INY
         INY
         INY
         JMP ST1LOOP
ST2:     LDX #13
         STX WSKSP0
         INX
         STX WSKSP1
         INX
         STX WSKSP2
KOLOR:   LDA #13
         STA VIC+39
         STA VIC+40
         STA VIC+41
         LDA #000110
         STA VIC+16
         LDA #215
         LDX #50
         STA VIC
         STX VIC+1
         LDA #7
         STA VIC+2
         STX VIC+3
         ADC #48
         STA VIC+4
         STX VIC+5
         LDA #000111
         STA VIC+23
         STA VIC+29
         STA VIC+27
         STA VIC+21
         LDX #<MAIN
         LDY #>MAIN
         SEI
         STX IRQWKT
         STY IRQWKT+1
         CLI
         RTS
STEROW:  JSR POBZNA
         BEQ WYLACZ
         JSR PRZECT
         JSR ANAWYR
         ROL 13
         BCS NASTAW
ILLQUA:  JMP 47000
NASTAW:  JSR POBTXT
         CMP #6
         BNE ILLQUA
         LDY #5
         SEI
NASTPE:  LDA (34),Y
         SEC
         SBC #48
         STA CZAS,Y
         DEY
         BPL NASTPE
         LDA #60
         STA LICZNIK
         CLI
         JMP START
WYLACZ:  LDX #$31
         LDY #$EA
         SEI
         STX IRQWKT
         STY IRQWKT+1
         CLI
         LDA #$00
         STA VIC+21
         RTS

In the next episode I will present a BASIC program that uses above clock to sound an alarm (sound and visual effects) at predefined time.

Thanks for reading.

Marek Karcz
2/3/2013


Monday, November 5, 2012

Super Forth 64 – dynamic load and execution of machine language object.


Super Forth 64 – dynamic load and execution of machine language object.

I struggled with some project for the past week and finally I figured it out. Forth system is great to have huge dictionary of commands at user's disposal, which all reside in memory and are as a result able to load and execute fast. However it would be also nice to have ability to load the ML code from disk to memory buffer, execute it and return control to Forth. After all, you can not stuff everything in 20+ kB of RAM.

Super Forth 64 has ability to do just that with dictionary definitions SYS and SYSCALL:

SYS (.A .X .Y ADDR --- .A .X .Y STATUS)

Above word interfaces with ML subroutines which are external to FORTH system (e.g: C64 KERNAL routines).

.A, .X and .Y are values which will be loaded to corresponding MOS6510 registers prior to performing JSR to the routine at ADDR.

Upon return, CPU registers and status word will be put on parameter stack.

The called routine must end with RTS and the hardware stack must be left in the same condition as when the routine was first called.

SYSCALL (.A .X .Y ADDR --- )

Alternative to SYS, which does not return CPU registers and status on the parameter stack.

I wanted my machine language object to be fully relocatable so FORTH could load it to any memory location that was available, preferably to predefined buffer. To achieve that, jumps to absolute address locations within program can not be performed and the access to data must be performed via zero page vector.

The design assumes that ML program contains at the beginning 3 bytes long unconditional branch jump to the main routine and between that jump instruction and the code, data section is located. Byte #4 (index=3, assuming byte #1 is at offset 0) in ML object contains zero page address that ML program will use to access its data section. Starting at byte #5 (index=4 from the beginning of the object) data section is located.

I used Turbo Assembler to write my ML demo program and save its object to disk. It does not matter what the starting address is during compilation, since program does not use absolute addresses to access its own data and code. I assembled program to $C000, however it will be loadable to any address:

; MODULE: HELLOSF64
; PURPOSE: THE DEMO OF RELOCATEABLE ML
; PROGRAM RUNNING IN SUPER
; FORTH 64 ENVIRONMENT.
; DATE: 11/05/2012
; AUTHOR: MAREK KARCZ

ZPGl   = $02
CHROUT = $FFD2

   *= $C000

   CLC
   BCC MAIN

; ZERO PAGE POINTER, TO BE READ BY SF64
; CALLING ROUTINE TO DETERMINE WHERE TO
; PUT DATA SECTION ADDRESS.
; CALCULATED AT RUNTIME BY ADDING 3 TO
; THE PROGRAM LOAD ADDRESS.

ZPGPTR

   .BYTE ZPG1

; DATA SECTION, CALCULATED BY ADDING 4
; TO THE PROGRAM LOAD ADDRESS.

DATA

   .BYTE $0D
   .TEXT "HELLO FORTH!"
   .BYTE $0D,$00
   .TEXT "GOODBYE FORTH!"
   .BYTE $0D,$FF

MAIN

   LDY #0

LOOP

   LDA (ZPG1), Y
   BEQ TX2
   CMP #$FF
   BEQ END
   JSR CHROUT
TX2
   INY
   BNE LOOP
END
   RTS

As mentioned before, the first byte after branch jump contains zero page address (ZPGPTR). This is information for Super Forth's run time where ML program will read its data section vector from. It is important because Super Forth's routine calling the ML object code will have to save and then restore values at that memory location, otherwise SF64 will crash.

Now we need to define framework in SF64 supporting dynamic ML load and execution and then return to SF64 interpreter. We will need buffer for code (MLBUF), a function that loads the ML object from disk to the buffer (LOAD-ML) and function that executes the code (EXEC-ML). The EXEC-ML function saves values at zero page location that will be used by ML program as a data vector, then loads to that location the calculated data section of the ML program thus creating the actual data section vector for a ML program. Then the function executes ML code and upon return restores zero page address to its former values. Below is the FORTH code:

Load above definitions with:

1 LOAD

Please see screenshots below from Turbo Assembler and SF64 sessions testing the concept:

Turbo Assembler - program code edit.
Code assembled to disk file.
FORTH code loaded.
ML code buffer properly initialized.
Switch to disk #9 where ML object resides.
ML object loaded and MLBUF dumped for verification.
...and execute - Success!

Both functions (loading and executing) can be of course combined into one. Also, the ML object name prompt can be replaced with reading file name from standard stream used by FORTH. Anyway, concept is proven to work and after refinement  can be a valuable tool while working in FORTH's environment.

Thanks for visiting my blog.

Marek Karcz
11/5/2012