2018-06-22 20:51:16 +01:00
|
|
|
import std.stdio;
|
|
|
|
import derelict.sdl2.sdl;
|
2018-06-22 23:54:39 +01:00
|
|
|
import derelict.imgui.imgui;
|
2018-06-22 20:51:16 +01:00
|
|
|
import glad.gl.all;
|
|
|
|
|
2018-06-22 23:54:39 +01:00
|
|
|
import window;
|
|
|
|
import imgui;
|
|
|
|
|
2018-06-22 20:51:16 +01:00
|
|
|
string doCapture(string sym, uint start, uint end)(){
|
|
|
|
|
|
|
|
import std.string : format;
|
|
|
|
|
|
|
|
enum bits = end - start;
|
|
|
|
static if (bits > 16) {
|
|
|
|
alias RT = uint;
|
|
|
|
} else static if (bits > 8) {
|
|
|
|
alias RT = ushort;
|
|
|
|
} else {
|
|
|
|
alias RT = ubyte;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto str = "0b";
|
|
|
|
foreach (i; 0 .. end) {
|
|
|
|
if (i >= start) str ~= "1";
|
|
|
|
else str ~= "0";
|
|
|
|
}
|
|
|
|
|
|
|
|
return format(q{
|
|
|
|
%s result = ((%s & %s) >> %d);
|
|
|
|
return result;
|
|
|
|
}, RT.stringof, sym, str, start);
|
|
|
|
|
|
|
|
} // doCapture
|
|
|
|
|
|
|
|
auto capture(uint start, uint end)(ushort op) {
|
|
|
|
|
|
|
|
enum str = doCapture!(op.stringof, start, end)();
|
|
|
|
mixin(str);
|
|
|
|
|
|
|
|
} // capture
|
|
|
|
|
|
|
|
struct Chip8 {
|
|
|
|
|
|
|
|
alias OpCode = ushort;
|
|
|
|
alias ProgramCounter = ushort;
|
|
|
|
alias Memory = ubyte[4096];
|
|
|
|
alias Stack = ushort[16];
|
|
|
|
|
|
|
|
alias Register = ubyte;
|
|
|
|
alias Registers = Register[16];
|
|
|
|
alias IndexRegister = ushort;
|
|
|
|
|
|
|
|
alias KeyPad = ubyte[16];
|
|
|
|
|
|
|
|
struct CPU {
|
|
|
|
|
|
|
|
OpCode opcode = 0; //current opcode
|
|
|
|
ProgramCounter pc = 0x200;
|
|
|
|
|
|
|
|
Registers v; //16 general purpose registers
|
|
|
|
IndexRegister i;
|
|
|
|
|
|
|
|
Register delay_timer;
|
|
|
|
Register sound_timer;
|
|
|
|
|
|
|
|
} // CPU
|
|
|
|
|
|
|
|
CPU cpu;
|
|
|
|
Memory ram;
|
|
|
|
|
|
|
|
Stack stack;
|
|
|
|
ubyte sp;
|
|
|
|
|
|
|
|
KeyPad kp;
|
|
|
|
|
|
|
|
ubyte[64*32] screen_buf;
|
|
|
|
ubyte[3][64*32] screen_data;
|
|
|
|
|
|
|
|
bool run_flag;
|
|
|
|
bool draw_flag;
|
|
|
|
|
|
|
|
void load(size_t offset, in ubyte[] data) {
|
|
|
|
|
|
|
|
assert(offset + data.length < ram.length);
|
|
|
|
|
|
|
|
ram[offset .. offset + data.length] = data[];
|
|
|
|
|
|
|
|
} // load
|
|
|
|
|
|
|
|
void step() {
|
|
|
|
|
|
|
|
cpu.opcode = ram[cpu.pc] << 8 | ram[cpu.pc + 1];
|
|
|
|
auto pc_target = cast(ProgramCounter)(cpu.pc + 2);
|
|
|
|
|
|
|
|
writefln("opcode: 0x%X", cpu.opcode);
|
|
|
|
|
|
|
|
switch (cpu.opcode & 0xF000) with (cpu) {
|
|
|
|
|
|
|
|
case 0x0000:
|
|
|
|
|
|
|
|
switch (cpu.opcode & 0x0FFF) {
|
|
|
|
|
|
|
|
case 0x00E0: // 0x00E0 Clears the screen.
|
|
|
|
|
|
|
|
screen_buf[0..$] = 0;
|
|
|
|
draw_flag = true;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x00EE: // 0x00EE Returns from a subroutine.
|
|
|
|
|
|
|
|
pc_target = stack[--sp];
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
default: // 0x0NNN Calls RCA 1802 program at address NNN. Not necessary for most ROMs.
|
|
|
|
//assert(0, "0x0NNN RCA 1802 program opcode not implemented!");
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x1000: // 0x1NNN Jumps to address NNN.
|
|
|
|
|
|
|
|
pc_target = cpu.opcode.capture!(0, 12);
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x2000: // 0x2NNN Calls subroutine at NNN.
|
|
|
|
|
|
|
|
stack[sp++] = cpu.pc;
|
|
|
|
pc_target = cpu.opcode.capture!(0, 12);
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x3000: // 0x3XNN Skips the next instruction if VX equals NN.
|
|
|
|
|
|
|
|
if (cpu.v[cpu.opcode.capture!(8, 12)] == cpu.opcode.capture!(0, 8)) {
|
|
|
|
pc_target += 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x4000: // 0x4XNN Skips the next instruction if VX doesn't equal NN.
|
|
|
|
|
|
|
|
if (cpu.v[cpu.opcode.capture!(8, 12)] != cpu.opcode.capture!(0, 8)) {
|
|
|
|
pc_target += 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x5000: // 0x5XYO Skips the next instruction if VX equals VY.
|
|
|
|
|
|
|
|
if (cpu.v[cpu.opcode.capture!(8, 12)] == cpu.v[cpu.opcode.capture!(4, 8)]) {
|
|
|
|
pc_target += 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x6000: // 0x6XNN Sets VX to NN.
|
|
|
|
|
|
|
|
cpu.v[cpu.opcode.capture!(8, 12)] = cpu.opcode.capture!(0, 8);
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x7000: // 0x7XNN Adds NN to VX.
|
|
|
|
|
|
|
|
cpu.v[cpu.opcode.capture!(8, 12)] += cpu.opcode.capture!(0, 8);
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x8000:
|
|
|
|
|
|
|
|
auto x = cpu.opcode.capture!(8, 12);
|
|
|
|
auto y = cpu.opcode.capture!(4, 8);
|
|
|
|
|
|
|
|
switch (cpu.opcode & 0x000F) {
|
|
|
|
|
|
|
|
case 0x0000: // 0x8XY0 Sets VX to the value of VY.
|
|
|
|
|
|
|
|
cpu.v[x] = cpu.v[y];
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x0001: // 0x8XY1 Sets VX to VX or VY.
|
|
|
|
|
|
|
|
cpu.v[x] = cpu.v[x] | cpu.v[y];
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x0002: // 0x8XY2 Sets VX to VX and VY.
|
|
|
|
|
|
|
|
cpu.v[x] = cpu.v[x] & cpu.v[y];
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x0003: // 0x8XY3 Sets VX to VX xor VY.
|
|
|
|
|
|
|
|
cpu.v[x] = cpu.v[x] ^ cpu.v[y];
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x0004: // 0x8XY4 Adds VY to VX. VF is set to 1 when there's a carry, and to 0 when there isn't.
|
|
|
|
|
|
|
|
cpu.v[x] += cpu.v[y]; //TODO carry flag
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x0005: // 0x8XY5 VY is subtracted from VX. VF is set to 0 when there's a borrow, and 1 when there isn't.
|
|
|
|
|
|
|
|
cpu.v[x] -= cpu.v[y]; //TODO borrow flag
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x0006: // 0x8XY6 Shifts VX right by one. VF is set to the value of the least significant bit of VX before the shift.
|
|
|
|
|
|
|
|
auto vx = cpu.v[x];
|
|
|
|
cpu.v[0xF] = (vx & 0b10000000) >> 7;
|
|
|
|
cpu.v[x] >>= 1;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x0007: // 0x8XY7 Sets VX to VY minus VX. VF is set to 0 when there's a borrow, and 1 when there isn't.
|
|
|
|
|
|
|
|
cpu.v[x] = cast(Register)(cpu.v[y] - cpu.v[x]); //TODO borrow flag
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x000E: // 0x8XYE Shifts VX left by one. VF is set to the value of the most significant bit of VX before the shift.
|
|
|
|
|
|
|
|
auto vx = cpu.v[x];
|
|
|
|
cpu.v[0xF] = (vx & 0b10000000) >> 7;
|
|
|
|
cpu.v[x] <<= 1;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
default: //unhandled for some reason
|
|
|
|
writefln("unknown opcode: 0x%x", cpu.opcode);
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x9000: // 0x9XYO Skips the next instruction if VX doesn't equal VY.
|
|
|
|
|
|
|
|
if (cpu.v[cpu.opcode.capture!(8, 12)] != cpu.v[cpu.opcode.capture!(4, 8)]) {
|
|
|
|
pc_target += 2; //do skip yes
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0xA000: // 0xANNN Sets I to the address NNN.
|
|
|
|
|
|
|
|
cpu.i = cpu.opcode.capture!(0, 12);
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0xB000: // 0xBNNN Jumps to the address NNN plus V0.
|
|
|
|
|
|
|
|
pc_target = cast(ubyte)cpu.opcode.capture!(0, 12) + cpu.v[0x0];
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0xC000: // 0xCXNN Sets VX to the result of a bitwise and operation on a random number and NN.
|
|
|
|
|
|
|
|
import std.random : uniform;
|
|
|
|
auto x = cpu.opcode.capture!(8, 12);
|
|
|
|
cpu.v[x] = uniform(Register.min, Register.max) & cpu.opcode.capture!(0, 8);
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
// 0xDXYN
|
|
|
|
// Sprites stored in memory at location in index register (I), 8bits wide.
|
|
|
|
// Wraps around the screen. If when drawn, clears a pixel, register VF is set to 1 otherwise it is zero.
|
|
|
|
// All drawing is XOR drawing (i.e. it toggles the screen pixels).
|
|
|
|
// Sprites are drawn starting at position VX, VY. N is the number of 8bit rows that need to be drawn.
|
|
|
|
// If N is greater than 1, second line continues at position VX, VY+1, and so on.
|
|
|
|
case 0xD000:
|
|
|
|
|
|
|
|
auto spr_addr = cpu.i;
|
|
|
|
auto x = cpu.opcode.capture!(8, 12);
|
|
|
|
auto y = cpu.opcode.capture!(4, 8);
|
|
|
|
auto n = cpu.opcode.capture!(0, 4);
|
|
|
|
ushort pixel;
|
|
|
|
|
|
|
|
foreach(int row; 0 .. n) {
|
|
|
|
|
|
|
|
pixel = ram[spr_addr + row];
|
|
|
|
|
|
|
|
foreach (int col; 0 .. 8) {
|
|
|
|
if ((pixel & (0x80 >> col)) != 0) {
|
|
|
|
if (screen_buf[(x + col + ((y + row) * 64))] == 1) {
|
|
|
|
cpu.v[0xF] = 1;
|
|
|
|
}
|
|
|
|
screen_buf[x + row + ((y + col) * 64)] ^= 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
draw_flag = true;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0xE000:
|
|
|
|
|
|
|
|
auto x = cpu.opcode.capture!(8, 12);
|
|
|
|
auto key = cpu.v[x];
|
|
|
|
|
|
|
|
switch (cpu.opcode & 0x000F) {
|
|
|
|
|
|
|
|
case 0x000E: // 0xEX9E Skips the next instruction if the key stored in VX is pressed.
|
|
|
|
writefln("0xEXA1: skip instruction if VX not pressed: %x", key);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x0001: // 0xEXA1 Skips the next instruction if the key stored in VX isn't pressed.
|
|
|
|
writefln("0xEXA1: skip instruction if VX not pressed: %x", key);
|
|
|
|
break;
|
|
|
|
|
|
|
|
default: //unhandled for some reason
|
|
|
|
writefln("unknown opcode: 0x%x", cpu.opcode);
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0xF000:
|
|
|
|
|
|
|
|
switch (cpu.opcode & 0x00FF) {
|
|
|
|
|
|
|
|
case 0x0007: // 0xFX07 Sets VX to the value of the delay timer.
|
|
|
|
|
|
|
|
cpu.v[cpu.opcode.capture!(8, 12)] = cpu.delay_timer;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x000A: // 0xFX0A A key press is awaited, and then stored in VX.
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x0015: // 0xFX15 Sets the delay timer to VX.
|
|
|
|
|
|
|
|
cpu.delay_timer = cpu.v[cpu.opcode.capture!(8, 12)];
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x0018: // 0xFX18 Sets the sound timer to VX.
|
|
|
|
|
|
|
|
cpu.sound_timer = cpu.v[cpu.opcode.capture!(8, 12)];
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x001E: // 0xFX1E Adds VX to I.
|
|
|
|
|
|
|
|
cpu.i += cpu.v[cpu.opcode.capture!(8, 12)];
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x0029: // 0xFX29 Sets I to the location of the sprite for the character in VX.
|
|
|
|
|
|
|
|
auto vx = cpu.v[cpu.opcode.capture!(8, 12)];
|
|
|
|
ushort char_addr = 0x200 + (vx * 40); // base of char sprites + value of vx * bits per character
|
|
|
|
cpu.i = char_addr;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
// 0xFX33 Stores the Binary-coded decimal representation of VX,
|
|
|
|
// with the most significant of three digits at the address in I,
|
|
|
|
// the middle digit at I plus 1, and the least significant digit at I plus 2.
|
|
|
|
case 0x0033:
|
|
|
|
|
|
|
|
auto vx = cpu.v[cpu.opcode.capture!(8, 12)];
|
|
|
|
ram[cpu.i] = vx / 100;
|
|
|
|
ram[cpu.i + 1] = (vx / 10) % 10;
|
|
|
|
ram[cpu.i + 2] = (vx % 100) % 10;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x0055: // 0xFX55 Stores V0 to VX in memory starting at address I.
|
|
|
|
|
|
|
|
auto addr = cpu.i;
|
|
|
|
foreach (reg; cpu.v) {
|
|
|
|
ram[addr++] = reg;
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x0065: // 0xFX65 Fills V0 to VX with values from memory starting at address I.
|
|
|
|
|
|
|
|
auto addr = cpu.i;
|
|
|
|
foreach (ref reg; cpu.v) {
|
|
|
|
reg = ram[addr++];
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
default: //unhandled for some reason
|
|
|
|
writefln("unknown opcode: 0x%x", cpu.opcode);
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
writefln("unknown opcode: 0x%x", cpu.opcode);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/* now update pc and timer registers. */
|
|
|
|
|
|
|
|
cpu.pc = pc_target;
|
|
|
|
|
|
|
|
if (cpu.delay_timer != 0u) {
|
|
|
|
--cpu.delay_timer;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cpu.sound_timer != 0u) {
|
|
|
|
|
|
|
|
if (cpu.sound_timer == 1) {
|
|
|
|
writefln("beep!");
|
|
|
|
}
|
|
|
|
|
|
|
|
--cpu.sound_timer;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} // step
|
|
|
|
|
2018-06-22 23:54:39 +01:00
|
|
|
void handle_event(ref SDL_Event ev) {
|
|
|
|
|
|
|
|
} // handle_event
|
|
|
|
|
2018-06-22 20:51:16 +01:00
|
|
|
void tick() {
|
|
|
|
|
|
|
|
if (run_flag) {
|
|
|
|
step();
|
|
|
|
}
|
|
|
|
|
|
|
|
} // tick
|
|
|
|
|
|
|
|
void draw() {
|
|
|
|
|
|
|
|
if (draw_flag) {
|
|
|
|
// update buffer with new pixel data
|
|
|
|
draw_flag = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// glPixelZoom(rt.width / 64, rt.height / 32);
|
|
|
|
// glDrawPixels(64, 32, GL_RGB, GL_UNSIGNED_BYTE, screen_data.ptr);
|
|
|
|
|
|
|
|
} // draw
|
|
|
|
|
|
|
|
} // Emulator
|
|
|
|
|
|
|
|
void load_libs() {
|
|
|
|
|
|
|
|
import glad.gl.loader;
|
|
|
|
|
|
|
|
DerelictSDL2.load();
|
2018-06-22 23:54:39 +01:00
|
|
|
DerelictImgui.load();
|
2018-06-22 20:51:16 +01:00
|
|
|
auto status = gladLoadGL();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void init_libs() {
|
|
|
|
|
|
|
|
SDL_Init(SDL_INIT_VIDEO);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void setup_imgui() {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
|
|
|
load_libs();
|
|
|
|
init_libs();
|
|
|
|
|
|
|
|
Emulator emu;
|
|
|
|
emu.create();
|
|
|
|
emu.run();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Emulator {
|
|
|
|
|
|
|
|
bool running;
|
|
|
|
|
|
|
|
Window window;
|
|
|
|
Chip8 chip8;
|
|
|
|
|
|
|
|
void create() {
|
|
|
|
|
|
|
|
// create window
|
|
|
|
window.create_window(640, 480);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void handle_events() {
|
|
|
|
|
|
|
|
SDL_Event event;
|
|
|
|
while (SDL_PollEvent(&event)) {
|
2018-06-22 23:54:39 +01:00
|
|
|
|
|
|
|
chip8.handle_event(event);
|
|
|
|
|
2018-06-22 20:51:16 +01:00
|
|
|
switch (event.type) with (SDL_EventType) {
|
|
|
|
case SDL_QUIT: {
|
|
|
|
running = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default: {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2018-06-22 23:54:39 +01:00
|
|
|
|
2018-06-22 20:51:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
} // handle_events
|
|
|
|
|
|
|
|
void run() {
|
|
|
|
|
|
|
|
running = true;
|
|
|
|
|
|
|
|
while (running) {
|
|
|
|
handle_events();
|
|
|
|
tick();
|
|
|
|
draw();
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void tick() {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void draw() {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|