Initial working object-based generator

This commit is contained in:
Rob McMullen 2017-06-20 15:59:56 -07:00
parent 8a1de40e0e
commit d02300573d

View File

@ -1,215 +1,267 @@
#!/usr/bin/python #!/usr/bin/python
import sys,os,png # system packages
import sys
import os
import argparse import argparse
import re
# external packages
import png # package name is "pypng" on pypi.python.org
class Colors: class Colors:
black,magenta,green,orange,blue,white,key = range(7) black,magenta,green,orange,blue,white,key = range(7)
def slugify(s):
"""Simplifies ugly strings into something that can be used as an assembler
label.
def main(argv): >>> print slugify("[Some] _ Article's Title--")
parser = argparse.ArgumentParser(description="Sprite compiler for 65C02/6502 to generate assembly code to render all shifts of the given sprite, optionally with exclusive-or drawing (if background will be non-black). Generated code has conditional compilation directives for the CC65 assembler to allow the same file to be compiled for either architecture.") SOME_ARTICLES_TITLE
parser.add_argument("-v", "--verbose", default=0, action="count")
parser.add_argument("-t", "--tables", action="store_true", default=False, help="output only lookup tables for horizontal sprite shifts (division and modulus 7)")
parser.add_argument("-x", "--xdraw", action="store_true", default=False, help="use XOR for sprite drawing")
parser.add_argument("files", metavar="IMAGE", nargs="+", help="a PNG image [or a list of them]. PNG files must not have an alpha channel!")
options, extra_args = parser.parse_known_args()
if options.tables: From https://gist.github.com/dolph/3622892#file-slugify-py
printHorizontalLookup() """
exit(0)
for pngfile in options.files: # "[Some] _ Article's Title--"
process(pngfile, options.xdraw) # "[SOME] _ ARTICLE'S TITLE--"
s = s.upper()
# "[SOME] _ ARTICLE'S_TITLE--"
# "[SOME]___ARTICLE'S_TITLE__"
for c in [' ', '-', '.', '/']:
s = s.replace(c, '_')
# "[SOME]___ARTICLE'S_TITLE__"
# "SOME___ARTICLES_TITLE__"
s = re.sub('\W', '', s)
# "SOME___ARTICLES_TITLE__"
# "SOME ARTICLES TITLE "
s = s.replace('_', ' ')
# "SOME ARTICLES TITLE "
# "SOME ARTICLES TITLE "
s = re.sub('\s+', ' ', s)
# "SOME ARTICLES TITLE "
# "SOME ARTICLES TITLE"
s = s.strip()
# "SOME ARTICLES TITLE"
# "SOME_ARTICLES_TITLE"
s = s.replace(' ', '_')
return s
def process(pngfile, xdraw=False): class Listing(object):
reader = png.Reader(pngfile) disclaimer = '''
try: ; This file was generated by HiSprite.py, a sprite compiler by Quinn Dunki.
pngdata = reader.asRGB8() ; If you feel the need to modify this file, you are probably doing it wrong.
except: '''
usage()
width = pngdata[0] def __init__(self):
height = pngdata[1] self.lines = [self.disclaimer]
pixelData = list(pngdata[2])
byteWidth = width/2+1+1 # TODO: Calculate a power of two for this
niceName = os.path.splitext(pngfile)[0].upper()
disclaimer() def __str__(self):
return "\n".join(self.lines) + "\n"
# Prologue def out(self, line):
print "%s: ;%d bytes per row" % (niceName,byteWidth) self.lines.append(line)
print "\tSAVE_AXY"
print "\tldy PARAM0"
print "\tldx MOD7_2,y"
print ".ifpC02"
print "\tjmp (%s_JMP,x)\n" % (niceName)
offset_suffix = ""
# Bit-shift jump table for 65C02
print "%s_JMP:" % (niceName)
for shift in range(0,7):
print "\t.addr %s_SHIFT%d" % (niceName,shift)
print ".else" class Sprite(Listing):
# Fast jump table routine; faster and smaller than self-modifying code def __init__(self, pngfile, xdraw=False):
print "\tlda %s_JMP+1,x" % (niceName) Listing.__init__(self)
print "\tpha"
print "\tlda %s_JMP,x" % (niceName)
print "\tpha"
print "\trts\n"
# Bit-shift jump table for generic 6502 reader = png.Reader(pngfile)
print "%s_JMP:" % (niceName) try:
for shift in range(0,7): pngdata = reader.asRGB8()
print "\t.addr %s_SHIFT%d-1" % (niceName,shift) except:
print ".endif" raise RuntimeError
# Blitting functions self.xdraw = xdraw
print "\n" self.niceName = slugify(os.path.splitext(pngfile)[0])
for shift in range(0,7): self.width = pngdata[0]
self.height = pngdata[1]
self.pixelData = list(pngdata[2])
self.calcStorage()
self.jumpTable()
for i in range(self.numShifts):
self.blitShift(i)
def calcStorage(self):
self.byteWidth = self.width/2+1+1 # TODO: Calculate a power of two for this
self.numShifts = 7
def jumpTable(self):
# Prologue
self.out("%s: ;%d bytes per row" % (self.niceName, self.byteWidth))
self.out("\tSAVE_AXY")
self.out("\tldy PARAM0")
self.out("\tldx MOD7_2,y")
self.out(".ifpC02")
self.out("\tjmp (%s_JMP,x)\n" % (self.niceName))
offset_suffix = ""
# Bit-shift jump table for 65C02
self.out("%s_JMP:" % (self.niceName) )
for shift in range(self.numShifts):
self.out("\t.addr %s_SHIFT%d" % (self.niceName, shift))
self.out(".else")
# Fast jump table routine; faster and smaller than self-modifying code
self.out("\tlda %s_JMP+1,x" % (self.niceName))
self.out("\tpha")
self.out("\tlda %s_JMP,x" % (self.niceName))
self.out("\tpha")
self.out("\trts\n")
# Bit-shift jump table for generic 6502
self.out("%s_JMP:" % (self.niceName))
for shift in range(self.numShifts):
self.out("\t.addr %s_SHIFT%d-1" % (self.niceName,shift))
self.out(".endif")
def blitShift(self, shift):
# Blitting functions
self.out("\n")
# Track cycle count of the blitter. We start with fixed overhead: # Track cycle count of the blitter. We start with fixed overhead:
# SAVE_AXY + RESTORE_AXY + rts + sprite jump table # SAVE_AXY + RESTORE_AXY + rts + sprite jump table
cycleCount = 9 + 12 + 6 + 3 + 4 + 6 cycleCount = 9 + 12 + 6 + 3 + 4 + 6
print "%s_SHIFT%d:" % (niceName,shift) self.out("%s_SHIFT%d:" % (self.niceName,shift))
print "\tldx PARAM1" self.out("\tldx PARAM1")
cycleCount += 3 cycleCount += 3
rowStartCode,extraCycles = rowStartCalculatorCode(); rowStartCode,extraCycles = self.rowStartCalculatorCode();
print rowStartCode self.out(rowStartCode)
cycleCount += extraCycles cycleCount += extraCycles
spriteChunks = layoutSpriteChunk(pixelData,width,height,shift,xdraw,cycleCount) spriteChunks = self.layoutSpriteChunk(shift, cycleCount)
for row in range(height): for row in range(self.height):
for chunkIndex in range(len(spriteChunks)): for chunkIndex in range(len(spriteChunks)):
print spriteChunks[chunkIndex][row] self.out(spriteChunks[chunkIndex][row])
print "\n" def layoutSpriteChunk(self, shift, cycleCount):
colorStreams = self.byteStreamsFromPixels(shift, bitsForColor, highBitForColor)
# print colorStreams
maskStreams = self.byteStreamsFromPixels(shift, bitsForMask, highBitForMask)
# print maskStreams
code = self.generateBlitter(colorStreams, maskStreams, cycleCount)
def layoutSpriteChunk(pixelData,width,height,shift,xdraw,cycleCount): return code
colorStreams = byteStreamsFromPixels(pixelData,width,height,shift,bitsForColor,highBitForColor) def byteStreamsFromPixels(self, shift, bitDelegate, highBitDelegate):
maskStreams = byteStreamsFromPixels(pixelData,width,height,shift,bitsForMask,highBitForMask) byteStreams = ["" for x in range(self.height)]
code = generateBlitter(colorStreams,maskStreams,height,xdraw,cycleCount) byteWidth = self.width/2+1+1
return code for row in range(self.height):
bitStream = ""
# Compute raw bitstream for row from PNG pixels
for pixelIndex in range(self.width):
pixel = pixelColor(self.pixelData,row,pixelIndex)
bitStream += bitDelegate(pixel)
def byteStreamsFromPixels(pixelData,width,height,shift,bitDelegate,highBitDelegate): # Shift bit stream as needed
bitStream = shiftStringRight(bitStream,shift)
bitStream = bitStream[:byteWidth*8]
byteStreams = ["" for x in range(height)] # Split bitstream into bytes
byteWidth = width/2+1+1 bitPos = 0
byteSplits = [0 for x in range(byteWidth)]
for row in range(height): for byteIndex in range(byteWidth):
bitStream = "" remainingBits = len(bitStream) - bitPos
# Compute raw bitstream for row from PNG pixels bitChunk = ""
for pixelIndex in range(width):
pixel = pixelColor(pixelData,row,pixelIndex)
bitStream += bitDelegate(pixel)
# Shift bit stream as needed if remainingBits < 0:
bitStream = shiftStringRight(bitStream,shift) bitChunk = "0000000"
bitStream = bitStream[:byteWidth*8]
# Split bitstream into bytes
bitPos = 0
byteSplits = [0 for x in range(byteWidth)]
for byteIndex in range(byteWidth):
remainingBits = len(bitStream) - bitPos
bitChunk = ""
if remainingBits < 0:
bitChunk = "0000000"
else:
if remainingBits < 7:
bitChunk = bitStream[bitPos:]
bitChunk += fillOutByte(7-remainingBits)
else: else:
bitChunk = bitStream[bitPos:bitPos+7] if remainingBits < 7:
bitChunk = bitStream[bitPos:]
bitChunk += fillOutByte(7-remainingBits)
else:
bitChunk = bitStream[bitPos:bitPos+7]
bitChunk = bitChunk[::-1] bitChunk = bitChunk[::-1]
# Determine palette bit from first pixel on each row # Determine palette bit from first pixel on each row
highBit = highBitDelegate(pixelData[row][0]) highBit = highBitDelegate(self.pixelData[row][0])
byteSplits[byteIndex] = highBit + bitChunk byteSplits[byteIndex] = highBit + bitChunk
bitPos += 7 bitPos += 7
byteStreams[row] = byteSplits; byteStreams[row] = byteSplits;
return byteStreams return byteStreams
def generateBlitter(self, colorStreams, maskStreams, baseCycleCount):
byteWidth = len(colorStreams[0])
spriteChunks = [["" for y in range(self.height)] for x in range(byteWidth)]
def generateBlitter(colorStreams,maskStreams,height,xdraw,baseCycleCount): cycleCount = baseCycleCount
optimizationCount = 0
byteWidth = len(colorStreams[0]) for row in range(self.height):
spriteChunks = [["" for y in range(height)] for x in range(byteWidth)]
cycleCount = baseCycleCount byteSplits = colorStreams[row]
optimizationCount = 0
for row in range(height): # Generate blitting code
for chunkIndex in range(len(byteSplits)):
byteSplits = colorStreams[row] # Optimization
if byteSplits[chunkIndex] != "00000000" and \
byteSplits[chunkIndex] != "10000000":
# Generate blitting code # Store byte into video memory
for chunkIndex in range(len(byteSplits)): if self.xdraw:
spriteChunks[chunkIndex][row] = \
# Optimization "\tlda (SCRATCH0),y\n" + \
if byteSplits[chunkIndex] != "00000000" and \ "\teor #%%%s\n" % byteSplits[chunkIndex] + \
byteSplits[chunkIndex] != "10000000": "\tsta (SCRATCH0),y\n";
cycleCount += 5 + 2 + 6
# Store byte into video memory else:
if xdraw: spriteChunks[chunkIndex][row] = \
spriteChunks[chunkIndex][row] = \ "\tlda #%%%s\n" % byteSplits[chunkIndex] + \
"\tlda (SCRATCH0),y\n" + \ "\tsta (SCRATCH0),y\n";
"\teor #%%%s\n" % byteSplits[chunkIndex] + \ cycleCount += 2 + 6
"\tsta (SCRATCH0),y\n";
cycleCount += 5 + 2 + 6
else: else:
spriteChunks[chunkIndex][row] = \ optimizationCount += 1
"\tlda #%%%s\n" % byteSplits[chunkIndex] + \
"\tsta (SCRATCH0),y\n"; # Increment indices
cycleCount += 2 + 6 if chunkIndex == len(byteSplits)-1:
spriteChunks[chunkIndex][row] += "\n"
else:
spriteChunks[chunkIndex][row] += "\tiny"
cycleCount += 2
# Finish the row
if row<self.height-1:
rowStartCode, extraCycles = self.rowStartCalculatorCode()
spriteChunks[chunkIndex][row] += "\tinx\n" + rowStartCode;
cycleCount += 2 + extraCycles
else: else:
optimizationCount += 1 spriteChunks[chunkIndex][row] += "\tRESTORE_AXY\n"
spriteChunks[chunkIndex][row] += "\trts\t;Cycle count: %d, Optimized %d rows." % (cycleCount,optimizationCount) + "\n"
# Increment indices return spriteChunks
if chunkIndex == len(byteSplits)-1:
spriteChunks[chunkIndex][row] += "\n"
else:
spriteChunks[chunkIndex][row] += "\tiny"
cycleCount += 2
# Finish the row def rowStartCalculatorCode(self):
if row<height-1: return \
rowStartCode,extraCycles = rowStartCalculatorCode() "\tlda HGRROWS_H1,x\n" + \
spriteChunks[chunkIndex][row] += "\tinx\n" + rowStartCode; "\tsta SCRATCH1\n" + \
cycleCount += 2 + extraCycles "\tlda HGRROWS_L,x\n" + \
else: "\tsta SCRATCH0\n" + \
spriteChunks[chunkIndex][row] += "\tRESTORE_AXY\n" "\tldy PARAM0\n" + \
spriteChunks[chunkIndex][row] += "\trts\t;Cycle count: %d, Optimized %d rows." % (cycleCount,optimizationCount) + "\n" "\tlda DIV7_2,y\n" + \
"\ttay\n", 4 + 3 + 4 + 3 + 3 + 4 + 2;
return spriteChunks
def rowStartCalculatorCode():
return \
"\tlda HGRROWS_H1,x\n" + \
"\tsta SCRATCH1\n" + \
"\tlda HGRROWS_L,x\n" + \
"\tsta SCRATCH0\n" + \
"\tldy PARAM0\n" + \
"\tlda DIV7_2,y\n" + \
"\ttay\n", 4 + 3 + 4 + 3 + 3 + 4 + 2;
def fillOutByte(numBits): def fillOutByte(numBits):
@ -235,7 +287,6 @@ def shiftStringRight(string,shift):
def bitsForColor(pixel): def bitsForColor(pixel):
if pixel == Colors.black: if pixel == Colors.black:
return "00" return "00"
else: else:
@ -250,7 +301,6 @@ def bitsForColor(pixel):
def bitsForMask(pixel): def bitsForMask(pixel):
if pixel == Colors.black: if pixel == Colors.black:
return "00" return "00"
@ -258,7 +308,6 @@ def bitsForMask(pixel):
def highBitForColor(pixel): def highBitForColor(pixel):
# Note that we prefer high-bit white because blue fringe is less noticeable than magenta. # Note that we prefer high-bit white because blue fringe is less noticeable than magenta.
highBit = "0" highBit = "0"
if pixel == Colors.orange or pixel == Colors.blue or pixel == Colors.white: if pixel == Colors.orange or pixel == Colors.blue or pixel == Colors.white:
@ -268,7 +317,6 @@ def highBitForColor(pixel):
def highBitForMask(pixel): def highBitForMask(pixel):
return "1" return "1"
@ -298,26 +346,35 @@ def pixelColor(pixelData,row,col):
return color return color
def printHorizontalLookup(): class HorizontalLookup(Listing):
disclaimer() def __init__(self):
Listing.__init__(self)
self.generate_tables()
print "DIV7_2:" def generate_tables(self):
for pixel in range(140): self.out("DIV7_2:")
print "\t.byte $%02x" % ((pixel / 7)*2) for pixel in range(140):
self.out("\t.byte $%02x" % ((pixel / 7)*2))
print "\n\nMOD7_2:" self.out("\n\nMOD7_2:")
for pixel in range(140): for pixel in range(140):
print "\t.byte $%02x" % ((pixel % 7)*2) self.out("\t.byte $%02x" % ((pixel % 7)*2))
def disclaimer():
print '''
; This file was generated by HiSprite.py, a sprite compiler by Quinn Dunki.
; If you feel the need to modify this file, you are probably doing it wrong.
'''
return
if __name__ == "__main__": if __name__ == "__main__":
main(sys.argv[1:]) parser = argparse.ArgumentParser(description="Sprite compiler for 65C02/6502 to generate assembly code to render all shifts of the given sprite, optionally with exclusive-or drawing (if background will be non-black). Generated code has conditional compilation directives for the CC65 assembler to allow the same file to be compiled for either architecture.")
parser.add_argument("-v", "--verbose", default=0, action="count")
parser.add_argument("-t", "--tables", action="store_true", default=False, help="output only lookup tables for horizontal sprite shifts (division and modulus 7)")
parser.add_argument("-x", "--xdraw", action="store_true", default=False, help="use XOR for sprite drawing")
parser.add_argument("files", metavar="IMAGE", nargs="*", help="a PNG image [or a list of them]. PNG files must not have an alpha channel!")
options, extra_args = parser.parse_known_args()
if options.tables:
print HorizontalLookup()
exit(0)
for pngfile in options.files:
try:
print Sprite(pngfile, options.xdraw)
except RuntimeError:
parser.usage()