Created
July 4, 2019 20:01
-
-
Save kraptor/517443a2645b54ccdc7dbf2017a837b3 to your computer and use it in GitHub Desktop.
Mu - Chip-8 Emulator UI made in Nim with Imgui
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import math | |
import strutils | |
import strformat | |
import nimgl/opengl | |
import nimgl/glfw | |
import nimgl/imgui | |
import nimgl/imgui/impl_opengl | |
import nimgl/imgui/impl_glfw | |
import ./emulator | |
import ./opcode | |
import ./opcode_impl | |
import ./opcode_exec | |
import ./opcode_disasm | |
import ../dialog/dialog | |
type EmulatorUI* = object of RootObj | |
emulator*: Emulator | |
memory_width*: range[1..16] | |
display_texture_id*: GLuint | |
display_zoom*: float32 | |
follow_pc*: int | |
step_many*: int32 | |
step_speed*: int32 | |
proc create_emulator_ui*(emulator: Emulator): EmulatorUI = | |
EmulatorUI( | |
emulator: emulator, | |
memory_width: 8, | |
display_texture_id: 0, | |
display_zoom: 4.0, | |
follow_pc: 1, | |
step_many: 10, | |
step_speed: 10 | |
) | |
proc tooltip(text: string): void = | |
if igIsItemHovered(): | |
igBeginTooltip() | |
igTextUnformatted(text) | |
igEndTooltip() | |
proc render_rom_info(ui: EmulatorUI) = | |
discard igBegin(fmt"Chip-8 - Rom Info") | |
block: # Display rom information | |
igText(fmt"ROM: {ui.emulator.get_rom_filename}") | |
igEnd() | |
proc render_controls(ui: EmulatorUI) = | |
discard igBegin(fmt"Chip-8 - Controls", nil, ImGuiWindowFlags_AlwaysAutoResize) | |
block: # Control buttons and status text | |
let btn_width = 150.0 | |
let btn_size = ImVec2(x:btn_width, y:0) | |
var file_name: string | |
if igButton("Load ROM", btn_size): | |
let file_filters = [ | |
"Chip-8 roms (*.ch8)" , "*.ch8", | |
"All files (*.*)" , "*.*" | |
].join("\0") | |
let c_file_name: cstring = file_dialog_open(NOC_FILE_DIALOG_OPEN, file_filters, getCurrentDir(), nil) | |
if c_file_name != nil: | |
file_name = $c_file_name | |
if ui.emulator.load_rom(file_name): | |
echo fmt"Rom loaded: {file_name}" | |
else: | |
igOpenPopup("load_rom_error") | |
else: | |
# rom loading cancelled | |
discard | |
if igBeginPopupModal("load_rom_error"): | |
igText(fmt"Error loading rom: {file_name}") | |
if igButton("Close", btn_size): | |
igCloseCurrentPopup() | |
igEndPopup() | |
if not ui.emulator.is_running(): | |
if igButton("Run", btn_size): | |
ui.emulator.start() | |
tooltip("Start emulation") | |
else: | |
if igButton("Pause", btn_size): | |
ui.emulator.pause() | |
tooltip("Start emulation") | |
var run_status = if ui.emulator.is_running(): "Running" else: "Stopped" | |
igTextUnformatted(fmt"Status: {run_status}") | |
igSeparator() | |
igText("Debug") | |
if igButton("Step to unknown", btn_size): | |
ui.emulator.start({EF_STOP_ON_UNKNOWN_OPCODE}) | |
if igButton("Step One", btn_size): | |
ui.emulator.start({}, 1) | |
tooltip("Step") | |
if igButton(fmt"Step +{ui.step_many}", btn_size): | |
ui.emulator.start({}, ui.step_many) | |
tooltip("Step emulation") | |
igSetNextItemWidth(btn_width); | |
discard igInputInt("##step_many", ui.step_many.unsafe_addr, 1, 1) | |
if igButton("Skip One", btn_size): | |
ui.emulator.PC += 2 | |
tooltip("Step") | |
igSeparator() | |
igText("Emulation speed:") | |
igSetNextItemWidth(btn_width); | |
discard igInputInt("##step_speed", ui.step_speed.unsafe_addr, 1, 1) | |
igSeparator() | |
if igButton("Reset", btn_size): | |
ui.emulator.reset() | |
tooltip("Start emulation") | |
if igButton("Reload ROM", btn_size): | |
discard ui.emulator.reload_rom() | |
tooltip("Reload ROM") | |
igSeparator() | |
igText("Display size:") | |
igSetNextItemWidth(btn_width); | |
discard igInputFloat("##display_zoom", ui.display_zoom.unsafe_addr, 1.1, 2.0) | |
igEnd() | |
proc render_memory(ui: EmulatorUI): void = | |
igSetNextWindowSizeConstraints(ImVec2(x: 400, y:0), ImVec2(x: float.high, y: float.high)) | |
if igBegin("Chip-8 - CPU Memory"): | |
igSetNextItemWidth(-1); | |
discard igSliderInt("##", cast[ptr int32](ui.memory_width.unsafe_addr), 1, 16, "%d") | |
discard igBeginChild( | |
"Memory", | |
ImVec2(x: -1, y: -1), | |
false, | |
ImGuiWindowFlags_HorizontalScrollbar | |
) | |
block: | |
var clipper: ImGuiListClipper | |
let total_addresses = ui.emulator.memory.len | |
let lines_to_draw = ceil(total_addresses / ui.memory_width).int | |
let last_line_address = (lines_to_draw - 1) * ui.memory_width | |
var last_line_items = total_addresses mod ui.memory_width | |
if last_line_items == 0: | |
last_line_items = ui.memory_width.int | |
begin(clipper.addr, lines_to_draw.int32) | |
while step(clipper.addr): | |
for offset in clipper.displayStart ..< clipper.displayEnd: | |
let address = offset * ui.memory_width | |
if address == ui.emulator.base_address.int32: | |
igSeparator() | |
let max_items = ui.memory_width.int | |
var item_count = max_items | |
if address == last_line_address: | |
item_count = last_line_items | |
# display address | |
igText(fmt"0x{int(address):>03x} |") | |
# display address content (hex) | |
for base in 0 ..< item_count: | |
igSameLine() | |
igText(fmt"{ui.emulator.memory[address+base]:>02x}") | |
for _ in item_count ..< max_items: | |
igSameLine() | |
igText("..") | |
# display address content (ascii) | |
igSameLine() | |
var text = "| " | |
for base in 0 ..< item_count: | |
text &= fmt"{ui.emulator.memory[address+base].chr}" | |
for _ in item_count ..< max_items: | |
text &= fmt" " | |
igText(text) | |
igEndChild() | |
igEnd() | |
proc render_cpu_dissasembler(ui: EmulatorUI): void = | |
if igBegin("Chip-8 - CPU Dissasembler"): | |
discard igCheckbox("Follow PC:", cast[ptr bool](ui.follow_pc.unsafeAddr)) | |
discard igBeginChild( | |
"Dissasembler", | |
ImVec2(x: -1, y: -1), | |
false, | |
ImGuiWindowFlags_HorizontalScrollbar | |
) | |
block: | |
const bytes_per_instruction = 2 | |
let total_addresses = ui.emulator.memory.len | |
let lines_to_draw = ceil(total_addresses / bytes_per_instruction).int | |
var last_line_items = total_addresses mod bytes_per_instruction | |
if last_line_items == 0: | |
last_line_items = bytes_per_instruction | |
for line in 0 ..< lines_to_draw: | |
let address = line * bytes_per_instruction | |
discard igSelectable("", address == ui.emulator.PC.int32, ImGuiSelectableFlags_None, ImVec2(x:0, y:0)) | |
if address == ui.emulator.PC.int32 and ui.follow_pc != 0: | |
igSetScrollHereY(0) | |
igSameLine() | |
# display address | |
igText(fmt"0x{int(address):>03x} |") | |
# display address content (hex) | |
for base in 0 ..< bytes_per_instruction: | |
igSameLine() | |
igText(fmt"{ui.emulator.memory[address+base]:>02x}") | |
# display opcode dissasembly at address | |
igSameLine() | |
var text = fmt"| {ui.emulator.opcode_at(address.uint16).disasm}" | |
igText(text) | |
igEndChild() | |
igEnd() | |
proc render_cpu_state(ui: EmulatorUI): void = | |
igSetNextWindowSizeConstraints(ImVec2(x: 350, y:0), ImVec2(x: float.high, y: float.high)) | |
if igBegin("Chip-8 - CPU State", nil, ImGuiWindowFlags_AlwaysAutoResize): | |
igColumns(2) | |
igText("Step count:") | |
igNextColumn() | |
igText(fmt"{ui.emulator.step_count}") | |
igColumns(1) | |
igSeparator() | |
block: # Display internal registers | |
igColumns(4) | |
igText(fmt"PC") | |
igText(fmt"0x{ui.emulator.PC:>04x}") | |
igText(fmt" {ui.emulator.PC:>5}") | |
igNextColumn() | |
igText(fmt"I") | |
igText(fmt"0x{ui.emulator.I:>04x}") | |
igText(fmt" {ui.emulator.I:>5}") | |
igNextColumn() | |
igText(fmt"DT (delay)") | |
igText(fmt"0x{ui.emulator.DT:>04x}") | |
igText(fmt" {ui.emulator.DT:>5}") | |
igNextColumn() | |
igText(fmt"ST (sound)") | |
igText(fmt"0x{ui.emulator.ST:>04x}") | |
igText(fmt" {ui.emulator.ST:>5}") | |
igNextColumn() | |
igColumns(1) | |
igSpacing() | |
block: # Display Vx registers | |
let meta_index = [0..<8, 8..<16] | |
for indexes in meta_index: | |
igColumns(8) | |
igSeparator() | |
for index in indexes: | |
igText(fmt" V{index.ord:X}") | |
igText(fmt" {ui.emulator.V[index]:>3}") | |
igText(fmt"0x{ui.emulator.V[index]:>02x}") | |
igNextColumn() | |
igColumns(1) | |
igSeparator() | |
igSpacing() | |
block: # Display keyboard status | |
igText("Keys: ") | |
for k in ui.emulator.get_keys_pressed(): | |
igSameLine() | |
igText(fmt" {k} ({k.int:x})") | |
block: # Display Stack registers | |
igText(fmt"SP: 0x{ui.emulator.SP:>04x} ({ui.emulator.SP})") | |
if igCollapsingHeader("[stack]", ImGuiTreeNodeFlags_DefaultOpen): | |
igColumns(3) | |
for index, x in pairs(ui.emulator.stack): | |
igText(fmt"Depth {index}") | |
igNextColumn() | |
igText(fmt"{x:#x}") | |
igNextColumn() | |
igText(fmt"{x}") | |
igNextColumn() | |
igColumns(1) | |
igEnd() | |
proc render_display(ui: EmulatorUI) = | |
var run_status = if ui.emulator.is_running(): "Running" else: "Stopped" | |
discard igBegin( | |
fmt"Chip-8 - Display ({run_status})###chip8_display", | |
nil, | |
ImGuiWindowFlags_AlwaysAutoResize or ImGuiWindowFlags_NoCollapse | |
) | |
var w: Natural = ui.emulator.display_width | |
var h: Natural = ui.emulator.display_height | |
if ui.display_texture_id == 0: | |
glGenTextures(1, ui.display_texture_id.unsafe_addr) | |
glBindTexture(GL_TEXTURE_2D, ui.display_texture_id) | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST.ord) | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST.ord) | |
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0) | |
# upload new texture while running | |
glTexImage2D( | |
GL_TEXTURE_2D, | |
0, | |
GL_RGBA8.ord, | |
GLsizei(w), | |
GLsizei(h), | |
0, | |
GL_RGBA, | |
GL_UNSIGNED_BYTE, | |
ui.emulator.display.addr | |
) | |
igImage( | |
cast[ImTextureID](ui.display_texture_id), | |
ImVec2(x: 62 * ui.display_zoom, y:32 * ui.display_zoom), # ImVec2(x: float32(image_height), y: float32(image_width)), | |
Imvec2(x: 0, y: 0), # uv0 | |
Imvec2(x: 1, y: 1), # uv1 | |
ImVec4(x: 1, y: 1, z: 1, w: 1), # tint color | |
ImVec4(x: 1, y: 1, z: 1, w: 0.5f) # border color | |
) | |
igEnd() | |
proc ui_button(ui: EmulatorUI, title: string, k: EmulatorKey) = | |
discard igButton(title, ImVec2(x: 50, y:50)) | |
if igIsItemActivated(): | |
ui.emulator.key_press(k) | |
if igIsItemDeactivated(): | |
ui.emulator.key_release(k) | |
proc render_keyboard(ui: EmulatorUI) = | |
let btn_size = ImVec2(x: 50, y:50) | |
if igBegin("Chip-8 - Keyboard", nil, ImGuiWindowFlags_AlwaysAutoResize): | |
ui_button(ui, "1", KEY_1) | |
igSameLine() | |
ui_button(ui, "2", KEY_2) | |
igSameLine() | |
ui_button(ui, "3", KEY_3) | |
igSameLine() | |
ui_button(ui, "C", KEY_C) | |
ui_button(ui, "4", KEY_4) | |
igSameLine() | |
ui_button(ui, "5", KEY_5) | |
igSameLine() | |
ui_button(ui, "6", KEY_6) | |
igSameLine() | |
ui_button(ui, "D", KEY_D) | |
ui_button(ui, "7", KEY_7) | |
igSameLine() | |
ui_button(ui, "8", KEY_8) | |
igSameLine() | |
ui_button(ui, "9", KEY_9) | |
igSameLine() | |
ui_button(ui, "E", KEY_E) | |
ui_button(ui, "A", KEY_A) | |
igSameLine() | |
ui_button(ui, "0", KEY_0) | |
igSameLine() | |
ui_button(ui, "B", KEY_B) | |
igSameLine() | |
ui_button(ui, "F", KEY_F) | |
igEnd() | |
proc render*(ui: EmulatorUI) = | |
# igStyleColorsLight() | |
# igShowDemoWindow() | |
ui.render_controls() | |
ui.render_rom_info() | |
ui.render_cpu_state() | |
ui.render_cpu_dissasembler() | |
ui.render_memory() | |
ui.render_display() | |
ui.render_keyboard() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment