mirror of
https://github.com/robmcmullen/asmgen.git
synced 2025-02-10 12:30:56 +00:00
Added support for BW & Color HGR sprites & framework for supporting other screen types
This commit is contained in:
parent
1be718c206
commit
8b5a4afeab
401
HiSprite.py
401
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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user