From d02300573dd86fdc16e99dd521bbb5ed4fb1a794 Mon Sep 17 00:00:00 2001 From: Rob McMullen Date: Tue, 20 Jun 2017 15:59:56 -0700 Subject: [PATCH] Initial working object-based generator --- HiSprite.py | 465 +++++++++++++++++++++++++++++----------------------- 1 file changed, 261 insertions(+), 204 deletions(-) diff --git a/HiSprite.py b/HiSprite.py index b7b091d..080f99e 100755 --- a/HiSprite.py +++ b/HiSprite.py @@ -1,215 +1,267 @@ #!/usr/bin/python -import sys,os,png +# system packages +import sys +import os import argparse +import re + +# external packages +import png # package name is "pypng" on pypi.python.org + class Colors: 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): - 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() + >>> print slugify("[Some] _ Article's Title--") + SOME_ARTICLES_TITLE - if options.tables: - printHorizontalLookup() - exit(0) + From https://gist.github.com/dolph/3622892#file-slugify-py + """ - for pngfile in options.files: - process(pngfile, options.xdraw) + # "[Some] _ Article's Title--" + # "[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): - reader = png.Reader(pngfile) - try: - pngdata = reader.asRGB8() - except: - usage() +class Listing(object): + disclaimer = ''' +; 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. +''' - width = pngdata[0] - height = pngdata[1] - 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() - - # Prologue - print "%s: ;%d bytes per row" % (niceName,byteWidth) - 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) + def __init__(self): + self.lines = [self.disclaimer] - print ".else" - # Fast jump table routine; faster and smaller than self-modifying code - print "\tlda %s_JMP+1,x" % (niceName) - print "\tpha" - print "\tlda %s_JMP,x" % (niceName) - print "\tpha" - print "\trts\n" + def __str__(self): + return "\n".join(self.lines) + "\n" - # Bit-shift jump table for generic 6502 - print "%s_JMP:" % (niceName) - for shift in range(0,7): - print "\t.addr %s_SHIFT%d-1" % (niceName,shift) - print ".endif" + def out(self, line): + self.lines.append(line) - # Blitting functions - print "\n" - for shift in range(0,7): + +class Sprite(Listing): + def __init__(self, pngfile, xdraw=False): + Listing.__init__(self) + + reader = png.Reader(pngfile) + try: + pngdata = reader.asRGB8() + except: + raise RuntimeError + + self.xdraw = xdraw + self.niceName = slugify(os.path.splitext(pngfile)[0]) + 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: # SAVE_AXY + RESTORE_AXY + rts + sprite jump table cycleCount = 9 + 12 + 6 + 3 + 4 + 6 - print "%s_SHIFT%d:" % (niceName,shift) - print "\tldx PARAM1" + self.out("%s_SHIFT%d:" % (self.niceName,shift)) + self.out("\tldx PARAM1") cycleCount += 3 - rowStartCode,extraCycles = rowStartCalculatorCode(); - print rowStartCode + rowStartCode,extraCycles = self.rowStartCalculatorCode(); + self.out(rowStartCode) 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)): - print spriteChunks[chunkIndex][row] + self.out(spriteChunks[chunkIndex][row]) + + 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) + + return code + + def byteStreamsFromPixels(self, shift, bitDelegate, highBitDelegate): + byteStreams = ["" for x in range(self.height)] + byteWidth = self.width/2+1+1 + + for row in range(self.height): + bitStream = "" - print "\n" - - -def layoutSpriteChunk(pixelData,width,height,shift,xdraw,cycleCount): - - colorStreams = byteStreamsFromPixels(pixelData,width,height,shift,bitsForColor,highBitForColor) - maskStreams = byteStreamsFromPixels(pixelData,width,height,shift,bitsForMask,highBitForMask) - code = generateBlitter(colorStreams,maskStreams,height,xdraw,cycleCount) - - return code - - -def byteStreamsFromPixels(pixelData,width,height,shift,bitDelegate,highBitDelegate): - - byteStreams = ["" for x in range(height)] - byteWidth = width/2+1+1 - - for row in range(height): - bitStream = "" - - # Compute raw bitstream for row from PNG pixels - for pixelIndex in range(width): - pixel = pixelColor(pixelData,row,pixelIndex) - bitStream += bitDelegate(pixel) - - # Shift bit stream as needed - bitStream = shiftStringRight(bitStream,shift) - 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 + # Compute raw bitstream for row from PNG pixels + for pixelIndex in range(self.width): + pixel = pixelColor(self.pixelData,row,pixelIndex) + bitStream += bitDelegate(pixel) + + # Shift bit stream as needed + bitStream = shiftStringRight(bitStream,shift) + 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 = "" - bitChunk = "" - - if remainingBits < 0: - bitChunk = "0000000" - else: - if remainingBits < 7: - bitChunk = bitStream[bitPos:] - bitChunk += fillOutByte(7-remainingBits) + if remainingBits < 0: + bitChunk = "0000000" else: - bitChunk = bitStream[bitPos:bitPos+7] - - bitChunk = bitChunk[::-1] - - # Determine palette bit from first pixel on each row - highBit = highBitDelegate(pixelData[row][0]) - - byteSplits[byteIndex] = highBit + bitChunk - bitPos += 7 - - byteStreams[row] = byteSplits; - - return byteStreams - - -def generateBlitter(colorStreams,maskStreams,height,xdraw,baseCycleCount): - - byteWidth = len(colorStreams[0]) - spriteChunks = [["" for y in range(height)] for x in range(byteWidth)] - - cycleCount = baseCycleCount - optimizationCount = 0 - - for row in range(height): - - byteSplits = colorStreams[row] - - # Generate blitting code - for chunkIndex in range(len(byteSplits)): - - # Optimization - if byteSplits[chunkIndex] != "00000000" and \ - byteSplits[chunkIndex] != "10000000": - - # Store byte into video memory - if xdraw: - spriteChunks[chunkIndex][row] = \ - "\tlda (SCRATCH0),y\n" + \ - "\teor #%%%s\n" % byteSplits[chunkIndex] + \ - "\tsta (SCRATCH0),y\n"; - cycleCount += 5 + 2 + 6 - else: - spriteChunks[chunkIndex][row] = \ - "\tlda #%%%s\n" % byteSplits[chunkIndex] + \ - "\tsta (SCRATCH0),y\n"; - cycleCount += 2 + 6 - else: - optimizationCount += 1 - - # Increment indices - if chunkIndex == len(byteSplits)-1: - spriteChunks[chunkIndex][row] += "\n" - else: - spriteChunks[chunkIndex][row] += "\tiny" - cycleCount += 2 - - # Finish the row - if row