From 8b5a4afeab3f6286c0238bf295c5cf43c349cc4b Mon Sep 17 00:00:00 2001 From: Rob McMullen Date: Wed, 21 Jun 2017 14:03:53 -0700 Subject: [PATCH] Added support for BW & Color HGR sprites & framework for supporting other screen types --- HiSprite.py | 401 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 240 insertions(+), 161 deletions(-) diff --git a/HiSprite.py b/HiSprite.py index bd60625..9c6c35f 100755 --- a/HiSprite.py +++ b/HiSprite.py @@ -10,10 +10,6 @@ import re 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. @@ -56,7 +52,7 @@ def slugify(s): return s -class Syntax(object): +class AssemblerSyntax(object): def asm(self, text): return "\t%s" % text @@ -87,7 +83,7 @@ class Syntax(object): return "#%s" % format(value, "08b") -class Mac65(Syntax): +class Mac65(AssemblerSyntax): def address(self, text): return self.asm(".word %s" % text) @@ -101,7 +97,7 @@ class Mac65(Syntax): return "#$%02x ; %s" % (value, format(value, "08b")) -class CC65(Syntax): +class CC65(AssemblerSyntax): def label(self, text): return "%s:" % text @@ -136,6 +132,9 @@ class Listing(object): def comment(self, text): self.out_append_last(self.assembler.comment(text)) + def comment_line(self, text): + self.out(self.assembler.comment(text)) + def asm(self, text): self.out(self.assembler.asm(text)) @@ -173,8 +172,9 @@ class Listing(object): class Sprite(Listing): - def __init__(self, pngfile, assembler, xdraw=False, processor="any"): + def __init__(self, pngfile, assembler, screen, xdraw=False, processor="any"): Listing.__init__(self, assembler) + self.screen = screen reader = png.Reader(pngfile) try: @@ -188,22 +188,17 @@ class Sprite(Listing): self.width = pngdata[0] self.height = pngdata[1] self.pixelData = list(pngdata[2]) - self.calcStorage() self.jumpTable() - for i in range(self.numShifts): + for i in range(self.screen.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.label("%s" % self.niceName) - self.comment("%d bytes per row" % self.byteWidth) + self.comment("%d bytes per row" % self.screen.byteWidth(self.width)) self.asm("SAVE_AXY") self.asm("ldy PARAM0") - self.asm("ldx MOD7_2,y") + self.asm("ldx MOD%d_%d,y" % (self.screen.numShifts, self.screen.bitsPerPixel)) if self.processor == "any": self.out(".ifpC02") @@ -224,7 +219,7 @@ class Sprite(Listing): # Bit-shift jump table for 65C02 self.label("%s_JMP" % (self.niceName)) - for shift in range(self.numShifts): + for shift in range(self.screen.numShifts): self.addr("%s_SHIFT%d" % (self.niceName, shift)) def jump6502(self): @@ -237,7 +232,7 @@ class Sprite(Listing): # Bit-shift jump table for generic 6502 self.label("%s_JMP" % (self.niceName)) - for shift in range(self.numShifts): + for shift in range(self.screen.numShifts): self.addr("%s_SHIFT%d-1" % (self.niceName,shift)) def blitShift(self, shift): @@ -249,74 +244,25 @@ class Sprite(Listing): cycleCount = 9 + 12 + 6 + 3 + 4 + 6 self.label("%s_SHIFT%d" % (self.niceName,shift)) + + colorStreams = self.screen.byteStreamsFromPixels(shift, self) + for c in colorStreams: + self.comment_line(str(c)) + self.out("") + maskStreams = self.screen.byteStreamsFromPixels(shift, self, True) + self.asm("ldx PARAM1") cycleCount += 3 rowStartCode,extraCycles = self.rowStartCalculatorCode(); self.out(rowStartCode) cycleCount += extraCycles - spriteChunks = self.layoutSpriteChunk(shift, cycleCount) + spriteChunks = self.generateBlitter(colorStreams, maskStreams, cycleCount) for row in range(self.height): for chunkIndex in range(len(spriteChunks)): 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 = "" - - # 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 = "" - - if remainingBits < 0: - bitChunk = "0000000" - else: - if remainingBits < 7: - bitChunk = bitStream[bitPos:] - bitChunk += fillOutByte(7-remainingBits) - else: - bitChunk = bitStream[bitPos:bitPos+7] - - bitChunk = bitChunk[::-1] - - # Determine palette bit from first pixel on each row - highBit = highBitDelegate(self.pixelData[row][0]) - - byteSplits[byteIndex] = highBit + bitChunk - bitPos += 7 - - byteStreams[row] = byteSplits; - - return byteStreams - def generateBlitter(self, colorStreams, maskStreams, baseCycleCount): byteWidth = len(colorStreams[0]) spriteChunks = [["" for y in range(self.height)] for x in range(byteWidth)] @@ -377,7 +323,7 @@ class Sprite(Listing): "\tlda HGRROWS_L,x\n" + \ "\tsta SCRATCH0\n" + \ "\tldy PARAM0\n" + \ - "\tlda DIV7_2,y\n" + \ + "\tlda DIV%d_%d,y\n" % (self.screen.numShifts, self.screen.bitsPerPixel) + \ "\ttay\n", 4 + 3 + 4 + 3 + 3 + 4 + 2; @@ -389,11 +335,11 @@ def fillOutByte(numBits): return filler -def shiftStringRight(string,shift): +def shiftStringRight(string, shift, bitsPerPixel): if shift==0: return string - shift *=2 + shift *= bitsPerPixel result = "" for i in range(shift): @@ -403,108 +349,230 @@ def shiftStringRight(string,shift): return result -def bitsForColor(pixel): - if pixel == Colors.black: - return "00" - else: - if pixel == Colors.white: - return "11" + +class ScreenFormat(object): + numShifts = 8 + + bitsPerPixel = 1 + + screenWidth = 320 + + screenHeight = 192 + + def __init__(self): + self.offsets = self.generate_row_offsets() + self.numX = self.screenWidth / self.bitsPerPixel + + def byteWidth(self, png_width): + return (png_width * self.bitsPerPixel + self.numShifts - 1) // self.numShifts + 1 + + def bitsForColor(self, pixel): + raise NotImplementedError + + def bitsForMask(self, pixel): + raise NotImplementedError + + def pixelColor(self, pixelData, row, col): + raise NotImplementedError + + def generate_row_offsets(self): + offsets = [40 * y for y in range(self.screenHeight)] + return offsets + + def generate_row_addresses(self, baseAddr): + addrs = [baseAddr + offset for offset in self.offsets] + return addrs + + +class HGR(ScreenFormat): + numShifts = 7 + + bitsPerPixel = 2 + + screenWidth = 280 + + black,magenta,green,orange,blue,white,key = range(7) + + def bitsForColor(self, pixel): + if pixel == self.black: + return "00" else: - if pixel == Colors.green or pixel == Colors.orange: - return "01" - - # blue or magenta - return "10" - - -def bitsForMask(pixel): - if pixel == Colors.black: - return "00" - - return "11" - - -def highBitForColor(pixel): - # Note that we prefer high-bit white because blue fringe is less noticeable than magenta. - highBit = "0" - if pixel == Colors.orange or pixel == Colors.blue or pixel == Colors.white: - highBit = "1" - - return highBit - - -def highBitForMask(pixel): - return "1" - - -def pixelColor(pixelData,row,col): - r = pixelData[row][col*3] - g = pixelData[row][col*3+1] - b = pixelData[row][col*3+2] - color = Colors.black - - if r==255 and g==0 and b==255: - color = Colors.magenta - else: - if r==0 and g==255 and b==0: - color = Colors.green - else: - if r==0 and g==0 and b==255: - color = Colors.blue + if pixel == self.white: + return "11" else: - if r==255 and g>0 and b==0: - color = Colors.orange + if pixel == self.green or pixel == self.orange: + return "01" + + # blue or magenta + return "10" + + def bitsForMask(self, pixel): + if pixel == self.black: + return "00" + + return "11" + + def highBitForColor(self, pixel): + # Note that we prefer high-bit white because blue fringe is less noticeable than magenta. + highBit = "0" + if pixel == self.orange or pixel == self.blue or pixel == self.white: + highBit = "1" + + return highBit + + def highBitForMask(self, pixel): + return "1" + + def pixelColor(self, pixelData, row, col): + r = pixelData[row][col*3] + g = pixelData[row][col*3+1] + b = pixelData[row][col*3+2] + color = self.black + + if r==255 and g==0 and b==255: + color = self.magenta + else: + if r==0 and g==255 and b==0: + color = self.green + else: + if r==0 and g==0 and b==255: + color = self.blue else: - if r==255 and g==255 and b==255: - color = Colors.white + if r==255 and g>0 and b==0: + color = self.orange else: - if r==g and r==b and r!=0 and r!=255: # Any gray is chroma key - color = Colors.key - return color + if r==255 and g==255 and b==255: + color = self.white + else: + if r==g and r==b and r!=0 and r!=255: # Any gray is chroma key + color = self.key + return color + def byteStreamsFromPixels(self, shift, source, mask=False): + byteStreams = ["" for x in range(source.height)] + byteWidth = self.byteWidth(source.width) -class HorizontalLookup(Listing): - def __init__(self, assembler): - Listing.__init__(self, assembler) - self.generate_hgr() - self.generate_tables() + if mask: + bitDelegate = self.bitsForMask + highBitDelegate = self.highBitForMask + else: + bitDelegate = self.bitsForColor + highBitDelegate = self.highBitForColor - def generate_hgr(self): + for row in range(source.height): + bitStream = "" + + # Compute raw bitstream for row from PNG pixels + for pixelIndex in range(source.width): + pixel = self.pixelColor(source.pixelData,row,pixelIndex) + bitStream += bitDelegate(pixel) + + # Shift bit stream as needed + bitStream = shiftStringRight(bitStream, shift, self.bitsPerPixel) + 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: + bitChunk = bitStream[bitPos:bitPos+7] + + bitChunk = bitChunk[::-1] + + # Determine palette bit from first pixel on each row + highBit = highBitDelegate(source.pixelData[row][0]) + + byteSplits[byteIndex] = highBit + bitChunk + bitPos += 7 + + byteStreams[row] = byteSplits; + + return byteStreams + + def generate_row_offsets(self): offsets = [] - for y in range(192): + for y in range(self.screenHeight): # From Apple Graphics and Arcade Game Design a = y // 64 d = y - (64 * a) b = d // 8 c = d - 8 * b offsets.append((1024 * c) + (128 * b) + (40 * a)) + return offsets + +class HGRBW(HGR): + bitsPerPixel = 1 + + def bitsForColor(self, pixel): + if pixel == self.white: + return "1" + else: + return "0" + + def bitsForMask(self, pixel): + if pixel == self.key: + return "0" + return "1" + + def pixelColor(self, pixelData, row, col): + r = pixelData[row][col*3] + g = pixelData[row][col*3+1] + b = pixelData[row][col*3+2] + color = self.black + + if r==255 and g==255 and b==255: + color = self.white + elif r==g and r==b and r!=0 and r!=255: # Any gray is chroma key + color = self.key + else: + color = self.black + return color + + +class HorizontalLookup(Listing): + def __init__(self, assembler, screen): + Listing.__init__(self, assembler) + self.generate_y(screen) + self.generate_x(screen) + + def generate_y(self, screen): self.label("HGRROWS_H1") - for y in range(192): - addr = 0x2000 + offsets[y] + for addr in screen.generate_row_addresses(0x2000): self.byte("$%02x" % (addr // 256), 8) self.out("\n") self.label("HGRROWS_H2") - for y in range(192): - addr = 0x4000 + offsets[y] + for addr in screen.generate_row_addresses(0x4000): self.byte("$%02x" % (addr // 256), 8) self.out("\n") self.label("HGRROWS_L") - for y in range(192): - addr = offsets[y] + for addr in screen.generate_row_addresses(0x2000): self.byte("$%02x" % (addr & 0xff), 8) - def generate_tables(self): - self.label("DIV7_2") - for pixel in range(140): - self.byte("$%02x" % ((pixel / 7)*2), 7) + def generate_x(self, screen): + self.out("\n") + self.label("DIV%d_%d" % (screen.numShifts, screen.bitsPerPixel)) + for pixel in range(screen.numX): + self.byte("$%02x" % ((pixel / screen.numShifts) * screen.bitsPerPixel), screen.numShifts) self.out("\n") - self.label("MOD7_2") - for pixel in range(140): - self.byte("$%02x" % ((pixel % 7)*2), 7) + self.label("MOD%d_%d" % (screen.numShifts, screen.bitsPerPixel)) + for pixel in range(screen.numX): + self.byte("$%02x" % ((pixel % screen.numShifts) * screen.bitsPerPixel), screen.numShifts) if __name__ == "__main__": @@ -512,26 +580,37 @@ if __name__ == "__main__": 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("-s", "--syntax", default="cc65", nargs=1, choices=["cc65","mac65"], help="Assembler syntax (default: %(default)s)") - parser.add_argument("-p", "--processor", default="any", nargs=1, choices=["any","6502", "65C02"], help="Processor type (default: %(default)s)") + parser.add_argument("-a", "--assembler", default="cc65", choices=["cc65","mac65"], help="Assembler syntax (default: %(default)s)") + parser.add_argument("-p", "--processor", default="any", choices=["any","6502", "65C02"], help="Processor type (default: %(default)s)") + parser.add_argument("-s", "--screen", default="hgrcolor", choices=["hgrcolor","hgrbw"], help="Screen format (default: %(default)s)") 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.syntax[0].lower() == "cc65": - syntax = CC65() - elif options.syntax[0].lower() == "mac65": - syntax = Mac65() + if options.assembler.lower() == "cc65": + assembler = CC65() + elif options.assembler.lower() == "mac65": + assembler = Mac65() else: - print("Unknown assembler %s" % options.syntax) + print("Unknown assembler %s" % options.assembler) parser.print_help() + exit(1) + + if options.screen.lower() == "hgrcolor": + screen = HGR() + elif options.screen.lower() == "hgrbw": + screen = HGRBW() + else: + print("Unknown screen format %s" % options.screen) + parser.print_help() + exit(1) if options.tables: - print HorizontalLookup(syntax) + print HorizontalLookup(assembler, screen) exit(0) for pngfile in options.files: try: - print Sprite(pngfile, syntax, options.xdraw, options.processor[0]) + print Sprite(pngfile, assembler, screen, options.xdraw, options.processor) except RuntimeError, e: print e parser.print_help()