For some time now I've been developing a CHIP8 emulator to learn a bit of emulation, I've implemented all the instructions except the sound I'm still going to implement, but I'm already taking a long time to create a shape that I can draw the sprites on the screen, here's my source code:
main.cpp
#include <iostream>
#include <fstream>
#include <SDL/SDL.h>
#include "chip8.hpp"
#ifdef _WIN32
#include "targetver.hpp"
#endif // _WIN32
std::uint8_t keys[16] = {
SDLK_1, SDLK_2, SDLK_3, SDLK_4,
SDLK_q, SDLK_w, SDLK_e, SDLK_r,
SDLK_a, SDLK_s, SDLK_d, SDLK_f,
SDLK_z, SDLK_x, SDLK_c, SDLK_v
};
int main(int argc, char *argv[])
{
bool quit = false;
CHIP8 *chip8 = new CHIP8();
std::cout << "Input file: ";
std::cin >> argv[0];
std::fstream logfile("log.txt", std::ios::in | std::ios::out | std::ios::app);
std::cerr.rdbuf(logfile.rdbuf());
std::clog.rdbuf(logfile.rdbuf());
std::clog << "Jogo/Programa atual: " << argv[0] << " . \n" << std::endl;
if (!chip8->load(argv[0])) {
std::cerr << "Could not start emulator!" << std::endl;
logfile.close();
delete chip8;
return EXIT_FAILURE;
}
//Inicializa o SDL2
if (SDL_Init(SDL_INIT_EVERYTHING) != 0) {
std::cerr << "Could not init SDL2:" << SDL_GetError() << std::endl;
logfile.close();
delete chip8;
return EXIT_FAILURE;
}
//Cria a janela
SDL_Window *window = SDL_CreateWindow("CHIP8", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 320, SDL_WINDOW_SHOWN);
if (window == NULL) {
std::cerr << "Could not init window:" << SDL_GetError() << std::endl;
SDL_Quit();
logfile.close();
delete chip8;
return EXIT_FAILURE;
}
//Cria o renderizador
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (renderer == NULL) {
std::cerr << "Could not create the renderer:" << SDL_GetError() << std::endl;
SDL_DestroyWindow(window);
SDL_Quit();
logfile.close();
delete chip8;
return EXIT_FAILURE;
}
//Cria a textura para desenhar os pixels na janela
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 64, 32);
if (texture == NULL) {
std::cerr << "Could not create the texture:" << SDL_GetError() << std::endl;
SDL_DestroyTexture(texture);
SDL_DestroyWindow(window);
SDL_Quit();
logfile.close();
delete chip8;
return EXIT_FAILURE;
}
//Processa os eventos
SDL_Event ev;
while (!quit) {
while (SDL_PollEvent(&ev)) {
chip8->exec();
if (chip8->hasDraw) {
chip8->hasDraw = false;
Uint32 pixels[2048];
for (int i = 0; i < 2048; ++i) {
std::uint8_t pixel = chip8->gfx[i];
pixels[i] = (0x00FFFFFF * pixel) | 0xFF000000;
}
//SDL_UpdateTexture(texture, NULL, chip8->gfx, 2048);
SDL_UpdateTexture(texture, NULL, pixels, 64 * sizeof(Uint32));
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
}
switch (ev.type) {
case SDL_QUIT:
quit = true;
break;
case SDL_KEYDOWN:
for (int i = 0; i < 16; i++) {
if (ev.key.keysym.sym == keys[i]) {
chip8->keys[i] = 1;
}
}
break;
case SDL_KEYUP:
for (int i = 0; i < 16; i++) {
if (ev.key.keysym.sym == keys[i]) {
chip8->keys[i] = 0;
}
}
break;
}
}
}
SDL_DestroyTexture(texture);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
std::clog << "\n---------------------------------------------\n";
logfile.close();
delete chip8;
return EXIT_SUCCESS;
}
chip8.hpp
#ifndef CHIP8_HPP
#define CHIP8_HPP
#include <cstdint>
class CHIP8 {
public:
CHIP8();
~CHIP8();
//Ler o conteudo da ROM
bool load(const char* fileName);
//Busca e executa os opcodes
void exec();
private:
void message_log(const char *message);
void message_log(const char *message, std::uint16_t opcode);
private:
std::uint8_t rom[0xFFF];
//Registers
std::uint8_t Vx[16], SP = 0, DT = 0, ST = 0;
std::uint16_t I = 0, PC = 0x200, stack[16];
public:
bool debug = true, hasDraw = false;
std::uint8_t gfx[2048], keys[16];
};
#endif // !CHIP8_HPP
chip8.cpp
#include <fstream>
#include <iostream>
#include <cstdlib>
#include "chip8.hpp"
const std::uint8_t fontset[80] = {
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
};
CHIP8::CHIP8()
{
//Inicializa os valores
memset(rom, 0, sizeof(rom));
memset(gfx, 0, sizeof(gfx));
memset(Vx, 0, sizeof(Vx));
memset(stack, 0, sizeof(stack));
memset(keys, 0, sizeof(keys));
for (int i = 0x000; i < 0x1FF; i++) {
rom[i] = fontset[i];
}
}
CHIP8::~CHIP8()
{
memset(rom, 0, sizeof(rom));
memset(gfx, 0, sizeof(gfx));
memset(Vx, 0, sizeof(Vx));
memset(stack, 0, sizeof(stack));
memset(keys, 0, sizeof(keys));
DT = 0;
ST = 0;
}
void CHIP8::message_log(const char *message)
{
if (debug) {
std::clog << message << std::endl;
}
}
void CHIP8::message_log(const char *message, std::uint16_t opcode)
{
if (debug) {
std::clog << message << std::uppercase << std::hex << opcode << std::endl;
}
}
bool CHIP8::load(const char *fileName)
{
std::fstream inputFile(fileName, std::ios::in | std::ios::binary);
if (!inputFile.is_open()) {
return false;
}
for (int i = 0x200; i < 0xFFF; i++) {
rom[i] = (std::uint8_t)inputFile.get();
}
inputFile.close();
return true;
}
void CHIP8::exec()
{
std::uint16_t opcode = rom[PC] << 8 | rom[PC+1];
unsigned nnn = (opcode & 0x0FFF); //nnn or addr - A 12-bit value, the lowest 12 bits of the instruction
unsigned n = (opcode & 0x000F); //n or nibble - A 4-bit value, the lowest 4 bits of the instruction
unsigned x = (opcode & 0x0F00) >> 8; //x - A 4-bit value, the lower 4 bits of the high byte of the instruction
unsigned y = (opcode & 0x00F0) >> 4; //y - A 4-bit value, the upper 4 bits of the low byte of the instruction
unsigned kk = (opcode & 0x00FF); //kk or byte - An 8-bit value, the lowest 8 bits of the instruction
switch (opcode & 0xF000) {
case 0x0000:
switch (opcode & 0x00FF) {
case 0x00E0:
memset(gfx, 0, sizeof(gfx));
hasDraw = true;
PC += 2;
message_log("Opcode CLS executado: 0x", opcode);
break;
case 0x00EE:
PC = stack[--SP];
message_log("Opcode RET executado: 0x", opcode);
break;
}
break;
case 0x1000:
PC = nnn;
message_log("Opcode JP addr executado: 0x", opcode);
break;
case 0x2000:
stack[SP++] = PC + 2;
PC = nnn;
message_log("Opcode CALL addr executado: 0x", opcode);
break;
case 0x3000:
if (Vx[x] == kk)
PC += 2;
PC += 2;
message_log("Opcode SE Vx, byte executado: 0x", opcode);
break;
case 0x4000:
if (Vx[x] != kk)
PC += 2;
PC += 2;
message_log("Opcode SNE Vx, byte executado: 0x", opcode);
break;
case 0x5000:
if (Vx[x] == Vx[y])
PC += 2;
PC += 2;
message_log("Opcode SE Vx, Vy executado: 0x", opcode);
break;
case 0x6000:
Vx[x] = kk;
PC += 2;
message_log("Opcode LD Vx, byte executado: 0x", opcode);
break;
case 0x7000:
Vx[x] = Vx[x] + kk;
PC += 2;
message_log("Opcode ADD Vx, byte executado: 0x", opcode);
break;
//8xy_
case 0x8000:
switch (opcode & 0x000F) {
case 0x0000:
Vx[x] = Vx[y];
PC += 2;
message_log("Opcode LD Vx, Vy executado: 0x", opcode);
break;
case 0x0001:
Vx[x] |= Vx[y];
PC += 2;
message_log("Opcode OR Vx, Vy executado: 0x", opcode);
break;
case 0x0002:
Vx[x] &= Vx[y];
PC += 2;
message_log("Opcode AND Vx, Vy executado: 0x", opcode);
break;
case 0x0003:
Vx[x] ^= Vx[y];
PC += 2;
message_log("Opcode XOR Vx, Vy executado: 0x", opcode);
break;
case 0x0004:
if ((Vx[x] += Vx[y]) > 255)
Vx[0xF] = 1;
else
Vx[0xF] = 0;
PC += 2;
message_log("Opcode ADD Vx, Vy executado: 0x", opcode);
break;
case 0x0005:
if (Vx[x] > Vx[y])
Vx[0xF] = 1;
else
Vx[0xF] = 0;
Vx[x] -= Vx[y];
PC += 2;
message_log("Opcode SUB Vx, Vy executado: 0x", opcode);
break;
case 0x0006:
Vx[0xF] = Vx[x] & 0x1;
Vx[x] >>= 1;
PC += 2;
message_log("Opcode SHR Vx {, Vy} executado: 0x", opcode);
break;
case 0x0007:
if (Vx[y] > Vx[x])
Vx[0xF] = 1;
else
Vx[0xF] = 0;
Vx[x] -= Vx[y];
PC += 2;
message_log("Opcode SUBN Vx, Vy executado: 0x", opcode);
break;
case 0x000E:
Vx[0xF] = Vx[x] >> 7;
Vx[x] <<= 1;
PC += 2;
message_log("Opcode SHL Vx {, Vy} executado: 0x", opcode);
break;
default:
message_log("Opcode nao inplementado: 0x8xy", (opcode & 0x000F));
break;
}
break;
case 0x9000:
if (Vx[x] != Vx[y])
PC += 2;
PC += 2;
message_log("Opcode SNE Vx, Vy executado: 0x", opcode);
break;
case 0xA000:
I = nnn;
PC += 2;
message_log("Opcode LD I, addr executado: 0x", opcode);
break;
case 0xB000:
PC = nnn + Vx[0];
message_log("Opcode JP V0, addr executado: 0x", opcode);
break;
case 0xC000:
Vx[x] = (std::rand() % 255) & kk;
PC += 2;
message_log("Opcode RND V x , byte executado: 0x", opcode);
break;
case 0xD000:
{
Vx[0xF] = 0;
for (int yline = 0; yline < Vx[x]; yline++) {
std::uint16_t pixel = rom[I + yline];
for (int xline = 0; xline < 8; xline++) {
if ((pixel & (0x80 >> xline)) != 0) {
if (gfx[Vx[x] + xline + ((Vx[y] + yline) * 64)] == 1)
Vx[0xF] = 1;
gfx[Vx[x] + xline + ((Vx[y] + yline) * 64)] ^= 1;
}
}
}
}
hasDraw = true;
PC += 2;
message_log("Opcode DRW Vx, Vy, nibble executado: 0x", opcode);
break;
case 0xE000:
switch (opcode & 0x00FF) {
case 0x009E:
if (keys[Vx[x]] == 1)
PC += 2;
PC += 2;
message_log("Opcode SKP Vx executado: 0x", opcode);
break;
case 0x00A1:
if (keys[Vx[x]] == 0)
PC += 2;
PC += 2;
message_log("Opcode SKNP Vx executado: 0x", opcode);
break;
}
break;
case 0xF000:
switch (opcode & 0x00FF) {
case 0x0007:
Vx[x] = DT;
PC += 2;
message_log("Opcode LD Vx, DT executado: 0x", opcode);
break;
case 0x000A:
{
bool pressed = false;
for (int i = 0; i < 16; i++) {
if (i == 1) {
Vx[x] = keys[i];
pressed;
}
}
if (!pressed)
return;
PC += 2;
}
message_log("Opcode LD Vx, K executado: 0x", opcode);
break;
case 0x0015:
DT = Vx[x];
PC += 2;
message_log("Opcode LD DT, Vx executado: 0x", opcode);
break;
case 0x0018:
ST = Vx[x];
PC += 2;
message_log("Opcode LD ST, Vx executado: 0x", opcode);
break;
case 0x001E:
I = I + Vx[x];
PC += 2;
message_log("Opcode ADD I, Vx executado: 0x", opcode);
break;
case 0x0029:
I = Vx[x] * 5;
PC += 2;
message_log("Opcode LD F, Vx executado: 0x", opcode);
break;
case 0x0033:
rom[I] = Vx[x] % 1000 / 100;
rom[I + 1] = Vx[x] % 100 / 10;
rom[I + 2] = Vx[x] % 10;
PC += 2;
message_log("Opcode LD B, Vx executado: 0x", opcode);
break;
case 0x0055:
for (int i = 0; i <= x; i++) {
rom[I + i] = Vx[i];
}
PC += 2;
message_log("Opcode LD [I], Vx executado: 0x", opcode);
break;
case 0x0065:
for (int i = 0; i <= x; i++) {
Vx[i] = rom[I + i];
}
PC += 2;
message_log("Opcode LD Vx, [I] executado: 0x", opcode);
break;
default:
message_log("Opcode nao inplementado: 0x", (opcode & 0x00FF));
break;
}
break;
default:
message_log("Opcode nao inplementado: 0x", (opcode & 0xF000));
break;
}
if (DT > 0)
--DT;
if (ST > 0)
--ST;
}
Note: The targetver.hpp header only configures the windows SDK version for Windows 7
I relied on this documentation to write the emulator and in this repository for graphics events since I had no clue how to do this and I wanted to go testing my code and go showing the result on the screen, the opcodes responsible for drawing sprites are 0xD000
and 0x00E0
, the first thing I did not understand was if the pixels are stored in an array of bytes with the size of 2048, so why did he store it in a Uint32 Array of the same size and why did he do that masking and then next in size he indicated 64 * sizeof (Uint32), if the buffer are 2048 pixels then why not just put 2048 " Thinking about it, I just did it:
SDL_UpdateTexture(texture, NULL, chip8->gfx, 2048);
And I had this result:
AsyoucanseerightawayIwasdisappointed,soItriedtherepositoryway,thistimeitworked(oralmost),butthespritesareshowninarandomway:
I plan to study the DXyn opcode later to improve this code but I would need the graphical part running for now to actually test the emulator, if the keys are working fine etc, but I can not seem to find a solution to this problem.