initial upload

This commit is contained in:
Luxferre
2023-05-11 17:31:52 +03:00
commit 1dbe229529
8 changed files with 749 additions and 0 deletions
+26
View File
@@ -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>
+126
View File
@@ -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.
+353
View File
@@ -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
}
+13
View File
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
+231
View File
@@ -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))}