Skip to content

Instantly share code, notes, and snippets.

@kraptor
Created July 4, 2019 20:01
Show Gist options
  • Save kraptor/517443a2645b54ccdc7dbf2017a837b3 to your computer and use it in GitHub Desktop.
Save kraptor/517443a2645b54ccdc7dbf2017a837b3 to your computer and use it in GitHub Desktop.
Mu - Chip-8 Emulator UI made in Nim with Imgui
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