Skip to content

Instantly share code, notes, and snippets.

@tehmaze

tehmaze/c64xbin.py

Last active Aug 29, 2015
Embed
What would you like to do?
C64 PRG to XBIN converter
#!/usr/bin/env python
#
# (c) 2015 Wijnand Modderman-Lenstra, https://maze.io/
#
# Convert a C64 memory dump (VICE) to eXtended Binary (XBIN)
#
# To capture a dump in VICE, attach to the monitor, and run the following::
# > $01 $35
# save "dump.ram" 0 $0000 $FFFF
#
# Now, using dump.ram, you can invoke this script and get an XBIN. Jay.
#
# Thanks to the folks over at NURDspace https://nurdspace.nl/ for explaining
# the C64 and VIC-II memory layout.
import argparse
import os
import struct
import sys
# VICE chargen ROM
# FIXME should actually read the chargen ROM from the C64 memory map, but for
# now this will do as none of my PRGs have custom fonts
chargen = os.path.expanduser('~/.vice/C64/chargen')
# http://www.pepto.de/projects/colorvic/
colors = [
0x000000, 0xffffff, 0x68372b, 0x70a4b2, 0x6f3d86, 0x588d43, 0x352879, 0xb8c76f,
0x6f4f25, 0x433900, 0x9a6759, 0x444444, 0x6c6c6c, 0x9ad284, 0x6c5eb5, 0x959595
]
def load_dump(filename):
with open(filename, 'rb') as fp:
data = bytearray(fp.read())
# VICE dumps include the loader address
if len(data) == 0x10002 and data[0] == 0x00 and data[1] == 0x00:
data = data[2:]
assert len(data) == 0x10000, 'not a 64k dump'
return data
def run():
parser = argparse.ArgumentParser()
parser.add_argument('ram', nargs=1)
args = parser.parse_args()
dump = load_dump(args.ram[0])
base = os.path.splitext(os.path.basename(args.ram[0]))[0]
# http://codebase64.org/doku.php?id=base:vicii_memory_organizing
print '$d018 = {:08b}'.format(dump[0xd018])
bitmap = ((dump[0xd018] >> 3) & 0b1) * 0x2000
charmemp = ((dump[0xd018] >> 1) & 0b111) * 0x0800
charmem = 0xd800
screenmem = ((dump[0xd018] >> 4) & 0b1111) * 0x0400
if charmemp == 0x1000:
case = 'upper'
elif charmemp == 0x1800:
case = 'mixed'
else:
case = 'unknown'
print ' bitmap ${:04x}'.format(bitmap)
print ' charmem ${:04x}'.format(charmem)
print ' charmem ${:04x} ({} case)'.format(charmemp, case)
print ' screenmem ${:04x}'.format(screenmem)
print '$dd00 = {:08b}'.format(dump[0xdd00]),
bank = [3, 2, 1, 0][dump[0xdd00] & 3]
print '-> bank{}'.format(bank)
# Dump text map, 40x25 characters in screenmem
textmap = []
for offset in xrange(0x0000, 0x03e8, 40):
p = screenmem + offset
t = dump[p:p + 40]
textmap.append(t)
# Dump color map, 40x25 characters, 3 bits per color
background = dump[0xd021] & 0xf
colormap = []
for offset in xrange(0x0000, 0x03e8, 40):
p = charmem + offset
c = dump[p:p + 40]
colormap.append(map(lambda x: x & 0xf, c))
# Let's make an XBin
xbin = bytearray('XBIN\x1a') # ID + EOF char
xbin.extend(struct.pack('<HH', 40, 25)) # Width and Height
xbin.extend(struct.pack('<B', 8)) # FontSize 8x256
xbin.extend(struct.pack('<B',
0b00001011, # Nonblink, Palette & Font
)) # Flags
for color in colors:
r = (color >> 16) & 0xff
g = (color >> 8) & 0xff
b = (color >> 0) & 0xff
xbin.extend(struct.pack('<BBB', r >> 2, g >> 2, b >> 2))
with open(chargen, 'rb') as fp:
if case == 'mixed': # skip first 256 glyphs
fp.read(2048)
xbin.extend(fp.read(2048))
for h in range(25):
for w in range(40):
xbin.append(textmap[h][w])
xbin.append(colormap[h][w] | (background << 4))
# SAUCE stuff
size = len(xbin)
xbin.extend('\x1aSAUCE00')
xbin.extend('\x00' * 35) # Title
xbin.extend('\x00' * 20) # Author
xbin.extend('\x00' * 20) # Group
xbin.extend('19700101') # Date
xbin.extend(struct.pack('<I', size))
xbin.extend(struct.pack('<B', 6)) # DataType XBin
xbin.extend(struct.pack('<B', 0)) # FileType None
xbin.extend(struct.pack('<HHHH', 8, 25, 0, 0)) # Char Width, Lines, N/A, N/A
xbin.extend(struct.pack('<B', 0)) # Comments
xbin.extend(struct.pack('<B', 0b00001001)) # Legacy Ratio, iCE Colors
xbin.extend('\x00' * 22) # TInfoS
print 'saving to {}.xb'.format(base)
with open(base + '.xb', 'wb') as fp:
fp.write(xbin)
if __name__ == '__main__':
sys.exit(run())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment