mirror of
https://github.com/robmcmullen/asmgen.git
synced 2025-01-09 06:32:41 +00:00
Initial working object-based generator
This commit is contained in:
parent
8a1de40e0e
commit
d02300573d
245
HiSprite.py
245
HiSprite.py
@ -1,116 +1,171 @@
|
||||
#!/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):
|
||||
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.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
self.lines = [self.disclaimer]
|
||||
|
||||
def __str__(self):
|
||||
return "\n".join(self.lines) + "\n"
|
||||
|
||||
def out(self, line):
|
||||
self.lines.append(line)
|
||||
|
||||
|
||||
class Sprite(Listing):
|
||||
def __init__(self, pngfile, xdraw=False):
|
||||
Listing.__init__(self)
|
||||
|
||||
reader = png.Reader(pngfile)
|
||||
try:
|
||||
pngdata = reader.asRGB8()
|
||||
except:
|
||||
usage()
|
||||
raise RuntimeError
|
||||
|
||||
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()
|
||||
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)
|
||||
|
||||
disclaimer()
|
||||
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
|
||||
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)
|
||||
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
|
||||
print "%s_JMP:" % (niceName)
|
||||
for shift in range(0,7):
|
||||
print "\t.addr %s_SHIFT%d" % (niceName,shift)
|
||||
self.out("%s_JMP:" % (self.niceName) )
|
||||
for shift in range(self.numShifts):
|
||||
self.out("\t.addr %s_SHIFT%d" % (self.niceName, shift))
|
||||
|
||||
print ".else"
|
||||
self.out(".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"
|
||||
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
|
||||
print "%s_JMP:" % (niceName)
|
||||
for shift in range(0,7):
|
||||
print "\t.addr %s_SHIFT%d-1" % (niceName,shift)
|
||||
print ".endif"
|
||||
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
|
||||
print "\n"
|
||||
for shift in range(0,7):
|
||||
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])
|
||||
|
||||
print "\n"
|
||||
def layoutSpriteChunk(self, shift, cycleCount):
|
||||
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
def byteStreamsFromPixels(pixelData,width,height,shift,bitDelegate,highBitDelegate):
|
||||
|
||||
byteStreams = ["" for x in range(height)]
|
||||
byteWidth = width/2+1+1
|
||||
|
||||
for row in range(height):
|
||||
for row in range(self.height):
|
||||
bitStream = ""
|
||||
|
||||
# Compute raw bitstream for row from PNG pixels
|
||||
for pixelIndex in range(width):
|
||||
pixel = pixelColor(pixelData,row,pixelIndex)
|
||||
for pixelIndex in range(self.width):
|
||||
pixel = pixelColor(self.pixelData,row,pixelIndex)
|
||||
bitStream += bitDelegate(pixel)
|
||||
|
||||
# Shift bit stream as needed
|
||||
@ -138,7 +193,7 @@ def byteStreamsFromPixels(pixelData,width,height,shift,bitDelegate,highBitDelega
|
||||
bitChunk = bitChunk[::-1]
|
||||
|
||||
# Determine palette bit from first pixel on each row
|
||||
highBit = highBitDelegate(pixelData[row][0])
|
||||
highBit = highBitDelegate(self.pixelData[row][0])
|
||||
|
||||
byteSplits[byteIndex] = highBit + bitChunk
|
||||
bitPos += 7
|
||||
@ -147,16 +202,14 @@ def byteStreamsFromPixels(pixelData,width,height,shift,bitDelegate,highBitDelega
|
||||
|
||||
return byteStreams
|
||||
|
||||
|
||||
def generateBlitter(colorStreams,maskStreams,height,xdraw,baseCycleCount):
|
||||
|
||||
def generateBlitter(self, colorStreams, maskStreams, baseCycleCount):
|
||||
byteWidth = len(colorStreams[0])
|
||||
spriteChunks = [["" for y in range(height)] for x in range(byteWidth)]
|
||||
spriteChunks = [["" for y in range(self.height)] for x in range(byteWidth)]
|
||||
|
||||
cycleCount = baseCycleCount
|
||||
optimizationCount = 0
|
||||
|
||||
for row in range(height):
|
||||
for row in range(self.height):
|
||||
|
||||
byteSplits = colorStreams[row]
|
||||
|
||||
@ -168,7 +221,7 @@ def generateBlitter(colorStreams,maskStreams,height,xdraw,baseCycleCount):
|
||||
byteSplits[chunkIndex] != "10000000":
|
||||
|
||||
# Store byte into video memory
|
||||
if xdraw:
|
||||
if self.xdraw:
|
||||
spriteChunks[chunkIndex][row] = \
|
||||
"\tlda (SCRATCH0),y\n" + \
|
||||
"\teor #%%%s\n" % byteSplits[chunkIndex] + \
|
||||
@ -190,8 +243,8 @@ def generateBlitter(colorStreams,maskStreams,height,xdraw,baseCycleCount):
|
||||
cycleCount += 2
|
||||
|
||||
# Finish the row
|
||||
if row<height-1:
|
||||
rowStartCode,extraCycles = rowStartCalculatorCode()
|
||||
if row<self.height-1:
|
||||
rowStartCode, extraCycles = self.rowStartCalculatorCode()
|
||||
spriteChunks[chunkIndex][row] += "\tinx\n" + rowStartCode;
|
||||
cycleCount += 2 + extraCycles
|
||||
else:
|
||||
@ -200,8 +253,7 @@ def generateBlitter(colorStreams,maskStreams,height,xdraw,baseCycleCount):
|
||||
|
||||
return spriteChunks
|
||||
|
||||
|
||||
def rowStartCalculatorCode():
|
||||
def rowStartCalculatorCode(self):
|
||||
return \
|
||||
"\tlda HGRROWS_H1,x\n" + \
|
||||
"\tsta SCRATCH1\n" + \
|
||||
@ -235,7 +287,6 @@ def shiftStringRight(string,shift):
|
||||
|
||||
|
||||
def bitsForColor(pixel):
|
||||
|
||||
if pixel == Colors.black:
|
||||
return "00"
|
||||
else:
|
||||
@ -250,7 +301,6 @@ def bitsForColor(pixel):
|
||||
|
||||
|
||||
def bitsForMask(pixel):
|
||||
|
||||
if pixel == Colors.black:
|
||||
return "00"
|
||||
|
||||
@ -258,7 +308,6 @@ def bitsForMask(pixel):
|
||||
|
||||
|
||||
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:
|
||||
@ -268,7 +317,6 @@ def highBitForColor(pixel):
|
||||
|
||||
|
||||
def highBitForMask(pixel):
|
||||
|
||||
return "1"
|
||||
|
||||
|
||||
@ -298,26 +346,35 @@ def pixelColor(pixelData,row,col):
|
||||
return color
|
||||
|
||||
|
||||
def printHorizontalLookup():
|
||||
disclaimer()
|
||||
class HorizontalLookup(Listing):
|
||||
def __init__(self):
|
||||
Listing.__init__(self)
|
||||
self.generate_tables()
|
||||
|
||||
print "DIV7_2:"
|
||||
def generate_tables(self):
|
||||
self.out("DIV7_2:")
|
||||
for pixel in range(140):
|
||||
print "\t.byte $%02x" % ((pixel / 7)*2)
|
||||
self.out("\t.byte $%02x" % ((pixel / 7)*2))
|
||||
|
||||
print "\n\nMOD7_2:"
|
||||
self.out("\n\nMOD7_2:")
|
||||
for pixel in range(140):
|
||||
print "\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
|
||||
self.out("\t.byte $%02x" % ((pixel % 7)*2))
|
||||
|
||||
|
||||
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()
|
||||
|
Loading…
Reference in New Issue
Block a user