From 587fda2d498321e954e1e080f1bb2bbb0205c0b9 Mon Sep 17 00:00:00 2001 From: Zoe <62722391+juls0730@users.noreply.github.com> Date: Fri, 7 Mar 2025 15:05:22 +0000 Subject: [PATCH] disassembler, assembler, bug fixes, and more --- .vscode/launch.json | 17 + Makefile | 9 +- README.md | 13 +- assembler/.clangd | 8 + assembler/assember.cpp | 1825 +++++++++++++++++++++++++++++++++ disassembler/disassembler.cpp | 533 ++++++++++ disassembler/main.cpp | 9 - examples/font2.asm | 401 ++++++++ examples/sink.asm | 44 + examples/syntax.asm | 299 ++++++ libs/reader.hpp | 12 +- src/main.cpp | 1062 ------------------- src/voidemu.cpp | 1156 +++++++++++++++++++++ test.sh | 1 + 14 files changed, 4307 insertions(+), 1082 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 assembler/.clangd create mode 100644 assembler/assember.cpp create mode 100644 disassembler/disassembler.cpp delete mode 100644 disassembler/main.cpp create mode 100644 examples/font2.asm create mode 100644 examples/sink.asm create mode 100644 examples/syntax.asm delete mode 100644 src/main.cpp create mode 100644 src/voidemu.cpp diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1002bc6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Debug assembler", + "type": "gdb", + "request": "launch", + "target": "./bin/assembler", + "arguments": "${workspaceRoot}/examples/syntax.asm ${workspaceRoot}/bin/test.ch8", + "cwd": "${workspaceRoot}", + "valuesFormatting": "parseText" + } + ] +} \ No newline at end of file diff --git a/Makefile b/Makefile index ac64c41..61c5bff 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,22 @@ CXX := g++ -CXXFLAGS := -Wall -Wextra -std=c++23 -g -Werror -Ilibs `sdl2-config --cflags` +CXXFLAGS := -Wall -Wextra -std=c++23 -g -Werror -Ilibs LDFLAGS := `sdl2-config --libs` BIN_DIR := bin -all: voidEmu disassembler +all: voidEmu assembler disassembler run: all ./bin/voidEmu $(FILE) +assembler: $(wildcard assembler/*.cpp) | $(BIN_DIR) + $(CXX) $(CXXFLAGS) $^ -o ${BIN_DIR}/$@ + disassembler: $(wildcard disassembler/*.cpp) | $(BIN_DIR) $(CXX) $(CXXFLAGS) $^ -o ${BIN_DIR}/$@ voidEmu: $(wildcard src/*.cpp) | $(BIN_DIR) - $(CXX) $(CXXFLAGS) $^ -o ${BIN_DIR}/$@ $(LDFLAGS) + $(CXX) $(CXXFLAGS) `sdl2-config --cflags` $^ -o ${BIN_DIR}/$@ $(LDFLAGS) $(BIN_DIR): mkdir -p $@ diff --git a/README.md b/README.md index bcdeeb4..9fa5017 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,13 @@ 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. - ## TODO - [X] Implement sound - [X] Implement keyboard input -- [ ] Fix games like Tetris and pong +- [X] Fix games like Tetris and pong +- [X] Implement a disassembler - [ ] Implement better e2e testing for visuals and other things -- [ ] Implement a disassembler - [ ] Get better debugging ## Why @@ -21,7 +19,10 @@ I wanted to learn how to use C++ and SDL, and I recently saw a youtube video lis * [Cowgod's Chip-8 Technical Reference](http://devernay.free.fr/hacks/chip8/C8TECH10.HTM) - [Chip-8 Test Suite](https://github.com/Timendus/chip8-test-suite) - +- [Wernsey's Chip-8 Assembler](https://github.com/wernsey/chip8), for syntax reference **only** + # License -This is free and unencumbered software released into the public domain, much to the detriment of my "heirs and successors". Unlicense everything \ No newline at end of file +All code in this repository that is not under The Unlicense has its license at the top of the file. + +This is free and unencumbered software released into the public domain, much to the detriment of my "heirs and successors". Unlicense everything. \ No newline at end of file diff --git a/assembler/.clangd b/assembler/.clangd new file mode 100644 index 0000000..67ebd52 --- /dev/null +++ b/assembler/.clangd @@ -0,0 +1,8 @@ +CompileFlags: + Add: + - -I../libs + - -Wall + - -Wextra + - -std=c++23 + - -g + - -Werror diff --git a/assembler/assember.cpp b/assembler/assember.cpp new file mode 100644 index 0000000..b544fd0 --- /dev/null +++ b/assembler/assember.cpp @@ -0,0 +1,1825 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BYTECODE_READER_IMPLEMENTATION +#include "reader.hpp" + +bool ishex(char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'); +} + +bool is_control(std::string &str) { + // pointer I + if (str.length() == 3 && str.starts_with("[") && str.ends_with("]")) { + if (tolower(str[1]) == 'i') { + return true; + } + } + + std::set special_registers = {"i", "k", "b", "f", "dt", "st"}; + if ((special_registers.contains(str))) { + return true; + } + + return false; +} + +bool is_register(std::string &str) { + std::transform(str.begin(), str.end(), str.begin(), ::tolower); + if ((str.starts_with("v") || str.starts_with("V")) && str.length() == 2 && + ishex(str[1])) { + return true; + } + + return is_control(str); +} + +bool is_base16(std::string &str) { + if (str.starts_with("0x")) { + for (char c : str.substr(2)) { + if (!ishex(c)) { + return false; + } + } + return true; + } + + return false; +} + +bool is_base2(std::string &str) { + if (str.starts_with("0b")) { + for (char c : str.substr(2)) { + if (c != '0' && c != '1') { + return false; + } + } + return true; + } + + return false; +} + +bool is_base10(std::string &str) { + for (char c : str) { + if (!isdigit(c)) { + return false; + } + return true; + } + + return false; +} + +bool is_numeric(std::string &str) { + return is_base16(str) || is_base2(str) || is_base10(str); +} + +bool is_arithmetic_operator(std::string &str) { + return str == "+" || str == "-" || str == "*" || str == "/"; +} + +size_t get_numeric(std::string &str) { + if (is_base16(str)) { + return std::stoi(&str[2], nullptr, 16); + } + + if (is_base2(str)) { + return std::stoi(&str[2], nullptr, 2); + } + + if (is_base10(str)) { + return std::stoi(str, nullptr, 10); + } + + fprintf(stderr, "Invalid numeric literal: %s\n", str.c_str()); + exit(1); +} + +uint8_t get_reg(std::string &str) { + if (str.length() != 2) { + fprintf(stderr, "Invalid register: %s\n", str.c_str()); + exit(1); + } + + if (tolower(str[0]) != 'v') { + fprintf(stderr, "Invalid register: %s\n", str.c_str()); + exit(1); + } + + return std::stoi(&str[1], nullptr, 16); +} + +// as with all hashing functions, there is a chance of collision, but we can +// pretend it's not an issue by means of mathematical improbability +constexpr uint32_t hash(const std::string data) noexcept { + uint32_t hash = 5381; + + for (const char &c : data) { + hash = ((hash << 5) + hash) + (uint8_t)c; + } + + return hash; +} + +bool is_directive(std::string &str) { + switch (hash(str)) { + case hash("text"): + case hash("offset"): + case hash("include"): + case hash("define"): + case hash("db"): + case hash("dw"): + return true; + default: + return false; + } +} + +bool is_reserved(std::string &str) { + return is_register(str) || is_directive(str) || is_numeric(str); +} + +class OperandNode; +class InstructionNode; +class LabelNode; +class RootNode; + +enum class ValueType { REGISTER, IMMEDIATE, STRING, ATIHMETIC, LABEL }; + +class OperandNode { + public: + ValueType type; + std::string value; + OperandNode(ValueType type, std::string value) : type(type), value(value) {} +}; + +enum class DirectiveType { DEFINE, TEXT, DB, DW, OFFSET, INCLUDE }; +class DirectiveNode { + public: + DirectiveType directive; + std::vector operands; + + DirectiveNode(const DirectiveType directive, + const std::vector &operands = {}) + : directive(directive), operands(operands) {} +}; + +class InstructionNode { + public: + std::string opcode; + std::vector operands; + + InstructionNode(const std::string &opcode, + const std::vector &operands = {}) + : opcode(opcode), operands(operands) {} +}; + +struct SectionElement { + enum class Type { Instruction, Directive }; + std::variant element; + Type type; + + SectionElement(Type t, InstructionNode instr) : element(instr), type(t) {} + + SectionElement(Type t, DirectiveNode dir) : element(dir), type(t) {} +}; + +class SectionNode { + public: + std::optional label_name; + std::vector elements; + + SectionNode(const std::string &name) : label_name(name) {} + SectionNode(std::optional name, + const std::vector &elements = {}) + : label_name(name), elements(elements) {} +}; + +class RootNode { + public: + std::vector sections; + // label name -> section index + std::unordered_map labels; + + RootNode(const std::vector §ions = {}) + : sections(sections) {} +}; + +enum class TokenType { + Label, + LabelDecleration, + Instruction, + Directive, + Register, + Immediate, + ArithmeticOperator, + String, + Comment, + EOFToken +}; + +bool is_operand(TokenType type) { + switch (type) { + case TokenType::Label: + case TokenType::Register: + case TokenType::Immediate: + case TokenType::String: + case TokenType::ArithmeticOperator: + case TokenType::Comment: + return true; + default: + return false; + } +} + +// Token structure +struct Token { + TokenType type; + std::string value; +}; + +class Lexer { + public: + Lexer(const std::string &input) : input(input), pos(0) {} + + Token get_next_token() { + std::string accum = ""; + bool is_operand = next_operand; + next_operand = false; + + // strip whitespace from the beginning of the line, but dont use the + // is_whitespace function, since that will strip away newlines, + // which are very important in terms of context for us + while (pos < input.size() && isspace(input[pos])) { + pos++; + } + + while (pos < input.size()) { + char current_char = input[pos++]; + + // if we find a comment, we make sure we havent reached the end of + // our current token, and if we havent, collect the commend and + // return the comment token + if (current_char == ';') { + if (!accum.empty()) { + // rewind so we dont consume the ; character + pos--; + break; + } + + next_operand = false; + + while (current_char != '\n' && pos < input.size()) { + accum += current_char; + current_char = input[pos++]; + } + + return Token{TokenType::Comment, accum}; + } + + if (current_char == ':') { + if (accum.empty()) { + fprintf(stderr, + "Expected label before ':' but got nothing\n"); + exit(1); + } + + if (is_reserved(accum)) { + fprintf(stderr, "Use of reserved token: %s\n", + accum.c_str()); + exit(1); + } + + next_operand = false; + + return Token{TokenType::LabelDecleration, accum}; + } + + if (current_char == '"') { + if (!accum.empty()) { + fprintf(stderr, "Expected string before '\"'\n"); + exit(1); + } + + current_char = input[pos++]; + while (current_char != '"' && pos < input.size()) { + if (current_char == '\\') { + current_char = input[pos++]; + switch (current_char) { + case 'a': + accum += '\a'; + break; + case 'b': + accum += '\b'; + break; + case 'e': + accum += 0x1b; + break; + case 'f': + accum += '\f'; + break; + case 'n': + accum += '\n'; + break; + case 'r': + accum += '\r'; + break; + case 't': + accum += '\t'; + break; + case 'v': + accum += '\v'; + break; + // TODO: hex escapes + default: + accum += current_char; + break; + } + + current_char = input[pos++]; + + continue; + } + + accum += current_char; + current_char = input[pos++]; + } + + return Token{TokenType::String, accum}; + } + + if (current_char == '\n') { + // we hit end of line, we are onto the next instruction + next_operand = false; + break; + } + + // if we find a delimiting character, we have reached the end of + // our current token + if (isspace(current_char) || current_char == ',') { + // next token is an operand + next_operand = true; + break; + } + + accum += current_char; + } + + if (pos >= input.size() && accum.empty()) { + return Token{TokenType::EOFToken, ""}; + } + + if (accum.empty()) { + return get_next_token(); + } + + if (is_arithmetic_operator(accum)) { + return Token{TokenType::ArithmeticOperator, accum}; + } + + if (is_directive(accum)) { + return Token{TokenType::Directive, accum}; + } + + if (is_numeric(accum)) { + return Token{TokenType::Immediate, accum}; + } + + if (is_register(accum)) { + return Token{TokenType::Register, accum}; + } + + if (is_operand) { + return Token{TokenType::Label, accum}; + } + + return Token{TokenType::Instruction, accum}; + } + + private: + std::string input; + size_t pos; + bool next_operand; +}; + +class Parser { + public: + Parser(Lexer &lexer) + : lexer(lexer), current_token(lexer.get_next_token()) {} + + RootNode parse() { + RootNode root; + std::optional cur_section = std::nullopt; + while (true) { + if (current_token.type == TokenType::EOFToken) { + break; + } + + if (current_token.type == TokenType::LabelDecleration) { + SectionNode new_section(current_token.value); + if (root.labels.contains(current_token.value)) { + fprintf(stderr, "Label %s already exists\n", + current_token.value.c_str()); + exit(1); + } + root.sections.push_back(new_section); + root.sections.back().label_name = current_token.value; + root.labels[current_token.value] = root.sections.size() - 1; + cur_section = + std::optional(&root.sections.back()); + current_token = lexer.get_next_token(); + continue; + } + + if (!cur_section.has_value()) { + root.sections.push_back(SectionNode(std::nullopt)); + cur_section = + std::optional(&root.sections.back()); + } + + if (current_token.type == TokenType::Instruction) { + // this will set the current token to the next token once it + // finds a token that is not an operand + cur_section.value()->elements.push_back( + {SectionElement::Type::Instruction, parse_instruction()}); + } else if (current_token.type == TokenType::Directive) { + // this will set the current token to the next token once it + // finds a token that is not an operand + // cur_section->directives.push_back(parse_directive(&pc)); + cur_section.value()->elements.push_back( + {SectionElement::Type::Directive, parse_directive()}); + } else { + current_token = lexer.get_next_token(); + } + } + return root; + } + + private: + Lexer &lexer; + Token current_token; + + InstructionNode parse_instruction() { + InstructionNode instruction = InstructionNode(current_token.value); + instruction.opcode = current_token.value; + current_token = lexer.get_next_token(); + + while (is_operand(current_token.type)) { + if (current_token.type == TokenType::Comment) { + current_token = lexer.get_next_token(); + continue; + } + // the token is either a label, a number, or a register + instruction.operands.push_back(parse_operand()); + current_token = lexer.get_next_token(); + } + + return instruction; + } + + DirectiveNode parse_directive() { + DirectiveType directive_type; + switch (hash(current_token.value)) { + case hash("define"): { + directive_type = DirectiveType::DEFINE; + break; + } + case hash("text"): { + directive_type = DirectiveType::TEXT; + break; + } + case hash("db"): { + directive_type = DirectiveType::DB; + break; + } + case hash("dw"): { + directive_type = DirectiveType::DW; + break; + } + case hash("offset"): { + directive_type = DirectiveType::OFFSET; + break; + } + case hash("include"): { + directive_type = DirectiveType::INCLUDE; + break; + } + } + DirectiveNode directive = DirectiveNode(directive_type); + current_token = lexer.get_next_token(); + + // directives require an operand + if (!is_operand(current_token.type)) { + fprintf(stderr, "Expected operand, found %s", + current_token.value.c_str()); + exit(1); + } + + while (is_operand(current_token.type)) { + if (current_token.type == TokenType::Comment) { + current_token = lexer.get_next_token(); + continue; + } + // the token is either a label, a number, or a register + directive.operands.push_back(parse_operand()); + current_token = lexer.get_next_token(); + } + + return directive; + } + + OperandNode parse_operand() { + switch (current_token.type) { + case TokenType::Label: + return OperandNode(ValueType::LABEL, current_token.value); + case TokenType::Register: + return OperandNode(ValueType::REGISTER, current_token.value); + case TokenType::Immediate: + return OperandNode(ValueType::IMMEDIATE, current_token.value); + case TokenType::String: + return OperandNode(ValueType::STRING, current_token.value); + case TokenType::ArithmeticOperator: + return OperandNode(ValueType::ATIHMETIC, current_token.value); + default: + fprintf(stderr, "Unexpected operand type: %d\n", + static_cast(current_token.type)); + exit(1); + } + } +}; + +struct Defines { + enum ValueType type; + std::string value; +}; + +class Assembler { + public: + uint8_t *assembler(RootNode root, int rom_fd) { + printf("Assembling...\n"); + std::map defines; + + // rom space labels + std::unordered_map labels; + + size_t pc = 0; + for (auto §ion : root.sections) { + if (section.label_name.has_value()) { + labels[section.label_name.value()] = pc; + } + + for (auto &element : section.elements) { + switch (element.type) { + case SectionElement::Type::Instruction: { + pc += 2; + break; + } + + case SectionElement::Type::Directive: { + DirectiveNode directive_node = + std::get(element.element); + for (auto &operand : directive_node.operands) { + if (operand.type == ValueType::LABEL) { + // check if the label is really a reference + // to a constant (ie a value defined by a + // defines directive) + if (defines.contains(operand.value)) { + operand.type = defines[operand.value].type; + operand.value = defines[operand.value].value; + } + } + } + switch (directive_node.directive) { + case DirectiveType::DEFINE: { + if (directive_node.operands.size() != 2) { + fprintf(stderr, "Expected 2 operands for define\n"); + exit(1); + } + + if (root.labels.contains( + directive_node.operands[0].value)) { + fprintf(stderr, + "Redecleration of label %s as " + "constant\n", + directive_node.operands[0].value.c_str()); + exit(1); + } + + if (defines.contains( + directive_node.operands[0].value)) { + // TODO: should we allow redeclaration of + // constants? + fprintf(stderr, "Redecleration of constant %s\n", + directive_node.operands[0].value.c_str()); + exit(1); + } + + if (directive_node.operands[1].type == + ValueType::LABEL) { + // "label" may be a constant + if (defines.contains( + directive_node.operands[0].value)) { + defines[directive_node.operands[0].value] = + defines.at( + directive_node.operands[0].value); + break; + } + } + + defines[directive_node.operands[0].value] = { + .type = directive_node.operands[1].type, + .value = directive_node.operands[1].value}; + break; + } + case DirectiveType::TEXT: { + if (directive_node.operands.size() != 1) { + fprintf(stderr, "Expected 1 operand for text\n"); + exit(1); + } + + pc += directive_node.operands[0].value.size() + 1; + break; + } + case DirectiveType::DB: { + if (directive_node.operands.size() == 0) { + fprintf(stderr, "Expected operands for db\n"); + exit(1); + } + + pc += directive_node.operands.size(); + break; + } + case DirectiveType::DW: { + if (directive_node.operands.size() == 0) { + fprintf(stderr, "Expected operands for dw\n"); + exit(1); + } + + pc += directive_node.operands.size() * 2; + break; + } + case DirectiveType::OFFSET: { + if (directive_node.operands.size() != 1) { + fprintf(stderr, "Expected 1 operand for offset\n"); + exit(1); + } + + size_t offset = + get_numeric(directive_node.operands[0].value); + if (directive_node.operands[0].type != + ValueType::IMMEDIATE) { + fprintf(stderr, "Expected immediate " + "operand for offset\n"); + exit(1); + } + + pc += offset; + break; + } + case DirectiveType::INCLUDE: { + fprintf(stderr, "extraneous include directive! This is " + "likely a compiler bug\n"); + exit(1); + } + } + } + } + } + } + + uint8_t *rom_buf = NULL; + rom_buf = (uint8_t *)calloc(pc, sizeof(uint8_t)); + if (rom_buf == NULL) { + fprintf(stderr, "Failed to allocate memory!\n"); + exit(1); + } + + pc = 0; + for (auto §ion : root.sections) { + for (auto &element : section.elements) { + switch (element.type) { + case SectionElement::Type::Instruction: { + InstructionNode instruction_node = + std::get(element.element); + std::vector flattened_operands; + // "flatten" opperands, aka, take things like labels and + // defines, and replace them with their real values, so that + // instructon dont need knowledge of these concepts + for (size_t i = 0; i < instruction_node.operands.size(); + i++) { + OperandNode &operand = instruction_node.operands[i]; + if (operand.type == ValueType::LABEL) { + // check if the label is really a reference + // to a constant (ie a value defined by a + // defines directive) + if (defines.contains(operand.value)) { + operand.type = defines[operand.value].type; + operand.value = defines[operand.value].value; + } + + // might seem redundant, but if we transformed the + // "label" into a label via defines, we need to + // transform the label into the position of the + // label, but if the defines is NOT a label, we dont + // want to crash out + if (operand.type == ValueType::LABEL) { + if (labels.contains(operand.value)) { + // the label points to a label we have + // already discovered, so, we can + // transform the label into a memory + // space address, ready for the + // instruction to use + operand.type = ValueType::IMMEDIATE; + operand.value = std::to_string( + labels[operand.value] + 0x200); + } else { + fprintf(stderr, "Label %s not found\n", + operand.value.c_str()); + exit(1); + } + } + } + + if (operand.type == ValueType::ATIHMETIC) { + // collect previous operand and next operand, and + // flatten the arithmetic + if (i == 0) { + fprintf(stderr, "expected immediate before " + "arithmetic operator\n"); + exit(1); + } + + if (i + 1 >= instruction_node.operands.size()) { + fprintf(stderr, + "expected 2 operands for arithmetic " + "operator, but got only %lu\n", + instruction_node.operands.size()); + exit(1); + } + + OperandNode prev_operand = + flattened_operands.back(); + flattened_operands.pop_back(); + OperandNode next_operand = + instruction_node.operands[i + 1]; + + if (prev_operand.type != ValueType::IMMEDIATE || + prev_operand.type != ValueType::IMMEDIATE) { + fprintf(stderr, + "Arithmetic can only be performed on " + "immediate operands.\n"); + exit(1); + } + + switch (hash(operand.value)) { + case hash("+"): + operand = OperandNode( + ValueType::IMMEDIATE, + std::to_string( + get_numeric(prev_operand.value) + + get_numeric(next_operand.value))); + break; + case hash("-"): + operand = OperandNode( + ValueType::IMMEDIATE, + std::to_string( + get_numeric(prev_operand.value) - + get_numeric(next_operand.value))); + break; + case hash("*"): + operand = OperandNode( + ValueType::IMMEDIATE, + std::to_string( + get_numeric(prev_operand.value) * + get_numeric(next_operand.value))); + break; + case hash("/"): + operand = OperandNode( + ValueType::IMMEDIATE, + std::to_string( + get_numeric(prev_operand.value) / + get_numeric(next_operand.value))); + break; + default: + fprintf(stderr, + "unexpected arithmetic operator: %s\n", + operand.value.c_str()); + } + + i += 1; + flattened_operands.push_back(operand); + continue; + } + + flattened_operands.push_back(operand); + } + + instruction_node.operands = flattened_operands; + + uint16_t instruction = 0; + switch (hash(instruction_node.opcode)) { + case hash("exit"): { + if (instruction_node.operands.size() != 1) { + fprintf(stderr, "Expected 1 operand for exit\n"); + exit(1); + } + + if (instruction_node.operands[0].type != + ValueType::IMMEDIATE) { + fprintf(stderr, "Expected immediate " + "operand for exit\n"); + exit(1); + } + + uint8_t byte = + get_numeric(instruction_node.operands[0].value); + if (byte > 0x0F) { + fprintf(stderr, + "Invalid exit code: %d, must be a " + "value " + "less that 0x10\n", + byte); + exit(1); + } + + instruction = 0x0010 | byte; + break; + } + case hash("cls"): { + instruction = 0x00E0; + break; + } + case hash("ret"): { + instruction = 0x00EE; + break; + } + case hash("sys"): { + if (instruction_node.operands.size() != 1) { + fprintf(stderr, + "Expected 1 operand for sys got %lu\n", + instruction_node.operands.size()); + exit(1); + } + + uint16_t address = + get_numeric(instruction_node.operands[0].value) & + 0xFFF; + instruction = 0x0000 | address; + break; + } + case hash("jp"): { + if (instruction_node.operands.size() == 2) { + if (instruction_node.operands[0].type != + ValueType::REGISTER || + get_reg(instruction_node.operands[0].value) != + 0x0) { + fprintf( + stderr, + "V0 expected for jp with two arguments\n"); + exit(1); + } + + if (instruction_node.operands[1].type != + ValueType::IMMEDIATE) { + fprintf(stderr, + "Expected immediate operand for jp\n"); + exit(1); + } + + uint16_t address = + get_numeric( + instruction_node.operands[1].value) & + 0xFFF; + + instruction = 0xB000 | address; + break; + } + + if (instruction_node.operands.size() != 1) { + fprintf(stderr, + "Expected 1 operand for jp got %lu\n", + instruction_node.operands.size()); + exit(1); + } + + if (instruction_node.operands[0].type != + ValueType::IMMEDIATE) { + fprintf( + stderr, + "Expected immediate operand for jp got %s\n", + instruction_node.operands[1].value.c_str()); + exit(1); + } + + uint16_t address = + get_numeric(instruction_node.operands[0].value) & + 0xFFF; + instruction = 0x1000 | address; + break; + } + case hash("call"): { + if (instruction_node.operands.size() != 1) { + fprintf(stderr, + "Expected 1 operand for call got %lu\n", + instruction_node.operands.size()); + exit(1); + } + + uint16_t address = + get_numeric(instruction_node.operands[0].value) & + 0xFFF; + instruction = 0x2000 | address; + break; + } + case hash("se"): { + if (instruction_node.operands.size() != 2) { + fprintf(stderr, + "Expected 2 operands for se got %lu\n", + instruction_node.operands.size()); + exit(1); + } + + if (instruction_node.operands[0].type != + ValueType::REGISTER) { + fprintf(stderr, + "Expected register operand for se\n"); + exit(1); + } + + if (is_control(instruction_node.operands[0].value)) { + fprintf(stderr, "Expected register operand for " + "se\n"); + exit(1); + } + + if (instruction_node.operands[1].type == + ValueType::IMMEDIATE) { + // se reg, nnn + instruction = + 0x3000 | + (get_reg(instruction_node.operands[0].value) + << 8) | + get_numeric(instruction_node.operands[1].value); + break; + } + if (instruction_node.operands[1].type == + ValueType::REGISTER) { + // se reg, reg + if (is_control( + instruction_node.operands[1].value)) { + fprintf(stderr, "Expected register operand for " + "se\n"); + exit(1); + } + instruction = + 0x5000 | + (get_reg(instruction_node.operands[0].value) + << 8) | + (get_reg(instruction_node.operands[1].value) + << 4); + break; + } + fprintf(stderr, "Expected register or immediate " + "operand for se\n"); + exit(1); + } + case hash("sne"): { + if (instruction_node.operands.size() != 2) { + fprintf(stderr, + "Expected 2 operands for sne got %lu\n", + instruction_node.operands.size()); + exit(1); + } + + if (instruction_node.operands[0].type != + ValueType::REGISTER) { + fprintf(stderr, + "Expected register operand for sne\n"); + exit(1); + } + + if (is_control(instruction_node.operands[0].value)) { + fprintf(stderr, "Expected register operand for " + "se\n"); + exit(1); + } + + if (instruction_node.operands[1].type == + ValueType::IMMEDIATE) { + // sne reg, nnn + instruction = + 0x4000 | + (get_reg(instruction_node.operands[0].value) + << 8) | + get_numeric(instruction_node.operands[1].value); + break; + } else if (instruction_node.operands[1].type == + ValueType::REGISTER) { + if (is_control( + instruction_node.operands[1].value)) { + fprintf(stderr, "Expected register operand for " + "se\n"); + exit(1); + } + + // sne reg, reg + instruction = + 0x9000 | + (get_reg(instruction_node.operands[0].value) + << 8) | + (get_reg(instruction_node.operands[1].value) + << 4); + break; + } + fprintf(stderr, "Expected register or immediate " + "operand for se\n"); + exit(1); + } + case hash("ld"): { + if (instruction_node.operands.size() != 2) { + fprintf(stderr, + "Expected 2 operands for ld got %lu\n", + instruction_node.operands.size()); + exit(1); + } + + if (instruction_node.operands[0].type != + ValueType::REGISTER) { + fprintf(stderr, + "Expected register operand for ld\n"); + exit(1); + } + + std::string reg0 = instruction_node.operands[0].value; + + if (is_control(reg0)) { + std::transform(reg0.begin(), reg0.end(), + reg0.begin(), ::tolower); + switch (hash(reg0)) { + case hash("i"): { + instruction = + 0xA000 | + (get_numeric( + instruction_node.operands[1].value) & + 0xFFF); + break; + } + case hash("k"): { + instruction = + 0xF00A | + (get_reg(instruction_node.operands[1].value) + << 8); + break; + } + case hash("f"): { + instruction = + 0xF029 | + (get_reg(instruction_node.operands[1].value) + << 8); + break; + } + case hash("b"): { + instruction = + 0xF033 | + (get_reg(instruction_node.operands[1].value) + << 8); + break; + } + case hash("dt"): { + instruction = + 0xF015 | + (get_reg(instruction_node.operands[1].value) + << 8); + break; + } + case hash("st"): { + instruction = + 0xF018 | + (get_reg(instruction_node.operands[1].value) + << 8); + break; + } + case hash("[i]"): { + instruction = + 0xF055 | + (get_reg(instruction_node.operands[1].value) + << 8); + break; + } + default: { + fprintf(stderr, + "Unknown control register: %s\n", + reg0.c_str()); + exit(1); + } + } + break; + } + + if (instruction_node.operands[1].type == + ValueType::IMMEDIATE) { + // ld reg, nnn + uint16_t val = + get_numeric(instruction_node.operands[1].value); + if (val > 0xFF) { + fprintf(stderr, + "Invalid immediate value for ld\n"); + exit(1); + } + instruction = + 0x6000 | + (get_reg(instruction_node.operands[0].value) + << 8) | + val; + break; + } + + // ld reg, control + if (instruction_node.operands[1].type == + ValueType::REGISTER) { + std::string reg1 = + instruction_node.operands[1].value; + if (is_control(reg1)) { + std::transform(reg1.begin(), reg1.end(), + reg1.begin(), ::tolower); + switch ( + hash(instruction_node.operands[1].value)) { + case hash("dt"): { + instruction = + 0xF007 | + (get_reg( + instruction_node.operands[0].value) + << 8); + break; + } + case hash("k"): { + instruction = + 0xF00A | + (get_reg( + instruction_node.operands[0].value) + << 8); + break; + } + case hash("[i]"): { + instruction = + 0xF065 | + (get_reg( + instruction_node.operands[0].value) + << 8); + break; + } + } + break; + } + + // ld reg, reg + instruction = + 0x8000 | + (get_reg(instruction_node.operands[0].value) + << 8) | + (get_reg(instruction_node.operands[1].value) + << 4); + break; + } + + fprintf(stderr, "Expected register or immediate " + "operand for se\n"); + exit(1); + break; + } + case hash("add"): { + if (instruction_node.operands.size() != 2) { + fprintf(stderr, "Expected 2 operands for add\n"); + exit(1); + } + if (instruction_node.operands[0].type != + ValueType::REGISTER) { + fprintf(stderr, + "Expected register operand for add\n"); + exit(1); + } + + if (is_control(instruction_node.operands[0].value)) { + // add I, reg + instruction = + 0xF01E | + (get_reg(instruction_node.operands[1].value) + << 8); + break; + } + + // add reg, nn + if (instruction_node.operands[1].type == + ValueType::REGISTER) { + instruction = + 0x8004 | + (get_reg(instruction_node.operands[0].value) + << 8) | + (get_reg(instruction_node.operands[1].value) + << 4); + break; + } + + instruction = + 0x7000 | + (get_reg(instruction_node.operands[0].value) << 8) | + get_numeric(instruction_node.operands[1].value); + break; + } + case hash("or"): { + if (instruction_node.operands.size() != 2) { + fprintf(stderr, + "Expected 2 operands for or, got %lu", + instruction_node.operands.size()); + exit(1); + } + + if (instruction_node.operands[0].type != + ValueType::REGISTER || + instruction_node.operands[1].type != + ValueType::REGISTER) { + fprintf(stderr, "Expected two registers for or " + "instruction\n"); + exit(1); + } + + instruction = + 0x8001 | + (get_reg(instruction_node.operands[0].value) << 8) | + (get_reg(instruction_node.operands[1].value) << 4); + break; + } + case hash("and"): { + if (instruction_node.operands.size() != 2) { + fprintf( + stderr, + "Expected two registers for and instruction\n"); + exit(1); + } + + if (instruction_node.operands[0].type != + ValueType::REGISTER || + instruction_node.operands[1].type != + ValueType::REGISTER) { + fprintf( + stderr, + "Expected two registers for and instruction\n"); + exit(1); + } + + instruction = + 0x8002 | + (get_reg(instruction_node.operands[0].value) << 8) | + (get_reg(instruction_node.operands[1].value) << 4); + break; + } + case hash("xor"): { + if (instruction_node.operands.size() != 2) { + fprintf( + stderr, + "Expected two registers for xor instruction\n"); + exit(1); + } + + if (instruction_node.operands[0].type != + ValueType::REGISTER || + instruction_node.operands[1].type != + ValueType::REGISTER) { + fprintf( + stderr, + "Expected two registers for xor instruction\n"); + exit(1); + } + + instruction = + 0x8003 | + (get_reg(instruction_node.operands[0].value) << 8) | + (get_reg(instruction_node.operands[1].value) << 4); + break; + } + case hash("sub"): { + if (instruction_node.operands.size() != 2) { + fprintf( + stderr, + "Expected two registers for sub instruction\n"); + exit(1); + } + + if (instruction_node.operands[0].type != + ValueType::REGISTER || + instruction_node.operands[1].type != + ValueType::REGISTER) { + fprintf( + stderr, + "Expected two registers for sub instruction\n"); + exit(1); + } + + instruction = + 0x8005 | + (get_reg(instruction_node.operands[0].value) << 8) | + (get_reg(instruction_node.operands[1].value) << 4); + break; + } + case hash("shr"): { + if (instruction_node.operands.size() == 1) { + if (instruction_node.operands[0].type != + ValueType::REGISTER) { + fprintf(stderr, + "Expected register operand for shr\n"); + exit(1); + } + + instruction = + 0x8006 | + (get_reg(instruction_node.operands[0].value) + << 8) | + (get_reg(instruction_node.operands[0].value) + << 4); + break; + } + + if (instruction_node.operands.size() != 2) { + fprintf( + stderr, + "Expected two registers for shr instruction\n"); + exit(1); + } + + if (instruction_node.operands[0].type != + ValueType::REGISTER || + instruction_node.operands[1].type != + ValueType::REGISTER) { + fprintf( + stderr, + "Expected two registers for shr instruction\n"); + exit(1); + } + + instruction = + 0x8006 | + (get_reg(instruction_node.operands[0].value) << 8) | + (get_reg(instruction_node.operands[1].value) << 4); + break; + } + case hash("subn"): { + if (instruction_node.operands.size() != 2) { + fprintf(stderr, "Expected two registers for subn " + "instruction\n"); + exit(1); + } + + if (instruction_node.operands[0].type != + ValueType::REGISTER || + instruction_node.operands[1].type != + ValueType::REGISTER) { + fprintf(stderr, "Expected two registers for subn " + "instruction\n"); + exit(1); + } + + instruction = + 0x8007 | + (get_reg(instruction_node.operands[0].value) << 8) | + (get_reg(instruction_node.operands[1].value) << 4); + break; + } + case hash("shl"): { + if (instruction_node.operands.size() == 1) { + if (instruction_node.operands[0].type != + ValueType::REGISTER) { + fprintf(stderr, + "Expected register operand for shl\n"); + exit(1); + } + + instruction = + 0x800E | + (get_reg(instruction_node.operands[0].value) + << 8) | + (get_reg(instruction_node.operands[0].value) + << 4); + break; + } + + if (instruction_node.operands.size() != 2) { + fprintf( + stderr, + "Expected two registers for shl instruction\n"); + exit(1); + } + + if (instruction_node.operands[0].type != + ValueType::REGISTER || + instruction_node.operands[1].type != + ValueType::REGISTER) { + fprintf( + stderr, + "Expected two registers for shl instruction\n"); + exit(1); + } + + instruction = + 0x800E | + (get_reg(instruction_node.operands[0].value) << 8) | + (get_reg(instruction_node.operands[1].value) << 4); + break; + } + case hash("rnd"): { + if (instruction_node.operands.size() != 2) { + fprintf(stderr, "Expected 2 operands for rnd\n"); + exit(1); + } + + if (instruction_node.operands[0].type != + ValueType::REGISTER) { + fprintf(stderr, + "Expected register operand for rnd\n"); + exit(1); + } + + if (instruction_node.operands[1].type != + ValueType::IMMEDIATE) { + fprintf(stderr, + "Expected immediate operand for rnd\n"); + exit(1); + } + + if (get_numeric(instruction_node.operands[1].value) > + 0xFF) { + fprintf(stderr, + "Invalid immediate value for rnd\n"); + exit(1); + } + + instruction = + 0xC000 | + (get_reg(instruction_node.operands[0].value) << 8) | + (get_numeric(instruction_node.operands[1].value) & + 0xFF); + break; + } + case hash("drw"): { + if (instruction_node.operands.size() != 3) { + fprintf(stderr, "Expected 3 operands for drw\n"); + exit(1); + } + + if (instruction_node.operands[0].type != + ValueType::REGISTER) { + fprintf(stderr, + "Expected register operand for drw\n"); + exit(1); + } + + if (instruction_node.operands[1].type != + ValueType::REGISTER) { + fprintf(stderr, + "Expected register operand for drw\n"); + exit(1); + } + + if (instruction_node.operands[2].type != + ValueType::IMMEDIATE) { + fprintf(stderr, + "Expected immediate operand for drw\n"); + exit(1); + } + + if (get_numeric(instruction_node.operands[2].value) > + 0xF) { + fprintf(stderr, + "Invalid immediate value for drw\n"); + exit(1); + } + + instruction = + 0xD000 | + (get_reg(instruction_node.operands[0].value) << 8) | + (get_reg(instruction_node.operands[1].value) << 4) | + (get_numeric(instruction_node.operands[2].value) & + 0xF); + break; + } + case hash("skp"): { + if (instruction_node.operands.size() != 1) { + fprintf(stderr, "Expected 1 operand for skp\n"); + exit(1); + } + + if (instruction_node.operands[0].type != + ValueType::REGISTER) { + fprintf(stderr, + "Expected register operand for skp\n"); + exit(1); + } + + instruction = + 0xE09E | + (get_reg(instruction_node.operands[0].value) << 8); + break; + } + case hash("sknp"): { + if (instruction_node.operands.size() != 1) { + fprintf(stderr, "Expected 1 operand for sknp\n"); + exit(1); + } + + if (instruction_node.operands[0].type != + ValueType::REGISTER) { + fprintf(stderr, + "Expected register operand for sknp\n"); + exit(1); + } + + instruction = + 0xE0A1 | + (get_reg(instruction_node.operands[0].value) << 8); + break; + } + case hash("hires"): { + // jp to 260 + instruction = 0x1000 | 0x260; + break; + } + default: { + fprintf(stderr, "Unhandled instruction %s!\n", + instruction_node.opcode.c_str()); + break; + } + } + + // le to be + rom_buf[pc] = (instruction >> 8) & 0xFF; + rom_buf[pc + 1] = instruction & 0xFF; + + pc += 2; + break; + } + case SectionElement::Type::Directive: { + DirectiveNode directive_node = + std::get(element.element); + for (auto &operand : directive_node.operands) { + if (operand.type == ValueType::LABEL) { + // check if the label is really a reference + // to a constant (ie a value defined by a + // defines directive) + if (defines.contains(operand.value)) { + operand.type = defines[operand.value].type; + operand.value = defines[operand.value].value; + } + } + } + + switch (directive_node.directive) { + case DirectiveType::DEFINE: { + // defines are preprocessed, so we can ignore them + break; + } + case DirectiveType::TEXT: { + if (directive_node.operands.size() != 1) { + fprintf(stderr, "Expected 1 operand for text\n"); + exit(1); + } + + if (directive_node.operands[0].type == + ValueType::LABEL) { + if (defines.contains( + directive_node.operands[0].value)) { + } + } + + if (directive_node.operands[0].type != + ValueType::STRING) { + fprintf(stderr, + "Expected string operand for text\n"); + exit(1); + } + + size_t len = + directive_node.operands[0].value.size() + 1; + + pc += len; + + // write stirng to rom including null terminator + memcpy(rom_buf + pc, + directive_node.operands[0].value.c_str(), len); + break; + } + case DirectiveType::DB: { + if (directive_node.operands.size() == 0) { + fprintf(stderr, "Expected operands for db\n"); + exit(1); + } + + for (auto &operand : directive_node.operands) { + uint8_t byte = get_numeric(operand.value); + if (operand.type != ValueType::IMMEDIATE) { + fprintf(stderr, "Expected immediate " + "operand for db\n"); + exit(1); + } + + rom_buf[pc] = byte; + pc += 1; + } + break; + } + case DirectiveType::DW: { + if (directive_node.operands.size() == 0) { + fprintf(stderr, "Expected operands for dw\n"); + exit(1); + } + + for (auto &operand : directive_node.operands) { + uint16_t word = get_numeric(operand.value); + if (operand.type != ValueType::IMMEDIATE) { + fprintf(stderr, "Expected immediate " + "operand for dw\n"); + exit(1); + } + + rom_buf[pc] = word & 0xFF; + rom_buf[pc + 1] = word >> 8; + pc += 2; + } + break; + } + case DirectiveType::OFFSET: { + if (directive_node.operands.size() != 1) { + fprintf(stderr, "Expected 1 operand for offset\n"); + exit(1); + } + + size_t offset = + get_numeric(directive_node.operands[0].value); + if (directive_node.operands[0].type != + ValueType::IMMEDIATE) { + fprintf(stderr, "Expected immediate " + "operand for offset\n"); + exit(1); + } + + pc += offset; + break; + } + case DirectiveType::INCLUDE: { + fprintf(stderr, "extraneous include directive! This is " + "likely a compiler bug\n"); + exit(1); + } + } + + break; + } + } + } + } + + write(rom_fd, rom_buf, pc); + + return nullptr; + } + + private: +}; + +std::optional +process_includes(char *data, size_t size, std::string include_dir, + std::unordered_set &seen_includes) { + std::string asm_str = std::string(data, size); + size_t str_idx = 0; + // find `include "%s"` and replace with the contents of the + // file iterate over each line in the file + std::stringstream lss(asm_str); + std::string line; + while (std::getline(lss, line)) { + str_idx += line.size() + 1; + + if (line.starts_with("include")) { + size_t pos = line.find_first_of("\"") + 1; + size_t n = line.find_first_of("\"", pos) - pos; + std::string file_name = line.substr(pos, n); + std::string path = include_dir + file_name; + + if (seen_includes.contains(path)) { + fprintf(stderr, "Recursive include detected: %s\n", + path.c_str()); + return std::nullopt; + } + + seen_includes.insert(path); + + // open the file + int include_fd = open(path.c_str(), O_RDONLY); + if (include_fd < 0) { + fprintf(stderr, "Failed to open include file: %s\n", + path.c_str()); + return std::nullopt; + } + + // read the file + size_t include_size = lseek(include_fd, 0, SEEK_END); + lseek(include_fd, 0, SEEK_SET); + char *include_data = (char *)calloc(include_size, sizeof(char)); + if (include_data == nullptr) { + fprintf(stderr, + "Failed to allocate memory for include " + "file: %s\n", + path.c_str()); + return std::nullopt; + } + + ssize_t read_size = read(include_fd, include_data, include_size); + if (read_size < 0) { + fprintf(stderr, "Failed to read include file: %s\n", + path.c_str()); + return std::nullopt; + } + close(include_fd); + + // recursively process the included file + std::optional new_data = process_includes( + include_data, include_size, include_dir, seen_includes); + if (!new_data.has_value()) { + return std::nullopt; + } + free(include_data); + + // replace the include line with the included file + asm_str.reserve(asm_str.size() + new_data.value().size()); + asm_str.replace(str_idx - (line.size() + 1), line.size(), + new_data.value()); + } + } + + return asm_str; +} + +int main(int argc, char **argv) { + if (argc < 3) { + printf("Usage: %s \n", argv[0]); + return 1; + } + + int asm_fd = open(argv[1], O_RDONLY); + if (asm_fd < 0) { + fprintf(stderr, "Failed to open file: %s\n", argv[1]); + return 1; + } + + int rom_fd = open(argv[2], O_RDWR | O_CREAT, 0644); + if (rom_fd < 0) { + fprintf(stderr, "Failed to open file: %s\n", argv[2]); + return 1; + } + + ftruncate(rom_fd, 0); + + size_t asm_size = lseek(asm_fd, 0, SEEK_END); + (void)lseek(asm_fd, 0, SEEK_SET); + + void *asm_buf = calloc(asm_size, sizeof(char)); + if (asm_buf == NULL) { + fprintf(stderr, "Failed to allocate memory!\n"); + return 1; + } + read(asm_fd, asm_buf, asm_size); + + std::string file_name = std::string(argv[1]); + std::string includes_dir = + file_name.substr(0, file_name.find_last_of('/')) + "/"; + + std::unordered_set seen_includes = {file_name}; + std::optional asm_str = process_includes( + static_cast(asm_buf), asm_size, includes_dir, seen_includes); + if (!asm_str.has_value()) { + return 1; + } + + Lexer lexer = Lexer(asm_str.value()); + Parser parser = Parser(lexer); + close(asm_fd); + + Assembler assembler = Assembler(); + assembler.assembler(parser.parse(), rom_fd); + + return 0; +} \ No newline at end of file diff --git a/disassembler/disassembler.cpp b/disassembler/disassembler.cpp new file mode 100644 index 0000000..8eb8696 --- /dev/null +++ b/disassembler/disassembler.cpp @@ -0,0 +1,533 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define BYTECODE_READER_IMPLEMENTATION +#include "reader.hpp" + +#include + +struct label { + enum type { + type_instruction, + type_byte, + } type; + uint16_t length; + std::string name; +}; + +uint16_t *find_closest_label(const std::unordered_map &map, + uint16_t target) { + + uint16_t *closest_label = nullptr; + + for (const auto &pair : map) { + if (pair.first <= target) { + if (closest_label != nullptr && pair.first < *closest_label) { + continue; + } + if (closest_label == nullptr) { + closest_label = (uint16_t *)calloc(1, sizeof(uint16_t)); + if (closest_label == NULL) { + throw std::runtime_error("Failed to allocate memory"); + } + } + *closest_label = pair.first; + } else { + break; + } + } + + return closest_label; +} + +std::string to_hex(size_t number) { return std::format("{:04x}", number); } + +std::string +get_label_for_byte(uint16_t byte, + std::unordered_map labels, + enum label::type target_type) { + std::string operand_str; + operand_str.append("0x"); + operand_str.append(to_hex(byte)); + if (labels.find(byte - 0x200) != labels.end()) { + operand_str.clear(); + operand_str = labels.at(byte - 0x200).name; + } else { + // try to see if the operand is offset from a label + if (byte > 0x200) { + uint16_t *closest_label = find_closest_label(labels, byte - 0x200); + if (closest_label != nullptr && + labels.at(*closest_label).type == target_type) { + uint16_t offset = (byte - 0x200) - (*closest_label); + + // discard our result if the offset is greater than the + // length of the label + if (offset > labels.at(*closest_label).length) { + return operand_str; + } + operand_str.clear(); + operand_str = labels.at(*closest_label).name; + operand_str.append(" + 0x"); + operand_str.append(to_hex(offset)); + operand_str.append(""); + } + } + } + + return operand_str; +} + +// I could emit something like omni assembly, nut that is significantly more +// complex than just emitting the assembly like this, so I am just going to +// emit the assembly like this for now +void print_instruction(Bytecode bytecode, uint16_t pc, + std::unordered_map labels) { + switch (bytecode.instruction_type) { + case HLT: + printf("halt\n"); + break; + case EXIT: + printf("exit 0x%02x\n", bytecode.operand.byte); + break; + case SYS: + if (labels.find(bytecode.operand.word - 0x200) == labels.end()) { + fprintf(stderr, "No label found for %04x\n", + bytecode.operand.word - 0x200); + exit(1); + } + + printf("sys %s\n", + labels.find(bytecode.operand.word - 0x200)->second.name.c_str()); + break; + case CLS: + printf("cls\n"); + break; + case RET: + printf("ret\n"); + break; + case JP: + if (pc == 0 && bytecode.operand.word == 0x260) { + printf("hires\n"); + break; + } + if (labels.find(bytecode.operand.word - 0x200) == labels.end()) { + fprintf(stderr, "No label found for %04x\n", + bytecode.operand.word - 0x200); + exit(1); + } + + printf("jp %s\n", + labels.find(bytecode.operand.word - 0x200)->second.name.c_str()); + break; + case CALL: + if (labels.find(bytecode.operand.word - 0x200) == labels.end()) { + fprintf(stderr, "No label found for %04x\n", + bytecode.operand.word - 0x200); + exit(1); + } + + printf("call %s\n", + labels.find(bytecode.operand.word - 0x200)->second.name.c_str()); + break; + case SKIP_INSTRUCTION_BYTE: + printf("se v%x, 0x%02x\n", bytecode.operand.byte_reg.reg, + bytecode.operand.byte_reg.byte); + break; + case SKIP_INSTRUCTION_NE_BYTE: + printf("sne v%x, 0x%02x\n", bytecode.operand.byte_reg.reg, + bytecode.operand.byte_reg.byte); + break; + case SKIP_INSTRUCTION_REG: + printf("se v%x, v%x\n", bytecode.operand.reg_reg.x, + bytecode.operand.reg_reg.y); + break; + case SKIP_INSTRUCTION_NE_REG: + printf("sne v%x, v%x\n", bytecode.operand.reg_reg.x, + bytecode.operand.reg_reg.y); + break; + case LOAD_BYTE: { + printf("ld v%x, 0x%02x\n", bytecode.operand.byte_reg.reg, + bytecode.operand.byte_reg.byte); + break; + } + case ADD_BYTE: + printf("add v%x, 0x%02x\n", bytecode.operand.byte_reg.reg, + bytecode.operand.byte_reg.byte); + break; + case LOAD_REG: + printf("ld v%x, v%x\n", bytecode.operand.reg_reg.x, + bytecode.operand.reg_reg.y); + break; + case ADD_REG: + printf("add v%x, v%x\n", bytecode.operand.reg_reg.x, + bytecode.operand.reg_reg.y); + break; + case OR_REG: + printf("or v%x, v%x\n", bytecode.operand.reg_reg.x, + bytecode.operand.reg_reg.y); + break; + case AND_REG: + printf("and v%x, v%x\n", bytecode.operand.reg_reg.x, + bytecode.operand.reg_reg.y); + break; + case XOR_REG: + printf("xor v%x, v%x\n", bytecode.operand.reg_reg.x, + bytecode.operand.reg_reg.y); + break; + case SHR_REG: + printf("shr v%x\n", bytecode.operand.reg_reg.x); + break; + case SUB_REG: + printf("sub v%x, v%x\n", bytecode.operand.reg_reg.x, + bytecode.operand.reg_reg.y); + break; + case SUBN_REG: + printf("subn v%x, v%x\n", bytecode.operand.reg_reg.x, + bytecode.operand.reg_reg.y); + break; + case SHL_REG: + printf("shl v%x\n", bytecode.operand.reg_reg.x); + break; + case LOAD_I_BYTE: { + printf("ld I, %s\n", get_label_for_byte(bytecode.operand.word, labels, + label::type_byte) + .c_str()); + break; + } + case JP_V0_BYTE: { + printf("jp v0, %s\n", get_label_for_byte(bytecode.operand.word, labels, + label::type_instruction) + .c_str()); + break; + } + case RND: + printf("rnd v%x, 0x%02x\n", bytecode.operand.byte_reg.reg, + bytecode.operand.byte_reg.byte); + break; + case DRW: + printf("drw v%x, v%x, 0x%x\n", bytecode.operand.reg_reg_nibble.x, + bytecode.operand.reg_reg_nibble.y, + bytecode.operand.reg_reg_nibble.nibble); + break; + case SKIP_PRESSED_REG: + printf("skp v%x\n", bytecode.operand.byte); + break; + case SKIP_NOT_PRESSED_REG: + printf("sknp v%x\n", bytecode.operand.byte); + break; + case LD_REG_DT: + printf("ld v%x, dt\n", bytecode.operand.byte); + break; + case LD_REG_K: + printf("ld v%x, k\n", bytecode.operand.byte); + break; + case LD_DT_REG: + printf("ld dt, v%x\n", bytecode.operand.byte); + break; + case LD_ST_REG: + printf("ld st, v%x\n", bytecode.operand.byte); + break; + case ADD_I_REG: + printf("add I, v%x\n", bytecode.operand.byte); + break; + case LD_F_REG: + printf("ld f, v%x\n", bytecode.operand.byte); + break; + case LD_B_REG: + printf("ld b, v%x\n", bytecode.operand.byte); + break; + case LD_PTR_I_REG: + printf("ld [I], v%x\n", bytecode.operand.byte); + break; + case LD_REG_PTR_I: + printf("ld v%x, [I]\n", bytecode.operand.byte); + break; + case UNKNOWN_INSTRUCTION: + printf("?\n"); + } +} + +struct hole { + uint16_t start; + uint16_t end; +}; + +struct assembly_node { + uint16_t address; + enum type { + type_byte, + type_instruction, + } type; + union { + uint8_t byte; + Bytecode instruction; + }; +}; + +void write_assembly(std::vector assembly, + std::unordered_map labels) { + std::sort(assembly.begin(), assembly.end(), + [](const struct assembly_node &a, const struct assembly_node &b) { + return a.address < b.address; + }); + uint16_t last_byte_address = 0x0000; + + for (auto &node : assembly) { + if (node.type != assembly_node::type_byte && node.address != 0 && + node.address - 1 == last_byte_address) { + printf("\n"); + } + + if (labels.find(node.address) != labels.end()) { + if (node.address != 0x0000) { + // add whitespacing between labels, but not for the _start label + printf("\n"); + } + + printf("%s:\n", labels.at(node.address).name.c_str()); + } + + switch (node.type) { + case assembly_node::type_byte: + if (node.address != last_byte_address + 1) { + printf("db "); + } else { + printf(",\n "); + } + + printf("0x%02x", node.byte); + last_byte_address = node.address; + break; + case assembly_node::type_instruction: + // if previous assembly was a byte, then we need to emit a new line + print_instruction(node.instruction, node.address, labels); + break; + } + } +} + +void disassemble(uint8_t *rom, int rom_size) { + // evaluate the bytecode, but dont actually execute it, just print it out, + // and when we reach a branching instruction, follow it. Make sure that if + // we enter an inifinite loop we dont just loop forever, so make sure we + // keep track of what we have already visited + std::unordered_set addresses_visited; + std::queue work_queue; + std::vector holes; + std::unordered_map labels; + size_t label_idx = 0; + uint16_t *stack = (uint16_t *)calloc(16, sizeof(uint16_t)); + if (stack == NULL) { + fprintf(stderr, "Failed to allocate stack!"); + exit(1); + } + // holds the start of a label + size_t stack_idx = 0; + + std::vector assembly; + + // start at the beginning of the rom + work_queue.push(0x0000); + labels.emplace( + 0x0000, + label{.type = label::type_instruction, .length = 0, .name = "_start"}); + + while (!work_queue.empty()) { + uint16_t pc = work_queue.front(); + work_queue.pop(); + + if (pc >= (uint16_t)rom_size) { + // if we are reading past the end of the rom, we are done + break; + } + + if (addresses_visited.find(pc) != addresses_visited.end()) + continue; + addresses_visited.insert(pc); + + uint16_t opcode = (rom[pc] << 8) | rom[pc + 1]; + Bytecode bytecode = parse(opcode); + + assembly.push_back({.address = pc, + .type = assembly_node::type_instruction, + .instruction = bytecode}); + + switch (bytecode.instruction_type) { + case JP: { + if (pc == 0 && bytecode.operand.word == 0x260) { + work_queue.push(0x2C0); + break; + } + + if (!labels.contains(bytecode.operand.word - 0x200)) { + labels.emplace( + bytecode.operand.word - 0x200, + label{.type = label::type_instruction, + .length = 0, + .name = "_" + std::to_string(label_idx++)}); + } + work_queue.push(bytecode.operand.word - 0x200); + break; + } + case CALL: { + if (stack_idx == 16) { + fprintf(stderr, "Stack overflow!\n"); + exit(1); + } + + if (!labels.contains(bytecode.operand.word - 0x200)) { + labels.emplace( + bytecode.operand.word - 0x200, + label{.type = label::type_instruction, + .length = 0, + .name = "_" + std::to_string(label_idx++)}); + } + stack[stack_idx++] = pc + 2; + + work_queue.push(bytecode.operand.word - 0x200); + break; + } + case SKIP_INSTRUCTION_BYTE: + case SKIP_INSTRUCTION_NE_BYTE: + case SKIP_INSTRUCTION_REG: + case SKIP_INSTRUCTION_NE_REG: + case SKIP_PRESSED_REG: + case SKIP_NOT_PRESSED_REG: { + work_queue.push(pc + 2); + work_queue.push(pc + 4); + break; + } + case RET: { + if (stack_idx == 0) { + fprintf(stderr, "Stack underflow!\n"); + exit(1); + } + + uint16_t ret_pc = stack[--stack_idx]; + work_queue.push(ret_pc); + break; + } + case HLT: { // Stop following + break; + } + case UNKNOWN_INSTRUCTION: { + fprintf(stderr, "Unknown instruction: %04x\n", opcode); + // we failed at disassembling smartly + break; + } + default: + work_queue.push(pc + 2); + } + } + + bool skip = false; + uint16_t *last_seen_byte_array = nullptr; + uint16_t start_of_last_contiguous_block = 0x0000; + for (uint16_t pc = 0x00; pc < rom_size; pc++) { + if (skip) { + skip = false; + continue; + } + + if (addresses_visited.find(pc) != addresses_visited.end()) { + // when there is an instruction that we have already visited, we + // want to skip this byte and the next byte, but we cant rely of the + // instructions being aligned to 0x02 bytes, so instead we tell the + // next run of the loop to skip + skip = true; + continue; + } + + // this seems scary, but it's fine because the if block will jump down + // if the first condition is met, so it will never dereference a null + // pointer + if (last_seen_byte_array == nullptr || + *last_seen_byte_array != pc - 1) { + if (last_seen_byte_array == nullptr) { + last_seen_byte_array = new uint16_t; + } + + start_of_last_contiguous_block = pc; + + // we are not in a contiguous block of bytes, so we need to add a + // label + if (!labels.contains(pc)) { + labels.emplace( + pc, label{.type = label::type_byte, + .length = 1, + .name = "_" + std::to_string(label_idx++)}); + } + } else { + // we are in a contiguous block of bytes, so we need to update the + // label's length by one + labels[start_of_last_contiguous_block].length++; + } + + *last_seen_byte_array = pc; + + assembly.push_back( + {.address = pc, .type = assembly_node::type_byte, .byte = rom[pc]}); + } + + for (auto &pair : labels) { + uint16_t pc = pair.first; + uint16_t label_length = 0; + // while we havent reached the end of the rom, and we havent crossed + // into a new label + while (pc < rom_size) { + label_length++; + switch (pair.second.type) { + case label::type_byte: + pc++; + break; + case label::type_instruction: + pc += 2; + break; + } + + if (labels.find(pc) != labels.end()) + break; + } + + pair.second.length = label_length; + } + + write_assembly(assembly, labels); +} + +int main(int argc, char **argv) { + if (argc < 2) { + printf("Usage: %s \n", argv[0]); + return 1; + } + + int rom_fd = open(argv[1], O_RDONLY); + if (rom_fd < 0) { + fprintf(stderr, "Failed to open file: %s\n", argv[1]); + return 1; + } + + int rom_size = lseek(rom_fd, 0, SEEK_END); + (void)lseek(rom_fd, 0, SEEK_SET); + + uint8_t *rom = (uint8_t *)calloc(rom_size, sizeof(uint8_t)); + if (rom == NULL) { + fprintf(stderr, "Failed to allocate memory!\n"); + return 1; + } + + read(rom_fd, rom, rom_size); + + disassemble(rom, rom_size); + + return 0; +} \ No newline at end of file diff --git a/disassembler/main.cpp b/disassembler/main.cpp deleted file mode 100644 index 345992f..0000000 --- a/disassembler/main.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#define BYTECODE_READER_IMPLEMENTATION -#include "reader.hpp" - -#include - -int main() { - printf("Hello World!\n"); - return 0; -} \ No newline at end of file diff --git a/examples/font2.asm b/examples/font2.asm new file mode 100644 index 0000000..9ff613a --- /dev/null +++ b/examples/font2.asm @@ -0,0 +1,401 @@ +; This file is a modification of font2.asm from Wernsey's Chip-8 Assembler +; Apache License +; Version 2.0, January 2004 +; http://www.apache.org/licenses/ +; +; TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +; +; 1. Definitions. +; +; "License" shall mean the terms and conditions for use, reproduction, +; and distribution as defined by Sections 1 through 9 of this document. +; +; "Licensor" shall mean the copyright owner or entity authorized by +; the copyright owner that is granting the License. +; +; "Legal Entity" shall mean the union of the acting entity and all +; other entities that control, are controlled by, or are under common +; control with that entity. For the purposes of this definition, +; "control" means (i) the power, direct or indirect, to cause the +; direction or management of such entity, whether by contract or +; otherwise, or (ii) ownership of fifty percent (50%) or more of the +; outstanding shares, or (iii) beneficial ownership of such entity. +; +; "You" (or "Your") shall mean an individual or Legal Entity +; exercising permissions granted by this License. +; +; "Source" form shall mean the preferred form for making modifications, +; including but not limited to software source code, documentation +; source, and configuration files. +; +; "Object" form shall mean any form resulting from mechanical +; transformation or translation of a Source form, including but +; not limited to compiled object code, generated documentation, +; and conversions to other media types. +; +; "Work" shall mean the work of authorship, whether in Source or +; Object form, made available under the License, as indicated by a +; copyright notice that is included in or attached to the work +; (an example is provided in the Appendix below). +; +; "Derivative Works" shall mean any work, whether in Source or Object +; form, that is based on (or derived from) the Work and for which the +; editorial revisions, annotations, elaborations, or other modifications +; represent, as a whole, an original work of authorship. For the purposes +; of this License, Derivative Works shall not include works that remain +; separable from, or merely link (or bind by name) to the interfaces of, +; the Work and Derivative Works thereof. +; +; "Contribution" shall mean any work of authorship, including +; the original version of the Work and any modifications or additions +; to that Work or Derivative Works thereof, that is intentionally +; submitted to Licensor for inclusion in the Work by the copyright owner +; or by an individual or Legal Entity authorized to submit on behalf of +; the copyright owner. For the purposes of this definition, "submitted" +; means any form of electronic, verbal, or written communication sent +; to the Licensor or its representatives, including but not limited to +; communication on electronic mailing lists, source code control systems, +; and issue tracking systems that are managed by, or on behalf of, the +; Licensor for the purpose of discussing and improving the Work, but +; excluding communication that is conspicuously marked or otherwise +; designated in writing by the copyright owner as "Not a Contribution." +; +; "Contributor" shall mean Licensor and any individual or Legal Entity +; on behalf of whom a Contribution has been received by Licensor and +; subsequently incorporated within the Work. +; +; 2. Grant of Copyright License. Subject to the terms and conditions of +; this License, each Contributor hereby grants to You a perpetual, +; worldwide, non-exclusive, no-charge, royalty-free, irrevocable +; copyright license to reproduce, prepare Derivative Works of, +; publicly display, publicly perform, sublicense, and distribute the +; Work and such Derivative Works in Source or Object form. +; +; 3. Grant of Patent License. Subject to the terms and conditions of +; this License, each Contributor hereby grants to You a perpetual, +; worldwide, non-exclusive, no-charge, royalty-free, irrevocable +; (except as stated in this section) patent license to make, have made, +; use, offer to sell, sell, import, and otherwise transfer the Work, +; where such license applies only to those patent claims licensable +; by such Contributor that are necessarily infringed by their +; Contribution(s) alone or by combination of their Contribution(s) +; with the Work to which such Contribution(s) was submitted. If You +; institute patent litigation against any entity (including a +; cross-claim or counterclaim in a lawsuit) alleging that the Work +; or a Contribution incorporated within the Work constitutes direct +; or contributory patent infringement, then any patent licenses +; granted to You under this License for that Work shall terminate +; as of the date such litigation is filed. +; +; 4. Redistribution. You may reproduce and distribute copies of the +; Work or Derivative Works thereof in any medium, with or without +; modifications, and in Source or Object form, provided that You +; meet the following conditions: +; +; (a) You must give any other recipients of the Work or +; Derivative Works a copy of this License; and +; +; (b) You must cause any modified files to carry prominent notices +; stating that You changed the files; and +; +; (c) You must retain, in the Source form of any Derivative Works +; that You distribute, all copyright, patent, trademark, and +; attribution notices from the Source form of the Work, +; excluding those notices that do not pertain to any part of +; the Derivative Works; and +; +; (d) If the Work includes a "NOTICE" text file as part of its +; distribution, then any Derivative Works that You distribute must +; include a readable copy of the attribution notices contained +; within such NOTICE file, excluding those notices that do not +; pertain to any part of the Derivative Works, in at least one +; of the following places: within a NOTICE text file distributed +; as part of the Derivative Works; within the Source form or +; documentation, if provided along with the Derivative Works; or, +; within a display generated by the Derivative Works, if and +; wherever such third-party notices normally appear. The contents +; of the NOTICE file are for informational purposes only and +; do not modify the License. You may add Your own attribution +; notices within Derivative Works that You distribute, alongside +; or as an addendum to the NOTICE text from the Work, provided +; that such additional attribution notices cannot be construed +; as modifying the License. +; +; You may add Your own copyright statement to Your modifications and +; may provide additional or different license terms and conditions +; for use, reproduction, or distribution of Your modifications, or +; for any such Derivative Works as a whole, provided Your use, +; reproduction, and distribution of the Work otherwise complies with +; the conditions stated in this License. +; +; 5. Submission of Contributions. Unless You explicitly state otherwise, +; any Contribution intentionally submitted for inclusion in the Work +; by You to the Licensor shall be under the terms and conditions of +; this License, without any additional terms or conditions. +; Notwithstanding the above, nothing herein shall supersede or modify +; the terms of any separate license agreement you may have executed +; with Licensor regarding such Contributions. +; +; 6. Trademarks. This License does not grant permission to use the trade +; names, trademarks, service marks, or product names of the Licensor, +; except as required for reasonable and customary use in describing the +; origin of the Work and reproducing the content of the NOTICE file. +; +; 7. Disclaimer of Warranty. Unless required by applicable law or +; agreed to in writing, Licensor provides the Work (and each +; Contributor provides its Contributions) on an "AS IS" BASIS, +; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +; implied, including, without limitation, any warranties or conditions +; of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +; PARTICULAR PURPOSE. You are solely responsible for determining the +; appropriateness of using or redistributing the Work and assume any +; risks associated with Your exercise of permissions under this License. +; +; 8. Limitation of Liability. In no event and under no legal theory, +; whether in tort (including negligence), contract, or otherwise, +; unless required by applicable law (such as deliberate and grossly +; negligent acts) or agreed to in writing, shall any Contributor be +; liable to You for damages, including any direct, indirect, special, +; incidental, or consequential damages of any character arising as a +; result of this License or out of the use or inability to use the +; Work (including but not limited to damages for loss of goodwill, +; work stoppage, computer failure or malfunction, or any and all +; other commercial damages or losses), even if such Contributor +; has been advised of the possibility of such damages. +; +; 9. Accepting Warranty or Additional Liability. While redistributing +; the Work or Derivative Works thereof, You may choose to offer, +; and charge a fee for, acceptance of support, warranty, indemnity, +; or other liability obligations and/or rights consistent with this +; License. However, in accepting such obligations, You may act only +; on Your own behalf and on Your sole responsibility, not on behalf +; of any other Contributor, and only if You agree to indemnify, +; defend, and hold each Contributor harmless for any liability +; incurred by, or claims asserted against, such Contributor by reason +; of your accepting any such warranty or additional liability. +; +; END OF TERMS AND CONDITIONS +; +; APPENDIX: How to apply the Apache License to your work. +; +; To apply the Apache License to your work, attach the following +; boilerplate notice, with the fields enclosed by brackets "{}" +; replaced with your own identifying information. (Don't include +; the brackets!) The text should be enclosed in the appropriate +; comment syntax for the file format. We also recommend that a +; file or class name and description of purpose be included on the +; same "printed page" as the copyright notice for easier +; identification within third-party archives. +; +; Copyright {yyyy} {name of copyright owner} +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, +; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +; See the License for the specific language governing permissions and +; limitations under the License. + + +; This is the 8*10 font for SCHIP. +; Run it through the assembler to get the +; hex codes for the fonts that you can copy +; and paste into chip8.c + +font: +db ; '0' + 0b01111100, + 0b10000010, + 0b10000010, + 0b10000010, + 0b10000010, + 0b10000010, + 0b10000010, + 0b10000010, + 0b01111100, + 0b00000000 + +db ; '1' + 0b00001000, + 0b00011000, + 0b00111000, + 0b00001000, + 0b00001000, + 0b00001000, + 0b00001000, + 0b00001000, + 0b00111100, + 0b00000000 + +db ; '2' + 0b01111100, + 0b10000010, + 0b00000010, + 0b00000010, + 0b00000100, + 0b00011000, + 0b00100000, + 0b01000000, + 0b11111110, + 0b00000000 + +db ; '3' + 0b01111100, + 0b10000010, + 0b00000010, + 0b00000010, + 0b00111100, + 0b00000010, + 0b00000010, + 0b10000010, + 0b01111100, + 0b00000000 + +db ; '4' + 0b10000100, + 0b10000100, + 0b10000100, + 0b10000100, + 0b11111110, + 0b00000100, + 0b00000100, + 0b00000100, + 0b00000100, + 0b00000000 + +db ; '5' + 0b11111110, + 0b10000000, + 0b10000000, + 0b10000000, + 0b11111100, + 0b00000010, + 0b00000010, + 0b10000010, + 0b01111100, + 0b00000000 + +db ; '6' + 0b01111100, + 0b10000010, + 0b10000000, + 0b10000000, + 0b11111100, + 0b10000010, + 0b10000010, + 0b10000010, + 0b01111100, + 0b00000000 + +db ; '7' + 0b11111110, + 0b00000010, + 0b00000100, + 0b00001000, + 0b00010000, + 0b00100000, + 0b00100000, + 0b00100000, + 0b00100000, + 0b00000000 + +db ; '8' + 0b01111100, + 0b10000010, + 0b10000010, + 0b10000010, + 0b01111100, + 0b10000010, + 0b10000010, + 0b10000010, + 0b01111100, + 0b00000000 + +db ; '9' + 0b01111100, + 0b10000010, + 0b10000010, + 0b10000010, + 0b01111110, + 0b00000010, + 0b00000010, + 0b10000010, + 0b01111100, + 0b00000000 + +db ; 'A' + 0b00010000, + 0b00101000, + 0b01000100, + 0b10000010, + 0b10000010, + 0b11111110, + 0b10000010, + 0b10000010, + 0b10000010, + 0b00000000 + +db ; 'B' + 0b11111100, + 0b10000010, + 0b10000010, + 0b10000010, + 0b11111100, + 0b10000010, + 0b10000010, + 0b10000010, + 0b11111100, + 0b00000000 + +db ; 'C' + 0b01111100, + 0b10000010, + 0b10000000, + 0b10000000, + 0b10000000, + 0b10000000, + 0b10000000, + 0b10000010, + 0b01111100, + 0b00000000 + +db ; 'D' + 0b11111100, + 0b10000010, + 0b10000010, + 0b10000010, + 0b10000010, + 0b10000010, + 0b10000010, + 0b10000010, + 0b11111100, + 0b00000000 + +db ; 'E' + 0b11111110, + 0b10000000, + 0b10000000, + 0b10000000, + 0b11111000, + 0b10000000, + 0b10000000, + 0b10000000, + 0b11111110, + 0b00000000 + +db ; 'F' + 0b11111110, + 0b10000000, + 0b10000000, + 0b10000000, + 0b11111000, + 0b10000000, + 0b10000000, + 0b10000000, + 0b10000000, + 0b00000000 \ No newline at end of file diff --git a/examples/sink.asm b/examples/sink.asm new file mode 100644 index 0000000..5c2c179 --- /dev/null +++ b/examples/sink.asm @@ -0,0 +1,44 @@ +; kitchen sink for instructions +_start: + exit 0x00 ; 001N + cls ; 00E0 + ret ; 00EE + sys 0x00 ; 0NNN + jp 0x123 ; 1NNN + jp v0, 0x123 ; BNNN + call foo ; 2NNN + se V1, 0xAA ; 3xkk + sne V2, 0xAA ; 4xkk + se V1, V2 ; 5xy0 + sne V1, V2 ; 9xy0 + add v1, 0x12 ; 7xkk + add v1, v2 ; 8xy4 + add I, V8 ; Fx1E + or V2, v3 ; 8xy1 + and VA, vb ; 8xy2 + xor VA, vb ; 8xy3 + sub v1, v2 ; 8xy5 + shr VA, vb ; 8xy6 shifts VA right by vb + shr VA ; 8xx6 + subn VA, vb ; 8xy7 + shl VA, vb ; 8xyE + shl VA ; 8xxE + rnd VD, 0xFF ; Cxkk + drw VE, VF, 0x4 ; Dxxy + skp VE ; Ex9E + sknp VA ; ExA1 + ld V1, 0xAA ; 6xkk + ld V2, v3 ; 8xy0 + ld I, 0xAA ; ANNN + ld v2, DT ; Fx07 + ld v2, K ; Fx0A + ld DT, V5 ; Fx15 + ld ST, V5 ; Fx18 + ld F, V5 ; Fx29 + ld B, V5 ; Fx33 + ld [I], VA ; Fx55 + ld VA, [I] ; Fx65 + +foo: + add v8, v9 + ret \ No newline at end of file diff --git a/examples/syntax.asm b/examples/syntax.asm new file mode 100644 index 0000000..8577ece --- /dev/null +++ b/examples/syntax.asm @@ -0,0 +1,299 @@ +; basically stolen from https://github.com/wernsey/chip8/blob/master/examples/syntax.asm +; This file is a modification of syntax.asm from Wernsey's Chip-8 Assembler +; Apache License +; Version 2.0, January 2004 +; http://www.apache.org/licenses/ +; +; TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +; +; 1. Definitions. +; +; "License" shall mean the terms and conditions for use, reproduction, +; and distribution as defined by Sections 1 through 9 of this document. +; +; "Licensor" shall mean the copyright owner or entity authorized by +; the copyright owner that is granting the License. +; +; "Legal Entity" shall mean the union of the acting entity and all +; other entities that control, are controlled by, or are under common +; control with that entity. For the purposes of this definition, +; "control" means (i) the power, direct or indirect, to cause the +; direction or management of such entity, whether by contract or +; otherwise, or (ii) ownership of fifty percent (50%) or more of the +; outstanding shares, or (iii) beneficial ownership of such entity. +; +; "You" (or "Your") shall mean an individual or Legal Entity +; exercising permissions granted by this License. +; +; "Source" form shall mean the preferred form for making modifications, +; including but not limited to software source code, documentation +; source, and configuration files. +; +; "Object" form shall mean any form resulting from mechanical +; transformation or translation of a Source form, including but +; not limited to compiled object code, generated documentation, +; and conversions to other media types. +; +; "Work" shall mean the work of authorship, whether in Source or +; Object form, made available under the License, as indicated by a +; copyright notice that is included in or attached to the work +; (an example is provided in the Appendix below). +; +; "Derivative Works" shall mean any work, whether in Source or Object +; form, that is based on (or derived from) the Work and for which the +; editorial revisions, annotations, elaborations, or other modifications +; represent, as a whole, an original work of authorship. For the purposes +; of this License, Derivative Works shall not include works that remain +; separable from, or merely link (or bind by name) to the interfaces of, +; the Work and Derivative Works thereof. +; +; "Contribution" shall mean any work of authorship, including +; the original version of the Work and any modifications or additions +; to that Work or Derivative Works thereof, that is intentionally +; submitted to Licensor for inclusion in the Work by the copyright owner +; or by an individual or Legal Entity authorized to submit on behalf of +; the copyright owner. For the purposes of this definition, "submitted" +; means any form of electronic, verbal, or written communication sent +; to the Licensor or its representatives, including but not limited to +; communication on electronic mailing lists, source code control systems, +; and issue tracking systems that are managed by, or on behalf of, the +; Licensor for the purpose of discussing and improving the Work, but +; excluding communication that is conspicuously marked or otherwise +; designated in writing by the copyright owner as "Not a Contribution." +; +; "Contributor" shall mean Licensor and any individual or Legal Entity +; on behalf of whom a Contribution has been received by Licensor and +; subsequently incorporated within the Work. +; +; 2. Grant of Copyright License. Subject to the terms and conditions of +; this License, each Contributor hereby grants to You a perpetual, +; worldwide, non-exclusive, no-charge, royalty-free, irrevocable +; copyright license to reproduce, prepare Derivative Works of, +; publicly display, publicly perform, sublicense, and distribute the +; Work and such Derivative Works in Source or Object form. +; +; 3. Grant of Patent License. Subject to the terms and conditions of +; this License, each Contributor hereby grants to You a perpetual, +; worldwide, non-exclusive, no-charge, royalty-free, irrevocable +; (except as stated in this section) patent license to make, have made, +; use, offer to sell, sell, import, and otherwise transfer the Work, +; where such license applies only to those patent claims licensable +; by such Contributor that are necessarily infringed by their +; Contribution(s) alone or by combination of their Contribution(s) +; with the Work to which such Contribution(s) was submitted. If You +; institute patent litigation against any entity (including a +; cross-claim or counterclaim in a lawsuit) alleging that the Work +; or a Contribution incorporated within the Work constitutes direct +; or contributory patent infringement, then any patent licenses +; granted to You under this License for that Work shall terminate +; as of the date such litigation is filed. +; +; 4. Redistribution. You may reproduce and distribute copies of the +; Work or Derivative Works thereof in any medium, with or without +; modifications, and in Source or Object form, provided that You +; meet the following conditions: +; +; (a) You must give any other recipients of the Work or +; Derivative Works a copy of this License; and +; +; (b) You must cause any modified files to carry prominent notices +; stating that You changed the files; and +; +; (c) You must retain, in the Source form of any Derivative Works +; that You distribute, all copyright, patent, trademark, and +; attribution notices from the Source form of the Work, +; excluding those notices that do not pertain to any part of +; the Derivative Works; and +; +; (d) If the Work includes a "NOTICE" text file as part of its +; distribution, then any Derivative Works that You distribute must +; include a readable copy of the attribution notices contained +; within such NOTICE file, excluding those notices that do not +; pertain to any part of the Derivative Works, in at least one +; of the following places: within a NOTICE text file distributed +; as part of the Derivative Works; within the Source form or +; documentation, if provided along with the Derivative Works; or, +; within a display generated by the Derivative Works, if and +; wherever such third-party notices normally appear. The contents +; of the NOTICE file are for informational purposes only and +; do not modify the License. You may add Your own attribution +; notices within Derivative Works that You distribute, alongside +; or as an addendum to the NOTICE text from the Work, provided +; that such additional attribution notices cannot be construed +; as modifying the License. +; +; You may add Your own copyright statement to Your modifications and +; may provide additional or different license terms and conditions +; for use, reproduction, or distribution of Your modifications, or +; for any such Derivative Works as a whole, provided Your use, +; reproduction, and distribution of the Work otherwise complies with +; the conditions stated in this License. +; +; 5. Submission of Contributions. Unless You explicitly state otherwise, +; any Contribution intentionally submitted for inclusion in the Work +; by You to the Licensor shall be under the terms and conditions of +; this License, without any additional terms or conditions. +; Notwithstanding the above, nothing herein shall supersede or modify +; the terms of any separate license agreement you may have executed +; with Licensor regarding such Contributions. +; +; 6. Trademarks. This License does not grant permission to use the trade +; names, trademarks, service marks, or product names of the Licensor, +; except as required for reasonable and customary use in describing the +; origin of the Work and reproducing the content of the NOTICE file. +; +; 7. Disclaimer of Warranty. Unless required by applicable law or +; agreed to in writing, Licensor provides the Work (and each +; Contributor provides its Contributions) on an "AS IS" BASIS, +; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +; implied, including, without limitation, any warranties or conditions +; of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +; PARTICULAR PURPOSE. You are solely responsible for determining the +; appropriateness of using or redistributing the Work and assume any +; risks associated with Your exercise of permissions under this License. +; +; 8. Limitation of Liability. In no event and under no legal theory, +; whether in tort (including negligence), contract, or otherwise, +; unless required by applicable law (such as deliberate and grossly +; negligent acts) or agreed to in writing, shall any Contributor be +; liable to You for damages, including any direct, indirect, special, +; incidental, or consequential damages of any character arising as a +; result of this License or out of the use or inability to use the +; Work (including but not limited to damages for loss of goodwill, +; work stoppage, computer failure or malfunction, or any and all +; other commercial damages or losses), even if such Contributor +; has been advised of the possibility of such damages. +; +; 9. Accepting Warranty or Additional Liability. While redistributing +; the Work or Derivative Works thereof, You may choose to offer, +; and charge a fee for, acceptance of support, warranty, indemnity, +; or other liability obligations and/or rights consistent with this +; License. However, in accepting such obligations, You may act only +; on Your own behalf and on Your sole responsibility, not on behalf +; of any other Contributor, and only if You agree to indemnify, +; defend, and hold each Contributor harmless for any liability +; incurred by, or claims asserted against, such Contributor by reason +; of your accepting any such warranty or additional liability. +; +; END OF TERMS AND CONDITIONS +; +; APPENDIX: How to apply the Apache License to your work. +; +; To apply the Apache License to your work, attach the following +; boilerplate notice, with the fields enclosed by brackets "{}" +; replaced with your own identifying information. (Don't include +; the brackets!) The text should be enclosed in the appropriate +; comment syntax for the file format. We also recommend that a +; file or class name and description of purpose be included on the +; same "printed page" as the copyright notice for easier +; identification within third-party archives. +; +; Copyright {yyyy} {name of copyright owner} +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, +; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +; See the License for the specific language governing permissions and +; limitations under the License. + +; This program just demonstrates and tests all the instructions in the assembler. +; Don't try to run it + +; Comments start with semicolons +_start: ; labels are identifiers followed by colons + cls + jp _start + jp 0x123 ; Hexadecimal numbers are preceded by 0x + jp V0, 0x123 ; jumps to 0x123 + v0 in memory space + jp V0, end + call end + call 0x203 + se V1, 0xAA + se V2, v3 + sne V1, 0xAA + sne V2, v3 + +end: + ret + add V1, 0xAA + add V2, v3 + ld V1, 0xAA + ld V2, v3 + or V2, v3 + and VA, vb + xor VA, vb + shr VA, vb + shr VA + subn VA, vb + shl VA, vb + shl VA + rnd VD, 0xFF + drw VE, VF, 0x4 + skp VE + sknp VA + add I, V8 + ld I, 0xAAA + ld V5, DT + ld V5, K + ld DT, V5 + ld ST, V5 + ld F, V5 + ld B, V5 + ld [I], VA + ld VA, [I] + +; "define" can be used to define constants +define aaa 0x222 + ; this instruction will be directly after the ld VA, [I] instruction + jp aaa + +; "define" can also be used to define aliases for registers +define bbb vd + ld bbb, 0b01010101 ; Binary literals start with 0b + jp 0b101001010101 + jp x + ld I, x + +; You can enable hires mode by defining the `hires` directive + hires + ld VA, 0x12 + ld VB, 0x34 + drw VA, VB, 0x1 + se VF, 0x01 + jp 0x238 + jp 0x5b6 + +offset 0x280 ; interst 0x280 zero bytes + +; This is how you can define sprites: +; "db" emits raw bytes, separated by commas. +; "dw" can emit 16-bit words. + +x: db 0x11, 0x22, 0x33, 0x44 +y: db + 0b00100100, + 0b11111111, + 0b01011010, + 0b00111100, + 0b00100100 + cls + +; You can load text data into the output through the `text` directive: +string1: + text "hello" +; (The label `string1` can be used later like so: `ld I, string1`) + +; The above is equivalent to this `db` directive +; db 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x00 + +; The string can also contain special symbols escaped with '\' +string2: text "\"\e\r\n" + +; This is how you can include another file: +include "font2.asm" \ No newline at end of file diff --git a/libs/reader.hpp b/libs/reader.hpp index 4c56809..4092a58 100644 --- a/libs/reader.hpp +++ b/libs/reader.hpp @@ -96,6 +96,8 @@ enum instruction { LD_PTR_I_REG, // operand is a pointer to a uint8_t LD_REG_PTR_I, + // no operand + HLT, UNKNOWN_INSTRUCTION, }; @@ -136,11 +138,16 @@ inline Bytecode parse(uint16_t opcode) { bytecode.instruction_type = RET; break; } + case 0x0000: { + // HLT 0x0000 + bytecode.instruction_type = HLT; + 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. + //? difference is between it and the JP NNN 0x1NNN opcode. bytecode.instruction_type = SYS; break; } @@ -207,7 +214,7 @@ inline Bytecode parse(uint16_t 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. + // instructions were implemented on the COSMAC VIP. bytecode.instruction_type = SHR_REG; break; } @@ -316,6 +323,7 @@ inline Bytecode parse(uint16_t opcode) { switch (bytecode.instruction_type) { case UNKNOWN_INSTRUCTION: + case HLT: case RET: case CLS: { // no operand diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index a284046..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,1062 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define BYTECODE_READER_IMPLEMENTATION -#include "reader.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -const 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_FRAMERATE = 60; -const int TARGET_MS_PER_FRAME = 1000 / TARGET_FRAMERATE; -const int TARGET_MS_PER_TICK = 1000 / 60; -static int SCREEN_WIDTH = 64; -static int SCREEN_HEIGHT = 32; -static int SCALE = 10; -static int BG_COLOR = 0x081820; -static int FG_COLOR = 0x88c070; - -// Spcae Invaders by David Winter uses misaligned addresses, so we might not -// always want to align pc -#define ALIGN_PC true -// the SYS instruction technically shouldnt be used, so it can be compiled out. -#define SYS_INSTRUCTION false - -const int SAMPLE_RATE = 44100; -const int FREQUENCY = 440; // A4 -const int AMPLITUDE = 28000; -const int SAMPLES_PER_CYCLE = SAMPLE_RATE / FREQUENCY; - -void audioCallback(void *userdata, Uint8 *stream, int len) { - (void)userdata; - static int phase = 0; - Sint16 *buffer = (Sint16 *)stream; - int length = len / 2; - - for (int i = 0; i < length; i++) { - buffer[i] = (phase < SAMPLES_PER_CYCLE / 4) ? AMPLITUDE : -AMPLITUDE; - phase = (phase + 1) % SAMPLES_PER_CYCLE; - } -} - -void initAudio() { - SDL_AudioSpec want, have; - SDL_zero(want); - want.freq = SAMPLE_RATE; - want.format = AUDIO_S16SYS; - want.channels = 1; - want.samples = 2048; // Buffer size - want.callback = audioCallback; - - if (SDL_OpenAudio(&want, &have) < 0) { - SDL_Log("Failed to open audio: %s", SDL_GetError()); - } -} - -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 -}; - -void clear_framebuffer(bool **fb, int width, int height) { - // since the pixel array is just one big array, we can just memset the - // first element of the array and that will clear the entire pixel array - memset(fb[0], 0, width * height); -} - -bool **allocate_framebuffer(int width, int height) { - // allocate the frambuffer, which is a pointer to an array of booleans, the - // pointer to the arrays should be height elements long. The boolean arrays - // should be wdith elements long. - bool **fb = (bool **)malloc(height * sizeof(bool *)); - if (fb == NULL) { - return NULL; - } - - bool *pixel_array = (bool *)malloc(width * height * sizeof(bool)); - if (pixel_array == NULL) { - return NULL; - } - - for (int y = 0; y < height; y++) { - fb[y] = pixel_array + (y * width); - } - - clear_framebuffer(fb, width, height); - - return fb; -} - -void free_framebuffer(bool **fb) { - // since the pixel array is just one big array, we can just free the - // first element of the array and that will free the entire pixel array - free(fb[0]); - free(fb); -} - -class Chip8 { - public: - Chip8(char *rom_path) { - int rom_fd = open(rom_path, O_RDONLY); - if (rom_fd < 0) { - fprintf(stderr, "Failed to open file: %s\n", rom_path); - exit(1); - } - - ram = (uint8_t *)malloc(RAM_SIZE); - if (ram == NULL) { - fprintf(stderr, "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) { - fprintf(stderr, "Failed to read file: %s\n", rom_path); - exit(1); - } - - if (err != file_size) { - fprintf(stderr, "Failed to read file: %s\n", rom_path); - exit(1); - } - - close(rom_fd); - - stack = (uint16_t *)malloc(sizeof(uint16_t) * 16); - if (stack == NULL) { - fprintf(stderr, "Failed to allocate stack!"); - exit(1); - } - - fb = allocate_framebuffer(SCREEN_WIDTH, SCREEN_HEIGHT); - fb_length = SCREEN_HEIGHT * SCREEN_WIDTH; - - if (fb == NULL) { - fprintf(stderr, "Failed to allocate framebuffer!\n"); - exit(1); - } - } - - ~Chip8() { - free(ram); - free(stack); - free_framebuffer(fb); - } - - int run(); - void view_ram(); - void dump_ram(); - void view_stack(); - - // only allow addresses in the program space to be executed, addresses - // not protected, but in the reserved space, for example, the font, - // should not be executable - bool is_executable(size_t addr) { return addr > 0x1FF; } - - int read_mem(size_t addr) { return this->ram[addr]; } - - void write_mem(size_t addr, uint8_t val) { this->ram[addr] = val; } - - void set_sound_timer(uint8_t val) { this->sound_timer = val; } - - void set_pixel(int x, int y, uint8_t val) { - assert(fb != NULL); - assert(x >= 0 && x < SCREEN_WIDTH); - assert(y >= 0 && y < SCREEN_HEIGHT); - assert(this->fb[y] != NULL); - this->fb[y][x] = val; - } - - uint8_t get_pixel(int x, int y) { - assert(fb != NULL); - assert(x >= 0 && x < SCREEN_WIDTH); - assert(y >= 0 && y < SCREEN_HEIGHT); - assert(this->fb[y] != NULL); - return this->fb[y][x]; - } - - size_t fb_length = 0; - bool **fb = nullptr; - std::atomic_uint8_t delay = 0; - std::atomic_uint8_t sound_timer = 0; - std::mutex key_mutex = {}; - std::condition_variable key_cv = {}; - bool key_pressed_map[0x10] = {}; - std::atomic last_key = 0xFF; - - private: - uint8_t *ram = nullptr; - uint16_t pc = 0x200; - uint16_t *stack = nullptr; - uint8_t sp = 0; - uint8_t v[16] = {0}; - uint16_t i = 0; - // bool compat; -}; - -size_t pixels_length = 0; -uint32_t *pixels_array = nullptr; - -void draw(SDL_Renderer *renderer, SDL_Texture *texture, Chip8 *chip8) { - if (pixels_array == NULL || - pixels_length != (size_t)(SCREEN_HEIGHT * SCREEN_WIDTH)) { - if (pixels_array != NULL) { - free(pixels_array); - } - - pixels_array = (uint32_t *)malloc(sizeof(uint32_t) * - (SCREEN_HEIGHT * SCREEN_WIDTH)); - if (pixels_array == NULL) { - fprintf(stderr, "Failed to allocated pixels buffer!"); - exit(1); - } - pixels_length = (size_t)(SCREEN_HEIGHT * SCREEN_WIDTH); - } - - for (int i = 0; i < SCREEN_HEIGHT; i++) { - for (int j = 0; j < SCREEN_WIDTH; j++) { - pixels_array[i * SCREEN_WIDTH + j] = - chip8->fb[i][j] ? FG_COLOR : BG_COLOR; - } - } - - SDL_UpdateTexture(texture, nullptr, pixels_array, - SCREEN_WIDTH * sizeof(uint32_t)); - SDL_RenderClear(renderer); - SDL_RenderCopy(renderer, texture, nullptr, nullptr); - SDL_RenderPresent(renderer); -} - -void timer_thread(Chip8 *chip8) { - using namespace std::chrono; - - initAudio(); - - while (true) { - std::this_thread::sleep_for(milliseconds(TARGET_MS_PER_TICK)); // ~60Hz - if (chip8->delay > 0) { - chip8->delay--; - } - if (chip8->sound_timer > 0) { - SDL_PauseAudio(0); - chip8->sound_timer--; - } - - if (chip8->sound_timer == 0) { - SDL_PauseAudio(1); - } - } -} - -void render_thread(Chip8 *chip8) { - SDL_Window *sdl_window = SDL_CreateWindow( - "CHIP-8 Emulator", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, - SCREEN_WIDTH * SCALE, SCREEN_HEIGHT * SCALE, SDL_WINDOW_SHOWN); - SDL_Renderer *sdl_renderer = - SDL_CreateRenderer(sdl_window, -1, SDL_RENDERER_ACCELERATED); - SDL_Texture *render_texture = SDL_CreateTexture( - sdl_renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, - SCREEN_WIDTH, SCREEN_HEIGHT); - - int width, height; - SDL_GetWindowSize(sdl_window, &width, &height); - - width /= SCALE; - height /= SCALE; - - while (true) { - // check if we need to resize the window - if (width != SCREEN_WIDTH || height != SCREEN_HEIGHT) { - // resize the framebuffer - bool **new_fb = allocate_framebuffer(SCREEN_WIDTH, SCREEN_HEIGHT); - - if (new_fb == NULL) { - fprintf(stderr, "Failed to allocate framebuffer!\n"); - exit(1); - } - - // we need to copy the old framebuffer to the new one, but, it - // is not guranteed that the old framebuffer is smaller than the - // new framebuffer, so we need to make sure we only read at most - // the smaller framebuffer. - size_t smaller_fb_size = width * height; - if (width > SCREEN_WIDTH || height > SCREEN_HEIGHT) { - smaller_fb_size = SCREEN_WIDTH * SCREEN_HEIGHT; - } - memcpy(new_fb[0], chip8->fb[0], smaller_fb_size); - - free_framebuffer(chip8->fb); - chip8->fb_length = SCREEN_HEIGHT * SCREEN_WIDTH; - chip8->fb = new_fb; - - // resize the SDL window - SDL_SetWindowSize(sdl_window, SCREEN_WIDTH * SCALE, - SCREEN_HEIGHT * SCALE); - SDL_DestroyTexture(render_texture); - render_texture = SDL_CreateTexture( - sdl_renderer, SDL_PIXELFORMAT_RGBA8888, - SDL_TEXTUREACCESS_STREAMING, SCREEN_WIDTH, SCREEN_HEIGHT); - width = SCREEN_WIDTH; - height = SCREEN_HEIGHT; - } - - draw(sdl_renderer, render_texture, chip8); - std::this_thread::sleep_for( - std::chrono::milliseconds(TARGET_MS_PER_FRAME)); // 60Hz - } -} - -void input_thread(Chip8 *chip8, std::atomic_bool &running) { - SDL_Event event; - while (true) { - while (SDL_PollEvent(&event)) { - if (event.type == SDL_QUIT) - running = false; - if (event.type == SDL_KEYDOWN || event.type == SDL_KEYUP) { - std::unique_lock lock(chip8->key_mutex); - - // map the key to a CHIP-8 key in a numpad way, but not on - // the numpad since I dont have a numpad - uint8_t key; - switch (event.key.keysym.sym) { - case SDLK_1: - key = 0x01; - break; - case SDLK_2: - key = 0x02; - break; - case SDLK_3: - key = 0x03; - break; - case SDLK_q: - key = 0x04; - break; - case SDLK_w: - key = 0x05; - break; - case SDLK_e: - key = 0x06; - break; - case SDLK_a: - key = 0x07; - break; - case SDLK_s: - key = 0x08; - break; - case SDLK_d: - key = 0x09; - break; - case SDLK_x: - key = 0x00; - break; - case SDLK_z: - key = 0x0A; - break; - case SDLK_c: - key = 0x0B; - break; - case SDLK_4: - key = 0x0C; - break; - case SDLK_r: - key = 0x0D; - break; - case SDLK_f: - key = 0x0E; - break; - case SDLK_v: - key = 0x0F; - break; - default: - continue; - } - chip8->key_pressed_map[key] = event.type == SDL_KEYDOWN; - chip8->last_key = key; - chip8->key_cv.notify_one(); - lock.unlock(); - } - } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } -} - -int Chip8::run() { - using namespace std::chrono; - - constexpr auto cycle_time = milliseconds(TARGET_MS_PER_CYCLE); - - if (SDL_Init(SDL_INIT_VIDEO) < 0) { - fprintf(stderr, "Failed to initialize SDL: %s\n", SDL_GetError()); - return 1; - } - - std::atomic_bool running = true; - - std::thread render(render_thread, this); - std::this_thread::sleep_for(milliseconds(TARGET_MS_PER_FRAME)); - std::thread input(input_thread, this, std::ref(running)); - std::thread timer(timer_thread, this); - - int ret = 0; - - while (running) { - auto start_time = high_resolution_clock::now(); - - uint16_t op = (read_mem(pc) << 8) | read_mem(pc + 1); - if (!is_executable(pc)) { - fprintf(stderr, "Attempted to execute protected memory at 0x%04x\n", - pc); - view_ram(); - return 1; - } - printf("PC: 0x%04x OP: 0x%04x\n", pc, op); - pc += 2; - 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. - ret = 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. -#if SYS_INSTRUCTION - uint16_t addr = bytecode.operand.word & 0x0FFF; - assert(addr < RAM_SIZE); -#if ALIGN_PC - assert(addr % 0x2 == 0); -#endif // ALIGN_PC - - pc = bytecode.operand.word & 0x0FFF; -#else - (void)bytecode; - fprintf(stderr, "SYS instruction not enabled\n"); - exit(1); -#endif // SYS_INSTRUCTION - break; - } - case CLS: { - // Clear the screen. - clear_framebuffer(this->fb, 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. - - // --sp subtracts 1 from the stack pointer and returns the value - // after the subraction - pc = stack[--sp]; - break; - } - case JP: { - // Jump to location nnn. - // The interpreter sets the program counter to nnn. - - if ((pc - 2) == 0x200 && bytecode.operand.word == 0x260) { - printf("Entering Hi-Res mode\n"); - // This is the Hi-Res enable opcode (0x1260), we need to - // change the resolution to 64x64 and jump to 0x2C0 instead - // of 0x260. - bytecode.operand.word = 0x2C0; - // resize_framebuffer(this, 64, 64); - SCREEN_HEIGHT = 64; - } - - uint16_t addr = bytecode.operand.word & 0x0FFF; - assert(addr < RAM_SIZE); -#if ALIGN_PC - assert(addr % 0x2 == 0); -#endif - - 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. - - // sp++ increments the stack pointer and returns the value - // before the addition - 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; - - if (result > 0xFF) { - printf("Overflowed!\n"); - this->v[0xF] = 1; - } else { - this->v[0xF] = 0; - } - - 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. - bool borrow = this->v[bytecode.operand.reg_reg.x] >= - this->v[bytecode.operand.reg_reg.y]; - - this->v[bytecode.operand.reg_reg.x] = - (this->v[bytecode.operand.reg_reg.x] - - this->v[bytecode.operand.reg_reg.y]) & - 0xFF; - if (borrow) { - this->v[0xF] = 1; - } else { - this->v[0xF] = 0; - } - - if (borrow) { - this->v[0xF] = 1; - } else { - this->v[0xF] = 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. - bool carry = this->v[bytecode.operand.reg_reg.x] & 0x01; - this->v[bytecode.operand.reg_reg.x] >>= 1; - if (carry) { - this->v[0xF] = 1; - } else { - this->v[0xF] = 0; - } - 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. - bool borrow = this->v[bytecode.operand.reg_reg.y] >= - this->v[bytecode.operand.reg_reg.x]; - - this->v[bytecode.operand.reg_reg.x] = - (this->v[bytecode.operand.reg_reg.y] - - this->v[bytecode.operand.reg_reg.x]) & - 0xFF; - if (borrow) { - this->v[0xF] = 1; - } else { - this->v[0xF] = 0; - } - - if (borrow) { - this->v[0xF] = 1; - } else { - this->v[0xF] = 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. - bool carry = (this->v[bytecode.operand.reg_reg.x] >> 7) & 0x01; - this->v[bytecode.operand.reg_reg.x] <<= 1; - if (carry) { - this->v[0xF] = 1; - } else { - this->v[0xF] = 0; - } - 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(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. - 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; - if (!source) { - continue; - } - - if (this->get_pixel(x, y)) { - this->set_pixel(x, y, 0); - this->v[0x0F] = 1; - } else { - this->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. - uint8_t key = this->v[bytecode.operand.byte]; - - if (this->key_pressed_map[key]) { - pc += 2; - } - 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"); - uint8_t key = this->v[bytecode.operand.byte]; - - if (!this->key_pressed_map[key]) { - pc += 2; - } - break; - } - case LD_REG_DT: { - // Set Vx = delay timer value. - // The value of DT is placed into Vx. - this->v[bytecode.operand.byte] = 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. - std::unique_lock lock(this->key_mutex); - - this->key_cv.wait(lock, [&]() { - this->v[bytecode.operand.byte] = this->last_key.load(); - return this->key_pressed_map[this->last_key.load()]; - }); - lock.unlock(); - while (this->key_pressed_map[this->last_key.load()]) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - break; - } - case LD_DT_REG: { - // Set delay timer = Vx. - // DT is set equal to the value of Vx. - this->delay = this->v[bytecode.operand.byte]; - break; - } - case LD_ST_REG: { - // Set sound timer = Vx. - // ST is set equal to the value of Vx. - set_sound_timer(this->v[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 in 0x0000-0x01FF of the RAM where the - //? emulator is allowed to access. Since that area of RAM is - //? where the font is stored. - this->i = (uint16_t)(this->v[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. - write_mem(this->i, - (uint8_t)((this->v[bytecode.operand.byte] / 100) & 0x0F)); - write_mem(this->i + 1, - (uint8_t)((this->v[bytecode.operand.byte] % 100) / 10) & - 0x0F); - write_mem(this->i + 2, - (uint8_t)(this->v[bytecode.operand.byte] % 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.byte; i++) { - write_mem(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.byte; i++) { - this->v[i] = read_mem(this->i + i); - } - break; - } - case UNKNOWN_INSTRUCTION: { - fprintf(stderr, "Unknown instruction: %04x\n", op); - exit(1); - } - } - - auto elapsed_time = duration_cast( - high_resolution_clock::now() - start_time); - if (elapsed_time < cycle_time) { - std::this_thread::sleep_for(cycle_time - elapsed_time); - } - } - - timer.detach(); - render.detach(); - input.detach(); - - return ret; -} - -void Chip8::view_ram() { - printf("Hex dump:\n"); - for (size_t i = 0; i < RAM_SIZE / 16; i++) { - printf("%04x: ", (unsigned int)(i * 16)); - - 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); -} - -void Chip8::view_stack() { - printf("Stack:\n"); - for (int i = 0; i < 16; i++) { - printf("%04x ", stack[i]); - } - printf("\n"); -} - -void signal_handler(int signum) { - (void)signum; - exit(1); -} - -int main(int argc, char **argv) { - signal(SIGINT, signal_handler); - if (argc < 2) { - printf("Usage: %s [options]\n", argv[0]); - return 1; - } - - char *file_name = NULL; - - // is this memory safe? - // start from 1 to skip the first argument (the executable) - for (int i = 1; i < argc; i++) { - if (argc < i) { - printf("exceeded argc\n"); - } - - if (strcmp(argv[i], "--scale") == 0) { - - if (argc < i + 1) { - printf("Error: Missing scale value\n"); - return 1; - } - - long scale = strtol(argv[i + 1], NULL, 10); - if (scale < 1 || scale > 50) { - printf("Error: Invalid scale value\n"); - return 1; - } - - SCALE = scale; - - i++; - continue; - } - - if (strcmp(argv[i], "--bg") == 0) { - if (argc < i + 1) { - printf("Error: Missing bg value\n"); - return 1; - } - - long bg = strtol(argv[i + 1], NULL, 16); - if (bg < 0 || bg > 0xFFFFFF) { - printf("Error: Invalid bg value\n"); - return 1; - } - - // RSH by 8 to correct for the alpha channel - BG_COLOR = (int)(bg << 8); - i++; - continue; - } - - if (strcmp(argv[i], "--fg") == 0) { - if (argc < i + 1) { - printf("Error: Missing fg value\n"); - return 1; - } - - long fg = strtol(argv[i + 1], NULL, 16); - if (fg < 0 || fg > 0xFFFFFF) { - printf("Error: Invalid fg value\n"); - return 1; - } - - // RSH by 8 to correct for the alpha channel - FG_COLOR = (int)(fg << 8); - i++; - continue; - } - - // if the argument does not start with a dash, assume it is a file - if (argv[i][0] != '-') { - printf("Filename: %s\n", argv[i]); - file_name = argv[i]; - continue; - } - } - - srand(time(0)); - - Chip8 chip8 = Chip8(file_name); - chip8.dump_ram(); - int ret = chip8.run(); - - return ret; -} diff --git a/src/voidemu.cpp b/src/voidemu.cpp new file mode 100644 index 0000000..660e2b4 --- /dev/null +++ b/src/voidemu.cpp @@ -0,0 +1,1156 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BYTECODE_READER_IMPLEMENTATION +#include "reader.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const 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_FRAMERATE = 60; +const int TARGET_MS_PER_FRAME = 1000 / TARGET_FRAMERATE; +const int TARGET_MS_PER_TICK = 1000 / 60; +static int SCREEN_WIDTH = 64; +static int SCREEN_HEIGHT = 32; +static int SCALE = 10; +static int BG_COLOR = 0x081820; +static int FG_COLOR = 0x88c070; + +// Spcae Invaders by David Winter uses misaligned addresses, so we might not +// always want to align pc +#define ALIGN_PC true +// the SYS instruction technically shouldnt be used, so it can be compiled out. +#define SYS_INSTRUCTION false + +const int SAMPLE_RATE = 44100; +const int FREQUENCY = 440; // A4 +const int AMPLITUDE = 28000; +const int SAMPLES_PER_CYCLE = SAMPLE_RATE / FREQUENCY; + +void audioCallback(void *userdata, Uint8 *stream, int len) { + (void)userdata; + static int phase = 0; + Sint16 *buffer = (Sint16 *)stream; + int length = len / 2; + + for (int i = 0; i < length; i++) { + buffer[i] = (phase < SAMPLES_PER_CYCLE / 4) ? AMPLITUDE : -AMPLITUDE; + phase = (phase + 1) % SAMPLES_PER_CYCLE; + } +} + +void initAudio() { + SDL_AudioSpec want, have; + SDL_zero(want); + want.freq = SAMPLE_RATE; + want.format = AUDIO_S16SYS; + want.channels = 1; + want.samples = 2048; // Buffer size + want.callback = audioCallback; + + if (SDL_OpenAudio(&want, &have) < 0) { + SDL_Log("Failed to open audio: %s", SDL_GetError()); + } +} + +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 +}; + +void clear_framebuffer(bool **fb, int width, int height) { + // since the pixel array is just one big array, we can just memset the + // first element of the array and that will clear the entire pixel array + memset(fb[0], 0, width * height); +} + +bool **allocate_framebuffer(int width, int height) { + // allocate the frambuffer, which is a pointer to an array of booleans, the + // pointer to the arrays should be height elements long. The boolean arrays + // should be wdith elements long. + bool **fb = (bool **)calloc(height, sizeof(bool *)); + if (fb == NULL) { + return NULL; + } + + bool *pixel_array = (bool *)calloc(width * height, sizeof(bool)); + if (pixel_array == NULL) { + return NULL; + } + + for (int y = 0; y < height; y++) { + fb[y] = pixel_array + (y * width); + } + + clear_framebuffer(fb, width, height); + + return fb; +} + +void free_framebuffer(bool **fb) { + // since the pixel array is just one big array, we can just free the + // first element of the array and that will free the entire pixel array + free(fb[0]); + free(fb); +} + +size_t rom_size = 0; +uint8_t *rom = nullptr; + +class Chip8 { + public: + Chip8(char *rom_path) { + int rom_fd = open(rom_path, O_RDONLY); + if (rom_fd < 0) { + fprintf(stderr, "Failed to open file: %s\n", rom_path); + exit(1); + } + + ram = (uint8_t *)calloc(RAM_SIZE, sizeof(uint8_t)); + if (ram == NULL) { + fprintf(stderr, "Failed to allocate ram!"); + exit(1); + } + + memcpy(ram, FONT, sizeof(FONT)); + + rom_size = lseek(rom_fd, 0, SEEK_END); + (void)lseek(rom_fd, 0, SEEK_SET); + + printf("Reading file: %s for %ld bytes\n", rom_path, rom_size); + + rom = (uint8_t *)calloc(rom_size, sizeof(uint8_t)); + if (rom == NULL) { + fprintf(stderr, "Failed to allocate memory!\n"); + exit(1); + } + + int err = read(rom_fd, rom, rom_size); + if (err < 0) { + fprintf(stderr, "Failed to read file: %s\n", rom_path); + exit(1); + } + + memcpy(ram + 0x200, rom, rom_size); + + if (err != (int)rom_size) { + fprintf(stderr, "Failed to read file: %s\n", rom_path); + exit(1); + } + + close(rom_fd); + + stack = (uint16_t *)calloc(16, sizeof(uint16_t)); + if (stack == NULL) { + fprintf(stderr, "Failed to allocate stack!"); + exit(1); + } + + fb = allocate_framebuffer(SCREEN_WIDTH, SCREEN_HEIGHT); + fb_length = SCREEN_HEIGHT * SCREEN_WIDTH; + + if (fb == NULL) { + fprintf(stderr, "Failed to allocate framebuffer!\n"); + exit(1); + } + } + + ~Chip8() { + free(ram); + free(stack); + free_framebuffer(fb); + } + + void reset() { + SCREEN_HEIGHT = 32; + SCREEN_WIDTH = 64; + clear_framebuffer(this->fb, SCREEN_WIDTH, SCREEN_HEIGHT); + + // no need to manually reallocate the framebuffer, since it will be + // dealt with in the render thread + + this->pc = 0x200; + this->sp = 0; + for (int i = 0; i < 0x10; i++) { + this->v[i] = 0; + } + for (int i = 0; i < 0x10; i++) { + this->stack[i] = 0; + } + this->i = 0; + this->delay = 0; + this->sound_timer = 0; + + memset(ram, 0, RAM_SIZE); + memcpy(ram + 0x200, rom, rom_size); + + memcpy(ram, FONT, sizeof(FONT)); + } + + int run(); + uint8_t *step(); + void view_ram(); + void dump_ram(); + void view_stack(); + + // only allow addresses in the program space to be executed, addresses + // not protected, but in the reserved space, for example, the font, + // should not be executable + bool is_executable(size_t addr) { return addr > 0x1FF; } + + int read_mem(size_t addr) { return this->ram[addr]; } + + void write_mem(size_t addr, uint8_t val) { this->ram[addr] = val; } + + void set_sound_timer(uint8_t val) { this->sound_timer = val; } + + void set_pixel(int x, int y, uint8_t val) { + assert(fb != NULL); + assert(x >= 0 && x < SCREEN_WIDTH); + assert(y >= 0 && y < SCREEN_HEIGHT); + assert(this->fb[y] != NULL); + this->fb[y][x] = val; + } + + uint8_t get_pixel(int x, int y) { + assert(fb != NULL); + assert(x >= 0 && x < SCREEN_WIDTH); + assert(y >= 0 && y < SCREEN_HEIGHT); + assert(this->fb[y] != NULL); + return this->fb[y][x]; + } + + size_t fb_length = 0; + bool **fb = nullptr; + std::atomic_uint8_t delay = 0; + std::atomic_uint8_t sound_timer = 0; + std::mutex key_mutex = {}; + std::condition_variable key_cv = {}; + bool key_pressed_map[0x10] = {}; + std::atomic last_key = 0xFF; + std::atomic running = false; + std::atomic should_reset = false; + + private: + uint8_t *ram = nullptr; + uint16_t pc = 0x200; + uint16_t *stack = nullptr; + uint8_t sp = 0; + uint8_t v[16] = {0}; + uint16_t i = 0; + // bool compat; + static constexpr auto cycle_time = + std::chrono::milliseconds(TARGET_MS_PER_CYCLE); +}; + +size_t pixels_length = 0; +uint32_t *pixels_array = nullptr; + +void draw(SDL_Renderer *renderer, SDL_Texture *texture, Chip8 *chip8) { + if (pixels_array == NULL || + pixels_length != (size_t)(SCREEN_HEIGHT * SCREEN_WIDTH)) { + if (pixels_array != NULL) { + free(pixels_array); + } + + pixels_array = + (uint32_t *)calloc(SCREEN_HEIGHT * SCREEN_WIDTH, sizeof(uint32_t)); + if (pixels_array == NULL) { + fprintf(stderr, "Failed to allocated pixels buffer!"); + exit(1); + } + pixels_length = (size_t)(SCREEN_HEIGHT * SCREEN_WIDTH); + } + + for (int i = 0; i < SCREEN_HEIGHT; i++) { + for (int j = 0; j < SCREEN_WIDTH; j++) { + pixels_array[i * SCREEN_WIDTH + j] = + chip8->fb[i][j] ? FG_COLOR : BG_COLOR; + } + } + + SDL_UpdateTexture(texture, nullptr, pixels_array, + SCREEN_WIDTH * sizeof(uint32_t)); + SDL_RenderClear(renderer); + SDL_RenderCopy(renderer, texture, nullptr, nullptr); + SDL_RenderPresent(renderer); +} + +void timer_thread(Chip8 *chip8) { + using namespace std::chrono; + + initAudio(); + + while (chip8->running.load(std::memory_order::seq_cst)) { + std::this_thread::sleep_for(milliseconds(TARGET_MS_PER_TICK)); // ~60Hz + if (chip8->delay > 0) { + chip8->delay--; + } + if (chip8->sound_timer > 0) { + SDL_PauseAudio(0); + chip8->sound_timer--; + } + + if (chip8->sound_timer == 0) { + SDL_PauseAudio(1); + } + } +} + +void render_thread(Chip8 *chip8) { + SDL_Window *sdl_window = SDL_CreateWindow( + "CHIP-8 Emulator", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + SCREEN_WIDTH * SCALE, SCREEN_HEIGHT * SCALE, SDL_WINDOW_SHOWN); + SDL_Renderer *sdl_renderer = + SDL_CreateRenderer(sdl_window, -1, SDL_RENDERER_ACCELERATED); + SDL_Texture *render_texture = SDL_CreateTexture( + sdl_renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, + SCREEN_WIDTH, SCREEN_HEIGHT); + + int width, height; + SDL_GetWindowSize(sdl_window, &width, &height); + + width /= SCALE; + height /= SCALE; + + while (chip8->running.load(std::memory_order::seq_cst)) { + // check if we need to resize the window + if (width != SCREEN_WIDTH || height != SCREEN_HEIGHT) { + // resize the framebuffer + bool **new_fb = allocate_framebuffer(SCREEN_WIDTH, SCREEN_HEIGHT); + + if (new_fb == NULL) { + fprintf(stderr, "Failed to allocate framebuffer!\n"); + exit(1); + } + + // we need to copy the old framebuffer to the new one, but, it + // is not guranteed that the old framebuffer is smaller than the + // new framebuffer, so we need to make sure we only read at most + // the smaller framebuffer. + size_t smaller_fb_size = width * height; + if (width > SCREEN_WIDTH || height > SCREEN_HEIGHT) { + smaller_fb_size = SCREEN_WIDTH * SCREEN_HEIGHT; + } + memcpy(new_fb[0], chip8->fb[0], smaller_fb_size); + + free_framebuffer(chip8->fb); + chip8->fb_length = SCREEN_HEIGHT * SCREEN_WIDTH; + chip8->fb = new_fb; + + // resize the SDL window + SDL_SetWindowSize(sdl_window, SCREEN_WIDTH * SCALE, + SCREEN_HEIGHT * SCALE); + SDL_DestroyTexture(render_texture); + render_texture = SDL_CreateTexture( + sdl_renderer, SDL_PIXELFORMAT_RGBA8888, + SDL_TEXTUREACCESS_STREAMING, SCREEN_WIDTH, SCREEN_HEIGHT); + width = SCREEN_WIDTH; + height = SCREEN_HEIGHT; + } + + draw(sdl_renderer, render_texture, chip8); + std::this_thread::sleep_for( + std::chrono::milliseconds(TARGET_MS_PER_FRAME)); // 60Hz + } + + SDL_DestroyRenderer(sdl_renderer); + SDL_DestroyWindow(sdl_window); + SDL_Quit(); +} + +void input_thread(Chip8 *chip8) { + SDL_Event event; + while (chip8->running.load(std::memory_order::seq_cst)) { + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) + chip8->running = false; + if (event.type == SDL_KEYDOWN || event.type == SDL_KEYUP) { + std::unique_lock lock(chip8->key_mutex); + + // map the key to a CHIP-8 key in a numpad way, but not on + // the numpad since I dont have a numpad + uint8_t key; + switch (event.key.keysym.sym) { + case SDLK_1: + key = 0x01; + break; + case SDLK_2: + key = 0x02; + break; + case SDLK_3: + key = 0x03; + break; + case SDLK_q: + key = 0x04; + break; + case SDLK_w: + key = 0x05; + break; + case SDLK_e: + key = 0x06; + break; + case SDLK_a: + key = 0x07; + break; + case SDLK_s: + key = 0x08; + break; + case SDLK_d: + key = 0x09; + break; + case SDLK_x: + key = 0x00; + break; + case SDLK_z: + key = 0x0A; + break; + case SDLK_c: + key = 0x0B; + break; + case SDLK_4: + key = 0x0C; + break; + case SDLK_r: + key = 0x0D; + break; + case SDLK_f: + key = 0x0E; + break; + case SDLK_v: + key = 0x0F; + break; + case SDLK_DELETE: + chip8->should_reset.store(true); + continue; + default: + continue; + } + chip8->key_pressed_map[key] = event.type == SDL_KEYDOWN; + chip8->last_key = key; + chip8->key_cv.notify_one(); + lock.unlock(); + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +uint8_t *Chip8::step() { + uint16_t opcode = (read_mem(pc) << 8) | read_mem(pc + 1); + if (!is_executable(pc)) { + fprintf(stderr, "Attempted to execute protected memory at 0x%04x\n", + pc); + view_ram(); + uint8_t *ptr = (uint8_t *)calloc(1, sizeof(uint8_t)); + if (ptr == NULL) { + fprintf(stderr, "Failed to allocate memory!\n"); + exit(1); + } + *ptr = 1; + return ptr; + } + Bytecode bytecode = parse(opcode); + printf("PC: 0x%04x OP: 0x%04x\n INSTRUCTION_TYPE: %d\n", pc, + opcode, bytecode.instruction_type); + + pc += 2; + + switch (bytecode.instruction_type) { + case HLT: { + // From the Octo interpreter. Halt the emulator. + while (true) { + // spin + } + } + case EXIT: { + // From Peter Miller's chip8run. Exit emulator with a return + // value of N. + uint8_t *ptr = (uint8_t *)calloc(1, sizeof(uint8_t)); + if (ptr == NULL) { + fprintf(stderr, "Failed to allocate memory!\n"); + exit(1); + } + *ptr = bytecode.operand.byte; + return ptr; + } + 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. +#if SYS_INSTRUCTION + uint16_t addr = bytecode.operand.word & 0x0FFF; + assert(addr < RAM_SIZE); +#if ALIGN_PC + assert(addr % 0x2 == 0); +#endif // ALIGN_PC + + pc = bytecode.operand.word & 0x0FFF; +#else + (void)bytecode; + fprintf(stderr, "SYS instruction not enabled\n"); + exit(1); +#endif // SYS_INSTRUCTION + break; + } + case CLS: { + // Clear the screen. + clear_framebuffer(this->fb, 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. + + // --sp subtracts 1 from the stack pointer and returns the value + // after the subraction + pc = stack[--sp]; + break; + } + case JP: { + // Jump to location nnn. + // The interpreter sets the program counter to nnn. + + if ((pc - 2) == 0x200 && bytecode.operand.word == 0x260) { + printf("Entering Hi-Res mode\n"); + // This is the Hi-Res enable opcode (0x1260), we need to + // change the resolution to 64x64 and jump to 0x2C0 instead + // of 0x260. + bytecode.operand.word = 0x2C0; + SCREEN_HEIGHT = 64; + } + + uint16_t addr = bytecode.operand.word & 0x0FFF; + assert(addr < RAM_SIZE); +#if ALIGN_PC + assert(addr % 0x2 == 0); +#endif + + 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. + + // sp++ increments the stack pointer and returns the value + // before the addition + 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; + + if (result > 0xFF) { + printf("Overflowed!\n"); + this->v[0xF] = 1; + } else { + this->v[0xF] = 0; + } + + 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. + bool borrow = this->v[bytecode.operand.reg_reg.x] >= + this->v[bytecode.operand.reg_reg.y]; + + this->v[bytecode.operand.reg_reg.x] = + (this->v[bytecode.operand.reg_reg.x] - + this->v[bytecode.operand.reg_reg.y]) & + 0xFF; + if (borrow) { + this->v[0xF] = 1; + } else { + this->v[0xF] = 0; + } + + if (borrow) { + this->v[0xF] = 1; + } else { + this->v[0xF] = 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. + bool carry = this->v[bytecode.operand.reg_reg.x] & 0x01; + this->v[bytecode.operand.reg_reg.x] >>= 1; + if (carry) { + this->v[0xF] = 1; + } else { + this->v[0xF] = 0; + } + 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. + bool borrow = this->v[bytecode.operand.reg_reg.y] >= + this->v[bytecode.operand.reg_reg.x]; + + this->v[bytecode.operand.reg_reg.x] = + (this->v[bytecode.operand.reg_reg.y] - + this->v[bytecode.operand.reg_reg.x]) & + 0xFF; + if (borrow) { + this->v[0xF] = 1; + } else { + this->v[0xF] = 0; + } + + if (borrow) { + this->v[0xF] = 1; + } else { + this->v[0xF] = 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. + bool carry = (this->v[bytecode.operand.reg_reg.x] >> 7) & 0x01; + this->v[bytecode.operand.reg_reg.x] <<= 1; + if (carry) { + this->v[0xF] = 1; + } else { + this->v[0xF] = 0; + } + 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(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. + 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; + if (!source) { + continue; + } + + if (this->get_pixel(x, y)) { + this->set_pixel(x, y, 0); + this->v[0x0F] = 1; + } else { + this->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. + uint8_t key = this->v[bytecode.operand.byte]; + + if (this->key_pressed_map[key]) { + pc += 2; + } + 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"); + uint8_t key = this->v[bytecode.operand.byte]; + + if (!this->key_pressed_map[key]) { + pc += 2; + } + break; + } + case LD_REG_DT: { + // Set Vx = delay timer value. + // The value of DT is placed into Vx. + this->v[bytecode.operand.byte] = 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. + std::unique_lock lock(this->key_mutex); + + this->key_cv.wait(lock, [&]() { + this->v[bytecode.operand.byte] = this->last_key.load(); + return this->key_pressed_map[this->last_key.load()]; + }); + lock.unlock(); + while (this->key_pressed_map[this->last_key.load()]) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + break; + } + case LD_DT_REG: { + // Set delay timer = Vx. + // DT is set equal to the value of Vx. + this->delay = this->v[bytecode.operand.byte]; + break; + } + case LD_ST_REG: { + // Set sound timer = Vx. + // ST is set equal to the value of Vx. + set_sound_timer(this->v[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 in 0x0000-0x01FF of the RAM where the + //? emulator is allowed to access. Since that area of RAM is + //? where the font is stored. + this->i = (uint16_t)(this->v[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. + write_mem(this->i, + (uint8_t)((this->v[bytecode.operand.byte] / 100) & 0x0F)); + write_mem(this->i + 1, + (uint8_t)((this->v[bytecode.operand.byte] % 100) / 10) & + 0x0F); + write_mem(this->i + 2, + (uint8_t)(this->v[bytecode.operand.byte] % 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.byte; i++) { + write_mem(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.byte; i++) { + this->v[i] = read_mem(this->i + i); + } + break; + } + case UNKNOWN_INSTRUCTION: { + fprintf(stderr, "Unknown instruction: %04x\n", opcode); + uint8_t *ptr = (uint8_t *)calloc(1, sizeof(uint8_t)); + if (ptr == NULL) { + fprintf(stderr, "Failed to allocate memory!\n"); + exit(1); + } + *ptr = 1; + return ptr; + } + } + + return nullptr; +} + +int Chip8::run() { + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + fprintf(stderr, "Failed to initialize SDL: %s\n", SDL_GetError()); + return 1; + } + + std::thread render(render_thread, this); + std::this_thread::sleep_for(std::chrono::milliseconds(TARGET_MS_PER_FRAME)); + std::thread input(input_thread, this); + std::thread timer(timer_thread, this); + + // this value is higher than the max for uint8_t, so a program can never + // return 0x100, so we can use this value as an indicator that the emulator + // was restarted, but didnt exit + int ret = 0xFF; + this->running.store(true); + + while (this->running.load(std::memory_order::seq_cst)) { + if (this->should_reset.load(std::memory_order::seq_cst)) { + printf("Resetting emulator\n"); + this->should_reset.store(false); + this->reset(); + + continue; + } + + auto start_time = std::chrono::high_resolution_clock::now(); + + uint8_t *ptr = this->step(); + if (ptr != nullptr) { + ret = *ptr; + free(ptr); + break; + } + + auto elapsed_time = duration_cast( + std::chrono::high_resolution_clock::now() - start_time); + if (elapsed_time < cycle_time) { + printf("%ld", cycle_time.count() - elapsed_time.count()); + std::this_thread::sleep_for(cycle_time - elapsed_time); + } + } + + timer.detach(); + render.detach(); + input.detach(); + + return ret; +} + +void Chip8::view_ram() { + printf("Hex dump:\n"); + for (size_t i = 0; i < RAM_SIZE / 16; i++) { + printf("%04x: ", (unsigned int)(i * 16)); + + 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); +} + +void Chip8::view_stack() { + printf("Stack:\n"); + for (int i = 0; i < 16; i++) { + printf("%04x ", stack[i]); + } + printf("\n"); +} + +Chip8 *chip8 = nullptr; + +void signal_handler(int signum) { + if (chip8 == nullptr) { + return; + } + + (void)signum; + chip8->running = false; + exit(0); +} + +int main(int argc, char **argv) { + signal(SIGINT, signal_handler); + if (argc < 2) { + printf("Usage: %s [options]\n", argv[0]); + return 1; + } + + char *file_name = NULL; + + // is this memory safe? + // start from 1 to skip the first argument (the executable) + for (int i = 1; i < argc; i++) { + if (argc < i) { + printf("exceeded argc\n"); + } + + if (strcmp(argv[i], "--scale") == 0) { + + if (argc < i + 1) { + printf("Error: Missing scale value\n"); + return 1; + } + + long scale = strtol(argv[i + 1], NULL, 10); + if (scale < 1 || scale > 50) { + printf("Error: Invalid scale value\n"); + return 1; + } + + SCALE = scale; + + i++; + continue; + } + + if (strcmp(argv[i], "--bg") == 0) { + if (argc < i + 1) { + printf("Error: Missing bg value\n"); + return 1; + } + + long bg = strtol(argv[i + 1], NULL, 16); + if (bg < 0 || bg > 0xFFFFFF) { + printf("Error: Invalid bg value\n"); + return 1; + } + + // RSH by 8 to correct for the alpha channel + BG_COLOR = (int)(bg << 8); + i++; + continue; + } + + if (strcmp(argv[i], "--fg") == 0) { + if (argc < i + 1) { + printf("Error: Missing fg value\n"); + return 1; + } + + long fg = strtol(argv[i + 1], NULL, 16); + if (fg < 0 || fg > 0xFFFFFF) { + printf("Error: Invalid fg value\n"); + return 1; + } + + // RSH by 8 to correct for the alpha channel + FG_COLOR = (int)(fg << 8); + i++; + continue; + } + + // if the argument does not start with a dash, assume it is a file + if (argv[i][0] != '-') { + printf("Filename: %s\n", argv[i]); + file_name = argv[i]; + continue; + } + } + + srand(time(0)); + + Chip8 lchip8 = Chip8(file_name); + chip8 = &lchip8; + lchip8.dump_ram(); + int ret = lchip8.run(); + + return ret; +} diff --git a/test.sh b/test.sh index 6695284..f9a2870 100755 --- a/test.sh +++ b/test.sh @@ -25,6 +25,7 @@ extern_roms=( "https://github.com/kripod/chip8-roms/raw/refs/heads/master/hires/Hires%20Stars%20%5BSergey%20Naydenov,%202010%5D.ch8" "https://github.com/kripod/chip8-roms/raw/refs/heads/master/programs/Random%20Number%20Test%20%5BMatthew%20Mikolay,%202010%5D.ch8" "https://github.com/kripod/chip8-roms/raw/refs/heads/master/hires/Hires%20Sierpinski%20%5BSergey%20Naydenov,%202010%5D.ch8" + "https://github.com/kripod/chip8-roms/raw/refs/heads/master/games/Worm%20V4%20%5BRB-Revival%20Studios,%202007%5D.ch8" ) for rom in "${extern_roms[@]}"; do