├── .github └── FUNDING.yml ├── .gitmodules ├── LICENSE.md ├── README.md ├── assets ├── 1khz.wav ├── screenshot.png ├── speaker-mute.png └── speaker.png ├── conf.lua ├── keypad.lua ├── main.lua ├── ui.lua └── util.lua /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: tobiasvl # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "moonshine"] 2 | path = moonshine 3 | url = https://github.com/vrld/moonshine.git 4 | [submodule "love-imgui-filedialog"] 5 | path = love-imgui-filedialog 6 | url = https://github.com/tobiasvl/love-imgui-filedialog 7 | [submodule "moon6800"] 8 | path = moon6800 9 | url = git@github.com:tobiasvl/moon6800.git 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 Tobias V. Langhoff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Daterad Rekreationell Övnings-Mikrodator 2 | ======================================== 3 | 4 | DRÖM ("DREAM" in Swedish) is an emulator for the CHIP-8-based DREAM 6800 home computer, powered by [LÖVE](https://love2d.org) and [Moon6800](https://github.com/tobiasvl/moon6800). 5 | 6 | You can read about the development of DRÖM in [these blog posts](https://tobiasvl.github.io/tags/#dream-6800), and about the DREAM's history on the official [DREAM 6800 Archive Site](http://www.mjbauer.biz/DREAM6800.htm). 7 | 8 | ![Screenshot of CHIPOS in DRÖM](assets/screenshot.png) 9 | 10 | Features 11 | -------- 12 | 13 | * Faithful emulation of the DREAM 14 | * 4K of RAM 15 | * Can quickload and play CHIP-8 games 16 | * Includes software: 17 | * Michael J. Bauer's original CHIPOS monitor program 18 | * Michael J. Bauer's DREAM INVADERS game 19 | * A comprehensive debug interface, powered by [love-imgui](https://github.com/slages/love-imgui) 20 | * CRT shaders! 21 | 22 | A very special thanks to Michael J. Bauer who permitted me to include the original software. 23 | 24 | Memory map 25 | ---------- 26 | 27 | | Address range | Contents | 28 | |----------------|----------------| 29 | | `0000` - `03FF`| RAM | 30 | | `8010` - `8011`| PIA A (keypad) | 31 | | `8012` - `8013`| PIA B (speaker & cassette) | 32 | | `C000` - `C3FF`| EPROM | 33 | | `C400` - `FFFF`| Mirrored EPROM | 34 | 35 | CHIPOS 36 | ------ 37 | 38 | When CHIPOS starts up, it will display an address at the bottom of the screen. 39 | 40 | First, type a 4-digit hexadecimal number to enter a memory address. 41 | 42 | Then execute one of the following commands: 43 | 44 | * FN, 0: MEMOD (MEMory MODify): The contents of the address will be displayed as a 2-digit hexadecimal number. Type a new 2-digit number to replace the memory contents. The memory address will automatically advance by one. You can also press FN to advance one byte without modifying anyting. 45 | * FN, 3: Run: Executes a program starting at the current address. To run a CHIP-8 program (which is loaded at address `0200`), run the CHIP-8 interpreter at `C000`. 46 | 47 | Loading (FN, 1) and dumping (FN, 2) cassettes isn't supported yet, but instead you can use the "File" menu to quickload CHIP-8 games or DREAM INVADERS. 48 | 49 | At any time, press RESET to return to the monitor program. 50 | 51 | History 52 | ------- 53 | 54 | The DREAM 6800 (Domestic Recreational Educational and Adaptive Microcomputer) was a Motorola 6800-based microcomputer created by Michael J. Bauer. Its construction was detailed in _Electronics Australia_ in 1979, and it included a monitor program called CHIPOS (Compact Hexadecimal Interpretive Programming and Operating System) and a CHIP-8 interpreter. 55 | 56 | It had its own newsletter, [DREAMER](https://archive.org/search.php?query=creator%3A%22N.S.W.+6800+Users+Group%22), published between 1980 and 1982. 57 | 58 | You can read more about the DREAM's history on Michael's official [DREAM 6800 Archive Site](http://www.mjbauer.biz/DREAM6800.htm). 59 | -------------------------------------------------------------------------------- /assets/1khz.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobiasvl/drom/17e38471bfe1263b4bddea674b97e4f6a8b15ce4/assets/1khz.wav -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobiasvl/drom/17e38471bfe1263b4bddea674b97e4f6a8b15ce4/assets/screenshot.png -------------------------------------------------------------------------------- /assets/speaker-mute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobiasvl/drom/17e38471bfe1263b4bddea674b97e4f6a8b15ce4/assets/speaker-mute.png -------------------------------------------------------------------------------- /assets/speaker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobiasvl/drom/17e38471bfe1263b4bddea674b97e4f6a8b15ce4/assets/speaker.png -------------------------------------------------------------------------------- /conf.lua: -------------------------------------------------------------------------------- 1 | function love.conf(t) 2 | -- Debug mode 3 | t.console = false 4 | 5 | t.identity = "DRÖM" 6 | t.window.title = "DRÖM 6800" 7 | t.window.resizable = true 8 | 9 | t.version = "11.4" 10 | 11 | -- Disable stuff we don't use 12 | t.accelerometerjoystick = false 13 | t.modules.joystick = false 14 | t.modules.data = false 15 | t.modules.physics = false 16 | t.modules.touch = false 17 | t.modules.video = false 18 | end 19 | -------------------------------------------------------------------------------- /keypad.lua: -------------------------------------------------------------------------------- 1 | local keypad = {} 2 | 3 | keypad.keys_digitran = { 4 | layout = { 5 | [0]=0x0, 0x1, 0x2, 0x3, 6 | 0x4, 0x5, 0x6, 0x7, 7 | 0x8, 0x9, 0xA, 0xB, 8 | 0xC, 0xD, 0xE, 0xF 9 | }, 10 | lines = { 11 | -- The most significant nibble is inverted here from what the 12 | -- actual DREAM keypad sends, but when the PIA A peripheral pins 13 | -- send output to the keypad in the decoding routine, some 14 | -- pullups in the keypad invert the signal for decoding the 15 | -- most significant nibble. 16 | [0]=0xEE, 0xED, 0xEB, 0xE7, 17 | 0xDE, 0xDD, 0xDB, 0xD7, 18 | 0xBE, 0xBD, 0xBB, 0xB7, 19 | 0x7E, 0x7D, 0x7B, 0x77 20 | } 21 | } 22 | 23 | keypad.keys_original = { 24 | layout = { 25 | [0]=0xC, 0xD, 0xE, 0xF, 26 | 0x8, 0x9, 0xA, 0xB, 27 | 0x4, 0x5, 0x6, 0x7, 28 | 0x0, 0x1, 0x2, 0x3 29 | }, 30 | lines = { 31 | [0]=0x7E, 0x7D, 0x7B, 0x77, 32 | 0xBE, 0xBD, 0xBB, 0xB7, 33 | 0xDE, 0xDD, 0xDB, 0xD7, 34 | 0xEE, 0xED, 0xEB, 0xE7 35 | } 36 | } 37 | 38 | keypad.keys_cosmac = { 39 | layout = { 40 | [0]=0x1, 0x2, 0x3, 0xC, 41 | 0x4, 0x5, 0x6, 0xD, 42 | 0x7, 0x8, 0x9, 0xE, 43 | 0xA, 0x0, 0xB, 0xF 44 | }, 45 | lines = { 46 | [0]=0xED, 0xEB, 0xE7, 0x7E, 47 | 0xDE, 0xDD, 0xDB, 0x7D, 48 | 0xD7, 0xBE, 0xBD, 0x7B, 49 | 0xBB, 0xEE, 0xB7, 0x77 50 | } 51 | } 52 | 53 | keypad.keys_qwerty = { 54 | [0]="1", "2", "3", "4", 55 | "q", "w", "e", "r", 56 | "a", "s", "d", "f", 57 | "z", "x", "c", "v" 58 | } 59 | 60 | keypad.key_mapping = {} 61 | keypad.key_status = {} 62 | keypad.button_status = {} 63 | 64 | keypad.keys_dream = keypad.keys_digitran 65 | for i = 0, 15 do 66 | keypad.key_mapping[keypad.keys_qwerty[i]] = keypad.keys_dream.layout[i] 67 | keypad.key_status[keypad.keys_dream.layout[i]] = false 68 | end 69 | 70 | function keypad:connect(pia_port) 71 | self.pia = pia_port 72 | self.pia.p = 0x0F 73 | end 74 | 75 | function keypad:keypressed(key, scancode) 76 | local hexkey = self.key_mapping[scancode] 77 | if hexkey then 78 | self.key_status[hexkey] = true 79 | self.pia.c1 = true 80 | self.pia.p = self.keys_dream.lines[hexkey] 81 | elseif scancode == "lshift" or scancode == "rshift" then 82 | self.pia.c2 = true 83 | end 84 | end 85 | 86 | function keypad:keyreleased(key, scancode) 87 | local hexkey = self.key_mapping[scancode] 88 | if hexkey then 89 | self.key_status[hexkey] = false 90 | self.pia.c1 = false 91 | self.pia.p = 0x0F 92 | elseif scancode == "lshift" or scancode == "rshift" then 93 | self.pia.c2 = false 94 | end 95 | end 96 | 97 | return keypad -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | local CPU = require "moon6800.cpu" 2 | local UI = require "ui" 3 | local keypad = require "keypad" 4 | local util = require "util" 5 | 6 | local conf = require "conf" 7 | local t = {modules = {}, window = {}} 8 | love.conf(t) 9 | local debug = t.console 10 | 11 | num_instructions = 0 12 | cycles = 0 13 | 14 | function love.load(arg) 15 | --min_dt = 1/60--60 --fps 16 | --next_time = love.timer.getTime() 17 | --debug.debug() 18 | 19 | local memory = require "moon6800.memory" 20 | CPU:init(memory) 21 | 22 | local ram = require("moon6800.ram")(0x0FFF) 23 | memory:connect(0x0000, ram) 24 | 25 | local pia = require "moon6800.pia" 26 | memory:connect(0x8010, pia.a) 27 | memory:connect(0x8012, pia.b) 28 | 29 | keypad:connect(pia.a) 30 | 31 | -- ROM and ROM mirrors 32 | local rom = util.read_file("Dream6800Rom.bin") 33 | local eprom = require("moon6800.eprom")(rom) 34 | for address = 0xC000, 0xFFFF - rom.size + 1, rom.size do 35 | memory:connect(address, eprom) 36 | end 37 | 38 | UI:init(CPU, keypad) 39 | UI.ram = ram 40 | end 41 | 42 | function love.filedropped(file) 43 | util.read_file(file) -- TODO 44 | end 45 | 46 | function love.update(dt) 47 | if not CPU.pause then 48 | while cycles < 1000000 / 50 and not CPU.pause do 49 | cycles = cycles + CPU:cycle() 50 | num_instructions = num_instructions + 1 51 | end 52 | 53 | if cycles >= 1000000 / 50 then 54 | cycles = cycles - (1000000 / 50) 55 | --PIA should check its own interrupt flag and send IRQ til MPU... 56 | CPU.irq = true 57 | end 58 | 59 | for k = 0, 15 do 60 | if keypad.button_status[k] then 61 | keypad:keyreleased(keypad.keys_qwerty[k]) 62 | end 63 | end 64 | 65 | CPU.drawflag = true 66 | end 67 | end 68 | 69 | function love.draw() 70 | UI:draw() 71 | end 72 | 73 | function love.keypressed(key, scancode) 74 | UI:KeyPressed(key, scancode) 75 | if not UI.GetWantCaptureKeyboard() then 76 | keypad:keypressed(key, scancode) 77 | if key == "space" then 78 | CPU.pause = not CPU.pause 79 | elseif key == "escape" then 80 | CPU.reset = true 81 | end 82 | 83 | if CPU.pause and key == "right" then 84 | --next_time = next_time + min_dt 85 | --cycles = cycles + CPU:execute(CPU:decode(CPU:fetch())) 86 | cycles = cycles + CPU:cycle() 87 | CPU.drawflag = true 88 | num_instructions = num_instructions + 1 89 | if cycles >= 1000000 / 50 then 90 | cycles = cycles - (1000000 / 50) 91 | CPU.irq = true 92 | end 93 | end 94 | end 95 | end 96 | 97 | function love.keyreleased(key, scancode) 98 | UI.KeyReleased(key) 99 | if not UI.GetWantCaptureKeyboard() then 100 | keypad:keyreleased(key, scancode) 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /ui.lua: -------------------------------------------------------------------------------- 1 | local imgui = require "imgui" 2 | local util = require "util" 3 | local moonshine = require "moonshine" 4 | local disassembler = require "moon6800.disassembler" 5 | local Filedialog = require "love-imgui-filedialog.filedialog" 6 | local filedialog 7 | 8 | local lg = love.graphics 9 | 10 | local ui = {} 11 | 12 | local hex_input_flags = { 13 | "ImGuiInputTextFlags_CharsHexadecimal", 14 | "ImGuiInputTextFlags_CharsUppercase", 15 | "ImGuiInputTextFlags_EnterReturnsTrue" 16 | } 17 | 18 | function love.mousemoved(x, y) 19 | imgui.MouseMoved(x, y, true) 20 | end 21 | 22 | function love.mousepressed(_, _, button) 23 | imgui.MousePressed(button) 24 | end 25 | 26 | function love.mousereleased(_, _, button) 27 | imgui.MouseReleased(button) 28 | end 29 | 30 | function love.wheelmoved(_, y) 31 | imgui.WheelMoved(y) 32 | end 33 | 34 | love.textinput = imgui.TextInput 35 | love.quit = imgui.ShutDown 36 | 37 | function ui:KeyPressed(key, scancode) 38 | imgui.KeyPressed(key) 39 | if key == "return" and (love.keyboard.isDown("ralt") or love.keyboard.isDown("lalt")) then 40 | self.fullscreenDisplay = not self.fullscreenDisplay 41 | end 42 | end 43 | 44 | ui.KeyReleased = imgui.KeyReleased 45 | ui.GetWantCaptureKeyboard = imgui.GetWantCaptureKeyboard 46 | 47 | function ui:init(CPU, keypad) 48 | self.CPU = CPU 49 | self.keypad = keypad 50 | 51 | disassembler:disassemble(CPU.memory) 52 | 53 | self.canvases = { 54 | display = lg.newCanvas(64 * 8, 32 * 8), 55 | speaker = lg.newCanvas(60, 60), 56 | speaker_mute = lg.newCanvas(60, 60), 57 | speaker_active = lg.newCanvas(60, 60), 58 | speaker_mute_active = lg.newCanvas(60, 60) 59 | } 60 | 61 | self.shaders = { 62 | scanlines = true, 63 | glow = true, 64 | chromasep = true, 65 | crt = true 66 | } 67 | 68 | self.showDisplayWindow = true 69 | self.showKeypadWindow = true 70 | self.showCPUWindow = true 71 | self.showCHIPOSWindow = false 72 | self.showPIAWindow = false 73 | self.showInstructionsWindow = true 74 | self.showMemoryWindow = true 75 | self.showSpeakerWindow = true 76 | 77 | self.memoryScroll = 0 78 | self.memoryScrollNow = false 79 | 80 | self.followPC = true 81 | 82 | self.effect = 83 | moonshine(64 * 8, 32 * 8, moonshine.effects.scanlines).chain(moonshine.effects.glow).chain( 84 | moonshine.effects.chromasep 85 | ).chain(moonshine.effects.crt) 86 | self.effect.chromasep.angle = 0.15 87 | self.effect.chromasep.radius = 2 88 | self.effect.scanlines.width = 1 89 | self.effect.crt.distortionFactor = {1.02, 1.065} 90 | self.effect.glow.strength = 3 91 | self.effect.glow.min_luma = 0.9 92 | 93 | self.speaker = { 94 | mute = false, 95 | volume = 1.0, 96 | sound = love.audio.newSource("assets/1khz.wav", "static"), 97 | image = lg.newImage("assets/speaker.png"), 98 | image_mute = lg.newImage("assets/speaker-mute.png") 99 | } 100 | 101 | -- This is unfortunate, but I haven't figured out a way to use 102 | -- colors with imgui.Image (or imgui.ButtonImage), so I make lots 103 | -- of canvases instead... 104 | lg.setCanvas(self.canvases.speaker) 105 | lg.draw(self.speaker.image, 0, 0, 0, 0.25, 0.25) 106 | lg.setCanvas() 107 | 108 | lg.setCanvas(self.canvases.speaker_mute) 109 | lg.draw(self.speaker.image_mute, 0, 0, 0, 0.25, 0.25) 110 | lg.setCanvas() 111 | 112 | lg.setColor(1, 0, 0, 1) 113 | 114 | lg.setCanvas(self.canvases.speaker_active) 115 | lg.draw(self.speaker.image, 0, 0, 0, 0.25, 0.25) 116 | lg.setCanvas() 117 | 118 | lg.setCanvas(self.canvases.speaker_mute_active) 119 | lg.draw(self.speaker.image_mute, 0, 0, 0, 0.25, 0.25) 120 | lg.setCanvas() 121 | 122 | lg.setColor(1, 1, 1, 1) 123 | end 124 | 125 | function loadChip8File(filename) 126 | self = ui 127 | local chip8 = util.read_file(filename) 128 | if type(chip8) == "string" then 129 | print(chip8) 130 | else 131 | for address = 0, #chip8 do 132 | self.CPU.memory[address + 0x0200] = chip8[address] 133 | end 134 | self.CPU.registers.pc(0xC000) 135 | end 136 | end 137 | 138 | --function ui:update(dt) 139 | --local temp_key_status = {} 140 | --for k, v in pairs(button_status) do 141 | -- temp_key_status[k] = CPU.key_status[k] 142 | -- if v then 143 | -- CPU.key_status[k] = v 144 | -- end 145 | --end 146 | 147 | -- foo 148 | 149 | --for k, v in pairs(button_status) do 150 | -- CPU.key_status[k] = temp_key_status[k] 151 | --end 152 | --end 153 | 154 | function ui:draw() 155 | local sound_playing = bit.band(self.CPU.memory[0x8012], 0x40) ~= 0 -- TODO read from pins, not bus 156 | 157 | if sound_playing and not self.speaker.mute then 158 | self.speaker.sound:play() 159 | end 160 | 161 | imgui.NewFrame() 162 | 163 | if self.showDisplayWindow then 164 | local win_x, win_y 165 | 166 | if not self.fullscreenDisplay then 167 | imgui.SetNextWindowPos(0, 20, "ImGuiCond_FirstUseEver") 168 | imgui.SetNextWindowSize(556, 320, "ImGuiCond_FirstUseEver") 169 | self.showDisplayWindow = 170 | imgui.Begin("Display", nil, {"NoCollapse", (not self.fullscreenDisplay) and "MenuBar"}) 171 | --, { "ImGuiWindowFlags_AlwaysAutoResize" }) 172 | win_x, win_y = imgui.GetWindowSize() 173 | win_x = win_x - 16 174 | win_y = win_y - imgui.GetFrameHeight() * 2.8 175 | 176 | if imgui.BeginMenuBar() then 177 | self:drawDisplayMenu() 178 | imgui.EndMenuBar() 179 | end 180 | end 181 | 182 | if self.CPU.display and self.CPU.drawflag then 183 | lg.setCanvas(self.canvases.display) 184 | lg.clear() 185 | self.effect( 186 | function() 187 | lg.setColor(1, 1, 1) 188 | for y = 0, 31 do 189 | for x = 0, 7 do 190 | local byte = self.CPU.memory[0x100 + (y * 8) + x] 191 | for xx = 0, 7 do 192 | local pixel = bit.band(byte, 0x80) 193 | byte = bit.lshift(byte, 1) 194 | if pixel ~= 0 then 195 | lg.rectangle("fill", (x * 64) + (xx * 8), y * 8, 8, 8) 196 | end 197 | end 198 | end 199 | end 200 | end 201 | ) 202 | lg.setCanvas() 203 | self.CPU.drawflag = false 204 | end 205 | 206 | if self.fullscreenDisplay then 207 | love.graphics.draw( 208 | self.canvases.display, 209 | 0, 210 | imgui.GetFrameHeight(), 211 | nil, 212 | love.graphics.getWidth() / (64 * 8), 213 | love.graphics.getHeight() / ((32 * 8) + 8) 214 | ) 215 | else 216 | imgui.Image(self.canvases.display, win_x, win_y) 217 | imgui.End() 218 | end 219 | end 220 | 221 | if not self.fullscreenDisplay then 222 | if self.showSpeakerWindow then 223 | imgui.SetNextWindowPos(573, 53, "ImGuiCond_FirstUseEver") 224 | imgui.SetNextWindowSize(84, 101, "ImGuiCond_FirstUseEver") 225 | self.showSpeakerWindow = imgui.Begin("Speaker", true, {}) 226 | --, { "ImGuiWindowFlags_AlwaysAutoResize" }) 227 | 228 | local toggle = false 229 | if self.speaker.mute then 230 | if sound_playing then 231 | toggle = imgui.ImageButton(self.canvases.speaker_mute_active, 60, 60) 232 | else 233 | toggle = imgui.ImageButton(self.canvases.speaker_mute, 60, 60) 234 | end 235 | else 236 | if sound_playing then 237 | toggle = imgui.ImageButton(self.canvases.speaker_active, 60, 60) 238 | else 239 | toggle = imgui.ImageButton(self.canvases.speaker, 60, 60) 240 | end 241 | end 242 | if toggle then 243 | self.speaker.mute = not self.speaker.mute 244 | end 245 | 246 | imgui.End() 247 | end 248 | 249 | if self.showCPUWindow then 250 | imgui.SetNextWindowSize(113, 222, "ImGuiCond_FirstUseEver") 251 | imgui.SetNextWindowPos(681, 21, "ImGuiCond_FirstUseEver") 252 | self.showCPUWindow = imgui.Begin("CPU", true) 253 | imgui.Text(string.format("Cycles: %d", cycles)) 254 | for _, reg in ipairs({"a", "b"}) do 255 | local register = self.CPU.registers[reg] 256 | local reg_changed = register() ~= register.prev_value and num_instructions == register.prev_inst + 1 257 | if reg_changed then 258 | imgui.PushStyleColor("ImGui_Text", 1, 0, 0, 1) 259 | end 260 | imgui.Text(string.format(" %s: ", reg)) 261 | imgui.SameLine() 262 | local text, textinput = 263 | imgui.InputText("##reg-" .. reg, string.format("%02X", register()), 3, hex_input_flags) 264 | if reg_changed then 265 | imgui.PopStyleColor() 266 | end 267 | if textinput then 268 | register(tonumber(text, 16)) 269 | end 270 | end 271 | for _, reg in ipairs({"pc", "sp", "ix"}) do 272 | local register = self.CPU.registers[reg] 273 | local reg_changed = register() ~= register.prev_value and num_instructions == register.prev_inst + 1 274 | if reg_changed then 275 | imgui.PushStyleColor("ImGui_Text", 1, 0, 0, 1) 276 | end 277 | imgui.Text(string.format("%s: ", reg)) 278 | imgui.SameLine() 279 | local text, textinput = 280 | imgui.InputText("##reg-" .. reg, string.format("%04X", register()), 5, hex_input_flags) 281 | if reg_changed then 282 | imgui.PopStyleColor() 283 | end 284 | if textinput then 285 | register(tonumber(text, 16)) 286 | end 287 | end 288 | local s = "" 289 | for i, cc in ipairs({"h", "i", "n", "z", "v", "c"}) do 290 | s = s .. (self.CPU.registers.status[cc] and "1" or "0") 291 | end 292 | imgui.Text(string.format("CC: 11%s", s)) 293 | --string.format("CC: %02X", self.CPU.registers.status())) 294 | imgui.Text(" HINZVC") 295 | if self.CPU.irq then 296 | imgui.PushStyleColor("ImGui_Text", 1, 0, 0, 1) 297 | end 298 | imgui.Text("IRQ") 299 | if self.CPU.irq then 300 | imgui.PopStyleColor() 301 | end 302 | imgui.SameLine() 303 | if self.CPU.reset then 304 | imgui.PushStyleColor("ImGui_Text", 1, 0, 0, 1) 305 | end 306 | imgui.Text("RST") 307 | if self.CPU.reset then 308 | imgui.PopStyleColor() 309 | end 310 | imgui.End() 311 | end 312 | 313 | if self.showCHIPOSWindow then 314 | imgui.SetNextWindowSize(200, 200, "ImGuiCond_FirstUseEver") 315 | self.showCHIPOSWindow = imgui.Begin("CHIPOS", true) 316 | imgui.Text(string.format("PC: %02X%02X", self.CPU.memory[0x0022], self.CPU.memory[0x0023])) 317 | imgui.SameLine() 318 | imgui.Text(string.format("SP: %02X%02X", self.CPU.memory[0x0024], self.CPU.memory[0x0025])) 319 | imgui.Text(string.format("I: %02X%02X", self.CPU.memory[0x0026], self.CPU.memory[0x0027])) 320 | for i = 0, 0xF do 321 | imgui.Text(string.format("V%01X: %02X", i, self.CPU.memory[0x0030 + i])) 322 | if i % 2 == 0 then 323 | imgui.SameLine() 324 | end 325 | end 326 | imgui.End() 327 | end 328 | 329 | if self.showPIAWindow then 330 | imgui.SetNextWindowSize(200, 200, "ImGuiCond_FirstUseEver") 331 | self.showPIAWindow = imgui.Begin("PIA", true) 332 | imgui.End() 333 | end 334 | 335 | if self.showInstructionsWindow then 336 | imgui.SetNextWindowSize(206, 250, "ImGuiCond_FirstUseEver") 337 | imgui.SetNextWindowPos(352, 343, "ImGuiCond_FirstUseEver") 338 | self.showInstructionsWindow = imgui.Begin("Instructions", true, "MenuBar") 339 | if imgui.BeginMenuBar() then 340 | if imgui.BeginMenu("Settings") then 341 | if imgui.MenuItem("Follow PC", nil, self.followPC, true) then 342 | self.followPC = not self.followPC 343 | end 344 | imgui.EndMenu() 345 | end 346 | imgui.EndMenuBar() 347 | end 348 | imgui.Text("Breakpoint: ") 349 | imgui.SameLine() 350 | imgui.PushItemWidth(4 * 9) -- TODO get char width 351 | local text, textinput = 352 | imgui.InputText( 353 | "##breakpoint", 354 | self.CPU.breakpoint and string.format("%04X", self.CPU.breakpoint) or "", 355 | 5, 356 | hex_input_flags 357 | ) 358 | imgui.PopItemWidth() 359 | if textinput then 360 | self.CPU.breakpoint = tonumber(text, 16) 361 | end 362 | 363 | local padding = 2 -- Vertical space taken up by breakpoint and jump 364 | local font_height = imgui.GetFontSize() + 4 365 | imgui.BeginChild("##instructions", 0, -(font_height * padding)) 366 | local win_w, win_h = imgui.GetWindowSize() 367 | local line = math.floor(imgui.GetScrollY() / font_height) 368 | local j = 0 369 | for i = 0, 0xFFFF do -- TODO only to the largest mapped memory? 370 | if self.instructionsScrollNow and self.instructionsScroll == i - math.floor(win_h / font_height / 2) - 1 then 371 | imgui.SetScrollHere() 372 | line = self.instructionsScroll 373 | self.instructionsScrollNow = false 374 | end 375 | if disassembler.memory[i] then 376 | if i == self.CPU.registers.pc() then 377 | imgui.Text(">") 378 | if self.followPC then 379 | imgui.SetScrollHere() 380 | end 381 | elseif i == self.CPU.breakpoint then 382 | imgui.Text("!") 383 | else 384 | imgui.Text(" ") 385 | end 386 | -- Cull output so we don't bog down the UI 387 | if j > line - 1 and j < line + (win_h / font_height) + 1 then 388 | imgui.SameLine() 389 | imgui.Text(string.format("%04X: ", i) .. disassembler.memory[i]) 390 | end 391 | j = j + 1 392 | end 393 | end 394 | imgui.EndChild() 395 | -- Scroll 396 | imgui.Text("Scroll to: ") 397 | imgui.SameLine() 398 | imgui.PushItemWidth(4 * 9) -- TODO get char width 399 | local scroll, scroll_input = 400 | imgui.InputText( 401 | "##instructionscroll", 402 | string.format("%04X", self.instructionsScroll or 0), 403 | 5, 404 | hex_input_flags 405 | ) 406 | imgui.PopItemWidth() 407 | if scroll_input then 408 | self.instructionsScroll = tonumber(scroll, 16) 409 | self.instructionsScrollNow = true 410 | end 411 | imgui.End() 412 | end 413 | 414 | if self.showMemoryWindow then 415 | imgui.SetNextWindowSize(241, 355, "ImGuiCond_FirstUseEver") 416 | imgui.SetNextWindowPos(556, 244, "ImGuiCond_FirstUseEver") 417 | self.showMemoryWindow = imgui.Begin("Memory", true, "MenuBar") 418 | if imgui.BeginMenuBar() then 419 | if imgui.BeginMenu("Settings") then 420 | if imgui.MenuItem("Random uninitialized memory", nil, self.ram.uninitialized_random, true) then 421 | self.ram:set_uninitialized_value(self.ram.uninitialized_random and 0 or nil) 422 | end 423 | imgui.EndMenu() 424 | end 425 | imgui.EndMenuBar() 426 | end 427 | 428 | -- Memory breakpoint 429 | imgui.Text("Breakpoint: ") 430 | imgui.SameLine() 431 | imgui.PushItemWidth(4 * 9) -- TODO get char width 432 | local breakpoint = self.CPU.memory.breakpoint 433 | local new_breakpoint, breakpoint_input = 434 | imgui.InputText( 435 | "##membreakpoint", 436 | breakpoint.address and string.format("%04X", breakpoint.address) or "", 437 | 5, 438 | hex_input_flags 439 | ) 440 | imgui.PopItemWidth() 441 | if breakpoint_input then 442 | breakpoint.address = tonumber(new_breakpoint, 16) 443 | end 444 | imgui.SameLine() 445 | breakpoint.read = imgui.Checkbox("R", breakpoint.read) 446 | imgui.SameLine() 447 | breakpoint.write = imgui.Checkbox("W", breakpoint.write) 448 | 449 | -- Memory viewer 450 | local padding = 3 -- Vertical space taken up by breakpoint and jump 451 | local font_height = imgui.GetFontSize() + 4 452 | imgui.BeginChild("##memory", 0, -(font_height * padding)) 453 | local win_w, win_h = imgui.GetWindowSize() 454 | local line = math.floor(imgui.GetScrollY() / font_height) 455 | for i = 0, 0xFFFF do -- TODO only to the largest mapped memory? 456 | -- Set scroll if user has jumped to an address 457 | -- imgui.SetScrollHere() sets it immediately, and centers the viewport 458 | -- We want the address to be at the top (do we really?) so maths 459 | if self.memoryScrollNow and self.memoryScroll == i - math.floor(win_h / font_height / 2) - 1 then 460 | imgui.SetScrollHere() 461 | line = self.memoryScroll 462 | self.memoryScrollNow = false 463 | end 464 | -- Cull output so we don't bog down the UI 465 | if i > line - 1 and i < line + (win_h / font_height) + 1 then 466 | local c = self.CPU.memory[i] 467 | if i < self.ram.size and not self.ram.initialized[i] then 468 | imgui.PushStyleColor("ImGui_Text", 0.50, 0.50, 0.50, 1) 469 | end 470 | imgui.Text(string.format("%04X: %02X %03d", i, c, c)) 471 | imgui.SameLine() 472 | -- Print only printable ASCII characters 473 | if c > 31 and c < 127 then 474 | imgui.Text(string.char(c) .. " ") 475 | else 476 | imgui.Text(" ") 477 | end 478 | imgui.SameLine() 479 | local byte = {} 480 | for j = 1, 8 do 481 | byte[j] = bit.band(bit.rshift(c, 7 - j + 1), 1) 482 | end 483 | imgui.Text(table.concat(byte)) 484 | if i < self.ram.size and not self.ram.initialized[i] then 485 | imgui.PopStyleColor() 486 | end 487 | else 488 | imgui.Text("") 489 | end 490 | end 491 | -- If we should scroll but haven't yet, we want to scroll to the end 492 | if self.memoryScrollNow then 493 | imgui.SetScrollHere(1) 494 | self.memoryScrollNow = false 495 | end 496 | imgui.EndChild() 497 | 498 | -- Scroll 499 | imgui.Text("Scroll to: ") 500 | imgui.SameLine() 501 | imgui.PushItemWidth(4 * 9) -- TODO get char width 502 | local scroll, scroll_input = 503 | imgui.InputText("##memscroll", string.format("%04X", self.memoryScroll or 0), 5, hex_input_flags) 504 | imgui.PopItemWidth() 505 | if scroll_input then 506 | self.memoryScroll = tonumber(scroll, 16) 507 | self.memoryScrollNow = true 508 | end 509 | if imgui.Button("RAM") then 510 | self.memoryScroll = 0 511 | self.memoryScrollNow = true 512 | end 513 | imgui.SameLine() 514 | if imgui.Button("CHIP-8") then 515 | self.memoryScroll = 0x0200 516 | self.memoryScrollNow = true 517 | end 518 | imgui.SameLine() 519 | if imgui.Button("ROM") then 520 | self.memoryScroll = 0xC000 521 | self.memoryScrollNow = true 522 | end 523 | 524 | imgui.End() 525 | end 526 | 527 | if self.showKeypadWindow then 528 | imgui.SetNextWindowPos(4, 341, "ImGuiCond_FirstUseEver") 529 | imgui.SetNextWindowSize(228, 250, "ImGuiCond_FirstUseEver") 530 | self.showKeypadWindow = imgui.Begin("Keypad", true, {"NoScrollbar", "MenuBar"}) 531 | if imgui.BeginMenuBar() then 532 | if imgui.BeginMenu("Layout") then 533 | if imgui.MenuItem("Digitran", nil, self.keypad.keys_dream == self.keypad.keys_digitran, true) then 534 | self.keypad.keys_dream = self.keypad.keys_digitran 535 | end 536 | if imgui.IsItemHovered() then 537 | imgui.SetTooltip( 538 | "Used in many DREAM builds; de facto standard.\nStandard layout in the DREAMER newsletter.\nAlso used by the 40th anniversary DREAM reproduction" 539 | ) 540 | end 541 | if imgui.MenuItem("Original", nil, self.keypad.keys_dream == self.keypad.keys_original, true) then 542 | self.keypad.keys_dream = self.keypad.keys_original 543 | end 544 | if imgui.IsItemHovered() then 545 | imgui.SetTooltip( 546 | "The layout used in the Electronics Australia articles.\nUsed by the prototype DREAM 6800, and the CHIP-8 Classic Computer." 547 | ) 548 | end 549 | if imgui.MenuItem("COSMAC VIP", nil, self.keypad.keys_dream == self.keypad.keys_cosmac, true) then 550 | self.keypad.keys_dream = self.keypad.keys_cosmac 551 | end 552 | if imgui.IsItemHovered() then 553 | imgui.SetTooltip( 554 | "Used by RCA's COSMAC VIP, the DREAM's predecessor.\nUseful for CHIP-8 compatibility." 555 | ) 556 | end 557 | imgui.EndMenu() 558 | end 559 | imgui.EndMenuBar() 560 | end 561 | local win_w, win_h = imgui.GetWindowSize() 562 | local but_w, but_h = (win_w / 4) - 5, (win_h / 5) - 14 563 | for k = 0, 15 do 564 | -- TODO this can't be right 565 | if self.keypad.button_status[k] then 566 | self.keypad.button_status[k] = false 567 | self.keypad:keyreleased(nil, self.keypad.keys_qwerty[k]) 568 | end 569 | local button_pressed = false 570 | if self.keypad.key_status[k] then 571 | button_pressed = true 572 | --imgui.PushStyleColor("ImGuiCol_Button", 117 / 255, 138 / 255, 204 / 255, 1) 573 | imgui.PushStyleColor("ImGuiCol_Button", 0.4588, 0.5411, 0.8, 1) 574 | end 575 | self.keypad.button_status[k] = 576 | imgui.Button(string.format("%X", self.keypad.keys_dream.layout[k]), but_w, but_h) 577 | if self.keypad.button_status[k] then 578 | self.keypad:keypressed(nil, self.keypad.keys_qwerty[k]) 579 | end 580 | if button_pressed then 581 | imgui.PopStyleColor(1) 582 | end 583 | if imgui.IsItemHovered() then 584 | imgui.SetTooltip(self.keypad.keys_qwerty[k]) 585 | end 586 | if (k + 1) % 4 ~= 0 then 587 | imgui.SameLine(0, 4) 588 | end 589 | end 590 | imgui.Dummy(0, 2) 591 | if imgui.Button("FN", (but_w * 2) + 4, but_h) then 592 | self.keypad:keypressed(nil, "lshift") 593 | end 594 | if imgui.IsItemHovered() then 595 | imgui.SetTooltip("shift") 596 | end 597 | imgui.SameLine(0, 4) 598 | if imgui.Button("RESET", (but_w * 2) + 4, but_h) then 599 | self.CPU.reset = true 600 | end 601 | if imgui.IsItemHovered() then 602 | imgui.SetTooltip("escape") 603 | end 604 | imgui.End() 605 | end 606 | end 607 | 608 | if filedialog then 609 | filedialog = filedialog:draw() 610 | end 611 | 612 | if imgui.BeginMainMenuBar() then 613 | if imgui.BeginMenu("File") then 614 | if imgui.MenuItem("Quickload CHIP-8...") then 615 | filedialog = 616 | Filedialog.new( 617 | "open", 618 | loadChip8File, 619 | function() 620 | end, 621 | love.filesystem.getSaveDirectory() 622 | ) 623 | end 624 | if imgui.IsItemHovered() then 625 | imgui.SetTooltip("Load binary file at $0200 and jump to $C000") 626 | end 627 | if imgui.MenuItem("Quickload Dream Invaders") then 628 | local dream_invaders = util.read_file("invaders_code.bin") 629 | for address = 0, #dream_invaders do 630 | self.CPU.memory[address + 0x0200] = dream_invaders[address] 631 | end 632 | self.CPU.registers.pc(0x0200) 633 | end 634 | if imgui.IsItemHovered() then 635 | imgui.SetTooltip("Load Dream Invaders at $0200 and jump to $0200") 636 | end 637 | if imgui.MenuItem("Quit") then 638 | love.event.quit() 639 | end 640 | imgui.EndMenu() 641 | end 642 | if self.fullscreenDisplay and imgui.BeginMenu("Display") then 643 | self:drawDisplayMenu() 644 | imgui.EndMenu() 645 | end 646 | if imgui.BeginMenu("Windows", not self.fullscreenDisplay) then 647 | if imgui.MenuItem("Display", nil, self.showDisplayWindow, false) then 648 | self.showDisplayWindow = not self.showDisplayWindow 649 | end 650 | if 651 | imgui.MenuItem( 652 | "CPU status", 653 | nil, 654 | self.showCPUWindow and not self.fullscreenDisplay, 655 | not self.fullscreenDisplay 656 | ) 657 | then 658 | self.showCPUWindow = not self.showCPUWindow 659 | end 660 | if 661 | imgui.MenuItem( 662 | "CHIPOS/CHIP-8 status", 663 | nil, 664 | self.showCHIPOSWindow and not self.fullscreenDisplay, 665 | not self.fullscreenDisplay 666 | ) 667 | then 668 | self.showCHIPOSWindow = not self.showCHIPOSWindow 669 | end 670 | if imgui.MenuItem("PIA", nil, self.showPIAWindow and not self.fullscreenDisplay, not self.fullscreenDisplay) then 671 | self.showPIAWindow = not self.showPIAWindow 672 | end 673 | if 674 | imgui.MenuItem( 675 | "Keypad", 676 | nil, 677 | self.showKeypadWindow and not self.fullscreenDisplay, 678 | not self.fullscreenDisplay 679 | ) 680 | then 681 | self.showKeypadWindow = not self.showKeypadWindow 682 | end 683 | if 684 | imgui.MenuItem( 685 | "Instructions", 686 | nil, 687 | self.showInstructionsWindow and not self.fullscreenDisplay, 688 | not self.fullscreenDisplay 689 | ) 690 | then 691 | self.showInstructionsWindow = not self.showInstructionsWindow 692 | end 693 | if 694 | imgui.MenuItem( 695 | "Memory", 696 | nil, 697 | self.showMemoryWindow and not self.fullscreenDisplay, 698 | not self.fullscreenDisplay 699 | ) 700 | then 701 | self.showMemoryWindow = not self.showMemoryWindow 702 | end 703 | if 704 | imgui.MenuItem( 705 | "Speaker", 706 | nil, 707 | self.showSpeakerWindow and not self.fullscreenDisplay, 708 | not self.fullscreenDisplay 709 | ) 710 | then 711 | self.showSpeakerWindow = not self.showSpeakerWindow 712 | end 713 | imgui.EndMenu() 714 | end 715 | if imgui.BeginMenu("Help") then 716 | if imgui.MenuItem("CHIPOS manual...") then 717 | end 718 | if imgui.MenuItem("About...") then 719 | self.showAboutPopup = true 720 | imgui.OpenPopup("About") 721 | end 722 | imgui.EndMenu() 723 | end 724 | imgui.EndMainMenuBar() 725 | end 726 | 727 | if self.showAboutPopup then 728 | imgui.OpenPopup("About DRÖM") 729 | end 730 | 731 | imgui.SetNextWindowSize(0, 320, "ImGuiCond_FirstUseEver") 732 | self.showAboutPopup = imgui.BeginPopupModal("About DRÖM", self.showAboutPopup) 733 | if self.showAboutPopup then 734 | local drom_url = "https://github.com/tobiasvl/drom" 735 | local dream_url = "http://www.mjbauer.biz/DREAM6800.htm" 736 | local chip8_url = "https://johnearnest.github.io/chip8Archive/?sort=platform" 737 | imgui.TextWrapped("DRÖM is an emulator for DREAM 6800, a hobby computer created by Michael J. Bauer in 1979.") 738 | imgui.Dummy(1, 1) 739 | imgui.Text("DRÖM © 2020 Tobias V. Langhoff") 740 | imgui.Text("Source code available under MIT license here:") 741 | if imgui.Button(drom_url) then 742 | love.system.openURL(drom_url) 743 | end 744 | imgui.Dummy(1, 1) 745 | imgui.Text("CHIPOS © 1978 Michael J. Bauer") 746 | imgui.Text("Dream Invaders © 1980 Michael J. Bauer") 747 | imgui.TextWrapped("Used in DRÖM with permission. Source code and binaries available here:") 748 | if imgui.Button(dream_url) then 749 | love.system.openURL(dream_url) 750 | end 751 | imgui.Dummy(1, 1) 752 | imgui.TextWrapped("All CHIP-8 games included in DRÖM are in the public domain and are available here:") 753 | if imgui.Button("CHIP-8 Archive") then 754 | love.system.openURL(chip8_url) 755 | end 756 | if imgui.Button("Close") then 757 | imgui.CloseCurrentPopup() 758 | self.showAboutPopup = false 759 | end 760 | imgui.EndPopup() 761 | end 762 | 763 | imgui.Render() 764 | end 765 | 766 | function ui:drawDisplayMenu() 767 | if imgui.BeginMenu("Effects") then 768 | for k in pairs(self.shaders) do 769 | if imgui.MenuItem(k, nil, self.shaders[k], true) then 770 | self.shaders[k] = not self.shaders[k] 771 | if self.shaders[k] then 772 | self.effect.enable(k) 773 | else 774 | self.effect.disable(k) 775 | end 776 | end 777 | end 778 | imgui.EndMenu() 779 | end 780 | if imgui.BeginMenu("Tools") then 781 | if imgui.MenuItem("Save screenshot", nil, false, true) then 782 | self.canvases.display:newImageData():encode("png", os.time() .. ".png") 783 | end 784 | if imgui.MenuItem("Fill screen", "Alt + Enter", self.fullscreenDisplay, true) then 785 | self.fullscreenDisplay = not self.fullscreenDisplay 786 | end 787 | imgui.EndMenu() 788 | end 789 | end 790 | 791 | return ui 792 | -------------------------------------------------------------------------------- /util.lua: -------------------------------------------------------------------------------- 1 | local nativefs = require "love-imgui-filedialog.love-nativefs.nativefs" 2 | 3 | local util = {} 4 | 5 | function util.read_file(file) 6 | if type(file) == "string" then 7 | file = love.filesystem.newFile(file) 8 | end 9 | 10 | local ok, err = file:open("r") 11 | 12 | if ok then 13 | local rom = {} 14 | local address = 0 15 | 16 | while (not file:isEOF()) do 17 | local byte, len = file:read(1) 18 | -- Dropped files don't seem to report EOF 19 | if len ~= 1 or not string.byte(byte) then 20 | break 21 | end 22 | rom[address] = string.byte(byte) 23 | address = address + 1 24 | end 25 | 26 | file:close() 27 | rom.size = address 28 | return rom 29 | else 30 | return err 31 | end 32 | end 33 | 34 | return util 35 | --------------------------------------------------------------------------------