Skip to content
Permalink
Browse files

ROM support

  • Loading branch information...
irmen committed Jul 11, 2018
1 parent 3b93396 commit 64b5e1fe457d373162a4297c73e6af8d4c62e78a
Showing with 124 additions and 32 deletions.
  1. +3 −0 .gitignore
  2. +1 −1 pyc64/cputools.py
  3. +67 −25 pyc64/emulator.py
  4. +42 −4 pyc64/memory.py
  5. +6 −0 roms/readme.txt
  6. +5 −2 start64plusplus.py
@@ -16,3 +16,6 @@
*.asm
*.labels.txt
.mypy_cache/
/roms/basic
/roms/chargen
/roms/kernal
@@ -55,7 +55,7 @@ def run(self, pc=None, microsleep=None, loop_detect_delay=2):
break
if self.memory[self.pc] in stopcodes:
end_time = time.perf_counter()
raise InterruptedError("brk instruction")
raise InterruptedError("brk instruction at ${:04x}".format(self.pc))
duration = end_time - start_time
mips = instructions / duration / 1e6
print(self.name + " CPU simulator: {:d} instructions in {:.3f} seconds = {:.3f} mips (~{:.3f} times realtime)"
@@ -24,6 +24,38 @@
from .python import PythonInterpreter


def create_bitmaps_from_char_rom(temp_graphics_folder, roms_directory):
# create char bitmaps from the orignal c-64 chargen rom file
rom = open(roms_directory+"/"+"chargen", "rb").read()
def doublewidth_and_mirror(b):
result = 0
for _ in range(8):
bit = b&1
b >>= 1
result <<= 1
result |= bit
result <<= 1
result |= bit
x, y = divmod(result, 256)
return y, x
def writechar(c, rom_offset, filesuffix):
with open("{:s}/char{:s}-{:02x}.xbm".format(temp_graphics_folder, filesuffix, c), "wb") as outf:
outf.write(b"#define im_width 16\n")
outf.write(b"#define im_height 16\n")
outf.write(b"static char im_bits[] = {\n")
for y in range(8):
b1, b2 = doublewidth_and_mirror(rom[c*8 + y + rom_offset])
outf.write(bytes("0x{:02x}, 0x{:02x}, 0x{:02x}, 0x{:02x}, ".format(b1, b2, b1, b2), "ascii"))
outf.seek(-2, os.SEEK_CUR) # get rid of the last space and comma
outf.write(b"\n};\n")
# normal chars
for c in range(256):
writechar(c, 0, "")
# shifted chars
for c in range(256):
writechar(c, 256*8, "-sh")


class EmulatorWindowBase(tkinter.Tk):
temp_graphics_folder = "temp_gfx"
update_rate = 1000 // 20 # 20 hz screen refresh rate
@@ -38,7 +70,7 @@ class EmulatorWindowBase(tkinter.Tk):
colorpalette = []
welcome_message = "Welcome to the emulator!"

def __init__(self, screen, title):
def __init__(self, screen, title, roms_directory):
if len(self.colorpalette) not in (2, 4, 8, 16, 32, 64, 128, 256):
raise ValueError("colorpalette size not a valid power of 2")
if self.columns <= 0 or self.columns > 128 or self.rows <= 0 or self.rows > 128:
@@ -68,7 +100,8 @@ def __init__(self, screen, title):
self.refreshtick = threading.Event()
self.spritebitmapbytes = [None] * self.sprites
self.spritebitmaps = []
self.create_bitmaps()
self.roms_directory = roms_directory
self.create_bitmaps(self.roms_directory)
# create the character bitmaps for all character tiles, fixed on the canvas:
self.charbitmaps = []
for y in range(self.rows):
@@ -206,28 +239,34 @@ def repaint(self):
def smoothscroll(self, xs, ys):
return -xs * 2, -self.ys * 2

def create_bitmaps(self):
def create_bitmaps(self, roms_directory=""):
os.makedirs(self.temp_graphics_folder, exist_ok=True)
with open(self.temp_graphics_folder + "/readme.txt", "w") as f:
f.write("this is a temporary folder to cache pyc64 files for tkinter graphics bitmaps.\n")
# normal
with Image.open(io.BytesIO(pkgutil.get_data(__name__, "charset/" + self.charset_normal))) as source_chars:
for i in range(256):
filename = self.temp_graphics_folder + "/char-{:02x}.xbm".format(i)
chars = source_chars.copy()
row, col = divmod(i, source_chars.width // 16) # we assume 16x16 pixel chars (2x zoom)
ci = chars.crop((col * 16, row * 16, col * 16 + 16, row * 16 + 16))
ci = ci.convert(mode="1", dither=None)
ci.save(filename, "xbm")
# shifted
with Image.open(io.BytesIO(pkgutil.get_data(__name__, "charset/" + self.charset_shifted))) as source_chars:
for i in range(256):
filename = self.temp_graphics_folder + "/char-sh-{:02x}.xbm".format(i)
chars = source_chars.copy()
row, col = divmod(i, source_chars.width // 16) # we assume 16x16 pixel chars (2x zoom)
ci = chars.crop((col * 16, row * 16, col * 16 + 16, row * 16 + 16))
ci = ci.convert(mode="1", dither=None)
ci.save(filename, "xbm")
if roms_directory and os.path.isfile(roms_directory+"/"+"chargen"):
# create char bitmaps from the C64 chargen rom file.
print("creating char bitmaps from chargen rom")
create_bitmaps_from_char_rom(self.temp_graphics_folder, roms_directory)
else:
print("creating char bitmaps from png images in the package")
# normal
with Image.open(io.BytesIO(pkgutil.get_data(__name__, "charset/" + self.charset_normal))) as source_chars:
for i in range(256):
filename = self.temp_graphics_folder + "/char-{:02x}.xbm".format(i)
chars = source_chars.copy()
row, col = divmod(i, source_chars.width // 16) # we assume 16x16 pixel chars (2x zoom)
ci = chars.crop((col * 16, row * 16, col * 16 + 16, row * 16 + 16))
ci = ci.convert(mode="1", dither=None)
ci.save(filename, "xbm")
# shifted
with Image.open(io.BytesIO(pkgutil.get_data(__name__, "charset/" + self.charset_shifted))) as source_chars:
for i in range(256):
filename = self.temp_graphics_folder + "/char-sh-{:02x}.xbm".format(i)
chars = source_chars.copy()
row, col = divmod(i, source_chars.width // 16) # we assume 16x16 pixel chars (2x zoom)
ci = chars.crop((col * 16, row * 16, col * 16 + 16, row * 16 + 16))
ci = ci.convert(mode="1", dither=None)
ci.save(filename, "xbm")
# monochrome sprites (including their double-size variants)
sprites = self.screen.getsprites()
for i, sprite in sprites.items():
@@ -277,8 +316,8 @@ class C64EmulatorWindow(EmulatorWindowBase):
"use 'gopy' to enter Python mode\n\n\n\n" \
"(install the py64 library to be able to execute 6502 machine code)"

def __init__(self, screen, title):
super().__init__(screen, title)
def __init__(self, screen, title, roms_directory):
super().__init__(screen, title, roms_directory)
self.screen.memory[0x00fb] = EmulatorWindowBase.update_rate
self.hertztick = threading.Event()
self.interpret_thread = None
@@ -703,8 +742,11 @@ def runstop(self):


def start():
screen = ScreenAndMemory(columns=C64EmulatorWindow.columns, rows=C64EmulatorWindow.rows, sprites=C64EmulatorWindow.sprites)
emu = C64EmulatorWindow(screen, "Commodore-64 'emulator' in pure Python!")
screen = ScreenAndMemory(columns=C64EmulatorWindow.columns,
rows=C64EmulatorWindow.rows,
sprites=C64EmulatorWindow.sprites,
rom_directory="roms")
emu = C64EmulatorWindow(screen, "Commodore-64 'emulator' in pure Python!", "roms")
emu.start()
emu.mainloop()

@@ -10,6 +10,7 @@
License: MIT open-source.
"""

import os
import time
import struct
import codecs
@@ -52,6 +53,7 @@ def __init__(self, size=0x10000, endian="little"):
self.mem = bytearray(size)
self.hooked_reads = bytearray(size) # 'bitmap' of addresses that have read-hooks, for fast checking
self.hooked_writes = bytearray(size) # 'bitmap' of addresses that have write-hooks, for fast checking
self.rom_areas = set() # set of tuples (start, end) addresses of ROM (read-only) areas
self.endian = endian # 'little' or 'big', affects the way 16-bit words are read/written
self.write_hooks = defaultdict(list)
self.read_hooks = defaultdict(list)
@@ -61,7 +63,9 @@ def __len__(self):

def clear(self):
"""set all memory values to 0."""
self.mem = bytearray(self.size)
print("clear mem") # XXX
for a in range(0, 0x10000):
self[a] = 0

def getword(self, address, signed=False):
"""get a 16-bit (2 bytes) value from memory, no aligning restriction"""
@@ -116,7 +120,10 @@ def __setitem__(self, addr_or_slice, value):
newvalue = hook(addr_or_slice, self.mem[addr_or_slice], value)
if newvalue is not None:
value = newvalue
self.mem[addr_or_slice] = value
if self.rom_areas:
self._write_with_romcheck_addr(addr_or_slice, value)
else:
self.mem[addr_or_slice] = value
elif type(addr_or_slice) is slice:
if any(self.hooked_writes[addr_or_slice]):
# there's at least one address in the slice with a hook, so... slow mode
@@ -136,10 +143,30 @@ def __setitem__(self, addr_or_slice, value):
value = bytes([value]) * slice_len
elif len(value) != slice_len:
raise ValueError("value length differs from memory slice length")
self.mem[addr_or_slice] = value
if self.rom_areas:
self._write_with_romcheck_slice(addr_or_slice, value)
else:
self.mem[addr_or_slice] = value
else:
raise TypeError("invalid address type")

def _write_with_romcheck_addr(self, address, value):
for rom_start, rom_end in self.rom_areas:
if address >= rom_start and address <= rom_end:
return # don't write to ROM address
self.mem[address] = value

def _write_with_romcheck_slice(self, addrslice, value):
for rom_start, rom_end in self.rom_areas:
if addrslice.start <= rom_end and addrslice.stop >= rom_start+1:
# the slice could be *partially* in RAM and *partially* in ROM
# we're not figuring that out here, just write/check every byte individually.
for addr in range(*addrslice.indices(self.size)):
self[addr] = value
return
# whole slice is outside of all rom areas, just write it
self.mem[addrslice] = value

def intercept_write(self, address, hook):
"""
Register a hook function to be called when a write occurs to the given memory address.
@@ -156,6 +183,12 @@ def intercept_read(self, address, hook):
self.read_hooks[address].append(hook)
self.hooked_reads[address] = 1

def load_rom(self, romfile, address):
with open(romfile, "rb") as romf:
data = romf.read()
self.mem[address:address+len(data)] = data
self.rom_areas.add((address, address+len(data)-1))


class ScreenAndMemory:
colorpalette_morecontrast = ( # this is a palette with more contrast
@@ -213,11 +246,16 @@ class ScreenAndMemory:
0xADADAD, # 15 = light grey
)

def __init__(self, columns=40, rows=25, sprites=8):
def __init__(self, columns=40, rows=25, sprites=8, rom_directory=""):
# zeropage is from $0000-$00ff
# screen chars $0400-$07ff
# screen colors $d800-$dbff
self.memory = Memory(65536) # 64 Kb
if rom_directory:
for rom, address in (("basic", 0xa000), ("kernal", 0xe000)):
if os.path.isfile(rom_directory + "/" + rom):
print("loading rom file", rom, "at", hex(address))
self.memory.load_rom(rom_directory+"/"+rom, address)
self.hz = 60 # NTSC
self.columns = columns
self.rows = rows
@@ -0,0 +1,6 @@
Put the three C-64 ROM files here:

basic
kernal
chargen

@@ -53,8 +53,11 @@ def _border_positions(self):


def start():
screen = ScreenAndMemory(columns=EmulatorPlusWindow.columns, rows=EmulatorPlusWindow.rows, sprites=EmulatorPlusWindow.sprites)
emu = EmulatorPlusWindow(screen, "Commodore-64 \"PLUSPLUS\" 'emulator' in pure Python!")
screen = ScreenAndMemory(columns=EmulatorPlusWindow.columns,
rows=EmulatorPlusWindow.rows,
sprites=EmulatorPlusWindow.sprites,
rom_directory="roms")
emu = EmulatorPlusWindow(screen, "Commodore-64 \"PLUSPLUS\" 'emulator' in pure Python!", "roms")
emu.start()
emu.mainloop()

0 comments on commit 64b5e1f

Please sign in to comment.
You can’t perform that action at this time.