dale-8a

CHIP-8 emulator in POSIX AWK
git clone git://git.luxferre.top/dale-8a.git
Log | Files | Refs | README | LICENSE

dale8a.awk (14626B)


      1 #!/sbin/env awk -f
      2 # DALE-8A: a POSIX-compatible CHIP-8 emulator for AWK
      3 # Depends on the tgl.awk library, stty, time and od commands
      4 # Usage (w/o wrapper):
      5 # LANG=C awk -f tgl.awk -f dale8a.awk [-v vars ...] -- prog.ch8
      6 # Available vars to set:
      7 # - CLOCK_FACTOR (1 and above, default 20) - CPU cycles per frame
      8 # - PXL_COLOR (1 to 7) - foreground color of the screen
      9 # - BG_COLOR (0 to 7) - background color of the screen
     10 # - SBAR_COLOR (1 to 7) - foreground color of the statusbar
     11 # - SBAR_BG_COLOR (0 to 7) - background color of the statusbar
     12 # - EMU_QUIRK_[LSQ|STQ|VIP|JMP|CRY] - emulation quirk flags
     13 #
     14 # See README.md for details
     15 #
     16 # Created by Luxferre in 2023, released into public domain
     17 
     18 # fatal error reporting function
     19 function trapout(msg) {
     20   shutdown()
     21   cmd = "cat 1>&2"
     22   printf("Fatal: %s\n", msg) | cmd
     23   close(cmd)
     24   exit(1)
     25 }
     26 # graceful shutdown function - restore the terminal state
     27 function shutdown() {printf(SCR_CLR); altbufoff(); close(KEY_INPUT_STREAM); setterm(0)}
     28 
     29 function reportUnknownInstruction(msg) {
     30   msg = sprintf("unknown instruction at addr %04X: %02X%02X", pc-2, b1, b2)
     31   trapout(msg)
     32 }
     33 
     34 # terminal control routines
     35 function altbufon() {printf("\033[?47h")}
     36 function altbufoff() {printf("\033[?47l")}
     37 
     38 # render the statusbar + main screen area
     39 # all main rendering is done offscreen and then a single printf is called
     40 function drawscreen(s, i) {
     41   s = SCR_CLR SCR_SBAR # start with statusbar + main color mode switch
     42   for(i=64;i<2048;i++) { # render two pixel lines into one text line
     43     s = s SCR_PXL[screen[i-64] + 2*screen[i]]
     44     if(i%128 == 127) {
     45       s = s "\n"
     46       i += 64
     47     }
     48   }
     49   s = s SCR_SRESET # reset styling
     50   printf("%s", s) # output everything
     51 }
     52 
     53 # clear the screen (from inside the engine)
     54 function clearScreen(i) {
     55   for(i=0;i<2048;i++) screen[i] = 0
     56   renderScheduled = 1
     57 }
     58 
     59 # sprite drawing routine
     60 function drawSprite(x, y, bLen, i, j, realbyte, ind) {
     61   V[15] = 0
     62   for(i=0;i<bLen;i++) {
     63     realbyte = ram[iReg + i]
     64     for(j=0;realbyte>0;j++) { # loop while the byte is alive
     65       if(realbyte % 2) { # do anything only if the bit is set
     66         ind = ((y + i) % 32) * 64 + ((x + 7 - j) % 64) # calc the index
     67         if(screen[ind] == 1) {
     68           V[15] = 1
     69           screen[ind] = 0
     70         }
     71         else screen[ind] = 1
     72       }
     73       realbyte = int(realbyte / 2) # shift byte value
     74     }
     75   }
     76   renderScheduled = 1
     77 }
     78 
     79 function readkeynb(key) { # read a key, non-blocking fashion
     80   KEY_INPUT_STREAM | getline key # open the subprocess
     81   key = int(key) # read the key state
     82   close(KEY_INPUT_STREAM)
     83   if(key == 27) {shutdown(); exit(0)} # exit on Esc
     84   if(key in KBD_LAYOUT) { # if found, update the state and return the index
     85     key = KBD_LAYOUT[key]
     86     inputState[key] = 3 # introduce frame delay for the keypress
     87     return key
     88   }
     89   return -1 # if not found, return -1
     90 }
     91 
     92 function readkey(c) { # wait for a keypress and read the result
     93   drawscreen() # refresh the screen before blocking
     94   # drain input states
     95   for(i=0;i<16;i++) inputState[i] = 0
     96   # drain timers
     97   dtReg = stReg = 0
     98   do c = readkeynb() # read the code
     99   while(c < 0)
    100   inputState[c] = 0;
    101   return c
    102 }
    103 
    104 function wcf(dest, value, flag) { # write the result with carry/borrow flag
    105   V[dest] = value % 256
    106   V[15] = flag ? 1 : 0
    107   if(EMU_QUIRK_CRY) V[dest] = value % 256
    108 }
    109 
    110 # main CPU loop (direct adapted port from JS)
    111 
    112 function cpuLoop() {
    113   if(skip) { # skip once if marked so
    114     pc += 2
    115     skip = 0
    116   }
    117   b1 = ram[pc++]%256  # read the first byte and advance the counter
    118   b2 = ram[pc++]%256  # read the second byte and advance the counter
    119   d1 = int(b1/16) # extract the first instruction digit
    120   d2 = b1 % 16    # extract the second instruction digit
    121   d3 = int(b2/16) # extract the third instruction digit
    122   d4 = b2 % 16    # extract the fourth instruction digit
    123   nnn = d2 * 256 + b2 # extract the address for NNN style instructions
    124   if(pc < 512 || pc > 4095) trapout("instruction pointer out of bounds")
    125   #  Main challenge begins in 3... 2... 1...
    126   if(d1 == 0 && d2 == 0 && d3 == 14) { # omit everything except 00E0 and 00EE
    127     if(d4 == 0) clearScreen() # pretty obvious, isn't it?
    128     else if(d4 == 14) {if(sp > 0) pc = stack[--sp]} # return from the subroutine
    129     else reportUnknownInstruction()
    130   }
    131   else if(d1 == 1) pc = nnn # unconditional jumpstyle
    132   else if(d1 == 2) {stack[sp++] = pc; pc = nnn} # subroutine call
    133   # Skip the following instruction if the value of register V{d2} equals {b2}
    134   else if(d1 == 3) {if(V[d2] == b2) skip = 1}
    135   # Skip the following instruction if the value of register V{d2} is not equal to {b2}
    136   else if(d1 == 4) {if(V[d2] != b2) skip = 1}
    137   # Skip the following instruction if the value of register V{d2} equals V{d3}
    138   else if(d1 == 5) {if(V[d2] == V[d3]) skip = 1 }
    139   else if(d1 == 6) V[d2] = b2 # Store number {b2} in register V{d2}
    140   else if(d1 == 7) V[d2] = (V[d2] + b2) % 256 # Add the value {b2} to register V{d2}
    141   else if(d1 == 8) { # Monster #1
    142   # for all instructions in this section, d4 is the selector and d2 and d3 are the X and Y parameters respectively
    143     if(d4 == 0) V[d2] = V[d3] # Store the value of register VY in register VX
    144     # Set VX to VX OR VY
    145     else if(d4 == 1) {V[d2] = bw_or(V[d2], V[d3]); if(EMU_QUIRK_VIP) V[15] = 0}
    146     # Set VX to VX AND VY
    147     else if(d4 == 2) {V[d2] = bw_and(V[d2], V[d3]); if(EMU_QUIRK_VIP) V[15] = 0}
    148     # Set VX to VX XOR VY
    149     else if(d4 == 3) {V[d2] = bw_xor(V[d2], V[d3]); if(EMU_QUIRK_VIP) V[15] = 0}
    150     else if(d4 == 4) { # Add the value of register VY to register VX with overflow recorded in VF
    151       nnn = V[d2] + V[d3]
    152       wcf(d2, nnn, nnn > 255)
    153     }
    154     else if(d4 == 5) { # Set VX = VX - VY with underflow recorded in VF
    155       nnn = V[d2] - V[d3]
    156       wcf(d2, nnn + 256, nnn >= 0)
    157     }
    158     else if(d4 == 6) { # Store the value of register VY shifted right one bit in register VX, set register VF to the least significant bit prior to the shift
    159       if(EMU_QUIRK_LSQ) d3 = d2
    160       wcf(d2, int(V[d3]/2), V[d3]%2)
    161     }
    162     else if(d4 == 7) { # Set VX = VY - VX with underflow recorded in VF
    163       nnn = V[d3] - V[d2]
    164       wcf(d2, nnn + 256, nnn >= 0)
    165     }
    166     else if(d4 == 14) { # Store the value of register VY shifted left one bit in register VX, set register VF to the most significant bit prior to the shift
    167       if(EMU_QUIRK_LSQ) d3 = d2
    168       wcf(d2, V[d3]*2, int(V[d3]/128))
    169     }
    170     else reportUnknownInstruction()
    171   }
    172   # Skip the following instruction if the value of register V{d2} is not equal to the value of register V{d3}
    173   else if(d1 == 9) {if(V[d2] != V[d3]) skip = 1}
    174   else if(d1 == 10) iReg = nnn # Store memory address NNN in register I
    175   else if(d1 == 11) {
    176     if(EMU_QUIRK_JMP) pc = nnn + V[d2]
    177     else pc = nnn + V[0] # Jump to address NNN + V0
    178   }
    179   else if(d1 == 12) V[d2] = bw_and(int(rand()*256)%256, b2) # Set V{d2} to a random number with a mask of {b2}
    180   # Draw a sprite at position V{d2}, V{d3} with {d4} bytes of sprite data starting at the address stored in I
    181   # Set VF to 01 if any set pixels are changed to unset, and 00 otherwise
    182   else if(d1 == 13) drawSprite(V[d2], V[d3], d4)
    183   else if(d1 == 14) {
    184     # Skip the following instruction if the key corresponding to the hex value currently stored in register V{d2} is pressed
    185     if(b2 == 158) {if(inputState[V[d2]] > 0) skip = 1}
    186     # Skip the following instruction if the key corresponding to the hex value currently stored in register V{d2} is not pressed
    187     else if(b2 == 161) {if(inputState[V[d2]] == 0) skip = 1}
    188     else reportUnknownInstruction()
    189   }
    190   else if(d1 == 15) { # Monster #2
    191     # d2 is the parameter X for all these instructions, b2 is the selector
    192     if(b2 == 7) V[d2] = dtReg # Store the current value of the delay timer in register VX
    193     else if(b2 == 10) V[d2] = readkey() # Wait for a keypress and store the result in register VX
    194     else if(b2 == 21) dtReg = V[d2] # Set the delay timer to the value of register VX
    195     else if(b2 == 24) stReg = V[d2] # Set the sound timer to the value of register VX
    196     else if(b2 == 30) iReg = (iReg + V[d2]) % 65536 # Add the value stored in register VX to register I
    197     # Set I to the memory address of the sprite data corresponding to the hexadecimal digit stored in register VX
    198     else if(b2 == 41) iReg = (128 + V[d2] * 5) % 65536
    199     else if(b2 == 51) { # Store the binary-coded decimal equivalent of the value stored in register VX at addresses I, I+1, and I+2
    200       nnn = V[d2]
    201       ram[iReg % 4096] = int(nnn / 100)
    202       ram[(iReg % 4096) + 1] = int((nnn % 100) / 10)
    203       ram[(iReg % 4096) + 2] = nnn % 10
    204     }
    205     else if(b2 == 85) {
    206       # Store the values of registers V0 to VX inclusive in memory starting at address I
    207       # I is set to I + X + 1 after operation
    208       for(nnn=0;nnn<=d2;nnn++) ram[(iReg+nnn) % 4096] = V[nnn]
    209       if(!EMU_QUIRK_STQ) iReg = (iReg + d2 + 1) % 65536
    210     }
    211     else if(b2 == 101) {
    212       # Fill registers V0 to VX inclusive with the values stored in memory starting at address I
    213       # I is set to I + X + 1 after operation
    214       for(nnn=0;nnn<=d2;nnn++) V[nnn] = ram[(iReg+nnn) % 4096]
    215       if(!EMU_QUIRK_STQ) iReg = (iReg + d2 + 1) % 65536
    216     }
    217     else reportUnknownInstruction()
    218   }
    219   else reportUnknownInstruction()
    220 }
    221 
    222 # get current Unix timestamp with millisecond precision with various methods
    223 function timestampms(cmd, res) {
    224   cmd = "echo $EPOCHREALTIME"
    225   cmd | getline res
    226   close(cmd)
    227   sub(/[,\.]/,"", res)
    228   res = int(res)
    229   if(res) return res / 1000 # micro=>milli
    230   # otherwise we need to use an alternate, POSIX-compatible method
    231   cmd = "date +%s"
    232   cmd | getline res
    233   close(cmd)
    234   return int(res) * 1000 # s=>milli
    235 }
    236 
    237 # determine the amount of empty cycles needed to fill a single frame
    238 function hostprofile(cf, i, cps, sc, st, et) {
    239   sc = 2000000 # this is an arbitrarily large (but not too large) cycle count
    240   do {
    241     sc += 200000
    242     st = timestampms()
    243     a = 0
    244     for(i=0;i<sc;i++) a += i
    245     et = timestampms()
    246   } while(et == st)
    247   # now, we have our cps metric
    248   cps = 1000 * sc / (int(et) - int(st))
    249   # but we need 1/60 second and also consider other operations
    250   return int(cps / 60 - cf - 16)
    251 }
    252 
    253 # main code starts here
    254 
    255 BEGIN {
    256   if(ARGC < 2) trapout("no ROM file specified!")
    257   # preload the ROM - starting index is 0
    258   PRG_FNAME = ARGV[1]
    259   print "Loading", PRG_FNAME
    260   PRG_LEN = loadbin(PRG_FNAME, PRG_ROM, 0, 1)
    261   if(PRG_LEN < 1) trapout("could not read ROM!")
    262   PRG_END_ADDR = 512 + PRG_LEN # all CHIP-8 ROMs start at 0x200 = 512
    263   srand() # init the PRNG
    264   KEY_INPUT_STREAM = "od -tu1 -w1 -An -N1 -v"
    265   # tweak the per-frame performance here
    266   clockFactor = int(CLOCK_FACTOR > 0 ? CLOCK_FACTOR : 20)
    267   print "Profiling the frame timing..."
    268   framecycle = hostprofile(clockFactor) # get the amount of host cycles to skip 
    269   printf "Detected %u cycles per frame\n", framecycle
    270   # read the quirk flags from the filename and environment
    271   EMU_QUIRK_LSQ = !!EMU_QUIRK_LSQ
    272   EMU_QUIRK_STQ = !!EMU_QUIRK_STQ
    273   EMU_QUIRK_VIP = !!EMU_QUIRK_VIP
    274   EMU_QUIRK_JMP = !!EMU_QUIRK_JMP
    275   EMU_QUIRK_CRY = !!EMU_QUIRK_CRY
    276   if(PRG_FNAME ~ /\.sl\.ch8$/ || PRG_FNAME ~ /\.ls\.ch8$/) # check the extension
    277     EMU_QUIRK_LSQ = EMU_QUIRK_STQ = 1 # both quirks on
    278   else if(PRG_FNAME ~ /\.l\.ch8$/) EMU_QUIRK_LSQ = 1 # only LSQ on
    279   else if(PRG_FNAME ~ /\.s\.ch8$/) EMU_QUIRK_STQ = 1 # only STQ on
    280   qstatus = "|"
    281   if(EMU_QUIRK_LSQ) qstatus = qstatus " LSQ"
    282   if(EMU_QUIRK_STQ) qstatus = qstatus " STQ"
    283   if(EMU_QUIRK_VIP) qstatus = qstatus " VIP"
    284   if(EMU_QUIRK_JMP) qstatus = qstatus " JMP"
    285   if(EMU_QUIRK_CRY) qstatus = qstatus " CRY"
    286   # init main and statusbar color codes (from 1 to 7)
    287   if(!PXL_COLOR || PXL_COLOR > 7) PXL_COLOR = 2 # green by default
    288   if(!SBAR_COLOR || SBAR_COLOR > 7) SBAR_COLOR = 3 # yellow by default
    289   if(!BG_COLOR || BG_COLOR > 7) BG_COLOR = 0 # black backgrounds by default
    290   if(!SBAR_BG_COLOR || SBAR_BG_COLOR > 7) SBAR_BG_COLOR = 0 
    291   # init some string constants and parameters
    292   SCR_CLR = sprintf("\033[2J")
    293   SCR_PXL[0] = " " # empty space
    294   SCR_PXL[1] = wctomb(9600) # Unicode upper-half block
    295   SCR_PXL[2] = wctomb(9604) # Unicode lower-half block
    296   SCR_PXL[3] = wctomb(9608) # Unicode rectangular block
    297   HR = ""
    298   for(i=0;i<64;i++) HR = HR "-"
    299   SCR_SBAR = sprintf("\033[3%d;1;4%dmDALE-8A | %s %s\n" \
    300       "%s\n\033[3%d;4%dm", SBAR_COLOR, SBAR_BG_COLOR, PRG_FNAME, \
    301       qstatus, HR, PXL_COLOR, BG_COLOR)
    302   SCR_SRESET = sprintf("\033[0m\033[0;0H")
    303   # init CHR ROM - starting index is 1
    304   split("240 144 144 144 240 32 96 32 32 112 240 16 240 128 240 240 16 " \
    305         "240 16 240 144 144 240 16 16 240 128 240 16 240 240 128 240 144 " \
    306         "240 240 16 32 64 64 240 144 240 144 240 240 144 240 16 240 240 " \
    307         "144 240 144 144 224 144 224 144 224 240 128 128 128 240 224 144 " \
    308         "144 144 224 240 128 240 128 240 240 128 240 128 128", CHR_ROM)
    309   # init keyboard layout
    310   split("120 49 50 51 113 119 101 97 115 100 122 99 52 114 102 118", kbdx)
    311   for(i=1;i<=16;i++) KBD_LAYOUT[kbdx[i]] = i - 1
    312   # init main registers, stack, RAM and screen - starting index for all is 0
    313   for(i=0;i<4096;i++) {
    314     if(i < 16) V[i] = inputState[i] = 0
    315     if(i < 1792) stack[i] = 0 # also init call stack 
    316     if(i < 2048) screen[i] = 0 # screen is 2048 bytes long instead of bits
    317     if(i>= 128 && i < 208) { # a byte from CHR ROM which is 80 bytes long
    318       j = i - 127
    319       ram[i] = int(CHR_ROM[j]) % 256
    320       delete CHR_ROM[j]
    321     }
    322     else if(i>= 512 && i < PRG_END_ADDR) { # a byte from PRG ROM
    323       j = i - 512
    324       ram[i] = int(PRG_ROM[j]) % 256
    325       delete PRG_ROM[j]
    326     }
    327     else ram[i] = 0 # everything else must be initialized to 0
    328   }
    329   # main execution logic starts here
    330   altbufon() # enter the alternative screen buffer
    331   setterm(3) # enter the non-blocking input mode before the event loop
    332   pc = 512 # start at instruction 0x200
    333   iReg = dtReg = stReg = skip = 0 # init I, DT and ST registers and skip flag
    334   renderScheduled = 1 # only render the screen when necessary
    335   b1 = b2 = d1 = d2 = d3 = d4 = nnn = sp = 0 # init different opcode parts
    336   while(1) { # our event loop is here
    337     for(i=0;i<clockFactor;i++) cpuLoop() # call main CPU loop CF times
    338     if(renderScheduled) {
    339       drawscreen() # render the current screen state
    340       renderScheduled = 0
    341     }
    342     # timer register loops
    343     if(dtReg > 0) dtReg--
    344     if(stReg > 0) stReg--
    345     # decrement input states
    346     for(i=0;i<16;i++) if(inputState[i] > 0) inputState[i]--
    347     # read and update current key states
    348     readkeynb()
    349     a=0
    350     for(i=0;i<framecycle;i++) a+=i # sleep on 1/60 sec, more efficiently
    351   }
    352   shutdown() # restore the terminal state and exit
    353 }