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 }