I've recently started learning OpenGL and thought that a great way to start using it in its simplest form would be to create a Chip8 emulator using the keyboard callbacks and graphics that GLFW and OpenGL provide. As this question would be too large to request a review on my classes used with OpenGL and my Chip8 emulator, I'll keep it to just the Chip8.
I'm aware that not all op-codes have been implemented as I have only implemented the ones needed to get the 15 Chip8 games that I have copies of running. The code that I am most interested in having reviewed are the chip8.h and chip8.cpp files however I have included (the majority of) main.cpp for completeness.
An update that I would like to work on would be to switch from using a switch
statement to using function pointers but would like a hard review of the current working code before I begin with that.
Thanks to this tutorial which I used to get started and for opcode 0xDXYN
.
chip8.h
#ifndef CHIP8_H
#define CHIP8_H
#include <fstream>
#include <iostream>
class Chip8 {
public:
Chip8(const char* file, int seed);
~Chip8();
void PressKey(int k);
void ReleaseKey(int k);
void LoadProgram(const char* file, int seed);
void Reset();
void Cycle();
bool getDraw();
unsigned char* getGFX();
private:
unsigned short opcode;
unsigned char memory[4096];
unsigned char V[16];
unsigned short I;
unsigned short pc;
unsigned char gfx[64 * 32];
unsigned char delay_timer;
unsigned char sound_timer;
unsigned short stack[16];
unsigned char sp;
unsigned char key[16];
bool drawflag;
};
#endif
chip8.cpp
#include "chip8.h"
Chip8::Chip8(const char* file, int seed) {
LoadProgram(file, seed);
}
Chip8::~Chip8() {
}
void Chip8::PressKey(int k) {
key[k] = 1;
}
void Chip8::ReleaseKey(int k) {
key[k] = 0;
}
void Chip8::LoadProgram(const char* file, int seed) {
srand(seed);
Reset();
std::ifstream f(file,std::ios::binary);
char c;
int i = 512;
while (!f.eof() && i < 4096) {
f.get(c);
std::cout << c;
memory[i++] = c;
}
}
void Chip8::Reset() {
pc = 0x200; // Program counter starts at 0x200
opcode = 0; // Reset current opcode
I = 0; // Reset index register
sp = 0; // Reset stack pointer
for (int i = 0; i < 64 * 32; i++) {
gfx[i] = 0;
}
for (int i = 0; i < 16; i++) {
stack[i] = 0;
V[i] = 0;
key[i] = 0;
}
for (int i = 0; i < 4096; i++) {
memory[i] = 0;
}
unsigned char chip8_fontset[] = {
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
};
for (int i = 0; i < 80; i++) {
memory[i] = chip8_fontset[i];
}
sound_timer = 0;
delay_timer = 0;
drawflag = false;
}
void Chip8::Cycle() {
// Fetch opcode
opcode = memory[pc] << 8 | memory[pc + 1];
// Decode opcode
switch (opcode & 0xF000)
{
case 0x0000: // ANNN
switch (opcode & 0x0FFF) {
case 0x00E0:
for (int i = 0; i < 64 * 32; i++) {
gfx[i] = 0;
}
drawflag = true;
pc += 2;
break;
case 0x00EE:
sp--;
pc = stack[sp];
pc += 2;
break;
default:
std::cout << "Unknown opcode: 0x" << std::hex << opcode << std::endl;
}
break;
case 0x1000: // 1NNN
pc = opcode & 0x0FFF;
break;
case 0x2000: // 2NNN
stack[sp] = pc;
sp++;
pc = opcode & 0x0FFF;
break;
case 0x3000: // 3XNN
if (V[(opcode & 0x0F00) >> 8] == (opcode & 0x00FF)) {
pc += 2;
}
pc += 2;
break;
case 0x4000: // 4XNN
if (V[(opcode & 0x0F00) >> 8] != (opcode & 0x00FF)) {
pc += 2;
}
pc += 2;
break;
case 0x5000: // 5XY0
if (V[(opcode & 0x0F00) >> 8] == V[(opcode & 0x00F0) >> 4]) {
pc += 2;
}
pc += 2;
break;
case 0x6000: // 6XNN
V[(opcode & 0x0F00) >> 8] = opcode & 0x00FF;
pc += 2;
break;
case 0x7000: // 7XNN
V[15] = 0;
if (V[(opcode & 0x0F00) >> 8] + opcode & 0x00FF <= 0x00FF) {
V[(opcode & 0x0F00) >> 8] += opcode & 0x00FF;
}
else {
V[(opcode & 0x0F00) >> 8] += opcode & 0x00FF;
V[15] = 1;
}
pc += 2;
break;
case 0x8000: // 8XY#
switch (opcode & 0x000F) {
case 0x0000: // 8XY0
V[(opcode & 0x0F00) >> 8] = V[(opcode & 0x00F0) >> 4];
pc += 2;
break;
case 0x0001: // 8XY1
V[(opcode & 0x0F00) >> 8] |= V[(opcode & 0x00F0) >> 4];
pc += 2;
break;
case 0x0002: // 8XY2
V[(opcode & 0x0F00) >> 8] &= V[(opcode & 0x00F0) >> 4];
pc += 2;
break;
case 0x0003: // 8XY3
V[(opcode & 0x0F00) >> 8] ^= V[(opcode & 0x00F0) >> 4];
pc += 2;
break;
case 0x0004: // 8XY4
V[15] = 0;
if (V[(opcode & 0x0F00) >> 8] + V[(opcode & 0x00F0) >> 4] <= 0x00FF) {
V[(opcode & 0x0F00) >> 8] += V[(opcode & 0x00F0) >> 4];
}
else {
V[(opcode & 0x0F00) >> 8] += V[(opcode & 0x00F0) >> 4];
V[15] = 1;
}
pc += 2;
break;
case 0x0005: // 8XY5
V[15] = 0;
if (V[(opcode & 0x00F0) >> 4] > V[(opcode & 0x0F00) >> 8]) {
V[(opcode & 0x0F00) >> 8] -= V[(opcode & 0x00F0) >> 4];
}
else {
V[(opcode & 0x0F00) >> 8] -= V[(opcode & 0x00F0) >> 4];
V[15] = 1;
}
pc += 2;
break;
case 0x0006: // 8XY6
V[15] = V[(opcode & 0x0F00) >> 8] & 0x1;
V[(opcode & 0x0F00) >> 8] >>= 1;
pc += 2;
break;
case 0x0007: // 8XY7
V[15] = 0;
if (V[(opcode & 0x00F0) >> 4] > V[(opcode & 0x0F00) >> 8]) {
V[(opcode & 0x0F00) >> 8] = V[(opcode & 0x00F0) >> 4] - V[(opcode & 0x0F00) >> 8];
}
else {
V[(opcode & 0x0F00) >> 8] = V[(opcode & 0x00F0) >> 4] - V[(opcode & 0x0F00) >> 8];
V[15] = 1;
}
pc += 2;
break;
case 0x000E: // 8XYE
V[15] = (V[(opcode & 0x0F00) >> 8] >> 7) & 0x1;
V[(opcode & 0x0F00) >> 8] <<= 1;
pc += 2;
break;
default:
std::cout << "Unknown opcode: 0x" << std::hex << opcode << std::endl;
}
break;
case 0x9000: // 9XY0
if (V[(opcode & 0x0F00) >> 8] != V[(opcode & 0x00F0) >> 4]) {
pc += 2;
}
pc += 2;
break;
case 0xA000: // ANNN
I = opcode & 0x0FFF;
pc += 2;
break;
case 0xC000: // CXNN
V[(opcode & 0x0F00) >> 8] = (rand()%255) & (opcode & 0x00FF);
pc += 2;
break;
case 0xD000: // DXYN
{
unsigned short x = V[(opcode & 0x0F00) >> 8];
unsigned short y = V[(opcode & 0x00F0) >> 4];
unsigned short height = opcode & 0x000F;
unsigned short pixel;
V[15] = 0;
for (int yline = 0; yline < height; yline++)
{
pixel = memory[I + yline];
for (int xline = 0; xline < 8; xline++)
{
if ((pixel & (0x80 >> xline)) != 0)
{
if (gfx[(x + xline + ((y + yline) * 64))] == 1)
V[15] = 1;
gfx[x + xline + ((y + yline) * 64)] ^= 1;
}
}
}
drawflag = true;
pc += 2;
}
break;
case 0xE000: // EX##
switch (opcode & 0x00FF) {
case 0x009E: // EX9E
if (key[V[(opcode & 0x0F00) >> 8]] == 1) {
pc += 2;
}
pc += 2;
break;
case 0x00A1: // EXA1
if (key[V[(opcode & 0x0F00) >> 8]] != 1) {
pc += 2;
}
pc += 2;
break;
default:
std::cout << "Unknown opcode: 0x" << std::hex << opcode << std::endl;
}
break;
case 0xF000: // FXNN
switch (opcode & 0x00FF) {
case 0x000A: // FX0A
for (int i = 0; i < 16; i++) {
if (key[i]) {
V[(opcode & 0x0F00) >> 8] = i;
pc += 2;
break;
}
}
break;
case 0x0007: // FX07
V[(opcode & 0x0F00) >> 8] = delay_timer;
pc += 2;
break;
case 0x0015: // FX15
delay_timer = V[(opcode & 0x0F00) >> 8];
pc += 2;
break;
case 0x0018: // FX15
sound_timer = V[(opcode & 0x0F00) >> 8];
pc += 2;
break;
case 0x001E: // FX1E
V[15] = 0;
if (I + V[(opcode & 0x0F00) >> 8] < 0x0FFF) {
I += V[(opcode & 0x0F00) >> 8];
}
else {
I += V[(opcode & 0x0F00) >> 8];
V[15] = 1;
}
pc += 2;
break;
case 0x0029: // FX29
I = V[(opcode & 0x0F00) >> 8] * 0x5;
pc += 2;
break;
case 0x0033: // FX33
memory[I] = V[(opcode & 0x0F00) >> 8] / 100;
memory[I + 1] = (V[(opcode & 0x0F00) >> 8] /10) % 10;
memory[I + 2] = V[(opcode & 0x0F00) >> 8] % 10;
pc += 2;
break;
case 0x0055: // FX55
for (int i = 0; i <= ((opcode & 0x0F00) >> 8); i++) {
memory[I + i] = V[i];
}
pc += 2;
break;
case 0x0065: // FX65
for (int i = 0; i <= ((opcode & 0x0F00) >> 8); i++) {
V[i] = memory[I + i];
}
pc += 2;
break;
default:
std::cout << "Unknown opcode: 0x" << std::hex << opcode << std::endl;
}
break;
default:
std::cout << "Unknown opcode: 0x" << std::hex << opcode << std::endl;
}
// Update timers
if (delay_timer > 0)
delay_timer--;
if (sound_timer > 0)
{
if (sound_timer == 1)
std::cout << "Beep!" << std::endl;
sound_timer--;
}
}
bool Chip8::getDraw() {
return drawflag;
}
unsigned char* Chip8::getGFX() {
drawflag = false;
return gfx;
}
main.cpp
// GLEW
#define GLEW_STATIC
#include <GL/glew.h>
// GLFW
#include <GLFW/glfw3.h>
// Other includes
#include "shader.h"
#include "display.h"
#include "mesh.h"
#include "chip8.h"
#include <iostream>
// Consts
const GLuint WIDTH = 300, HEIGHT = 150;
// Function prototypes
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
void mouse_callback(GLFWwindow* window, int button, int action, int modifier);
Chip8 chip8("./PONG.c8", 1);
int main()
{
// Window Creation is here
// Game loop
while (!display.IsClosed())
{
chip8.Cycle();
if (chip8.getDraw() == true) {
unsigned char* gfx = chip8.getGFX();
// Drawing code is here
display.Update(true, true);
}
else {
display.Update(false, true);
}
}
return 0;
}
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
if ((key == GLFW_KEY_ESCAPE || key == GLFW_KEY_ENTER) && action == GLFW_PRESS){
glfwSetWindowShouldClose(window, GL_TRUE);
}
int k = -1;
if (key == GLFW_KEY_1) {
k = 0x1;
}
else if (key == GLFW_KEY_2) {
k = 0x2;
}
else if (key == GLFW_KEY_3) {
k = 0x3;
}
else if (key == GLFW_KEY_4) {
k = 0xc;
}
else if (key == GLFW_KEY_Q) {
k = 0x4;
}
else if (key == GLFW_KEY_W) {
k = 0x5;
}
else if (key == GLFW_KEY_E) {
k = 0x6;
}
else if (key == GLFW_KEY_R) {
k = 0xd;
}
else if (key == GLFW_KEY_A) {
k = 0x7;
}
else if (key == GLFW_KEY_S) {
k = 0x8;
}
else if (key == GLFW_KEY_D) {
k = 0x9;
}
else if (key == GLFW_KEY_F) {
k = 0xe;
}
else if (key == GLFW_KEY_Z) {
k = 0xa;
}
else if (key == GLFW_KEY_X) {
k = 0x0;
}
else if (key == GLFW_KEY_C) {
k = 0xb;
}
else if (key == GLFW_KEY_V) {
k = 0xf;
}
if (k > -1) {
if (action == GLFW_PRESS) {
chip8.PressKey(k);
}
if (action == GLFW_RELEASE) {
chip8.ReleaseKey(k);
}
}
}
void mouse_callback(GLFWwindow* window, int button, int action, int modifier)
{
}