Initial commit
This commit is contained in:
3
.clang-format
Normal file
3
.clang-format
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
BasedOnStyle: LLVM
|
||||||
|
IndentWidth: 4
|
||||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Set the default behavior for all files.
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Normalized and converts to native line endings on checkout.
|
||||||
|
*.c text
|
||||||
|
*.cc text
|
||||||
|
*.cxx
|
||||||
|
*.cpp text
|
||||||
|
*.h text
|
||||||
|
*.hxx text
|
||||||
|
*.hpp text
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
obj/
|
||||||
|
bin/
|
||||||
|
ram.bin
|
||||||
22
LICENSE
Normal file
22
LICENSE
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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.
|
||||||
24
Makefile
Normal file
24
Makefile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
CXX := g++
|
||||||
|
CXXFLAGS := -Wall -Wextra -std=c++23 -g -Werror -Ilibs `sdl2-config --cflags`
|
||||||
|
LDFLAGS := `sdl2-config --libs`
|
||||||
|
|
||||||
|
BIN_DIR := bin
|
||||||
|
|
||||||
|
all: voidEmu disassembler
|
||||||
|
|
||||||
|
run: all
|
||||||
|
./bin/voidEmu $(FILE)
|
||||||
|
|
||||||
|
disassembler: $(wildcard disassembler/*.cpp) | $(BIN_DIR)
|
||||||
|
$(CXX) $(CXXFLAGS) $^ -o ${BIN_DIR}/$@
|
||||||
|
|
||||||
|
voidEmu: $(wildcard src/*.cpp) | $(BIN_DIR)
|
||||||
|
$(CXX) $(CXXFLAGS) $^ -o ${BIN_DIR}/$@ $(LDFLAGS)
|
||||||
|
|
||||||
|
$(BIN_DIR) $(OBJ_DIR):
|
||||||
|
mkdir -p $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(OBJ_DIR) $(BIN_DIR)
|
||||||
|
|
||||||
|
.PHONY: all clean run
|
||||||
17
README.md
Normal file
17
README.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# VoidEmu
|
||||||
|
|
||||||
|
Current state: This project is a Chip8 emulator implemented according to [Cowgod's Chip-8 Technical Reference](http://devernay.free.fr/hacks/chip8/C8TECH10.HTM#2.2).
|
||||||
|
|
||||||
|
This is a simple emulator for the Game Boy. It is written in C++ and uses SDL for rendering.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
I wanted to learn how to use C++ and SDL, and I recently saw a youtube video listing out reasons for writing a Game Boy emulator. The name is VoidEmu, because I gurantee I will be staring into the void trying to make this thing work.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
* [Cowgod's Chip-8 Technical Reference](http://devernay.free.fr/hacks/chip8/C8TECH10.HTM)
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
This is free and unencumbered software released into the public domain, much to the detriment of my "heirs and successors"
|
||||||
3
disassembler/.clangd
Normal file
3
disassembler/.clangd
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
CompileFlags:
|
||||||
|
Add:
|
||||||
|
- -I../lib
|
||||||
8
disassembler/main.cpp
Normal file
8
disassembler/main.cpp
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#include "reader.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
printf("Hello World!\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
389
libs/reader.hpp
Normal file
389
libs/reader.hpp
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
#include <cassert>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
struct byte_reg {
|
||||||
|
uint8_t reg;
|
||||||
|
uint16_t byte;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct reg_reg {
|
||||||
|
uint8_t x;
|
||||||
|
uint8_t y;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct reg_reg_nibble {
|
||||||
|
uint8_t x;
|
||||||
|
uint8_t y;
|
||||||
|
uint8_t nibble;
|
||||||
|
};
|
||||||
|
|
||||||
|
// this definitely wont be confusing or difficult to implement at all, I am
|
||||||
|
// perfect at writing good code and documentation and I would never write
|
||||||
|
// something that is stupidly thought out or overly complicated
|
||||||
|
enum instruction {
|
||||||
|
// operand is a pointer to a uint8_t
|
||||||
|
EXIT = 0,
|
||||||
|
// operand is a pointer to a uint16_t
|
||||||
|
SYS,
|
||||||
|
// no operand
|
||||||
|
CLS,
|
||||||
|
// no operand
|
||||||
|
RET,
|
||||||
|
// operand is a pointer to a uint16_t
|
||||||
|
JP,
|
||||||
|
// operand is a pointer to a uint16_t
|
||||||
|
CALL,
|
||||||
|
// operand is a pointer to byte_reg
|
||||||
|
SKIP_INSTRUCTION_BYTE,
|
||||||
|
// operand is a pointer to byte_reg
|
||||||
|
SKIP_INSTRUCTION_NE_BYTE,
|
||||||
|
// operand is a pointer to reg_reg
|
||||||
|
SKIP_INSTRUCTION_REG,
|
||||||
|
// operand is a pointer to reg_byte
|
||||||
|
LOAD_BYTE,
|
||||||
|
// operand is a pointer to reg_byte
|
||||||
|
ADD_BYTE,
|
||||||
|
// operand is a pointer to reg_reg
|
||||||
|
LOAD_REG,
|
||||||
|
// operand is a pointer to reg_reg
|
||||||
|
OR_REG,
|
||||||
|
// operand is a pointer to reg_reg
|
||||||
|
AND_REG,
|
||||||
|
// operand is a pointer to reg_reg
|
||||||
|
XOR_REG,
|
||||||
|
// operand is a pointer to reg_reg
|
||||||
|
ADD_REG,
|
||||||
|
// operand is a pointer to reg_reg
|
||||||
|
SUB_REG,
|
||||||
|
// operand is a pointer to reg_reg
|
||||||
|
SHR_REG,
|
||||||
|
// operand is a pointer to reg_reg
|
||||||
|
SUBN_REG,
|
||||||
|
// operand is a pointer to reg_reg
|
||||||
|
SHL_REG,
|
||||||
|
// operand is a pointer to reg_reg
|
||||||
|
SKIP_INSTRUCTION_NE_REG,
|
||||||
|
// operand is a pointer to a uint16_t
|
||||||
|
LOAD_I_BYTE,
|
||||||
|
// operand is a pointer to a uint16_t
|
||||||
|
JP_V0_BYTE,
|
||||||
|
// operand is a pointer to a reg_byte
|
||||||
|
RND,
|
||||||
|
// operand is a pointer to a reg_reg_nibble
|
||||||
|
DRW,
|
||||||
|
// operand is a pointer to a uint8_t
|
||||||
|
SKIP_PRESSED_REG,
|
||||||
|
// operand is a pointer to a uint8_t
|
||||||
|
SKIP_NOT_PRESSED_REG,
|
||||||
|
// operand is a pointer to a uint8_t
|
||||||
|
LD_REG_DT,
|
||||||
|
// operand is a pointer to a uint8_t
|
||||||
|
LD_REG_K,
|
||||||
|
// operand is a pointer to a uint8_t
|
||||||
|
LD_DT_REG,
|
||||||
|
// operand is a pointer to a uint8_t
|
||||||
|
LD_ST_REG,
|
||||||
|
// operand is a pointer to a uint8_t
|
||||||
|
ADD_I_REG,
|
||||||
|
// operand is a pointer to a uint8_t
|
||||||
|
LD_F_REG,
|
||||||
|
// operand is a pointer to a uint8_t
|
||||||
|
LD_B_REG,
|
||||||
|
// operand is a pointer to a uint8_t
|
||||||
|
LD_PTR_I_REG,
|
||||||
|
// operand is a pointer to a uint8_t
|
||||||
|
LD_REG_PTR_I,
|
||||||
|
UNKNOWN_INSTRUCTION,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Bytecode {
|
||||||
|
public:
|
||||||
|
enum instruction instruction_type;
|
||||||
|
// should be interpreted by a reader depending on the instruction type
|
||||||
|
union {
|
||||||
|
uint8_t byte;
|
||||||
|
uint16_t word;
|
||||||
|
struct byte_reg byte_reg;
|
||||||
|
struct reg_reg reg_reg;
|
||||||
|
struct reg_reg_nibble reg_reg_nibble;
|
||||||
|
} operand;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline Bytecode parse(uint16_t opcode) {
|
||||||
|
struct Bytecode bytecode;
|
||||||
|
|
||||||
|
switch (opcode & 0xF000) {
|
||||||
|
case 0x0000: {
|
||||||
|
if ((opcode & 0x00F0) == 0x0010) {
|
||||||
|
// EXIT N 0x001N
|
||||||
|
// Specific to emulators, not part of the original chip-8
|
||||||
|
bytecode.instruction_type = EXIT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (opcode & 0x00FF) {
|
||||||
|
case 0x00E0: {
|
||||||
|
// CLS 0x00E0
|
||||||
|
// clears the screen
|
||||||
|
bytecode.instruction_type = CLS;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x00EE: {
|
||||||
|
// RET 0x00EE
|
||||||
|
bytecode.instruction_type = RET;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// SYS NNN
|
||||||
|
//? NOTE: This is an outdated opcode, but it's still
|
||||||
|
//? important for completeness. It's not clear what the
|
||||||
|
//? difference is between it and the JMP NNN 0x1NNN opcode.
|
||||||
|
bytecode.instruction_type = SYS;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x1000: {
|
||||||
|
bytecode.instruction_type = JP;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x2000: {
|
||||||
|
bytecode.instruction_type = CALL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x3000: {
|
||||||
|
bytecode.instruction_type = SKIP_INSTRUCTION_BYTE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x4000: {
|
||||||
|
bytecode.instruction_type = SKIP_INSTRUCTION_NE_BYTE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x5000: {
|
||||||
|
bytecode.instruction_type = SKIP_INSTRUCTION_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x6000: {
|
||||||
|
bytecode.instruction_type = LOAD_BYTE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x700: {
|
||||||
|
bytecode.instruction_type = ADD_BYTE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x8000: {
|
||||||
|
switch (opcode & 0x000F) {
|
||||||
|
case 0x0000: {
|
||||||
|
bytecode.instruction_type = LOAD_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x0001: {
|
||||||
|
bytecode.instruction_type = OR_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x0002: {
|
||||||
|
bytecode.instruction_type = AND_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x0003: {
|
||||||
|
bytecode.instruction_type = XOR_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x0004: {
|
||||||
|
bytecode.instruction_type = ADD_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x0005: {
|
||||||
|
bytecode.instruction_type = SUB_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x0006: {
|
||||||
|
// Set VX equal to VX bitshifted right 1. VF is set to the least
|
||||||
|
// significant bit of VX prior to the shift. Originally this opcode
|
||||||
|
// meant set VX equal to VY bitshifted right 1 but emulators and
|
||||||
|
// software seem to ignore VY now. Note: This instruction was
|
||||||
|
// originally undocumented but functional due to how the 8XXX
|
||||||
|
// instructions were implemented on teh COSMAC VIP.
|
||||||
|
bytecode.instruction_type = SHR_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x0007: {
|
||||||
|
bytecode.instruction_type = SUBN_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x000E: {
|
||||||
|
bytecode.instruction_type = SHL_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
bytecode.instruction_type = UNKNOWN_INSTRUCTION;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x9000: {
|
||||||
|
bytecode.instruction_type = SKIP_INSTRUCTION_NE_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0xA000: {
|
||||||
|
bytecode.instruction_type = LOAD_I_BYTE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0xB000: {
|
||||||
|
bytecode.instruction_type = JP_V0_BYTE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0xC000: {
|
||||||
|
bytecode.instruction_type = RND;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0xD000: {
|
||||||
|
bytecode.instruction_type = DRW;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0xE000: {
|
||||||
|
switch (opcode & 0x00FF) {
|
||||||
|
case 0x009E: {
|
||||||
|
bytecode.instruction_type = SKIP_PRESSED_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x00A1: {
|
||||||
|
bytecode.instruction_type = SKIP_NOT_PRESSED_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
bytecode.instruction_type = UNKNOWN_INSTRUCTION;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0xF000: {
|
||||||
|
switch (opcode & 0x00FF) {
|
||||||
|
case 0x0007: {
|
||||||
|
bytecode.instruction_type = LD_REG_DT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x000A: {
|
||||||
|
bytecode.instruction_type = LD_REG_K;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x0015: {
|
||||||
|
bytecode.instruction_type = LD_DT_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x0018: {
|
||||||
|
bytecode.instruction_type = LD_ST_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x001E: {
|
||||||
|
bytecode.instruction_type = ADD_I_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x0029: {
|
||||||
|
bytecode.instruction_type = LD_F_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x0033: {
|
||||||
|
bytecode.instruction_type = LD_B_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x0055: {
|
||||||
|
bytecode.instruction_type = LD_PTR_I_REG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x0065: {
|
||||||
|
bytecode.instruction_type = LD_REG_PTR_I;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
bytecode.instruction_type = UNKNOWN_INSTRUCTION;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
bytecode.instruction_type = UNKNOWN_INSTRUCTION;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (bytecode.instruction_type) {
|
||||||
|
case UNKNOWN_INSTRUCTION:
|
||||||
|
case RET:
|
||||||
|
case CLS: {
|
||||||
|
// no operand
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EXIT: {
|
||||||
|
bytecode.operand.byte = opcode & 0x000F;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SKIP_PRESSED_REG:
|
||||||
|
case SKIP_NOT_PRESSED_REG:
|
||||||
|
case LD_REG_DT:
|
||||||
|
case LD_REG_K:
|
||||||
|
case LD_DT_REG:
|
||||||
|
case LD_ST_REG:
|
||||||
|
case ADD_I_REG:
|
||||||
|
case LD_F_REG:
|
||||||
|
case LD_B_REG:
|
||||||
|
case LD_PTR_I_REG:
|
||||||
|
case LD_REG_PTR_I: {
|
||||||
|
bytecode.operand.byte = (uint8_t)((opcode & 0x0F00) >> 8);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SYS:
|
||||||
|
case JP:
|
||||||
|
case CALL:
|
||||||
|
case LOAD_I_BYTE:
|
||||||
|
case JP_V0_BYTE: {
|
||||||
|
bytecode.operand.word = (uint16_t)(opcode & 0x0FFF);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SKIP_INSTRUCTION_BYTE:
|
||||||
|
case SKIP_INSTRUCTION_NE_BYTE:
|
||||||
|
case LOAD_BYTE:
|
||||||
|
case ADD_BYTE:
|
||||||
|
case RND: {
|
||||||
|
bytecode.operand.byte_reg = (struct byte_reg){
|
||||||
|
.reg = (uint8_t)((opcode & 0x0F00) >> 8),
|
||||||
|
.byte = (uint16_t)(opcode & 0x00FF),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SKIP_INSTRUCTION_REG:
|
||||||
|
case LOAD_REG:
|
||||||
|
case OR_REG:
|
||||||
|
case AND_REG:
|
||||||
|
case XOR_REG:
|
||||||
|
case ADD_REG:
|
||||||
|
case SUB_REG:
|
||||||
|
case SHR_REG:
|
||||||
|
case SUBN_REG:
|
||||||
|
case SHL_REG:
|
||||||
|
case SKIP_INSTRUCTION_NE_REG: {
|
||||||
|
bytecode.operand.reg_reg = (struct reg_reg){
|
||||||
|
.x = (uint8_t)((opcode & 0x0F00) >> 8),
|
||||||
|
.y = (uint8_t)((opcode & 0x00F0) >> 4),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case DRW: {
|
||||||
|
bytecode.operand.reg_reg_nibble = (struct reg_reg_nibble){
|
||||||
|
.x = (uint8_t)((opcode & 0x0F00) >> 8),
|
||||||
|
.y = (uint8_t)((opcode & 0x00F0) >> 4),
|
||||||
|
.nibble = (uint8_t)(opcode & 0x000F),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytecode;
|
||||||
|
}
|
||||||
4
src/.clangd
Normal file
4
src/.clangd
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
CompileFlags:
|
||||||
|
Add:
|
||||||
|
- -I../libs
|
||||||
|
- -ISDL2
|
||||||
656
src/main.cpp
Normal file
656
src/main.cpp
Normal file
@@ -0,0 +1,656 @@
|
|||||||
|
#include <SDL2/SDL.h>
|
||||||
|
#include <cstring>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
#define BYTECODE_READER_IMPLEMENTATION
|
||||||
|
#include "reader.hpp"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <chrono>
|
||||||
|
#include <csignal>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <thread>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
const int SCREEN_WIDTH = 64;
|
||||||
|
const int SCREEN_HEIGHT = 32;
|
||||||
|
const int SCALE = 5;
|
||||||
|
static size_t RAM_SIZE = 0x1000;
|
||||||
|
const int TARGET_CYCLES_PER_SECOND = 500;
|
||||||
|
const int TARGET_MS_PER_CYCLE = 1000 / TARGET_CYCLES_PER_SECOND;
|
||||||
|
const int TARGET_MS_PER_FRAME = 1000 / 60;
|
||||||
|
const int BG_COLOR = 0x081820;
|
||||||
|
const int FG_COLOR = 0x88c070;
|
||||||
|
|
||||||
|
void draw(SDL_Renderer *renderer, SDL_Texture *texture,
|
||||||
|
bool framebuffer[SCREEN_HEIGHT][SCREEN_WIDTH]) {
|
||||||
|
|
||||||
|
printf("Drawing...\n");
|
||||||
|
|
||||||
|
uint32_t pixels[SCREEN_WIDTH * SCREEN_HEIGHT];
|
||||||
|
for (int i = 0; i < SCREEN_HEIGHT; i++) {
|
||||||
|
for (int j = 0; j < SCREEN_WIDTH; j++) {
|
||||||
|
pixels[i * SCREEN_WIDTH + j] =
|
||||||
|
framebuffer[i][j] ? FG_COLOR : BG_COLOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_UpdateTexture(texture, nullptr, pixels,
|
||||||
|
SCREEN_WIDTH * sizeof(uint32_t));
|
||||||
|
SDL_RenderClear(renderer);
|
||||||
|
SDL_RenderCopy(renderer, texture, nullptr, nullptr);
|
||||||
|
SDL_RenderPresent(renderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t FONT[0x10][0x05] = {
|
||||||
|
{0xF0, 0x90, 0x90, 0x90, 0xF0}, // 0
|
||||||
|
{0x20, 0x60, 0x20, 0x20, 0x70}, // 1
|
||||||
|
{0xF0, 0x10, 0xF0, 0x80, 0xF0}, // 2
|
||||||
|
{0xF0, 0x10, 0xF0, 0x10, 0xF0}, // 3
|
||||||
|
{0x90, 0x90, 0xF0, 0x10, 0x10}, // 4
|
||||||
|
{0xF0, 0x80, 0xF0, 0x10, 0xF0}, // 5
|
||||||
|
{0xF0, 0x80, 0xF0, 0x90, 0xF0}, // 6
|
||||||
|
{0xF0, 0x10, 0x20, 0x40, 0x40}, // 7
|
||||||
|
{0xF0, 0x90, 0xF0, 0x90, 0xF0}, // 8
|
||||||
|
{0xF0, 0x90, 0xF0, 0x10, 0xF0}, // 9
|
||||||
|
{0xF0, 0x90, 0xF0, 0x90, 0x90}, // A
|
||||||
|
{0xE0, 0x90, 0xE0, 0x90, 0xE0}, // B
|
||||||
|
{0xF0, 0x80, 0x80, 0x80, 0xF0}, // C
|
||||||
|
{0xE0, 0x90, 0x90, 0x90, 0xE0}, // D
|
||||||
|
{0xF0, 0x80, 0xF0, 0x80, 0xF0}, // E
|
||||||
|
{0xF0, 0x80, 0xF0, 0x80, 0x80}, // F
|
||||||
|
};
|
||||||
|
|
||||||
|
class Chip8 {
|
||||||
|
public:
|
||||||
|
Chip8(char *rom_path) {
|
||||||
|
int rom_fd = open(rom_path, O_RDONLY);
|
||||||
|
if (rom_fd < 0) {
|
||||||
|
printf("Failed to open file: %s\n", rom_path);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pc = 0x200;
|
||||||
|
ram = (uint8_t *)malloc(RAM_SIZE);
|
||||||
|
if (ram == NULL) {
|
||||||
|
printf("Failed to allocate ram!");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(ram, FONT, sizeof(FONT));
|
||||||
|
|
||||||
|
int file_size = lseek(rom_fd, 0, SEEK_END);
|
||||||
|
(void)lseek(rom_fd, 0, SEEK_SET);
|
||||||
|
|
||||||
|
printf("Reading file: %s for %d bytes\n", rom_path, file_size);
|
||||||
|
|
||||||
|
int err = read(rom_fd, ram + 0x200, file_size);
|
||||||
|
if (err < 0) {
|
||||||
|
printf("Failed to read file: %s\n", rom_path);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err != file_size) {
|
||||||
|
printf("Failed to read file: %s\n", rom_path);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(rom_fd);
|
||||||
|
|
||||||
|
stack = (uint16_t *)malloc(sizeof(uint16_t) * 16);
|
||||||
|
if (stack == NULL) {
|
||||||
|
printf("Failed to allocate stack!");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~Chip8() {
|
||||||
|
free(ram);
|
||||||
|
free(stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
int run();
|
||||||
|
void view_ram();
|
||||||
|
void dump_ram();
|
||||||
|
|
||||||
|
int is_protected(size_t addr) { return addr < 0x200; }
|
||||||
|
|
||||||
|
int read_mem(size_t addr) {
|
||||||
|
if (is_protected(addr)) {
|
||||||
|
printf("Attempted to read from protected address: 0x%04x\n",
|
||||||
|
(unsigned int)addr);
|
||||||
|
dump_ram();
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
return this->ram[addr];
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_mem(size_t addr, uint8_t val) {
|
||||||
|
if (is_protected(addr)) {
|
||||||
|
printf("Attempted to write to protected address: 0x%04x\n",
|
||||||
|
(unsigned int)addr);
|
||||||
|
dump_ram();
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
this->ram[addr] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_sound_timer(uint8_t val) {
|
||||||
|
// enable buzzer
|
||||||
|
this->sound_timer = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_pixel(int x, int y, uint8_t val) {
|
||||||
|
assert(x >= 0 && x < SCREEN_WIDTH);
|
||||||
|
assert(y >= 0 && y < SCREEN_HEIGHT);
|
||||||
|
this->fb[y][x] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t get_pixel(int x, int y) {
|
||||||
|
assert(x >= 0 && x < SCREEN_WIDTH);
|
||||||
|
assert(y >= 0 && y < SCREEN_HEIGHT);
|
||||||
|
return this->fb[y][x];
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint8_t *ram;
|
||||||
|
bool fb[SCREEN_HEIGHT][SCREEN_WIDTH];
|
||||||
|
uint16_t pc;
|
||||||
|
uint16_t *stack;
|
||||||
|
uint8_t sp;
|
||||||
|
uint8_t v[16];
|
||||||
|
uint16_t i;
|
||||||
|
uint8_t delay;
|
||||||
|
uint8_t sound_timer;
|
||||||
|
bool compat;
|
||||||
|
};
|
||||||
|
|
||||||
|
int Chip8::run() {
|
||||||
|
using namespace std::chrono;
|
||||||
|
|
||||||
|
int exit = 0;
|
||||||
|
constexpr auto cycle_time = milliseconds(TARGET_MS_PER_CYCLE);
|
||||||
|
constexpr auto timer_interval = milliseconds(TARGET_MS_PER_FRAME);
|
||||||
|
auto last_timer_update = high_resolution_clock::now();
|
||||||
|
|
||||||
|
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
|
||||||
|
printf("Failed to initialize SDL: %s\n", SDL_GetError());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Window *window = SDL_CreateWindow(
|
||||||
|
"CHIP-8 Emulator", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||||
|
SCREEN_WIDTH * SCALE, SCREEN_HEIGHT * SCALE, SDL_WINDOW_SHOWN);
|
||||||
|
SDL_Renderer *renderer =
|
||||||
|
SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
|
||||||
|
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888,
|
||||||
|
SDL_TEXTUREACCESS_STREAMING,
|
||||||
|
SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||||
|
|
||||||
|
bool running = true;
|
||||||
|
SDL_Event event;
|
||||||
|
|
||||||
|
while (running) {
|
||||||
|
while (SDL_PollEvent(&event)) {
|
||||||
|
if (event.type == SDL_QUIT) {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto start_time = high_resolution_clock::now();
|
||||||
|
|
||||||
|
uint16_t op = (read_mem(pc) << 8) | read_mem(pc + 1);
|
||||||
|
pc += 2;
|
||||||
|
printf("PC: 0x%04x OP: 0x%04x\n", pc, op);
|
||||||
|
Bytecode bytecode = parse(op);
|
||||||
|
|
||||||
|
printf("OPCODE: 0x%04x INSTRUCTION_TYPE: %d\n", op,
|
||||||
|
bytecode.instruction_type);
|
||||||
|
|
||||||
|
switch (bytecode.instruction_type) {
|
||||||
|
case EXIT: {
|
||||||
|
// From Peter Miller's chip8run. Exit emulator with a return value
|
||||||
|
// of N.
|
||||||
|
exit = bytecode.operand.byte;
|
||||||
|
running = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SYS: {
|
||||||
|
// Jump to a machine code routine at nnn.
|
||||||
|
// This instruction is only used on the old computers on which
|
||||||
|
// Chip-8 was originally implemented. It is ignored by modern
|
||||||
|
// interpreters.
|
||||||
|
uint16_t addr = bytecode.operand.word & 0x0FFF;
|
||||||
|
assert(addr < RAM_SIZE && addr % 0x2 == 0);
|
||||||
|
|
||||||
|
pc = bytecode.operand.word & 0x0FFF;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CLS: {
|
||||||
|
// Clear the screen.
|
||||||
|
// fprintf(stderr, "CLS not implemented\n");
|
||||||
|
memset(this->fb, 0, SCREEN_WIDTH * SCREEN_HEIGHT);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case RET: {
|
||||||
|
// Return from a subroutine.
|
||||||
|
// The interpreter sets the program counter to the address at the
|
||||||
|
// top of the stack, then subtracts 1 from the stack pointer.
|
||||||
|
|
||||||
|
pc = stack[--sp];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case JP: {
|
||||||
|
// Jump to location nnn.
|
||||||
|
// The interpreter sets the program counter to nnn.
|
||||||
|
|
||||||
|
uint16_t addr = bytecode.operand.word & 0x0FFF;
|
||||||
|
assert(addr < RAM_SIZE && addr % 0x2 == 0);
|
||||||
|
|
||||||
|
pc = bytecode.operand.word & 0x0FFF;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CALL: {
|
||||||
|
// Call subroutine at nnn.
|
||||||
|
// The interpreter increments the stack pointer, then puts the
|
||||||
|
// current PC on the top of the stack. The PC is then set to nnn.
|
||||||
|
|
||||||
|
stack[++sp] = pc;
|
||||||
|
pc = bytecode.operand.word & 0x0FFF;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SKIP_INSTRUCTION_BYTE: {
|
||||||
|
// Skip next instruction if Vx = kk.
|
||||||
|
// The interpreter compares register Vx to kk, and if they are
|
||||||
|
// equal, increments the program counter by 2.
|
||||||
|
if (this->v[bytecode.operand.byte_reg.reg] ==
|
||||||
|
bytecode.operand.byte_reg.byte) {
|
||||||
|
pc += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SKIP_INSTRUCTION_NE_BYTE: {
|
||||||
|
// Skip next instruction if Vx != kk.
|
||||||
|
// The interpreter compares register Vx to kk, and if they are not
|
||||||
|
// equal, increments the program counter by 2.
|
||||||
|
if (this->v[bytecode.operand.byte_reg.reg] !=
|
||||||
|
bytecode.operand.byte_reg.byte) {
|
||||||
|
pc += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SKIP_INSTRUCTION_REG: {
|
||||||
|
// Skip next instruction if Vx = Vy.
|
||||||
|
// The interpreter compares register Vx to register Vy, and if they
|
||||||
|
// are equal, increments the program counter by 2.
|
||||||
|
if (this->v[bytecode.operand.reg_reg.x] ==
|
||||||
|
this->v[bytecode.operand.reg_reg.y]) {
|
||||||
|
pc += 2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SKIP_INSTRUCTION_NE_REG: {
|
||||||
|
// Skip next instruction if Vx != Vy.
|
||||||
|
// The values of Vx and Vy are compared, and if they are not equal,
|
||||||
|
// the program counter is increased by 2.
|
||||||
|
if (this->v[bytecode.operand.reg_reg.x] !=
|
||||||
|
this->v[bytecode.operand.reg_reg.y]) {
|
||||||
|
pc += 2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LOAD_BYTE: {
|
||||||
|
// Set Vx = kk.
|
||||||
|
// The interpreter puts the value kk into register Vx.
|
||||||
|
this->v[bytecode.operand.byte_reg.reg] =
|
||||||
|
bytecode.operand.byte_reg.byte;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ADD_BYTE: {
|
||||||
|
// Set Vx = Vx + kk.
|
||||||
|
// Adds the value kk to the value of register Vx, then stores the
|
||||||
|
// result in Vx.
|
||||||
|
this->v[bytecode.operand.byte_reg.reg] +=
|
||||||
|
bytecode.operand.byte_reg.byte;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LOAD_REG: {
|
||||||
|
// Set Vx = Vy.
|
||||||
|
// Stores the value of register Vy in register Vx.
|
||||||
|
this->v[bytecode.operand.reg_reg.x] =
|
||||||
|
this->v[bytecode.operand.reg_reg.y];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ADD_REG: {
|
||||||
|
// Set Vx = Vx + Vy, set VF = carry.
|
||||||
|
// The values of Vx and Vy are added together. If the result is
|
||||||
|
// greater than 8 bits (i.e., > 255,) VF is set to 1, otherwise 0.
|
||||||
|
// Only the lowest 8 bits of the result are kept, and stored in Vx.
|
||||||
|
int result = this->v[bytecode.operand.reg_reg.x] +
|
||||||
|
this->v[bytecode.operand.reg_reg.y];
|
||||||
|
this->v[bytecode.operand.reg_reg.x] = result & 0xFF;
|
||||||
|
this->v[0xF] = result > 0xFF;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SUB_REG: {
|
||||||
|
// Set Vx = Vx - Vy, set VF = NOT borrow.
|
||||||
|
// If Vx > Vy, then VF is set to 1, otherwise 0. Then Vy is
|
||||||
|
// subtracted from Vx, and the results stored in Vx.
|
||||||
|
int result = this->v[bytecode.operand.reg_reg.x] -
|
||||||
|
this->v[bytecode.operand.reg_reg.y];
|
||||||
|
this->v[bytecode.operand.reg_reg.x] = result & 0xFF;
|
||||||
|
this->v[0xF] = result < 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OR_REG: {
|
||||||
|
// Set Vx = Vx OR Vy.
|
||||||
|
// Performs a bitwise OR on the values of Vx and Vy, then stores the
|
||||||
|
// result in Vx. A bitwise OR compares the corrseponding bits from
|
||||||
|
// two values, and if either bit is 1, then the same bit in the
|
||||||
|
// result is also 1. Otherwise, it is 0.
|
||||||
|
this->v[bytecode.operand.reg_reg.x] =
|
||||||
|
this->v[bytecode.operand.reg_reg.x] |
|
||||||
|
this->v[bytecode.operand.reg_reg.y];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AND_REG: {
|
||||||
|
// Set Vx = Vx AND Vy.
|
||||||
|
// Performs a bitwise AND on the values of Vx and Vy, then stores
|
||||||
|
// the result in Vx. A bitwise AND compares the corrseponding bits
|
||||||
|
// from two values, and if both bits are 1, then the same bit in the
|
||||||
|
// result is also 1. Otherwise, it is 0.
|
||||||
|
this->v[bytecode.operand.reg_reg.x] =
|
||||||
|
this->v[bytecode.operand.reg_reg.x] &
|
||||||
|
this->v[bytecode.operand.reg_reg.y];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case XOR_REG: {
|
||||||
|
// Set Vx = Vx XOR Vy.
|
||||||
|
// Performs a bitwise exclusive OR on the values of Vx and Vy, then
|
||||||
|
// stores the result in Vx. An exclusive OR compares the
|
||||||
|
// corrseponding bits from two values, and if the bits are not both
|
||||||
|
// the same, then the corresponding bit in the result is set to 1.
|
||||||
|
// Otherwise, it is 0.
|
||||||
|
this->v[bytecode.operand.reg_reg.x] =
|
||||||
|
this->v[bytecode.operand.reg_reg.x] ^
|
||||||
|
this->v[bytecode.operand.reg_reg.y];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SHR_REG: {
|
||||||
|
// Set Vx = Vx SHR 1.
|
||||||
|
// If the least-significant bit of Vx is 1, then VF is set to 1,
|
||||||
|
// otherwise 0. Then Vx is divided by 2.
|
||||||
|
this->v[0xF] = this->v[bytecode.operand.reg_reg.x] & 1;
|
||||||
|
this->v[bytecode.operand.reg_reg.x] =
|
||||||
|
this->v[bytecode.operand.reg_reg.x] >> 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SUBN_REG: {
|
||||||
|
// Set Vx = Vy - Vx, set VF = NOT borrow.
|
||||||
|
// If Vy > Vx, then VF is set to 1, otherwise 0. Then Vx is
|
||||||
|
// subtracted from Vy, and the results stored in Vx.
|
||||||
|
int result = this->v[bytecode.operand.reg_reg.y] -
|
||||||
|
this->v[bytecode.operand.reg_reg.x];
|
||||||
|
this->v[bytecode.operand.reg_reg.x] = result & 0xFF;
|
||||||
|
this->v[0xF] = result < 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SHL_REG: {
|
||||||
|
// Set Vx = Vx SHL 1.
|
||||||
|
// If the most-significant bit of Vx is 1, then VF is set to 1,
|
||||||
|
// otherwise to 0. Then Vx is multiplied by 2.
|
||||||
|
this->v[0xF] = this->v[bytecode.operand.reg_reg.x] >> 7;
|
||||||
|
this->v[bytecode.operand.reg_reg.x] =
|
||||||
|
this->v[bytecode.operand.reg_reg.x] << 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LOAD_I_BYTE: {
|
||||||
|
// Set I = nnn.
|
||||||
|
// The value of register I is set to nnn.
|
||||||
|
this->i = bytecode.operand.word & 0x0FFF;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case JP_V0_BYTE: {
|
||||||
|
// Jump to location nnn + V0.
|
||||||
|
// The program counter is set to nnn plus the value of V0.
|
||||||
|
pc = (bytecode.operand.word & 0x0FFF) + this->v[0];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case RND: {
|
||||||
|
// Set Vx = random byte AND kk.
|
||||||
|
// The interpreter generates a random number from 0 to 255, which is
|
||||||
|
// then ANDed with the value kk. The results are stored in Vx. See
|
||||||
|
// instruction 8xy2 for more information on AND.
|
||||||
|
this->v[bytecode.operand.byte_reg.reg] =
|
||||||
|
static_cast<uint8_t>(rand()) & bytecode.operand.byte_reg.byte;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case DRW: {
|
||||||
|
// Display n-byte sprite starting at memory location I
|
||||||
|
// at (Vx, Vy), set VF = collision.
|
||||||
|
// The interpreter reads n bytes from memory, starting at the
|
||||||
|
// address stored in I. These bytes are then displayed as sprites on
|
||||||
|
// screen at coordinates (Vx, Vy). Sprites are XORed onto the
|
||||||
|
// existing screen. If this causes any pixels to be erased, VF is
|
||||||
|
// set to 1, otherwise it is set to 0. If the sprite is positioned
|
||||||
|
// so part of it is outside the coordinates of the display, it wraps
|
||||||
|
// around to the opposite side of the screen. See instruction 8xy3
|
||||||
|
// for more information on XOR, and section 2.4, Display, for more
|
||||||
|
// information on the Chip-8 screen and sprites.
|
||||||
|
// fprintf(stderr, "DRW not implemented\n");
|
||||||
|
this->v[0x0F] = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < bytecode.operand.reg_reg_nibble.nibble; i++) {
|
||||||
|
uint8_t sprite = read_mem(this->i + i);
|
||||||
|
|
||||||
|
for (int j = 0; j < 8; j++) {
|
||||||
|
bool source = (sprite >> (7 - j)) & 0x1;
|
||||||
|
int x = (this->v[bytecode.operand.reg_reg_nibble.x] + j) %
|
||||||
|
SCREEN_WIDTH;
|
||||||
|
int y = (this->v[bytecode.operand.reg_reg_nibble.y] + i) %
|
||||||
|
SCREEN_HEIGHT;
|
||||||
|
|
||||||
|
printf("Sprite %d, bit %d %d\n", i, j,
|
||||||
|
sprite & (0x80 >> j));
|
||||||
|
if (!source) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get_pixel(x, y)) {
|
||||||
|
set_pixel(x, y, 0);
|
||||||
|
this->v[0x0F] = 1;
|
||||||
|
} else {
|
||||||
|
set_pixel(x, y, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SKIP_PRESSED_REG: {
|
||||||
|
// Skip next instruction if key with the value of Vx is
|
||||||
|
// pressed.
|
||||||
|
// Checks the keyboard, and if the key corresponding to the value of
|
||||||
|
// Vx is currently in the down position, PC is increased by 2.
|
||||||
|
fprintf(stderr, "SKIP_PRESSED_REG not implemented\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SKIP_NOT_PRESSED_REG: {
|
||||||
|
// Skip next instruction if key with the value of Vx is
|
||||||
|
// not pressed.
|
||||||
|
// Checks the keyboard, and if the key corresponding to the value of
|
||||||
|
// Vx is currently in the up position, PC is increased by 2.
|
||||||
|
fprintf(stderr, "SKIP_NOT_PRESSED_REG not implemented\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LD_REG_DT: {
|
||||||
|
// Set Vx = delay timer value.
|
||||||
|
// The value of DT is placed into Vx.
|
||||||
|
this->v[bytecode.operand.reg_reg.x] = this->delay;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LD_REG_K: {
|
||||||
|
// Wait for a key press, store the value of the key in
|
||||||
|
// Vx.
|
||||||
|
// All execution stops until a key is pressed, then the value of
|
||||||
|
// that key is stored in Vx.
|
||||||
|
fprintf(stderr, "LD_REG_K not implemented\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LD_DT_REG: {
|
||||||
|
// Set delay timer = Vx.
|
||||||
|
// DT is set equal to the value of Vx.
|
||||||
|
this->delay = this->v[bytecode.operand.reg_reg.x];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LD_ST_REG: {
|
||||||
|
// Set sound timer = Vx.
|
||||||
|
// ST is set equal to the value of Vx.
|
||||||
|
set_sound_timer(bytecode.operand.byte);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ADD_I_REG: {
|
||||||
|
// Set I = I + Vx.
|
||||||
|
// The values of I and Vx are added, and the results are stored in
|
||||||
|
// I.
|
||||||
|
this->i += this->v[bytecode.operand.byte];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LD_F_REG: {
|
||||||
|
// Set I = location of sprite for digit Vx.
|
||||||
|
// The value of I is set to the location for the hexadecimal sprite
|
||||||
|
// corresponding to the value of Vx. See section 2.4, Display, for
|
||||||
|
// more information on the Chip-8 hexadecimal font.
|
||||||
|
|
||||||
|
//? This is the ONLY spot where the emulator is allowed to access
|
||||||
|
//? 0x0000-0x01FF of the RAM. Since that area of RAM is reserved for
|
||||||
|
//? the emulator's own use.
|
||||||
|
this->i = (uint16_t)(bytecode.operand.byte * 5);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LD_B_REG: {
|
||||||
|
// Store BCD representation of Vx in memory locations I,
|
||||||
|
// I+1, and I+2.
|
||||||
|
// The interpreter takes the decimal value of Vx, and places the
|
||||||
|
// hundreds digit in memory at location in I, the tens digit at
|
||||||
|
// location I+1, and the ones digit at location I+2.
|
||||||
|
// TODO: is this correct?
|
||||||
|
this->ram[this->i] =
|
||||||
|
(uint8_t)((this->v[bytecode.operand.reg_reg.x] / 100) & 0x0F);
|
||||||
|
this->ram[this->i + 1] =
|
||||||
|
(uint8_t)((this->v[bytecode.operand.reg_reg.x] % 100) / 10) &
|
||||||
|
0x0F;
|
||||||
|
this->ram[this->i + 2] =
|
||||||
|
(uint8_t)(this->v[bytecode.operand.reg_reg.x] % 10) & 0x0F;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LD_PTR_I_REG: {
|
||||||
|
// Store registers V0 through Vx in memory starting at
|
||||||
|
// location I.
|
||||||
|
// The interpreter copies the values of registers V0 through Vx into
|
||||||
|
// memory, starting at the address in I.
|
||||||
|
for (int i = 0; i < bytecode.operand.reg_reg.x; i++) {
|
||||||
|
this->ram[this->i + i] = this->v[i];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LD_REG_PTR_I: {
|
||||||
|
// Read registers V0 through Vx from memory starting at
|
||||||
|
// location I.
|
||||||
|
// The interpreter reads values from memory starting at location I
|
||||||
|
// into registers V0 through Vx.
|
||||||
|
for (int i = 0; i < bytecode.operand.reg_reg.x; i++) {
|
||||||
|
this->v[i] = this->ram[this->i + i];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case UNKNOWN_INSTRUCTION: {
|
||||||
|
fprintf(stderr, "Unknown instruction type: %d\n",
|
||||||
|
bytecode.instruction_type);
|
||||||
|
exit = 1;
|
||||||
|
running = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Emulating...\n");
|
||||||
|
|
||||||
|
auto now = high_resolution_clock::now();
|
||||||
|
if (duration_cast<milliseconds>(now - last_timer_update) >=
|
||||||
|
timer_interval) {
|
||||||
|
printf("Updating...\n");
|
||||||
|
if (delay > 0)
|
||||||
|
--delay;
|
||||||
|
if (sound_timer > 0)
|
||||||
|
--sound_timer;
|
||||||
|
draw(renderer, texture, this->fb);
|
||||||
|
last_timer_update = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto elapsed_time = duration_cast<milliseconds>(
|
||||||
|
high_resolution_clock::now() - start_time);
|
||||||
|
if (elapsed_time < cycle_time) {
|
||||||
|
std::this_thread::sleep_for(cycle_time - elapsed_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_DestroyTexture(texture);
|
||||||
|
SDL_DestroyRenderer(renderer);
|
||||||
|
SDL_DestroyWindow(window);
|
||||||
|
SDL_Quit();
|
||||||
|
|
||||||
|
return exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Chip8::view_ram() {
|
||||||
|
printf("Hex dump:\n");
|
||||||
|
for (size_t i = 0; i < RAM_SIZE / 16; i++) {
|
||||||
|
size_t j = 0;
|
||||||
|
for (; j < 16; j++) {
|
||||||
|
printf("%02x ", this->ram[i * 16 + j]);
|
||||||
|
}
|
||||||
|
printf(" |");
|
||||||
|
j = 0;
|
||||||
|
for (; j < 16; j++) {
|
||||||
|
if (this->ram[i * 16 + j] >= 32 && this->ram[i * 16 + j] <= 126) {
|
||||||
|
printf("%c", this->ram[i * 16 + j]);
|
||||||
|
} else {
|
||||||
|
printf(".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printf("|\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Chip8::dump_ram() {
|
||||||
|
(void)remove("ram.bin");
|
||||||
|
|
||||||
|
FILE *fp = fopen("ram.bin", "wb");
|
||||||
|
if (fp == NULL) {
|
||||||
|
printf("Failed to open file\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fwrite(this->ram, RAM_SIZE, 1, fp);
|
||||||
|
fclose(fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
signal(SIGINT, exit);
|
||||||
|
|
||||||
|
if (argc < 2) {
|
||||||
|
printf("Usage: %s <file>\n", argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Chip8 chip8 = Chip8(argv[1]);
|
||||||
|
chip8.view_ram();
|
||||||
|
int ret = chip8.run();
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
22
test.sh
Executable file
22
test.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
roms=($(find tests/ -type f))
|
||||||
|
|
||||||
|
testOutput=$(mktemp)
|
||||||
|
|
||||||
|
if ${VERBOSE:-false}; then
|
||||||
|
testOutput=2
|
||||||
|
fi
|
||||||
|
|
||||||
|
for rom in "${roms[@]}"; do
|
||||||
|
echo -n "$rom "
|
||||||
|
./bin/voidEmu $rom 1>&$testOutput
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo -en "\x1b[2K\r"
|
||||||
|
cat $testOutput
|
||||||
|
echo -e "$rom \x1b[97m[\x1b[1;31mFAIL\x1b[97m]\x1b[0m"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e "\x1b[97m[\x1b[1;32mPASS\x1b[97m]\x1b[0m"
|
||||||
|
fi
|
||||||
|
done
|
||||||
1
tests/.gitignore
vendored
Normal file
1
tests/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
extern/**
|
||||||
BIN
tests/exit.ch8
Normal file
BIN
tests/exit.ch8
Normal file
Binary file not shown.
BIN
tests/jmp.ch8
Normal file
BIN
tests/jmp.ch8
Normal file
Binary file not shown.
Reference in New Issue
Block a user