diff --git a/source/app.d b/source/app.d index 65c0787..525b423 100644 --- a/source/app.d +++ b/source/app.d @@ -8,10 +8,175 @@ import imgui; struct Chip8Status { + struct Assembly { + + import assembler; + + alias OpCode = Assembler.OpCode; + alias Instr = Assembler.Instruction; + alias Arg = Assembler.Argument; + + enum ViewMode { + Memory, + Disassembly + } + + private { + + Chip8* emu_; + ViewMode mode_; + int range_start_; + int range_end_; + + // disassembly state + Instr[] disassembly_; + + } + + void initialize(Chip8* emu) { + emu_ = emu; + } // initialize + + static const (char)* formatQual(Arg a, bool with_comma = false) { + if (with_comma) { + switch (a) with (Arg) { + case Vx, Vy, V0: return "V%hu, "; + case nibble: return "%01X, "; + case beit: return "0x%02X, "; + case addr: return "0x%03X, "; + default: return "%s, "; + } + } else { + switch (a) with (Arg) { + case Vx, Vy, V0: return "V%hu"; + case nibble: return "0x%01X"; + case beit: return "0x%02X"; + case addr: return "0x%03X"; + default: return "%s"; + } + } + } + + void disassemble() { + ubyte[] instrs = emu_.ram[range_start_ .. range_end_]; + disassembly_ = Assembler.disassemble(instrs); + writefln("%s", disassembly_); + } + + void setRange(ushort start, ushort end) { + range_start_ = start; + range_end_ = end; + } + + void draw() { + + import std.range : chunks; + + if (mode_ == ViewMode.Memory) igBegin("Emulator Assembly - Memory View"); + else if (mode_ == ViewMode.Disassembly) igBegin("Emulator Assembly - Disassembly View"); + + if (igButton("Switch Mode")) { + mode_ = cast(ViewMode)((mode_ + 1) % 2); + } + + if (mode_ == ViewMode.Memory) { + } else if (mode_ == ViewMode.Disassembly) { + igInputInt("Range Start: ", &range_start_); + igInputInt("Range End: ", &range_end_); + if (igButton("Disassemble")) { + disassemble(); + } + } + + if (range_start_ < 0) range_start_ = 0; + if (range_start_ > ushort.max) range_start_ = 0; + + if (range_end_ < range_start_) range_end_ = 0; + if (range_end_ > ushort.max) range_end_ = 0; + + if (mode_ == ViewMode.Memory) { + /* + foreach (ref ubyte[] b; emu_.ram[0x200 + range_start_ .. range_end_].chunks(2)) { + ushort opcode = (*(cast(ushort*)(b.ptr))); + igText("0x%04X", opcode); + } + */ + } else if (mode_ == ViewMode.Disassembly) { + foreach (ref instr; disassembly_) { + switch (instr.args) { + case 3: + + igText("%s", instr.op); + igSameLine(); + + Arg a1_arg; + auto a1 = instr.a1(a1_arg); + igText(formatQual(a1_arg), a1, true); + igSameLine(); + + Arg a2_arg; + auto a2 = instr.a2(a2_arg); + igText(formatQual(a2_arg), a2, true); + igSameLine(); + + Arg a3_arg; + auto a3 = instr.a3(a3_arg); + igText(formatQual(a3_arg), a3); + + break; + + case 2: + + igText("%s", instr.op); + igSameLine(); + + Arg a1_arg; + auto a1 = instr.a1(a1_arg); + igText(formatQual(a1_arg), a1, true); + igSameLine(); + + Arg a2_arg; + auto a2 = instr.a2(a2_arg); + igText(formatQual(a2_arg), a2); + + break; + + case 1: + + igText("%s", instr.op); + igSameLine(); + + Arg a1_arg; + auto a1 = instr.a1(a1_arg); + igText(formatQual(a1_arg), a1); + + break; + + case 0: + + igText("%s", instr.op); + break; + + default: + break; + + } + } + } + + igEnd(); + + } // draw + + } // Assembly + // emu ptr Emulator* run_; Chip8* emu_; + // assembly/disassembly + Assembly ass_; + // loaded program const (char)* loaded_program; @@ -25,6 +190,7 @@ struct Chip8Status { void initialize(Emulator* run, Chip8* emu) { this.run_ = run; this.emu_ = emu; + ass_.initialize(emu); } // initialize alias Callback = float delegate(int idx, const char** out_text); @@ -65,6 +231,7 @@ struct Chip8Status { loaded_program = "chip8_picture.ch8"; auto buf = read("programs/chip8_picture.ch8"); emu_.load(0x200, buf); // do ze load yes, will copy all the data in + ass_.setRange(cast(ushort)0x200, cast(ushort)(0x200 + cast(ushort)buf.length)); } // loadShortcut @@ -94,6 +261,8 @@ struct Chip8Status { void draw() { + ass_.draw(); + if (!status_menu_) return; if (igBeginMainMenuBar()) { diff --git a/source/assembler.d b/source/assembler.d index c7a08e6..92e5f6f 100644 --- a/source/assembler.d +++ b/source/assembler.d @@ -43,44 +43,51 @@ struct Assembler { 0xFx65 - LD Vx, [I] */ - enum OpCode { + enum OpCode : const (char)* { - UNK, // UNKNOWN + UNK = "UNK", // UNKNOWN - CLS, // CLS - RET, // RET - CALL, // CALL addr + CLS = "CLS", // CLS + RET = "RET", // RET + CALL = "CALL", // CALL addr - ADD, // ADD Vx, Vy + ADD = "ADD", // ADD Vx, Vy // ADD Vx, byte // ADD I, Vx - SUB, // SUB Vx, Vy + SUB = "SUB", // SUB Vx, Vy // SUB Vx, byte - SUBN, // SUBN Vx, Vy + SUBN = "SUBN", // SUBN Vx, Vy - SE, // SE Vx, Vy + SE = "SE", // SE Vx, Vy // SE Vx, byte - SNE, // SNE Vx, byte - SKP, // SKP Vx - SKNP, // SKNP Vx + SNE = "SNE", // SNE Vx, byte + SKP = "SKP", // SKP Vx + SKNP = "SKNP", // SKNP Vx - SHL, // SHL Vx {, Vy} - SHR, // SHR Vx {, Vy} - XOR, // XOR Vx, Vy - AND, // AND Vx, Vy - OR, // OR Vx, Vy + SHL = "SHL", // SHL Vx {, Vy} + SHR = "SHR", // SHR Vx {, Vy} + XOR = "XOR", // XOR Vx, Vy + AND = "AND", // AND Vx, Vy + OR = "OR", // OR Vx, Vy - LD, // LD Vx, DT + LD = "LD", // LD Vx, DT // LD DT, Vx // LD ST, Vx // LD Vx, K // LD [I], Vx // LD Vx, [I] - RND // RND Vx, byte + RND = "RND", // RND Vx, byte, + + JP = "JP", // JP addr + // JP V0, addr + + DRW = "DRW", // DRW Vx, Vy, nibble + + SYS = "SYS" // SYS addr | probably unused? but still } @@ -90,20 +97,137 @@ struct Assembler { Vx, Vy, + V0, + I, + I_addr, DT, ST, K, + B, + F, + beit, + nibble, addr, - val } struct Instruction { - OpCode op; - Argument a1; - Argument a2; + + @property { + ubyte vx() { + return (v_ & 0x0F00) >> 8; + } + + ubyte vy() { + return (v_ & 0x00F0) >> 4; + } + + ubyte n() { + return (v_ & 0x000F); + } + + ubyte nn() { + return (v_ & 0x00FF); + } + + ushort nnn() { + return (v_ & 0x0FFF); + } + } + + private { + + ushort v_; // raw value + + OpCode op_; + Argument a1_; + Argument a2_; + Argument a3_; + + } + + void* extract(Argument a) { + final switch (a) with (Argument) { + + case Vx: return cast(void*)(vx()); + case Vy: return cast(void*)(vy()); + case V0: return cast(void*)(0); + + case I: return cast(void*)("I".ptr); + case I_addr: return cast(void*)("[I]".ptr); + + case DT: return cast(void*)("DT".ptr); + case ST: return cast(void*)("ST".ptr); + + case B: return cast(void*)("B".ptr); + case F: return cast(void*)("F".ptr); + case K: return cast(void*)("K".ptr); + + case nibble: return cast(void*)(n()); + case beit: return cast(void*)(nn()); + case addr: return cast(void*)(nnn()); + + case Nil: return cast(void*)(0); + + } + } + + @property + OpCode op() { + return op_; + } + + void* a1(out Argument a) { + a = a1_; + return extract(a1_); + } + + void* a2(out Argument a) { + a = a2_; + return extract(a2_); + } + + void* a3(out Argument a) { + a = a3_; + return extract(a3_); + } + + @property int args() { + int args = 0; + args += (a1_ != Argument.Nil); + args += (a2_ != Argument.Nil); + args += (a3_ != Argument.Nil); + return args; + } + + this(ushort v, OpCode op, Argument a) { + v_ = v; + op_ = op; + a1_ = a; + } + + this(ushort v, OpCode op, Argument a1, Argument a2) { + v_ = v; + op_ = op; + a1_ = a1; + a2_ = a2; + } + + this(ushort v, OpCode op, Argument a1, Argument a2, Argument a3) { + v_ = v; + op_ = op; + a1_ = a1; + a2_ = a2; + a3_ = a3; + } + + this(ushort v, OpCode op) { + v_ = v; + op_ = op; + } + } import std.array; @@ -129,52 +253,98 @@ struct Assembler { } // assemble - const (char)[] disassemble(ubyte[] instructions) { + static Instruction[] disassemble(ubyte[] instructions) { import std.range : chunks; - foreach (ref ubyte[] i; instructions.chunks(2)) { + auto instr_output = Appender!(Instruction[])(); - ushort opcode = (*(cast(ushort*)(i.ptr))); + foreach (ref ubyte[] i; instructions.chunks(2)) { + ushort opcode = i[0] << 8 | i[1]; switch (opcode & 0xF000) { case 0x0000: switch (opcode & 0x0FFF) { case 0x00E0: // 0x00E0 Clears the screen. | CLS - + instr_output ~= Instruction(opcode, OpCode.CLS); + break; case 0x00EE: // 0x00EE Returns from a subroutine. | RET + instr_output ~= Instruction(opcode, OpCode.RET); + break; default: // 0x0NNN Calls RCA 1802 program at address NNN. Not necessary for most ROMs. | SYS addr //assert(0, "0x0NNN RCA 1802 program opcode not implemented!"); + instr_output ~= Instruction(opcode, OpCode.SYS, Argument.addr); break; } break; case 0x1000: // 0x1NNN Jumps to address NNN. | JP addr + instr_output ~= Instruction(opcode, OpCode.JP, Argument.addr); + break; case 0x2000: // 0x2NNN Calls subroutine at NNN. | CALL addr + instr_output ~= Instruction(opcode, OpCode.CALL, Argument.addr); + break; case 0x3000: // 0x3XNN Skips the next instruction if VX equals NN. | SE Vx, byte + instr_output ~= Instruction(opcode, OpCode.SE, Argument.Vx, Argument.beit); + break; case 0x4000: // 0x4XNN Skips the next instruction if VX doesn't equal NN. | SNE Vx, byte + instr_output ~= Instruction(opcode, OpCode.SNE, Argument.Vx, Argument.beit); + break; case 0x5000: // 0x5XYO Skips the next instruction if VX equals VY. | SE Vx, Vy + instr_output ~= Instruction(opcode, OpCode.SE, Argument.Vx, Argument.Vy); + break; case 0x6000: // 0x6XNN Sets VX to NN. | LD Vx, byte - case 0x7000: // 0x7XNN Adds NN to VX. | AD Vx, byte + instr_output ~= Instruction(opcode, OpCode.LD, Argument.Vx, Argument.beit); + break; + case 0x7000: // 0x7XNN Adds NN to VX. | ADD Vx, byte + instr_output ~= Instruction(opcode, OpCode.ADD, Argument.Vx, Argument.beit); + break; case 0x8000: switch (opcode) { case 0x0000: // 0x8XY0 Sets VX to the value of VY. | LD Vx, Vy + instr_output ~= Instruction(opcode, OpCode.LD, Argument.Vx, Argument.Vy); + break; case 0x0001: // 0x8XY1 Sets VX to VX or VY. | OR Vx, Vy + instr_output ~= Instruction(opcode, OpCode.OR, Argument.Vx, Argument.Vy); + break; case 0x0002: // 0x8XY2 Sets VX to VX and VY. | AND Vx, Vy + instr_output ~= Instruction(opcode, OpCode.AND, Argument.Vx, Argument.Vy); + break; case 0x0003: // 0x8XY3 Sets VX to VX xor VY. | XOR Vx, Vy + instr_output ~= Instruction(opcode, OpCode.XOR, Argument.Vx, Argument.Vy); + break; case 0x0004: // 0x8XY4 Adds VY to VX. VF is set to 1 when there's a carry, and to 0 when there isn't. | ADD Vx, Vy + instr_output ~= Instruction(opcode, OpCode.ADD, Argument.Vx, Argument.Vy); + break; case 0x0005: // 0x8XY5 VY is subtracted from VX. VF is set to 0 when there's a borrow, and 1 when there isn't. | SUB Vx, Vy + instr_output ~= Instruction(opcode, OpCode.SUB, Argument.Vx, Argument.Vy); + break; case 0x0006: // 0x8XY6 Shifts VX right by one. VF is set to the value of the least significant bit of VX before the shift. | SHR Vx {, Vy} + instr_output ~= Instruction(opcode, OpCode.SHR, Argument.Vx); // FIXME? what about Vy here actually? + break; case 0x0007: // 0x8XY7 Sets VX to VY minus VX. VF is set to 0 when there's a borrow, and 1 when there isn't. | SUBN Vx, Vy + instr_output ~= Instruction(opcode, OpCode.SUBN, Argument.Vx, Argument.Vy); + break; case 0x000E: // 0x8XYE Shifts VX left by one. VF is set to the value of the most significant bit of VX before the shift. | SHL Vx {, Vy} + instr_output ~= Instruction(opcode, OpCode.SHL, Argument.Vx); // FIXME? what about Vy here actually? + break; default: // unhandled for some reason + instr_output ~= Instruction(opcode, OpCode.UNK); writefln("unknown opcode: 0x%x", opcode); break; } break; case 0x9000: // 0x9XYO Skips the next instruction if VX doesn't equal VY. | SNE Vx, Vy + instr_output ~= Instruction(opcode, OpCode.SNE, Argument.Vx, Argument.Vy); + break; case 0xA000: // 0xANNN Sets I to the address NNN. | LD I, addr + instr_output ~= Instruction(opcode, OpCode.LD, Argument.I, Argument.addr); + break; case 0xB000: // 0xBNNN Jumps to the address NNN plus V0. | JP V0, addr + instr_output ~= Instruction(opcode, OpCode.JP, Argument.V0, Argument.addr); + break; case 0xC000: // 0xCXNN Sets VX to the result of a bitwise and operation on a random number and NN. | RND Vx, byte + instr_output ~= Instruction(opcode, OpCode.RND, Argument.Vx, Argument.beit); + break; // 0xDXYN | DRW Vx, Vy, nibble // Sprites stored in memory at location in index register (I), 8bits wide. @@ -183,11 +353,18 @@ struct Assembler { // Sprites are drawn starting at position VX, VY. N is the number of 8bit rows that need to be drawn. // If N is greater than 1, second line continues at position VX, VY+1, and so on. case 0xD000: + instr_output ~= Instruction(opcode, OpCode.DRW, Argument.Vx, Argument.Vy, Argument.beit); + break; case 0xE000: switch (opcode & 0x000F) { case 0x000E: // 0xEX9E Skips the next instruction if the key stored in VX is pressed. | SKP Vx + instr_output ~= Instruction(opcode, OpCode.SKP, Argument.Vx); + break; case 0x0001: // 0xEXA1 Skips the next instruction if the key stored in VX isn't pressed. | SKNP Vx + instr_output ~= Instruction(opcode, OpCode.SKNP, Argument.Vx); + break; default: // unhandled for some reason + instr_output ~= Instruction(opcode, OpCode.UNK); writefln("unknown opcode: 0x%x", opcode); break; } @@ -195,28 +372,48 @@ struct Assembler { case 0xF000: switch (opcode & 0x00FF) { case 0x0007: // 0xFX07 Sets VX to the value of the delay timer. | LD Vx, DT + instr_output ~= Instruction(opcode, OpCode.LD, Argument.Vx, Argument.DT); + break; case 0x000A: // 0xFX0A A key press is awaited, and then stored in VX. | LD Vx, K + instr_output ~= Instruction(opcode, OpCode.LD, Argument.Vx, Argument.K); + break; case 0x0015: // 0xFX15 Sets the delay timer to VX. | LD DT, Vx + instr_output ~= Instruction(opcode, OpCode.LD, Argument.DT, Argument.Vx); + break; case 0x0018: // 0xFX18 Sets the sound timer to VX. | LD ST, Vx + instr_output ~= Instruction(opcode, OpCode.LD, Argument.ST, Argument.Vx); + break; case 0x001E: // 0xFX1E Adds VX to I. | ADD I, Vx + instr_output ~= Instruction(opcode, OpCode.ADD, Argument.I, Argument.DT); + break; case 0x0029: // 0xFX29 Sets I to the location of the sprite for the character in VX. | LD F, Vx + instr_output ~= Instruction(opcode, OpCode.LD, Argument.F, Argument.DT); + break; // 0xFX33 Stores the Binary-coded decimal representation of VX, // with the most significant of three digits at the address in I, // the middle digit at I plus 1, and the least significant digit at I plus 2. case 0x0033: // 0xFX33 ??? FIXME? | LD B, Vx + instr_output ~= Instruction(opcode, OpCode.LD, Argument.B, Argument.Vx); + break; case 0x0055: // 0xFX55 Stores V0 to VX in memory starting at address I. | LD [I], Vx + instr_output ~= Instruction(opcode, OpCode.LD, Argument.I_addr, Argument.DT); + break; case 0x0065: // 0xFX65 Fills V0 to VX with values from memory starting at address I. | LD Vx, [I] + instr_output ~= Instruction(opcode, OpCode.LD, Argument.Vx, Argument.I_addr); + break; default: // unhandled for some reason + instr_output ~= Instruction(opcode, OpCode.UNK); writefln("unknown opcode: 0x%x", opcode); break; } break; default: + instr_output ~= Instruction(opcode, OpCode.UNK); writefln("unknown opcode: 0x%x", opcode); } } - return ""; + return instr_output.data; } // dissassemble