Browse Source
* Passes all of the tests I've run against it and plays a bunch of games! * Kind of a bad sound implementation, but it's good enough for CHIP-8. ;)master
Ian Burgmyer
4 years ago
16 changed files with 705 additions and 37 deletions
@ -0,0 +1,264 @@
|
||||
/* Chip8.Ops.cpp
|
||||
* |
||||
* CHIP-8 opcodes. |
||||
*/ |
||||
|
||||
#include <iomanip> |
||||
#include <sstream> |
||||
|
||||
#include "../../PlipEmulationException.h" |
||||
|
||||
#include "Chip8.h" |
||||
|
||||
namespace Plip::Cpu { |
||||
void Chip8::Op0NNN(uint16_t address) { |
||||
// RCA 1802 routine. Not implemented.
|
||||
} |
||||
|
||||
void Chip8::Op00E0() { |
||||
// Clears the screen.
|
||||
for(auto i = 0; i < VideoSize; i++) |
||||
m_videoBuffer[i] = 0; |
||||
} |
||||
|
||||
void Chip8::Op00EE() { |
||||
// Returns from a subroutine.
|
||||
if(m_sp == 0) { |
||||
std::stringstream ex; |
||||
ex << "stack underflow\n\n" << DumpRegisters(); |
||||
throw PlipEmulationException(ex.str().c_str()); |
||||
} |
||||
|
||||
m_pc = m_stack[--m_sp]; |
||||
} |
||||
|
||||
void Chip8::Op1NNN(uint16_t address) { |
||||
// Jumps to an address.
|
||||
m_pc = address; |
||||
} |
||||
|
||||
void Chip8::Op2NNN(uint16_t address) { |
||||
// Calls a subroutine.
|
||||
if(m_sp >= StackSize) { |
||||
std::stringstream ex; |
||||
ex << "stack overflow\n\n" << DumpRegisters(); |
||||
throw PlipEmulationException(ex.str().c_str()); |
||||
} |
||||
|
||||
m_stack[m_sp++] = m_pc; |
||||
m_pc = address; |
||||
} |
||||
|
||||
void Chip8::Op3XNN(uint8_t reg, uint8_t value) { |
||||
// Skips the next instruction if reg == value.
|
||||
if(m_reg[reg] == value) m_pc += 2; |
||||
} |
||||
|
||||
void Chip8::Op4XNN(uint8_t reg, uint8_t value) { |
||||
// Skips the next instruction if reg != value.
|
||||
if(m_reg[reg] != value) m_pc += 2; |
||||
} |
||||
|
||||
void Chip8::Op5XY0(uint8_t left, uint8_t right) { |
||||
// Skips the next instruction if left == right.
|
||||
if(m_reg[left] == m_reg[right]) m_pc += 2; |
||||
} |
||||
|
||||
void Chip8::Op6XNN(uint8_t reg, uint8_t value) { |
||||
// Sets reg to value.
|
||||
m_reg[reg] = value; |
||||
} |
||||
|
||||
void Chip8::Op7XNN(uint8_t reg, uint8_t value) { |
||||
// Adds value to reg.
|
||||
m_reg[reg] += value; |
||||
} |
||||
|
||||
void Chip8::Op8XYO(uint8_t left, uint8_t right, uint8_t op) { |
||||
uint8_t old; |
||||
|
||||
// Various bitwise and math operations.
|
||||
switch(op) { |
||||
case 0x0: |
||||
// left = right
|
||||
m_reg[left] = m_reg[right]; |
||||
break; |
||||
|
||||
case 0x1: |
||||
// left |= right
|
||||
m_reg[left] |= m_reg[right]; |
||||
break; |
||||
|
||||
case 0x2: |
||||
// left &= right
|
||||
m_reg[left] &= m_reg[right]; |
||||
break; |
||||
|
||||
case 0x3: |
||||
// left ^= right
|
||||
m_reg[left] ^= m_reg[right]; |
||||
break; |
||||
|
||||
case 0x4: |
||||
// left += right, VF = 1 on carry, otherwise 0
|
||||
old = m_reg[left]; |
||||
m_reg[left] += m_reg[right]; |
||||
m_reg[0xF] = old > m_reg[left] ? 1 : 0; |
||||
break; |
||||
|
||||
case 0x5: |
||||
// left -= right, VF = 1 on borrow, otherwise 0
|
||||
old = m_reg[left]; |
||||
m_reg[left] -= m_reg[right]; |
||||
m_reg[0xF] = old < m_reg[left] ? 0 : 1; |
||||
break; |
||||
|
||||
case 0x6: |
||||
// left >>= 1, VF = LSB of right before shift
|
||||
m_reg[0xF] = m_reg[left] & 0x1; |
||||
m_reg[left] >>= 1; |
||||
break; |
||||
|
||||
case 0x7: |
||||
// left = right - left, VF = 1 on borrow, otherwise 0
|
||||
m_reg[left] = m_reg[right] - m_reg[left]; |
||||
m_reg[0xF] = m_reg[right] < m_reg[left] ? 0 : 1; |
||||
break; |
||||
|
||||
case 0xE: |
||||
// left <<= 1, VF = MSB of right before shift
|
||||
m_reg[0xF] = (m_reg[left] & 0x80) > 0 ? 1 : 0; |
||||
m_reg[left] <<= 1; |
||||
break; |
||||
|
||||
default: break; |
||||
} |
||||
} |
||||
|
||||
void Chip8::Op9XY0(uint8_t left, uint8_t right) { |
||||
// Skips the next instruction if left != right.
|
||||
if(m_reg[left] != m_reg[right]) m_pc += 2; |
||||
} |
||||
|
||||
void Chip8::OpANNN(uint16_t address) { |
||||
// Sets i to address.
|
||||
m_i = address; |
||||
} |
||||
|
||||
void Chip8::OpBNNN(uint16_t address) { |
||||
// Jumps to address + V0.
|
||||
m_pc = address + m_reg[0]; |
||||
} |
||||
|
||||
void Chip8::OpCXNN(uint8_t reg, uint8_t value) { |
||||
// Sets reg to random(0-255) & value.
|
||||
std::uniform_int_distribution<int> num(0, 255); |
||||
m_reg[reg] = num(m_rng) & value; |
||||
} |
||||
|
||||
void Chip8::OpDXYN(uint8_t xReg, uint8_t yReg, uint8_t size) { |
||||
// Draws an (8 x size) sprite at address I to (xReg, yReg).
|
||||
auto sprite = new uint64_t[size]; |
||||
auto x = m_reg[xReg]; |
||||
auto y = m_reg[yReg]; |
||||
|
||||
for(auto i = 0; i < size; i++) { |
||||
// Push sprites all the way to the highest bits, then
|
||||
// bump them to the right as much as necessary.
|
||||
sprite[i] = ((uint64_t)m_memory->GetByte(m_i + i) << 56) >> x; |
||||
} |
||||
|
||||
m_reg[0xF] = 0; |
||||
for(auto i = 0; i < size; i++) { |
||||
if(y + i >= VideoSize) return; // Don't attempt to write past maxY.
|
||||
|
||||
uint64_t oldRow = m_videoBuffer[y + i]; |
||||
m_videoBuffer[y + i] ^= sprite[i]; |
||||
|
||||
// Invert the old row, OR it with the new row, and invert the result.
|
||||
// If it's greater than 0, a bit has been turned off.
|
||||
oldRow = ~oldRow; |
||||
if(~(oldRow | m_videoBuffer[y + i]) > 0) |
||||
m_reg[0xF] = 1; |
||||
} |
||||
} |
||||
|
||||
void Chip8::OpEXOO(uint8_t reg, uint8_t op) { |
||||
// Input functions.
|
||||
switch(op) { |
||||
case 0x9E: |
||||
// Skips the next instruction if the key in reg is pressed.
|
||||
if(m_input->GetInput(m_reg[reg]).digital) m_pc += 2; |
||||
break; |
||||
|
||||
case 0xA1: |
||||
// Skips the next instruction if the key in reg is released.
|
||||
if(!m_input->GetInput(m_reg[reg]).digital) m_pc += 2; |
||||
break; |
||||
|
||||
default: break; |
||||
} |
||||
} |
||||
|
||||
void Chip8::OpFXOO(uint8_t reg, uint8_t op) { |
||||
std::stringstream ss; |
||||
std::string str; |
||||
|
||||
// Various other instructions.
|
||||
switch(op) { |
||||
case 0x07: |
||||
// Store the value of the delay timer in reg.
|
||||
m_reg[reg] = m_timerDelay; |
||||
break; |
||||
|
||||
case 0x0A: |
||||
// Wait for a keypress and store the result in reg.
|
||||
m_waitForKey = true; |
||||
m_keyRegister = reg; |
||||
break; |
||||
|
||||
case 0x15: |
||||
// Set the delay timer to reg.
|
||||
m_timerDelay = m_reg[reg]; |
||||
break; |
||||
|
||||
case 0x18: |
||||
// Set the audio timer to reg.
|
||||
m_timerAudio = m_reg[reg]; |
||||
break; |
||||
|
||||
case 0x1E: |
||||
// Add reg to I.
|
||||
m_i += m_reg[reg]; |
||||
break; |
||||
|
||||
case 0x29: |
||||
// Set I to the address of the charset value in reg.
|
||||
m_i = m_charsetAddress + (m_reg[reg] * 5); |
||||
break; |
||||
|
||||
case 0x33: |
||||
// Stores reg as a binary-coded decimal, starting at I.
|
||||
ss << std::setfill('0') << std::setw(3) << std::to_string(m_reg[reg]); |
||||
str = ss.str(); |
||||
for(auto i = 0; i < 3; i++) |
||||
m_memory->SetByte(m_i + i, str[i] - '0'); |
||||
break; |
||||
|
||||
case 0x55: |
||||
// Stores the values of V0 to reg, inclusive, in I and up.
|
||||
for(auto i = 0; i < reg + 1; i++) |
||||
m_memory->SetByte(m_i + i, m_reg[i]); |
||||
break; |
||||
|
||||
case 0x65: |
||||
// Fills registers V0 to reg, inclusive, with the values in
|
||||
// memory starting with I.
|
||||
for(auto i = 0; i < reg + 1; i++) |
||||
m_reg[i] = m_memory->GetByte(m_i + i); |
||||
break; |
||||
|
||||
default: break; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,113 @@
|
||||
/* Chip8.cpp
|
||||
* |
||||
* An implementation of a CHIP-8 CPU. |
||||
*/ |
||||
|
||||
#include <iomanip> |
||||
#include <sstream> |
||||
|
||||
#include "Chip8.h" |
||||
#include "../../PlipUtility.h" |
||||
|
||||
namespace Plip::Cpu { |
||||
Chip8::Chip8(long hz, PlipMemoryMap *memoryMap, uint16_t charset, Plip::PlipInput *input) |
||||
: PlipCpu(hz, memoryMap) { |
||||
m_charsetAddress = charset; |
||||
m_input = input; |
||||
m_videoBuffer = new uint64_t[VideoSize] {}; |
||||
} |
||||
|
||||
void Chip8::Cycle() { |
||||
auto inst = Fetch(); |
||||
auto left = GetReg1(inst); |
||||
auto right = GetReg2(inst); |
||||
auto val = GetValue(inst); |
||||
auto addr = GetAddress(inst); |
||||
|
||||
if(m_waitForKey) { |
||||
for(uint8_t i = 0; i < 0x10; i++) { |
||||
if(!m_input->GetInput(i).digital) continue; |
||||
|
||||
m_waitForKey = false; |
||||
m_reg[m_keyRegister] = i; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if(m_waitForKey) return; |
||||
|
||||
switch(inst & 0xF000) { |
||||
case 0x0000: |
||||
if(val == 0xE0) |
||||
Op00E0(); |
||||
else if(val == 0xEE) |
||||
Op00EE(); |
||||
else |
||||
Op0NNN(addr); |
||||
break; |
||||
|
||||
case 0x1000: Op1NNN(addr); break; |
||||
case 0x2000: Op2NNN(addr); break; |
||||
case 0x3000: Op3XNN(left, val); break; |
||||
case 0x4000: Op4XNN(left, val); break; |
||||
case 0x5000: Op5XY0(left, right); break; |
||||
case 0x6000: Op6XNN(left, val); break; |
||||
case 0x7000: Op7XNN(left, val); break; |
||||
case 0x8000: Op8XYO(left, right, inst & 0xF); break; |
||||
case 0x9000: Op9XY0(left, right); break; |
||||
case 0xA000: OpANNN(addr); break; |
||||
case 0xB000: OpBNNN(addr); break; |
||||
case 0xC000: OpCXNN(left, val); break; |
||||
case 0xD000: OpDXYN(left, right, inst & 0xF); break; |
||||
case 0xE000: OpEXOO(left, val); break; |
||||
case 0xF000: OpFXOO(left, val); break; |
||||
} |
||||
} |
||||
|
||||
void Chip8::DelayTimer() { |
||||
if(m_timerAudio > 0) m_timerAudio--; |
||||
if(m_timerDelay > 0) m_timerDelay--; |
||||
} |
||||
|
||||
std::string Chip8::DumpRegisters() { |
||||
using util = Plip::PlipUtility; |
||||
const char *regLabel = "0123456789ABCDEF"; |
||||
std::stringstream dump; |
||||
|
||||
dump << util::DumpValue(" PC", m_pc, 3) << '\n' |
||||
<< util::DumpValue(" SP", m_sp, StackSize > 16 ? 2 : 1) << "\n\n" |
||||
<< util::DumpValue("Audio", m_timerAudio, 2) << '\n' |
||||
<< util::DumpValue("Delay", m_timerDelay, 2) << "\n\n" |
||||
<< util::DumpValue(" I", m_i, 4) << '\n'; |
||||
|
||||
for(auto i = 0; i < 16; i++) { |
||||
std::stringstream label; |
||||
label << " V" << regLabel[i]; |
||||
dump << util::DumpValue(label.str(), m_reg[i], 2) << '\n'; |
||||
} |
||||
|
||||
dump << "\n\tStack: "; |
||||
for(auto i = 0; i < StackSize; i++) { |
||||
if(i == m_sp) dump << "["; |
||||
dump << util::FormatHex(m_stack[i], 3); |
||||
if(i == m_sp) dump << "]"; |
||||
dump << ' '; |
||||
} |
||||
|
||||
return dump.str(); |
||||
} |
||||
|
||||
void Chip8::Reset(uint32_t pc) { |
||||
m_timerAudio = 0; |
||||
m_timerDelay = 0; |
||||
m_sp = 0; |
||||
m_pc = pc; |
||||
m_i = 0; |
||||
|
||||
for(auto ® : m_reg) |
||||
reg = 0; |
||||
|
||||
for(auto &stack : m_stack) |
||||
stack = 0; |
||||
} |
||||
} |
@ -0,0 +1,79 @@
|
||||
/* Chip8.h
|
||||
* |
||||
* An implementation of a CHIP-8 CPU. |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <random> |
||||
|
||||
#include "../PlipCpu.h" |
||||
#include "../../Input/PlipInput.h" |
||||
#include "../../Video/PlipVideo.h" |
||||
|
||||
namespace Plip::Cpu { |
||||
class Chip8 : public PlipCpu { |
||||
public: |
||||
Chip8(long hz, PlipMemoryMap* memoryMap, uint16_t charset, Plip::PlipInput *input); |
||||
|
||||
void Cycle() override; |
||||
void DelayTimer(); |
||||
uint64_t* GetVideo() { return m_videoBuffer; } |
||||
[[nodiscard]] bool IsAudioPlaying() const { return m_timerAudio >= 2; } |
||||
void Reset(uint32_t pc) override; |
||||
|
||||
static const int VideoSize = 32; // 64 x 32
|
||||
|
||||
private: |
||||
std::string DumpRegisters(); |
||||
|
||||
inline uint16_t Fetch() { |
||||
uint8_t high = m_memory->GetByte(m_pc++); |
||||
uint8_t low = m_memory->GetByte(m_pc++); |
||||
return (high << 8) + low; |
||||
} |
||||
|
||||
static inline uint16_t GetAddress(uint16_t b) { return b & 0xFFF; } |
||||
static inline uint8_t GetReg1(uint16_t b) { return (b >> 8) & 0xF; } |
||||
static inline uint8_t GetReg2(uint16_t b) { return (b >> 4) & 0xF; } |
||||
static inline uint8_t GetValue(uint16_t b) { return b & 0xFF; } |
||||
|
||||
void Op0NNN(uint16_t address); |
||||
void Op00E0(); |
||||
void Op00EE(); |
||||
void Op1NNN(uint16_t address); |
||||
void Op2NNN(uint16_t address); |
||||
void Op3XNN(uint8_t reg, uint8_t value); |
||||
void Op4XNN(uint8_t reg, uint8_t value); |
||||
void Op5XY0(uint8_t left, uint8_t right); |
||||
void Op6XNN(uint8_t reg, uint8_t value); |
||||
void Op7XNN(uint8_t reg, uint8_t value); |
||||
void Op8XYO(uint8_t left, uint8_t right, uint8_t op); |
||||
void Op9XY0(uint8_t left, uint8_t right); |
||||
void OpANNN(uint16_t address); |
||||
void OpBNNN(uint16_t address); |
||||
void OpCXNN(uint8_t reg, uint8_t value); |
||||
void OpDXYN(uint8_t xReg, uint8_t yReg, uint8_t size); |
||||
void OpEXOO(uint8_t reg, uint8_t op); |
||||
void OpFXOO(uint8_t reg, uint8_t op); |
||||
|
||||
static const int StackSize = 12; |
||||
|
||||
std::default_random_engine m_rng; |
||||
|
||||
Plip::PlipInput *m_input; |
||||
uint64_t *m_videoBuffer; |
||||
|
||||
uint16_t m_charsetAddress; |
||||
uint8_t m_timerAudio = 0; |
||||
uint8_t m_timerDelay = 0; |
||||
uint16_t m_sp = 0; |
||||
uint16_t m_pc = 0; |
||||
uint16_t m_i = 0; |
||||
uint8_t m_reg[16] {}; |
||||
uint16_t m_stack[StackSize] {}; |
||||
|
||||
bool m_waitForKey = false; |
||||
uint8_t m_keyRegister = 0; |
||||
}; |
||||
} |
@ -0,0 +1,26 @@
|
||||
/* PlipCpu.cpp
|
||||
* |
||||
* Defines a CPU implementation. |
||||
*/ |
||||
|
||||
#include "PlipCpu.h" |
||||
|
||||
namespace Plip::Cpu { |
||||
PlipCpu::PlipCpu(long hz, PlipMemoryMap* memoryMap) { |
||||
SetHz(hz); |
||||
m_memory = memoryMap; |
||||
} |
||||
|
||||
long PlipCpu::GetCycleTime() const { |
||||
return m_cycle; |
||||
} |
||||
|
||||
long PlipCpu::GetHz(long hz) const { |
||||
return m_hz; |
||||
} |
||||
|
||||
void PlipCpu::SetHz(long hz) { |
||||
m_hz = hz; |
||||
m_cycle = 1000000000 / hz; |
||||
} |
||||
} |
@ -0,0 +1,31 @@
|
||||
/* PlipCpu.h
|
||||
* |
||||
* Defines a CPU implementation. |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <string> |
||||
|
||||
#include "../Memory/PlipMemoryMap.h" |
||||
|
||||
namespace Plip::Cpu { |
||||
class PlipCpu { |
||||
public: |
||||
[[nodiscard]] long GetCycleTime() const; |
||||
[[nodiscard]] long GetHz(long hz) const; |
||||
void SetHz(long hz); |
||||
|
||||
virtual void Cycle() = 0; |
||||
virtual void Reset(uint32_t pc) = 0; |
||||
|
||||
protected: |
||||
PlipCpu(long hz, PlipMemoryMap* memoryMap); |
||||
|
||||
long m_hz {}; |
||||
Plip::PlipMemoryMap *m_memory; |
||||
|
||||
private: |
||||
long m_cycle {}; |
||||
}; |
||||
} |
@ -0,0 +1,17 @@
|
||||
/* PlipEmulationException.h
|
||||
* |
||||
* An exception that may occur during emulation. |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <stdexcept> |
||||
#include <string> |
||||
#include <utility> |
||||
|
||||
namespace Plip { |
||||
struct PlipEmulationException : std::runtime_error { |
||||
explicit PlipEmulationException(const char *message) |
||||
: std::runtime_error(message) {} |
||||
}; |
||||
} |
@ -0,0 +1,25 @@
|
||||
/* PlipUtility.cpp
|
||||
* |
||||
* Miscellaneous helper functions. |
||||
*/ |
||||
|
||||
#include <iomanip> |
||||
#include <sstream> |
||||
|
||||
#include "PlipUtility.h" |
||||
|
||||
namespace Plip { |
||||
std::string PlipUtility::DumpValue(const std::string &label, uintmax_t value, int precision) { |
||||
std::stringstream dump; |
||||
dump << '\t' << label << ": " << FormatHex(value, precision); |
||||
return dump.str(); |
||||
} |
||||
|
||||
std::string PlipUtility::FormatHex(uintmax_t value, int precision) { |
||||
std::stringstream fmt; |
||||
fmt << "0x" << std::uppercase << std::setfill('0') << std::setw(precision) |
||||
<< std::hex << value; |
||||
|
||||
return fmt.str(); |
||||
} |
||||
} |
@ -0,0 +1,14 @@
|
||||
/* PlipUtility.h
|
||||
* |
||||
* Miscellaneous helper functions. |
||||
*/ |
||||
|
||||
#include <string> |
||||
|
||||
namespace Plip { |
||||
class PlipUtility { |
||||
public: |
||||
static std::string DumpValue(const std::string &label, uintmax_t value, int precision); |
||||
static std::string FormatHex(uintmax_t value, int precision); |
||||
}; |
||||
} |
Loading…
Reference in new issue