commit 1dbe229529788a40d076153c6ac81421b26a63f3
Author: Luxferre <lux@ferre>
Date: Thu, 11 May 2023 17:31:52 +0300
initial upload
Diffstat:
8 files changed, 749 insertions(+), 0 deletions(-)
diff --git a/COPYING b/COPYING
@@ -0,0 +1,26 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <https://unlicense.org>
+
+
diff --git a/README.md b/README.md
@@ -0,0 +1,126 @@
+# DALE-8A: A CHIP-8 platform emulator for POSIX AWK
+
+This is an advanced port of my previous JS-based CHIP-8 emulator, [DALE-8](https://gitlab.com/suborg/dale-8/), to the AWK programming language in its standard (POSIX) variation. The port was also inspired by [awk-chip8 by patsie75](https://github.com/patsie75/awk-chip8) although not a single piece of code is used from there as that emulator heavily depends on GAWK-specific features and doesn't emulate certain ROM quirks. Compared to the original DALE-8, DALE-8A drops the sound output capability but implements everything else using CLI pseudographics and also is fully compatible with low-res CHIP-8 ROMs developed using the [Octo](https://johnearnest.github.io/Octo/index.html) IDE. All the required interactive input and binary loading functions are provided by my own POSIX-compatible library, `tgl.awk` (The Great Library). As such, DALE-8A externally depends on the `stty` and `od` commands only.
+
+Since AWK environments can vary in terms of execution speed, DALE-8A performs all necessary profiling before running the main code in order to reduce dependency on external timing utilities like `sleep`. Because this profiling depends on the `$EPOCHREALTIME` environment variable, it's recommended to run DALE-8A from the shell that supports it, like Bash 5.x and above or BusyBox with the corresponding compilation flags. In case this variable is unavailable, a fallback timing method is used which is much less accurate and can make emulation too slow or too fast.
+
+DALE-8A was created more as an excercise to improve the algorithmic part of CHIP-8 emulation and to practice optimizing portable AWK code. Yet, combined with `busybox awk`, it can be practically used in some embedded environments where deploying any other VM is not easy.
+
+## Supported specification
+
+- Screen resolution: 64x32, 8px wide sprites (no extended screen mode support), 60 Hz refresh rate
+- Color palette: monochrome (both foreground and background colors are configurable)
+- Memory: 4096 bytes
+- 16 operation registers: V0 to VF
+- Service registers: address register I, delay timer DT and sound timer ST
+- 16-digit CHR ROM (loaded at 0x80)
+- 3584-byte PRG ROM (loaded at 0x200)
+- Subroutine call stack with the depth of 1792 (theoretically covers all loadable memory)
+- All standard 35 opcodes support (RCA-specific `0NNN` calls, except `00E0` and `00EE`, are ignored) - see the list below
+- Five optional CPU quirks required for some games are implemented - see below
+- Sound timer register is supported but has no effect
+
+DALE-8A passes all relevant tests from [Timendus' CHIP-8 test suite 4.0](https://github.com/Timendus/chip8-test-suite), as well as some others, which are included in the `testroms` directory of this repo. It is capable of running everything compiled for the bare CHIP-8 in Octo by default, as well as all old games using the `LSQ` and/or `STQ` quirks.
+
+## Usage
+
+### Running the emulator
+
+The most convenient way of running CHIP-8 ROMs is using the shell wrapper from this package:
+
+```
+sh dale8a.sh prog.ch8
+```
+
+You can also run the AWK file itself directly as follows:
+
+LANG=C awk -f tgl.awk -f dale8a.awk [params] -- prog.ch8
+
+If the ROM file has `.l.ch8` extension, additional `LSQ` emulation quirk will be applied (see below). If the file has `.s.ch8` extention, additional `STQ` emulation quirk will be applied (see below). Is the file has `.sl.ch8` or `.ls.ch8` extension, both quirks will be applied.
+
+### Configuration variables
+
+The DALE-8A script allows to pass a number of configuration variables to the engine using the standard `-v` option of AWK:
+
+- `CLOCK_FACTOR` - the number of CPU cycles executed per single frame, default 20
+- `PXL_COLOR` - set the main screen foreground color (1 to 7), default 2 (green)
+- `BG_COLOR` - set the main screen background color (0 to 7), default 0 (black)
+- `SBAR_COLOR`- set the statusbar foreground color (1 to 7), default 3 (yellow)
+- `SBAR_BG_COLOR` - set the statusbar background color (0 to 7), default 0 (black)
+- `EMU_QUIRK_LSQ` - apply `LSQ` quirk (if set with `-v`, overrides the filename-based setting)
+- `EMU_QUIRK_STQ` - apply `STQ` quirk (if set with `-v`, overrides the filename-based setting)
+- `EMU_QUIRK_VIP` - apply `VIP` quirk
+- `EMU_QUIRK_JMP` - apply `JMP` quirk
+- `EMU_QUIRK_CRY` - apply `CRY` quirk
+
+The clock factor variable change can be required by some games that were designed to run under high CPU rate.
+
+The color values from 0 to 7 correspond to the standard ANSI terminal codes: black, red, green, yellow, blue, magenta, cyan and white respectively. For the foreground values, the "bold" text attribute is also applied where supported, so they are brighter than usual.
+
+For the emulation quirks description, see the "Supported opcode list" section below. All the quirk-related variables are unset by default.
+
+### Status bar
+
+Above the virtual screen, a status bar is displayed. It contains the current ROM filename and the quirk emulation status in the following order: `LSQ STQ VIP JMP CRY`. If a quirk is off, it won't appear in this list.
+
+### Controls
+
+- **Exiting**: At any point, press Escape to exit the emulator. If running via the `dale8.sh` wrapper, it's also safe to press Ctrl+C.
+- **Keyboard mapping** is the same as the default one in the Octo emulator:
+
+Virtual |Keyboard
+---------|---------
+`1 2 3 C`|`1 2 3 4`
+`4 5 6 D`|`q w e r`
+`7 8 9 E`|`a s d f`
+`A 0 B F`|`z x c v`
+
+## Supported opcode list
+
+These are all the opcodes supported by DALE-8A. The list of mnemonics is taken [from here](http://devernay.free.fr/hacks/chip8/C8TECH10.HTM). All arithmetics is unsigned 8-bit (modulo 256). Arithmetics on the `I` register is unsigned 16-bit.
+
+Opcode | Assembly instruction | Meaning | Notes
+-------|----------------------|---------|------
+00E0 | CLS | Clear the screen |
+00EE | RET | Return from the subroutine | Does nothing if we're on the top of call stack
+0nnn | SYS addr | Machine ROM call at addr | Isn't used in any modern CHIP-8 programs and ignored by DALE-8A
+1nnn | JP addr | Unconditional jump to addr |
+2nnn | CALL addr | Call the subroutine at addr |
+3xkk | SE Vx, byte | Skip next instruction if Vx == byte |
+4xkk | SNE Vx, byte | Skip next instruction if Vx != byte |
+5xy0 | SE Vx, Vy | Skip next instruction if Vx == Vy |
+6xkk | LD Vx, byte | Set Vx = byte |
+7xkk | ADD Vx, byte | Set Vx = Vx + byte |
+8xy0 | LD Vx, Vy | Set Vx = Vy |
+8xy1 | OR Vx, Vy | Set Vx = Vx OR Vy | Bitwise OR | If `VIP` quirk is **on**, also clear the VF register
+8xy2 | AND Vx, Vy | Set Vx = Vx AND Vy | Bitwise AND | If `VIP` quirk is **on**, also clear the VF register
+8xy3 | XOR Vx, Vy | Set Vx = Vx XOR Vy | Bitwise XOR | If `VIP` quirk is **on**, also clear the VF register
+8xy4 | ADD Vx, Vy | Set Vx = Vx + Vy, set VF = carry\* | VF is set to 1 if the result would exceed 255, set to 0 otherwise
+8xy5 | SUB Vx, Vy | Set Vx = Vx - Vy, set VF = NOT borrow\* | VF is set to 0 if the result would be less than zero, set to 1 otherwise
+8xy6 | SHR Vx {, Vy} | Set Vx = Vy >> 1, VF is set to Vy&1 before the shift\* | If `LSQ` quirk is **on**, the instruction operates on Vx instead of Vy
+8xy7 | SUBN Vx, Vy | Set Vx = Vy - Vx, set VF = NOT borrow\* | VF is set to 0 if the result would be less than zero, set to 1 otherwise
+8xyE | SHL Vx {, Vy} | Set Vx = Vy << 1, VF is set to Vy&1 before the shift\* | If `LSQ` quirk is **on**, the instruction operates on Vx instead of Vy
+9xy0 | SNE Vx, Vy | Skip next instruction if Vx != Vy |
+Annn | LD I, addr | Set I = addr |
+Bnnn | JP V0, addr | Jump to location addr + V0 | If `JMP` quirk is **on**, V{addr>>8} is used instead of V0
+Cxkk | RND Vx, byte | Set Vx = random number AND byte | Vx = rnd(0,255) & byte
+Dxyn | DRW Vx, Vy, n | Display n-byte sprite (XOR with the video memory) starting at memory location I at (Vx, Vy), set VF = collision | VF if set to 1 if **any** existing pixel of the screen was already set to 1 and the sprite overwrote it with 1, making it 0, and VF is set to 0 otherwise. If the sprite is positioned so a part of it is outside of the display width, it wraps around to the opposite side of the screen
+Ex9E | SKP Vx | Skip next instruction if key with the value of Vx is pressed |
+ExA1 | SKNP Vx | Skip next instruction if key with the value of Vx is not pressed |
+Fx07 | LD Vx, DT | Set Vx to the value of delay timer register |
+Fx0A | LD Vx, K | Block the execution, wait for keyboard input and store the result digit into Vx |
+Fx15 | LD DT, Vx | Set delay timer register to the value of Vx |
+Fx18 | LD ST, Vx | Set sound timer register to the value of Vx |
+Fx1E | ADD I, Vx | Set I = I + Vx |
+Fx29 | LD F, Vx | Set I = location of sprite for digit stored in Vx |
+Fx33 | LD B, Vx | Store BCD representation of Vx in memory locations I, I+1, and I+2 |
+Fx55 | LD [I], Vx | Store registers V0 through Vx in memory starting at location I | If `STQ` quirk is **off**, the instruction modifies I to I + x + 1
+Fx65 | LD Vx, [I] | Read registers V0 through Vx from memory starting at location I | If `STQ` quirk is **off**, the instruction modifies I to I + x + 1
+
+\* If `CRY` quirk is on, modify the target register **after** setting the VF register in this operation
+
+## Credits
+
+Created by Luxferre in 2023, released into public domain.
+
+Made in Ukraine.
diff --git a/dale8a.awk b/dale8a.awk
@@ -0,0 +1,353 @@
+#!/sbin/env awk -f
+# DALE-8A: a POSIX-compatible CHIP-8 emulator for AWK
+# Depends on the tgl.awk library, stty, time and od commands
+# Usage (w/o wrapper):
+# LANG=C awk -f tgl.awk -f dale8a.awk [-v vars ...] -- prog.ch8
+# Available vars to set:
+# - CLOCK_FACTOR (1 and above, default 20) - CPU cycles per frame
+# - PXL_COLOR (1 to 7) - foreground color of the screen
+# - BG_COLOR (0 to 7) - background color of the screen
+# - SBAR_COLOR (1 to 7) - foreground color of the statusbar
+# - SBAR_BG_COLOR (0 to 7) - background color of the statusbar
+# - EMU_QUIRK_[LSQ|STQ|VIP|JMP|CRY] - emulation quirk flags
+#
+# See README.md for details
+#
+# Created by Luxferre in 2023, released into public domain
+
+# fatal error reporting function
+function trapout(msg) {
+ shutdown()
+ cmd = "cat 1>&2"
+ printf("Fatal: %s\n", msg) | cmd
+ close(cmd)
+ exit(1)
+}
+# graceful shutdown function - restore the terminal state
+function shutdown() {printf(SCR_CLR); altbufoff(); close(KEY_INPUT_STREAM); setterm(0)}
+
+function reportUnknownInstruction(msg) {
+ msg = sprintf("unknown instruction at addr %04X: %02X%02X", pc-2, b1, b2)
+ trapout(msg)
+}
+
+# terminal control routines
+function altbufon() {printf("\033[?47h")}
+function altbufoff() {printf("\033[?47l")}
+
+# render the statusbar + main screen area
+# all main rendering is done offscreen and then a single printf is called
+function drawscreen(s, i) {
+ s = SCR_CLR SCR_SBAR # start with statusbar + main color mode switch
+ for(i=64;i<2048;i++) { # render two pixel lines into one text line
+ s = s SCR_PXL[screen[i-64] + 2*screen[i]]
+ if(i%128 == 127) {
+ s = s "\n"
+ i += 64
+ }
+ }
+ s = s SCR_SRESET # reset styling
+ printf("%s", s) # output everything
+}
+
+# clear the screen (from inside the engine)
+function clearScreen(i) {
+ for(i=0;i<2048;i++) screen[i] = 0
+ renderScheduled = 1
+}
+
+# sprite drawing routine
+function drawSprite(x, y, bLen, i, j, realbyte, ind) {
+ V[15] = 0
+ for(i=0;i<bLen;i++) {
+ realbyte = ram[iReg + i]
+ for(j=0;realbyte>0;j++) { # loop while the byte is alive
+ if(realbyte % 2) { # do anything only if the bit is set
+ ind = ((y + i) % 32) * 64 + ((x + 7 - j) % 64) # calc the index
+ if(screen[ind] == 1) {
+ V[15] = 1
+ screen[ind] = 0
+ }
+ else screen[ind] = 1
+ }
+ realbyte = int(realbyte / 2) # shift byte value
+ }
+ }
+ renderScheduled = 1
+}
+
+function readkeynb(key) { # read a key, non-blocking fashion
+ KEY_INPUT_STREAM | getline key # open the subprocess
+ key = int(key) # read the key state
+ close(KEY_INPUT_STREAM)
+ if(key == 27) {shutdown(); exit(0)} # exit on Esc
+ if(key in KBD_LAYOUT) { # if found, update the state and return the index
+ key = KBD_LAYOUT[key]
+ inputState[key] = 3 # introduce frame delay for the keypress
+ return key
+ }
+ return -1 # if not found, return -1
+}
+
+function readkey(c) { # wait for a keypress and read the result
+ drawscreen() # refresh the screen before blocking
+ # drain input states
+ for(i=0;i<16;i++) inputState[i] = 0
+ # drain timers
+ dtReg = stReg = 0
+ do c = readkeynb() # read the code
+ while(c < 0)
+ inputState[c] = 0;
+ return c
+}
+
+function wcf(dest, value, flag) { # write the result with carry/borrow flag
+ V[dest] = value % 256
+ V[15] = flag ? 1 : 0
+ if(EMU_QUIRK_CRY) V[dest] = value % 256
+}
+
+# main CPU loop (direct adapted port from JS)
+
+function cpuLoop() {
+ if(skip) { # skip once if marked so
+ pc += 2
+ skip = 0
+ }
+ b1 = ram[pc++]%256 # read the first byte and advance the counter
+ b2 = ram[pc++]%256 # read the second byte and advance the counter
+ d1 = int(b1/16) # extract the first instruction digit
+ d2 = b1 % 16 # extract the second instruction digit
+ d3 = int(b2/16) # extract the third instruction digit
+ d4 = b2 % 16 # extract the fourth instruction digit
+ nnn = d2 * 256 + b2 # extract the address for NNN style instructions
+ if(pc < 512 || pc > 4095) trapout("instruction pointer out of bounds")
+ # Main challenge begins in 3... 2... 1...
+ if(d1 == 0 && d2 == 0 && d3 == 14) { # omit everything except 00E0 and 00EE
+ if(d4 == 0) clearScreen() # pretty obvious, isn't it?
+ else if(d4 == 14) {if(sp > 0) pc = stack[--sp]} # return from the subroutine
+ else reportUnknownInstruction()
+ }
+ else if(d1 == 1) pc = nnn # unconditional jumpstyle
+ else if(d1 == 2) {stack[sp++] = pc; pc = nnn} # subroutine call
+ # Skip the following instruction if the value of register V{d2} equals {b2}
+ else if(d1 == 3) {if(V[d2] == b2) skip = 1}
+ # Skip the following instruction if the value of register V{d2} is not equal to {b2}
+ else if(d1 == 4) {if(V[d2] != b2) skip = 1}
+ # Skip the following instruction if the value of register V{d2} equals V{d3}
+ else if(d1 == 5) {if(V[d2] == V[d3]) skip = 1 }
+ else if(d1 == 6) V[d2] = b2 # Store number {b2} in register V{d2}
+ else if(d1 == 7) V[d2] = (V[d2] + b2) % 256 # Add the value {b2} to register V{d2}
+ else if(d1 == 8) { # Monster #1
+ # for all instructions in this section, d4 is the selector and d2 and d3 are the X and Y parameters respectively
+ if(d4 == 0) V[d2] = V[d3] # Store the value of register VY in register VX
+ # Set VX to VX OR VY
+ else if(d4 == 1) {V[d2] = bw_or(V[d2], V[d3]); if(EMU_QUIRK_VIP) V[15] = 0}
+ # Set VX to VX AND VY
+ else if(d4 == 2) {V[d2] = bw_and(V[d2], V[d3]); if(EMU_QUIRK_VIP) V[15] = 0}
+ # Set VX to VX XOR VY
+ else if(d4 == 3) {V[d2] = bw_xor(V[d2], V[d3]); if(EMU_QUIRK_VIP) V[15] = 0}
+ else if(d4 == 4) { # Add the value of register VY to register VX with overflow recorded in VF
+ nnn = V[d2] + V[d3]
+ wcf(d2, nnn, nnn > 255)
+ }
+ else if(d4 == 5) { # Set VX = VX - VY with underflow recorded in VF
+ nnn = V[d2] - V[d3]
+ wcf(d2, nnn + 256, nnn >= 0)
+ }
+ 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
+ if(EMU_QUIRK_LSQ) d3 = d2
+ wcf(d2, int(V[d3]/2), V[d3]%2)
+ }
+ else if(d4 == 7) { # Set VX = VY - VX with underflow recorded in VF
+ nnn = V[d3] - V[d2]
+ wcf(d2, nnn + 256, nnn >= 0)
+ }
+ 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
+ if(EMU_QUIRK_LSQ) d3 = d2
+ wcf(d2, V[d3]*2, int(V[d3]/128))
+ }
+ else reportUnknownInstruction()
+ }
+ # Skip the following instruction if the value of register V{d2} is not equal to the value of register V{d3}
+ else if(d1 == 9) {if(V[d2] != V[d3]) skip = 1}
+ else if(d1 == 10) iReg = nnn # Store memory address NNN in register I
+ else if(d1 == 11) {
+ if(EMU_QUIRK_JMP) pc = nnn + V[d2]
+ else pc = nnn + V[0] # Jump to address NNN + V0
+ }
+ 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}
+ # Draw a sprite at position V{d2}, V{d3} with {d4} bytes of sprite data starting at the address stored in I
+ # Set VF to 01 if any set pixels are changed to unset, and 00 otherwise
+ else if(d1 == 13) drawSprite(V[d2], V[d3], d4)
+ else if(d1 == 14) {
+ # Skip the following instruction if the key corresponding to the hex value currently stored in register V{d2} is pressed
+ if(b2 == 158) {if(inputState[V[d2]] > 0) skip = 1}
+ # Skip the following instruction if the key corresponding to the hex value currently stored in register V{d2} is not pressed
+ else if(b2 == 161) {if(inputState[V[d2]] == 0) skip = 1}
+ else reportUnknownInstruction()
+ }
+ else if(d1 == 15) { # Monster #2
+ # d2 is the parameter X for all these instructions, b2 is the selector
+ if(b2 == 7) V[d2] = dtReg # Store the current value of the delay timer in register VX
+ else if(b2 == 10) V[d2] = readkey() # Wait for a keypress and store the result in register VX
+ else if(b2 == 21) dtReg = V[d2] # Set the delay timer to the value of register VX
+ else if(b2 == 24) stReg = V[d2] # Set the sound timer to the value of register VX
+ else if(b2 == 30) iReg = (iReg + V[d2]) % 65536 # Add the value stored in register VX to register I
+ # Set I to the memory address of the sprite data corresponding to the hexadecimal digit stored in register VX
+ else if(b2 == 41) iReg = (128 + V[d2] * 5) % 65536
+ 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
+ nnn = V[d2]
+ ram[iReg % 4096] = int(nnn / 100)
+ ram[(iReg % 4096) + 1] = int((nnn % 100) / 10)
+ ram[(iReg % 4096) + 2] = nnn % 10
+ }
+ else if(b2 == 85) {
+ # Store the values of registers V0 to VX inclusive in memory starting at address I
+ # I is set to I + X + 1 after operation
+ for(nnn=0;nnn<=d2;nnn++) ram[(iReg+nnn) % 4096] = V[nnn]
+ if(!EMU_QUIRK_STQ) iReg = (iReg + d2 + 1) % 65536
+ }
+ else if(b2 == 101) {
+ # Fill registers V0 to VX inclusive with the values stored in memory starting at address I
+ # I is set to I + X + 1 after operation
+ for(nnn=0;nnn<=d2;nnn++) V[nnn] = ram[(iReg+nnn) % 4096]
+ if(!EMU_QUIRK_STQ) iReg = (iReg + d2 + 1) % 65536
+ }
+ else reportUnknownInstruction()
+ }
+ else reportUnknownInstruction()
+}
+
+# get current Unix timestamp with millisecond precision with various methods
+function timestampms(cmd, res) {
+ cmd = "echo $EPOCHREALTIME"
+ cmd | getline res
+ close(cmd)
+ sub(/[,\.]/,"", res)
+ res = int(res)
+ if(res) return res / 1000 # micro=>milli
+ # otherwise we need to use an alternate, POSIX-compatible method
+ cmd = "date +%s"
+ cmd | getline res
+ close(cmd)
+ return int(res) * 1000 # s=>milli
+}
+
+# determine the amount of empty cycles needed to fill a single frame
+function hostprofile(cf, i, cps, sc, st, et) {
+ sc = 2000000 # this is an arbitrarily large (but not too large) cycle count
+ do {
+ sc += 200000
+ st = timestampms()
+ a = 0
+ for(i=0;i<sc;i++) a += i
+ et = timestampms()
+ } while(et == st)
+ # now, we have our cps metric
+ cps = 1000 * sc / (int(et) - int(st))
+ # but we need 1/60 second and also consider other operations
+ return int(cps / 60 - cf - 16)
+}
+
+# main code starts here
+
+BEGIN {
+ if(ARGC < 2) trapout("no ROM file specified!")
+ # preload the ROM - starting index is 0
+ PRG_FNAME = ARGV[1]
+ print "Loading", PRG_FNAME
+ PRG_LEN = loadbin(PRG_FNAME, PRG_ROM, 0, 1)
+ if(PRG_LEN < 1) trapout("could not read ROM!")
+ PRG_END_ADDR = 512 + PRG_LEN # all CHIP-8 ROMs start at 0x200 = 512
+ srand() # init the PRNG
+ KEY_INPUT_STREAM = "od -tu1 -w1 -An -N1 -v"
+ # tweak the per-frame performance here
+ clockFactor = int(CLOCK_FACTOR > 0 ? CLOCK_FACTOR : 20)
+ print "Profiling the frame timing..."
+ framecycle = hostprofile(clockFactor) # get the amount of host cycles to skip
+ printf "Detected %u cycles per frame\n", framecycle
+ # read the quirk flags from the filename and environment
+ EMU_QUIRK_LSQ = !!EMU_QUIRK_LSQ
+ EMU_QUIRK_STQ = !!EMU_QUIRK_STQ
+ EMU_QUIRK_VIP = !!EMU_QUIRK_VIP
+ EMU_QUIRK_JMP = !!EMU_QUIRK_JMP
+ EMU_QUIRK_CRY = !!EMU_QUIRK_CRY
+ if(PRG_FNAME ~ /\.sl\.ch8$/ || PRG_FNAME ~ /\.ls\.ch8$/) # check the extension
+ EMU_QUIRK_LSQ = EMU_QUIRK_STQ = 1 # both quirks on
+ else if(PRG_FNAME ~ /\.l\.ch8$/) EMU_QUIRK_LSQ = 1 # only LSQ on
+ else if(PRG_FNAME ~ /\.s\.ch8$/) EMU_QUIRK_STQ = 1 # only STQ on
+ qstatus = "|"
+ if(EMU_QUIRK_LSQ) qstatus = qstatus " LSQ"
+ if(EMU_QUIRK_STQ) qstatus = qstatus " STQ"
+ if(EMU_QUIRK_VIP) qstatus = qstatus " VIP"
+ if(EMU_QUIRK_JMP) qstatus = qstatus " JMP"
+ if(EMU_QUIRK_CRY) qstatus = qstatus " CRY"
+ # init main and statusbar color codes (from 1 to 7)
+ if(!PXL_COLOR || PXL_COLOR > 7) PXL_COLOR = 2 # green by default
+ if(!SBAR_COLOR || SBAR_COLOR > 7) SBAR_COLOR = 3 # yellow by default
+ if(!BG_COLOR || BG_COLOR > 7) BG_COLOR = 0 # black backgrounds by default
+ if(!SBAR_BG_COLOR || SBAR_BG_COLOR > 7) SBAR_BG_COLOR = 0
+ # init some string constants and parameters
+ SCR_CLR = sprintf("\033[2J")
+ SCR_PXL[0] = " " # empty space
+ SCR_PXL[1] = wctomb(9600) # Unicode upper-half block
+ SCR_PXL[2] = wctomb(9604) # Unicode lower-half block
+ SCR_PXL[3] = wctomb(9608) # Unicode rectangular block
+ HR = ""
+ for(i=0;i<64;i++) HR = HR "-"
+ SCR_SBAR = sprintf("\033[3%d;1;4%dmDALE-8A | %s %s\n" \
+ "%s\n\033[3%d;4%dm", SBAR_COLOR, SBAR_BG_COLOR, PRG_FNAME, \
+ qstatus, HR, PXL_COLOR, BG_COLOR)
+ SCR_SRESET = sprintf("\033[0m\033[0;0H")
+ # init CHR ROM - starting index is 1
+ split("240 144 144 144 240 32 96 32 32 112 240 16 240 128 240 240 16 " \
+ "240 16 240 144 144 240 16 16 240 128 240 16 240 240 128 240 144 " \
+ "240 240 16 32 64 64 240 144 240 144 240 240 144 240 16 240 240 " \
+ "144 240 144 144 224 144 224 144 224 240 128 128 128 240 224 144 " \
+ "144 144 224 240 128 240 128 240 240 128 240 128 128", CHR_ROM)
+ # init keyboard layout
+ split("120 49 50 51 113 119 101 97 115 100 122 99 52 114 102 118", kbdx)
+ for(i=1;i<=16;i++) KBD_LAYOUT[kbdx[i]] = i - 1
+ # init main registers, stack, RAM and screen - starting index for all is 0
+ for(i=0;i<4096;i++) {
+ if(i < 16) V[i] = inputState[i] = 0
+ if(i < 1792) stack[i] = 0 # also init call stack
+ if(i < 2048) screen[i] = 0 # screen is 2048 bytes long instead of bits
+ if(i>= 128 && i < 208) { # a byte from CHR ROM which is 80 bytes long
+ j = i - 127
+ ram[i] = int(CHR_ROM[j]) % 256
+ delete CHR_ROM[j]
+ }
+ else if(i>= 512 && i < PRG_END_ADDR) { # a byte from PRG ROM
+ j = i - 512
+ ram[i] = int(PRG_ROM[j]) % 256
+ delete PRG_ROM[j]
+ }
+ else ram[i] = 0 # everything else must be initialized to 0
+ }
+ # main execution logic starts here
+ altbufon() # enter the alternative screen buffer
+ setterm(3) # enter the non-blocking input mode before the event loop
+ pc = 512 # start at instruction 0x200
+ iReg = dtReg = stReg = skip = 0 # init I, DT and ST registers and skip flag
+ renderScheduled = 0 # only render the screen when necessary
+ b1 = b2 = d1 = d2 = d3 = d4 = nnn = sp = 0 # init different opcode parts
+ while(1) { # our event loop is here
+ for(i=0;i<clockFactor;i++) cpuLoop() # call main CPU loop CF times
+ if(renderScheduled) {
+ drawscreen() # render the current screen state
+ renderScheduled = 0
+ }
+ # timer register loops
+ if(dtReg > 0) dtReg--
+ if(stReg > 0) stReg--
+ # decrement input states
+ for(i=0;i<16;i++) if(inputState[i] > 0) inputState[i]--
+ # read and update current key states
+ readkeynb()
+ a=0
+ for(i=0;i<framecycle;i++) a+=i # sleep on 1/60 sec, more efficiently
+ }
+ shutdown() # restore the terminal state and exit
+}
diff --git a/dale8a.sh b/dale8a.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+AWKENGINE="mawk -W posix"
+#AWKENGINE="gawk -P"
+#AWKENGINE="busybox awk"
+ESC=$'\x1b'
+STTYSTATE="$(stty -g)" # save the stty-readable state
+trap cleanup 2 15 # trap Ctrl+C (SIGINT) and SIGTERM
+cleanup() { # restore the state of stty and screen
+ printf '%s' "${ESC}[?47l"
+ stty "$STTYSTATE"
+}
+LANG=C $AWKENGINE -f tgl.awk -f dale8a.awk -v CLOCK_FACTOR=20 -- $1 # run the emulator
+cleanup
diff --git a/testroms/corax-opcode.ch8 b/testroms/corax-opcode.ch8
Binary files differ.
diff --git a/testroms/ibm-logo.ch8 b/testroms/ibm-logo.ch8
Binary files differ.
diff --git a/testroms/keytest.ch8 b/testroms/keytest.ch8
Binary files differ.
diff --git a/tgl.awk b/tgl.awk
@@ -0,0 +1,231 @@
+# The Great Library of useful AWK functions
+# Fully POSIX-compatible but sometimes depends on other POSIX commands
+# Use with your programs like this:
+# LANG=C awk -f tgl.awk -f your_prog.awk [args]
+#
+# Current functionality:
+# * single character input: setterm, getchar
+# * ASCII and UTF-8 codepoint conversion: ord, wctomb, mbtowc
+# * loading binary files as decimal integers into arrays: loadbin
+# * saving binary files from arrays with decimal integers: savebin
+# * tangent and cotangent functions: tan, cotan
+# * signum, floor and ceiling functions: sign, floor, ceil
+# * test for native bitwise operation support: bw_native_support
+# * reimplementation of most bitwise operations (unsigned 32-bit):
+# - NOT: bw_compl
+# - AND: bw_and
+# - OR: bw_or
+# - XOR: bw_xor
+# - NAND: bw_nand
+# - NOR: bw_nor
+# - >>: bw_rshift
+# - <<: bw_lshift
+#
+# Created by Luxferre in 2023, released into public domain
+
+# set/restore the terminal input mode using stty
+# usage: setterm(0|1|2|3)
+# 0 - restore the original terminal input mode
+# 1 - blocking single-character input with echo
+# 2 - blocking single-character input without echo
+# 3 - non-blocking single-character input without echo
+# in pipes, this function doesn't do anything
+# (but returns 0 since it's not an error)
+# otherwise an actual stty exit code is returned
+function setterm(mode, cmd) {
+ if(system("stty >/dev/null 2>&1")) return 0 # exit code 0 means we're in a tty
+ if(!TGL_TERMMODE) { # cache the original terminal input mode
+ (cmd = "stty -g") | getline TGL_TERMMODE
+ close(cmd)
+ }
+ if(mode == 1) cmd = "-icanon"
+ else if(mode == 2) cmd = "-icanon -echo"
+ else if(mode == 3) cmd = "-icanon time 0 min 0 -echo"
+ else cmd = TGL_TERMMODE # restore the original mode
+ return system("stty " cmd ">/dev/null 2>&1") # execute the stty command
+}
+
+# getchar emulation using od
+# caches the read command for further usage
+# also able to capture null bytes, unlike read/printf approach
+# use in conjunction with setterm to achieve different input modes
+# setting LANG=C envvar is recommended, for GAWK it is required
+# usage: getchar() => integer
+function getchar(c) {
+ if(!TGL_GCH_CMD) TGL_GCH_CMD = "od -tu1 -w1 -N1 -An -v" # first time usage
+ TGL_GCH_CMD | getline c
+ close(TGL_GCH_CMD)
+ return int(c)
+}
+
+# get the ASCII code of a character
+# setting LANG=C envvar is recommended, for GAWK it is required
+# usage: ord(c) => integer
+function ord(c, b) {
+ # init char-to-ASCII mapping if it's not there yet
+ if(!TGL_ORD["#"]) for(b=0;b<256;b++) TGL_ORD[sprintf("%c", b)] = b
+ return int(TGL_ORD[c])
+}
+
+# encode a single integer UTF-8 codepoint into a byte sequence in a string
+# setting LANG=C envvar is recommended, for GAWK it is required
+# usage: wctomb(code) => string
+# we can safely use the string type for all codepoints above 0 as all
+# multibyte sequences have a high bit set, so no null byte is there
+# for invalid codepoints, an empty string will be returned
+function wctomb(code, s) {
+ code = int(code)
+ if(code < 0 || code > 1114109) s = "" # invalid codepoint
+ else if(code < 128) s = sprintf("%c", code) # single byte
+ else if(code < 2048) # 2-byte sequence
+ s = sprintf("%c%c", \
+ 192 + (int(code/64) % 32), \
+ 128 + (code % 64))
+ else if(code < 65536) # 3-byte sequence
+ s = sprintf("%c%c%c", \
+ 224 + (int(code/4096) % 16), \
+ 128 + (int(code/64) % 64), \
+ 128 + (code % 64))
+ else # 4-byte sequence
+ s = sprintf("%c%c%c%c", \
+ 240 + (int(code/262144) % 8), \
+ 128 + (int(code/4096) % 64), \
+ 128 + (int(code/64) % 64), \
+ 128 + (code % 64))
+ return s
+}
+
+# decode a byte string into a UTF-8 codepoint
+# setting LANG=C envvar is recommended, for GAWK it is required
+# usage: mbtowc(s) => integer
+# decoding stops on the first encountered invalid byte
+function mbtowc(s, len, code, b, pos) {
+ len = length(s)
+ code = 0
+ for(pos=1;pos<=len;pos++) {
+ code *= 64 # shift the code 6 bits left
+ b = ord(substr(s, pos, 1))
+ if(pos == 1) { # expect a single or header byte
+ if(b < 128) {code = b; break} # it resolves into a single byte
+ else if(b >= 192 && b < 224) # it's a header byte of 2-byte sequence
+ code += b % 32
+ else if(b >= 224 && b < 240) # it's a header byte of 3-byte sequence
+ code += b % 16
+ else if(b >= 240) # it's a header byte of 4-byte sequence
+ code += b % 8
+ else break # a trailer byte in the header position is invalid
+ }
+ else if(b >= 128 && b < 192) # it must be a trailer byte
+ code += b % 64
+ else break # a header byte in the trailer position is invalid
+ }
+ return code
+}
+
+# load any binary file into an AWK array (0-indexed), depends on od
+# returns the resulting array length
+# usage: loadbin(fname, arr, len, wordsize) => integer
+# len parameter is optional, specifies how many bytes to read
+# (if 0 or unset, read everything)
+# wordsize parameter is optional, 1 byte by default
+# multibyte words are considered little-endian
+function loadbin(fname, arr, len, wordsize, cmd, i) {
+ wordsize = int(wordsize)
+ if(wordsize < 1) wordsize = 1
+ len = int(len)
+ i = (len > 0) ? (" -N" len " ") : ""
+ cmd = "od -tu" wordsize " -An -w" wordsize i " -v \"" fname "\""
+ # every line should be a single decimal integer (with some whitespace)
+ i = 0
+ while((cmd | getline) > 0) # read the next line from the stream
+ if(NF) arr[i++] = int($1) # read the first and only field
+ close(cmd) # close the od process
+ return i
+}
+
+# save an AWK array (0-indexed) into a binary file
+# setting LANG=C envvar is recommended, for GAWK it is required
+# returns the amount of written elements
+# usage: savebin(fname, arr, len, wordsize) => integer
+# wordsize parameter is optional, 1 byte by default
+# multibyte words are considered little-endian
+function savebin(fname, arr, len, wordsize, i, j) {
+ wordsize = int(wordsize)
+ if(wordsize < 1) wordsize = 1
+ printf("") > fname # truncate the file and open the stream
+ for(i=0;i<len;i++) {
+ if(wordsize == 1) printf("%c", arr[i]) >> fname
+ else # we have a multibyte word size
+ for(j=0;j<wordsize;j++)
+ printf("%c", int(arr[i]/2^(8*j))%256) >> fname
+ }
+ close(fname) # close the output file
+ return i
+}
+
+# the missing tangent/cotangent functions
+
+function tan(x) {return sin(x)/cos(x)}
+function cotan(x) {return cos(x)/sin(x)}
+
+# the missing sign/floor/ceil functions
+
+function sign(x) {return x < 0 ? -1 : !!x}
+function floor(x, f) {
+ f = int(x)
+ if(x == f) return x
+ else return x >= 0 ? f : (f - 1)
+}
+function ceil(x, f) {
+ f = int(x)
+ if(x == f) return x
+ else return x >= 0 ? (f + 1) : f
+}
+
+# Bitwise operations section
+
+# test if the AWK engine has non-POSIX bitwise operation functions
+# (and, or, xor, compl, lshift, rshift) implemented natively:
+# if compl is missing, it will be concatenated with 1 and equal to 1
+# so the inverse of this condition will be the result
+function bw_native_support() {return (compl (1) != 1)}
+
+# now, the implementation of the operations themselves
+# note that all complements are 32-bit and all operands must be non-negative
+
+function bw_compl(a) {return 4294967295 - int(a)}
+function bw_lshift(a, b) {for(;b>0;b--) a = int(a/2);return a}
+function bw_rshift(a, b) {for(;b>0;b--) a *= 2;return int(a)}
+function bw_and(a, b, v, r) {
+ v = 1; r = 0
+ while(a > 0 || b > 0) {
+ if((a%2) == 1 && (b%2) == 1) r += v
+ a = int(a/2)
+ b = int(b/2)
+ v *= 2
+ }
+ return int(r)
+}
+function bw_or(a, b, v, r) {
+ v = 1; r = 0
+ while(a > 0 || b > 0) {
+ if((a%2) == 1 || (b%2) == 1) r += v
+ a = int(a/2)
+ b = int(b/2)
+ v *= 2
+ }
+ return int(r)
+}
+function bw_xor(a, b, v, r) {
+ v = 1; r = 0
+ while(a > 0 || b > 0) {
+ if((a%2) != (b%2)) r += v
+ a = int(a/2)
+ b = int(b/2)
+ v *= 2
+ }
+ return int(r)
+}
+function bw_nand(a, b) {return bw_compl(bw_and(a,b))}
+function bw_nor(a, b) {return bw_compl(bw_or(a,b))}
+