From eb0a2ad98ed2583e2e622c59c9007defe2498154 Mon Sep 17 00:00:00 2001 From: Gregory Maynard-Hoare Date: Thu, 9 Nov 2023 18:23:18 +0000 Subject: [PATCH] Redirect opcode functions in debug mode --- README.md | 3 +- scchip/constants.py | 6 +- scchip/cpu.py | 351 +++++++++++++++++++++++--------------------- superchocchip.py | 4 +- 4 files changed, 187 insertions(+), 177 deletions(-) diff --git a/README.md b/README.md index 063f4b4..602f9a3 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,6 @@ Notes ### Code Optimisation - Loops or condition blocks are avoided wherever possible to optimise emulation speed. For example, this CPU uses bitmasks and hashed dictionaries to find the right OpCodes, rather than huge conditions and switch (C/C++) statements. -- There are a few portions of the CPU code that are a bit repetitive, for example the checks to see if the debugger is active on each instruction. This is preferred to the shorter, but slower method (calling unified debug functionality and then checking to see if the debugger is active before returning). ### Quirks @@ -193,4 +192,4 @@ See the 'LICENSE' file for full details. The 'Affero' version of the GPL v3.0 ha --- -Copyright (C) 2022 Gregory Maynard-Hoare, licensed under GNU Affero General Public License v3.0 +Copyright (C) 2022-2023 Gregory Maynard-Hoare, licensed under GNU Affero General Public License v3.0 diff --git a/scchip/constants.py b/scchip/constants.py index 8befae3..dac8064 100644 --- a/scchip/constants.py +++ b/scchip/constants.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 -__copyright__ = "Copyright (C) 2022 Gregory Maynard-Hoare" +__copyright__ = "Copyright (C) 2023 Gregory Maynard-Hoare" __license__ = "GNU Affero General Public License v3.0" # App identification APP_NAME = "SuperChocChip Emulator" -APP_VERSION = "1.4.0" -APP_COPYRIGHT = "Copyright (C) 2022 Gregory Maynard-Hoare, licensed under GNU Affero General Public License v3.0" +APP_VERSION = "1.4.1" +APP_COPYRIGHT = "Copyright (C) 2023 Gregory Maynard-Hoare, licensed under GNU Affero General Public License v3.0" APP_INTRO = "{} V{} -- ".format(APP_NAME, APP_VERSION) # Emulated system architectures diff --git a/scchip/cpu.py b/scchip/cpu.py index a0aa42a..60273c1 100644 --- a/scchip/cpu.py +++ b/scchip/cpu.py @@ -4,19 +4,20 @@ CPU Emulator (CHIP-8, Super-CHIP 1.0, Super-CHIP 1.1 and XO-CHIP) Like a real computer, this is where most of the processing happens. The main -CPU can potentially cycle at hundreds of thousands of instructions every -second, so this has to be as fast as possible. +CPU can potentially cycle at millions of instructions every second, so this has +to be as fast as possible. Alterations should be checked against the included 'operations per second' benchmark, to ensure any changes are an improvement. """ -__copyright__ = "Copyright (C) 2022 Gregory Maynard-Hoare" +__copyright__ = "Copyright (C) 2023 Gregory Maynard-Hoare" __license__ = "GNU Affero General Public License v3.0" from time import perf_counter from random import randint from math import ceil +from functools import partial from .constants import APP_INTRO, ARCH_CHIP8_HIRES, ARCH_SUPERCHIP_1_0, ARCH_CHIP48, ARCH_SUPERCHIP_1_1, ARCH_XO_CHIP CPU_ENDIAN = "big" # CHIP-8 is big-endian @@ -41,7 +42,6 @@ def __init__(self, arch, ram, stack, framebuffer, inputs, audio, debugger, clock self.inputs = inputs self.audio = audio self.debugger = debugger - self.live_debug = self.debugger.is_live() self.sysfont_sm_loc = 0x50 self.sysfont_bg_loc = 0xA0 self.framebuffer.report_perf() @@ -184,6 +184,18 @@ def __init__(self, arch, ram, stack, framebuffer, inputs, audio, debugger, clock for n in range(4): self.instructions[0xF001 | n << 8] = self._Fn01 + if self.debugger.is_live(): + # Rewrite method dictionary so debug functions for non-masked opcodes are called first + # flake8: noqa: E731 + opcode_mask_funcs = {self._0nnn, self._5nnn_8nnn_9nnn, self._Ennn_Fnnn} + redirect_func = lambda debug_func, target_func: [debug_func(), target_func()] + + for opcode, func in self.instructions.items(): + if func not in opcode_mask_funcs: + self.instructions[opcode] = partial( + redirect_func, getattr(self, "_".join((func.__name__, "d"))), func + ) + # Initialise registers self.v = memoryview(bytearray(16)) # Bytearrays are mutable, so this should be fast when a register is updated self.i = 0 # Index register @@ -360,28 +372,28 @@ def _5nnn_8nnn_9nnn(self): def _Ennn_Fnnn(self): self._call_masked_instruction(self.opcode & 0xF0FF) - def _00E0(self): # CLS - if self.live_debug: - self.debug("CLS") + def _00E0_d(self): # CLS (debug) + self.debug("CLS") + def _00E0(self): # CLS self.framebuffer.clear() - def _00EE(self): # RET - if self.live_debug: - self.debug("RET") + def _00EE_d(self): # RET (debug) + self.debug("RET") + def _00EE(self): # RET self.pc = self.stack.pop() - def _0230(self): # CLSHI - if self.live_debug: - self.debug("CLSHI") + def _0230_d(self): # CLSHI (debug) + self.debug("CLSHI") + def _0230(self): # CLSHI self.framebuffer.clear() - def _1nnn(self): # JP addr - if self.live_debug: - self.debug("JP 0x{:03x}".format(self.addr)) + def _1nnn_d(self): # JP addr (debug) + self.debug("JP 0x{:03x}".format(self.addr)) + def _1nnn(self): # JP addr if self.arch_is_dblheight and self.pc == 0x202 and self.addr == 0x260: # Switch to double-height resolution mode and override the standard jump self.framebuffer.resize_vid(64, 64) @@ -389,10 +401,10 @@ def _1nnn(self): # JP addr else: self.pc = self.addr - def _2nnn(self): # CALL addr - if self.live_debug: - self.debug("CALL 0x{:03x}".format(self.addr)) + def _2nnn_d(self): # CALL addr (debug) + self.debug("CALL 0x{:03x}".format(self.addr)) + def _2nnn(self): # CALL addr self.stack.push(self.pc) self.pc = self.addr @@ -402,40 +414,39 @@ def _post_skip(self): self.inc_pc() - def _3xkk(self): # SE Vx, byte - if self.live_debug: - self.debug("SE V{:01x}, 0x{:02x}".format(self.vx, self.byte)) + def _3xkk_d(self): # SE Vx, byte (debug) + self.debug("SE V{:01x}, 0x{:02x}".format(self.vx, self.byte)) + def _3xkk(self): # SE Vx, byte if self.v[self.vx] == self.byte: self._post_skip() - def _4xkk(self): # SNE Vx, byte - if self.live_debug: - self.debug("SNE V{:01x}, 0x{:02x}".format(self.vx, self.byte)) + def _4xkk_d(self): # SNE Vx, byte (debug) + self.debug("SNE V{:01x}, 0x{:02x}".format(self.vx, self.byte)) + def _4xkk(self): # SNE Vx, byte if self.v[self.vx] != self.byte: self._post_skip() - def _5xy0(self): # SE Vx, Vy - if self.live_debug: - self.debug("SE V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _5xy0_d(self): # SE Vx, Vy (debug) + self.debug("SE V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _5xy0(self): # SE Vx, Vy if self.v[self.vx] == self.v[self.vy]: self._post_skip() - def _6xkk(self): # LD Vx, byte - if self.live_debug: - self.debug("LD V{:01x}, 0x{:02x}".format(self.vx, self.byte)) + def _6xkk_d(self): # LD Vx, byte (debug) + self.debug("LD V{:01x}, 0x{:02x}".format(self.vx, self.byte)) + def _6xkk(self): # LD Vx, byte self.v[self.vx] = self.byte + def _7xkk_d(self): # ADD Vx, byte (debug) + self.debug("ADD V{:01x}, 0x{:02x}".format(self.vx, self.byte)) + def _7xkk(self): # ADD Vx, byte vx = self.vx byte = self.byte - - if self.live_debug: - self.debug("ADD V{:01x}, 0x{:02x}".format(vx, byte)) - byte += self.v[vx] self.v[vx] = byte & 0xFF @@ -444,42 +455,41 @@ def _post_8xy0_8xy1_8xy2_8xy3(self): self.v[0xF] = 0 # All 8 instructions set Vf on original CHIP-8 - def _8xy0(self): # LD Vx, Vy - if self.live_debug: - self.debug("LD V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _8xy0_d(self): # LD Vx, Vy (debug) + self.debug("LD V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _8xy0(self): # LD Vx, Vy self.v[self.vx] = self.v[self.vy] self._post_8xy0_8xy1_8xy2_8xy3() - def _8xy1(self): # OR Vx, Vy - if self.live_debug: - self.debug("OR V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _8xy1_d(self): # OR Vx, Vy (debug) + self.debug("OR V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _8xy1(self): # OR Vx, Vy self.v[self.vx] |= self.v[self.vy] self._post_8xy0_8xy1_8xy2_8xy3() - def _8xy2(self): # AND Vx, Vy - if self.live_debug: - self.debug("AND V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _8xy2_d(self): # AND Vx, Vy (debug) + self.debug("AND V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _8xy2(self): # AND Vx, Vy self.v[self.vx] &= self.v[self.vy] self._post_8xy0_8xy1_8xy2_8xy3() - def _8xy3(self): # XOR Vx, Vy - if self.live_debug: - self.debug("XOR V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _8xy3_d(self): # XOR Vx, Vy (debug) + self.debug("XOR V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _8xy3(self): # XOR Vx, Vy self.v[self.vx] ^= self.v[self.vy] self._post_8xy0_8xy1_8xy2_8xy3() + def _8xy4_d(self): # ADD Vx, Vy (debug) + self.debug("ADD V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _8xy4(self): # ADD Vx, Vy vx = self.vx - vy = self.vy - if self.live_debug: - self.debug("ADD V{:01x}, V{:01x}".format(vx, vy)) - - val = self.v[vx] + self.v[vy] + val = self.v[vx] + self.v[self.vy] self.v[vx] = val & 0xFF self.v[0xF] = int(val > 0xFF) # Vf is set when carrying @@ -489,10 +499,10 @@ def _post_8xy5_8xy7(self, val): # Post-SUB/SUBN # parameters. Some XO-CHIP games fail unless this is done properly. self.v[0xF] = int(val >= 0) - def _8xy5(self): # SUB Vx, Vy - if self.live_debug: - self.debug("SUB V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _8xy5_d(self): # SUB Vx, Vy (debug) + self.debug("SUB V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _8xy5(self): # SUB Vx, Vy self._post_8xy5_8xy7(self.v[self.vx] - self.v[self.vy]) def _debug_8xy6_8xyE(self, direction): @@ -501,68 +511,66 @@ def _debug_8xy6_8xyE(self, direction): "{} V{:01x}, V{:01x}".format(direction, self.vx, self.vy) ) + def _8xy6_d(self): # SHR Vx {, Vy} (debug) + self._debug_8xy6_8xyE("SHR") + def _8xy6(self): # SHR Vx {, Vy} # On Super-CHIP, Vx is used. On CHIP-8 and XO-CHIP, Vy is used. - if self.live_debug: - self._debug_8xy6_8xyE("SHR") - val = self.v[self.vx if self.shift_quirks else self.vy] self.v[self.vx] = val >> 1 # Apparently the result is put in Vx either way self.v[0xF] = val & 1 # The whole byte gets set just for the flag - def _8xy7(self): # SUBN Vx, Vy - if self.live_debug: - self.debug("SUBN V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _8xy7_d(self): # SUBN Vx, Vy (debug) + self.debug("SUBN V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _8xy7(self): # SUBN Vx, Vy self._post_8xy5_8xy7(self.v[self.vy] - self.v[self.vx]) + def _8xyE_d(self): # SHL Vx {, Vy} (debug) + self._debug_8xy6_8xyE("SHL") + def _8xyE(self): # SHL Vx {, Vy} # On Super-CHIP, Vx is used. On CHIP-8 and XO-CHIP, Vy is used. - if self.live_debug: - self._debug_8xy6_8xyE("SHL") - val = self.v[self.vx if self.shift_quirks else self.vy] self.v[self.vx] = (val << 1) & 0xFF self.v[0xF] = val >> 7 - def _9xy0(self): # SNE Vx, Vy - if self.live_debug: - self.debug("SNE V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _9xy0_d(self): # SNE Vx, Vy (debug) + self.debug("SNE V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _9xy0(self): # SNE Vx, Vy if self.v[self.vx] != self.v[self.vy]: self._post_skip() - def _Annn(self): # LD I, addr - if self.live_debug: - self.debug("LD I, 0x{:03x}".format(self.addr)) + def _Annn_d(self): # LD I, addr (debug) + self.debug("LD I, 0x{:03x}".format(self.addr)) + def _Annn(self): # LD I, addr self.i = self.addr + def _Bnnn_d(self): # JP V0, addr (debug) + self.debug("JP V{:01x}, 0x{:03x}".format(self.vx if self.jump_quirks else 0, self.addr)) + def _Bnnn(self): # JP V0, addr # This is a nasty quirk which breaks lots of games if set incorrectly. It varies depending on the CPU # architecture and occasionally, the ROM. - vr = self.vx if self.jump_quirks else 0 + self.pc = (self.v[self.vx if self.jump_quirks else 0] + self.addr) & 0xFFF - if self.live_debug: - self.debug("JP V{:01x}, 0x{:03x}".format(vr, self.addr)) - - self.pc = (self.v[vr] + self.addr) & 0xFFF + def _Cxkk_d(self): # RND Vx, byte (debug) + self.debug("RND V{:01x}, 0x{:02x}".format(self.vx, self.byte)) def _Cxkk(self): # RND Vx, byte - if self.live_debug: - self.debug("RND V{:01x}, 0x{:02x}".format(self.vx, self.byte)) - # The ANDing here is intentional. This is not a random number between 0 and 'byte' inclusive. self.v[self.vx] = randint(0, 0xFF) & self.byte + def _Dxyn_d(self): # DRW Vx, Vy, nibble (debug) + self.debug("DRW V{:01x}, V{:01x}, 0x{:01x}".format(self.vx, self.vy, self.nibble)) + def _Dxyn(self): # DRW Vx, Vy, nibble # Main sprite drawing routine. If nibble == 0 and Super-CHIP arch, then draw 16x16 sprite height = self.nibble - if self.live_debug: - self.debug("DRW V{:01x}, V{:01x}, 0x{:01x}".format(self.vx, self.vy, height)) - if height == 0 and self.arch >= ARCH_SUPERCHIP_1_0: # Super-CHIP sprite. Show 16x16 even if in low res mode height = 16 @@ -620,30 +628,30 @@ def _Dxyn(self): # DRW Vx, Vy, nibble if self.sprite_delay_quirks: self.vblank_wait = True - def _Ex9E(self): # SKP Vx - if self.live_debug: - self.debug("SKP V{:01x}".format(self.vx)) + def _Ex9E_d(self): # SKP Vx (debug) + self.debug("SKP V{:01x}".format(self.vx)) + def _Ex9E(self): # SKP Vx if self.inputs.is_key_down(self.v[self.vx] & 0xF): self._post_skip() - def _ExA1(self): # SKNP Vx - if self.live_debug: - self.debug("SKNP V{:01x}".format(self.vx)) + def _ExA1_d(self): # SKNP Vx (debug) + self.debug("SKNP V{:01x}".format(self.vx)) + def _ExA1(self): # SKNP Vx if not self.inputs.is_key_down(self.v[self.vx] & 0xF): self._post_skip() - def _Fx07(self): # LD Vx, DT - if self.live_debug: - self.debug("LD V{:01x}, DT".format(self.vx)) + def _Fx07_d(self): # LD Vx, DT (debug) + self.debug("LD V{:01x}, DT".format(self.vx)) + def _Fx07(self): # LD Vx, DT self.v[self.vx] = self.dt - def _Fx0A(self): # LD Vx, K - if self.live_debug: - self.debug("LD V{:01x}, K".format(self.vx)) + def _Fx0A_d(self): # LD Vx, K (debug) + self.debug("LD V{:01x}, K".format(self.vx)) + def _Fx0A(self): # LD Vx, K # This opcode waits for a keypress, but since the sound and delay timers still need to expire correctly, and # framebuffer still needs updating, we'll return control to the CPU and simply decrement the incremented program # counter. @@ -662,28 +670,28 @@ def _Fx0A(self): # LD Vx, K self.v[self.vx] = key self.awaiting_keypress = False - def _Fx15(self): # LD DT, Vx - if self.live_debug: - self.debug("LD DT, V{:01x}".format(self.vx)) + def _Fx15_d(self): # LD DT, Vx (debug) + self.debug("LD DT, V{:01x}".format(self.vx)) + def _Fx15(self): # LD DT, Vx dt = self.v[self.vx] self.dt = dt self.dt_target = self.this_time + (dt / TIMER_FREQ) - def _Fx18(self): # LD ST, Vx - if self.live_debug: - self.debug("LD ST, V{:01x}".format(self.vx)) + def _Fx18_d(self): # LD ST, Vx (debug) + self.debug("LD ST, V{:01x}".format(self.vx)) + def _Fx18(self): # LD ST, Vx ds = self.v[self.vx] # Allow the program to start the buzzer, or immediately stop it before the sound timer hits zero self.audio.enable_buzzer(ds > 0) self.ds = ds self.ds_target = self.this_time + (ds / TIMER_FREQ) - def _Fx1E(self): # ADD I, Vx - if self.live_debug: - self.debug("ADD I, V{:01x}".format(self.vx)) + def _Fx1E_d(self): # ADD I, Vx (debug) + self.debug("ADD I, V{:01x}".format(self.vx)) + def _Fx1E(self): # ADD I, Vx val = self.i + self.v[self.vx] self.i = val & self.i_bitmask @@ -691,21 +699,22 @@ def _Fx1E(self): # ADD I, Vx if self.index_overflow_quirks: self.v[0xF] = (val > self.i_bitmask) - def _Fx29(self): # LD F, Vx - if self.live_debug: - self.debug("LD F, V{:01x}".format(self.vx)) + def _Fx29_d(self): # LD F, Vx (debug) + self.debug("LD F, V{:01x}".format(self.vx)) + def _Fx29(self): # LD F, Vx self.i = (self.sysfont_sm_loc + 5 * self.v[self.vx]) & self.i_bitmask - def _Fx33(self): # LD B, Vx - if self.live_debug: - self.debug("LD B, V{:01x}".format(self.vx)) + def _Fx33_d(self): # LD B, Vx (debug) + self.debug("LD B, V{:01x}".format(self.vx)) + def _Fx33(self): # LD B, Vx val = self.v[self.vx] i = self.i - self.ram.write(i, val // 100) # Most-significant digit - self.ram.write((i + 1) & self.i_bitmask, (val // 10) % 10) # Middle digit - self.ram.write((i + 2) & self.i_bitmask, val % 10) # Least-signifiant digit + ram_write = self.ram.write + ram_write(i, val // 100) # Most-significant digit + ram_write((i + 1) & self.i_bitmask, (val // 10) % 10) # Middle digit + ram_write((i + 2) & self.i_bitmask, val % 10) # Least-signifiant digit def _post_Fx55_Fx65(self): if self.load_quirks: @@ -716,10 +725,10 @@ def _post_Fx55_Fx65(self): self.i &= self.i_bitmask - def _Fx55(self): # LD [I], Vx - if self.live_debug: - self.debug("LD [I], V{:01x}".format(self.vx)) + def _Fx55_d(self): # LD [I], Vx (debug) + self.debug("LD [I], V{:01x}".format(self.vx)) + def _Fx55(self): # LD [I], Vx i = self.i i_bitmask = self.i_bitmask ram_write = self.ram.write @@ -729,10 +738,10 @@ def _Fx55(self): # LD [I], Vx self._post_Fx55_Fx65() - def _Fx65(self): # LD Vx, [I] - if self.live_debug: - self.debug("LD V{:01x}, [I]".format(self.vx)) + def _Fx65_d(self): # LD Vx, [I] (debug) + self.debug("LD V{:01x}, [I]".format(self.vx)) + def _Fx65(self): # LD Vx, [I] i = self.i i_bitmask = self.i_bitmask ram_read = self.ram.read @@ -744,36 +753,36 @@ def _Fx65(self): # LD Vx, [I] # Instructions for Super-CHIP 1.0 (and above) - def _00FD(self): # EXIT - if self.live_debug: - self.debug("EXIT") + def _00FD_d(self): # EXIT (debug) + self.debug("EXIT") + def _00FD(self): # EXIT self.dec_pc() - def _00FE(self): # LOW - if self.live_debug: - self.debug("LOW") + def _00FE_d(self): # LOW (debug) + self.debug("LOW") + def _00FE(self): # LOW if self.lo_res: self.framebuffer.reset_vid() else: self.framebuffer.resize_vid(64, 32) self.lo_res = True - def _00FF(self): # HIGH - if self.live_debug: - self.debug("HIGH") + def _00FF_d(self): # HIGH (debug) + self.debug("HIGH") + def _00FF(self): # HIGH if self.lo_res: self.framebuffer.resize_vid(128, 64) self.lo_res = False else: self.framebuffer.reset_vid() - def _Fx75(self): # LD R, Vx - if self.live_debug: - self.debug("LD R, V{:01x}".format(self.vx)) + def _Fx75_d(self): # LD R, Vx (debug) + self.debug("LD R, V{:01x}".format(self.vx)) + def _Fx75(self): # LD R, Vx vx = self.vx if self.arch < ARCH_XO_CHIP and vx > 7: @@ -784,10 +793,10 @@ def _Fx75(self): # LD R, Vx # Ensure with +1s that the final register is copied. self.rpl[:vx + 1] = self.v[:vx + 1] - def _Fx85(self): # LD Vx, R - if self.live_debug: - self.debug("LD V{:01x}, R".format(self.vx)) + def _Fx85_d(self): # LD Vx, R (debug) + self.debug("LD V{:01x}, R".format(self.vx)) + def _Fx85(self): # LD Vx, R vx = self.vx if self.arch < ARCH_XO_CHIP and vx > 7: @@ -800,30 +809,30 @@ def _Fx85(self): # LD Vx, R # Instructions for Super-CHIP 1.1 (and above) - def _00FB(self): # SCR - if self.live_debug: - self.debug("SCR") + def _00FB_d(self): # SCR (debug) + self.debug("SCR") + def _00FB(self): # SCR self.framebuffer.scroll_right(2 if self.lo_res else 4) - def _00FC(self): # SCL - if self.live_debug: - self.debug("SCL") + def _00FC_d(self): # SCL (debug) + self.debug("SCL") + def _00FC(self): # SCL self.framebuffer.scroll_left(2 if self.lo_res else 4) - def _Fx30(self): # LD HF, Vx - if self.live_debug: - self.debug("LD HF, V{:01x}".format(self.vx)) + def _Fx30_d(self): # LD HF, Vx (debug) + self.debug("LD HF, V{:01x}".format(self.vx)) + def _Fx30(self): # LD HF, Vx self.i = (self.sysfont_bg_loc + 10 * self.v[self.vx]) & self.i_bitmask + def _00Cn_d(self): # SCD n (debug) + self.debug("SCD 0x{:01x}".format(self.nibble)) + def _00Cn(self): # SCD n scroll_distance = self.nibble - if self.live_debug: - self.debug("SCD 0x{:01x}".format(scroll_distance)) - if self.lo_res: if scroll_distance & 1: raise CPUError("Scrolling down vertically by a half-pixel in low resolution mode is unsupported.") @@ -834,12 +843,12 @@ def _00Cn(self): # SCD n # Instructions for XO-CHIP + def _00Dn_d(self): # SCU n (debug) + self.debug("SCU 0x{:01x}".format(self.nibble)) + def _00Dn(self): # SCU n scroll_distance = self.nibble - if self.live_debug: - self.debug("SCU 0x{:01x}".format(scroll_distance)) - if self.lo_res: if scroll_distance & 1: raise CPUError("Scrolling up vertically by a half-pixel in low resolution mode is unsupported.") @@ -848,13 +857,12 @@ def _00Dn(self): # SCU n self.framebuffer.scroll_up(scroll_distance) + def _5xy2_d(self): # XST Vx, Vy (debug) + self.debug("XST V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _5xy2(self): # XST Vx, Vy vx = self.vx vy = self.vy - - if self.live_debug: - self.debug("XST V{:01x}, V{:01x}".format(vx, vy)) - i = self.i i_bitmask = self.i_bitmask iter_back = vx > vy @@ -863,13 +871,12 @@ def _5xy2(self): # XST Vx, Vy for offset in range(vx - vy + 1) if iter_back else range(vy - vx + 1): ram_write((i + offset) & i_bitmask, self.v[(vx - offset) if iter_back else (vx + offset)]) + def _5xy3_d(self): # XLD Vx, Vy (debug) + self.debug("XLD V{:01x}, V{:01x}".format(self.vx, self.vy)) + def _5xy3(self): # XLD Vx, Vy vx = self.vx vy = self.vy - - if self.live_debug: - self.debug("XLD V{:01x}, V{:01x}".format(vx, vy)) - i = self.i i_bitmask = self.i_bitmask iter_back = vx > vy @@ -878,34 +885,38 @@ def _5xy3(self): # XLD Vx, Vy for offset in range(vx - vy + 1) if iter_back else range(vy - vx + 1): self.v[(vx - offset) if iter_back else (vx + offset)] = ram_read((i + offset) & i_bitmask) + def _Fx00_d(self): # XLDL I, addr (debug) + if self.vx != 0: + self._opcode_unsupported() # Duplicate check is skipped when not debugging + + self.debug("XLDL I, 0x{:04x}".format(self.fetch())) + def _Fx00(self): # XLDL I, addr # Only F000 is supported if self.vx != 0: self._opcode_unsupported() - i = self.fetch() - - if self.live_debug: - self.debug("XLDL I, 0x{:04x}".format(i)) - - self.i = i + self.i = self.fetch() self.inc_pc() # Increment PC again as this is a double-length instruction - def _Fn01(self): # XPLA Vx - if self.live_debug: - self.debug("XPLA 0x{:01x}".format(self.vx)) + def _Fn01_d(self): # XPLA Vx (debug) + self.debug("XPLA 0x{:01x}".format(self.vx)) + def _Fn01(self): # XPLA Vx self.framebuffer.switch_planes(self.vx) + def _Fx02_d(self): # XSTA (debug) + if self.vx != 0: + self._opcode_unsupported() # Duplicate check is skipped when not debugging + + self.debug("XSTA") + def _Fx02(self): # XSTA # Only 0xF002 is supported by the CPU, but since there is no 0xF102, 0xF202, .., 0xFF02, we can catch the # entirety of Fx02 with the same bitmask as other 0xFs, and then just check the second nibble (Vx) is 0 if self.vx != 0: self._opcode_unsupported() - if self.live_debug: - self.debug("XSTA") - if self.audio_null: # Return without doing anything if we don't have a proper audio driver return @@ -914,10 +925,10 @@ def _Fx02(self): # XSTA # simply only be partially used. This situation is not worth catching. self.audio.set_buffer(self.ram.read_block(self.i, 16)) - def _Fx3A(self): # XPR Vx - if self.live_debug: - self.debug("XPR V{:01x}".format(self.vx)) + def _Fx3A_d(self): # XPR Vx (debug) + self.debug("XPR V{:01x}".format(self.vx)) + def _Fx3A(self): # XPR Vx if self.audio_null: # Return without doing anything if we don't have a proper audio driver return diff --git a/superchocchip.py b/superchocchip.py index d74313c..1663b28 100755 --- a/superchocchip.py +++ b/superchocchip.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 __author__ = "Gregory Maynard-Hoare" -__copyright__ = "Copyright (C) 2022 Gregory Maynard-Hoare" +__copyright__ = "Copyright (C) 2023 Gregory Maynard-Hoare" __license__ = "GNU Affero General Public License v3.0" -__version__ = "1.4.0" +__version__ = "1.4.1" from argparse import ArgumentParser from scchip import main