2016-07-21 03:39:11 +00:00
#!/usr/bin/python
import sys , os , png
2017-05-30 21:06:33 +00:00
import argparse
2016-07-21 03:39:11 +00:00
class Colors :
2017-06-20 13:37:57 +00:00
black , magenta , green , orange , blue , white , key = range ( 7 )
2016-09-10 18:04:57 +00:00
2016-07-21 03:39:11 +00:00
def main ( argv ) :
2017-06-20 13:37:57 +00:00
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 ( )
2017-05-30 21:06:33 +00:00
2017-06-20 13:37:57 +00:00
if options . tables :
printHorizontalLookup ( )
exit ( 0 )
2016-07-21 03:39:11 +00:00
2017-06-20 13:37:57 +00:00
for pngfile in options . files :
process ( pngfile , options . xdraw )
2016-07-21 03:39:11 +00:00
2017-05-30 21:06:33 +00:00
def process ( pngfile , xdraw = False ) :
2017-06-20 13:37:57 +00:00
reader = png . Reader ( pngfile )
try :
pngdata = reader . asRGB8 ( )
except :
usage ( )
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 " \t SAVE_AXY "
print " \t ldy PARAM0 "
print " \t ldx MOD7_2,y "
print " .ifpC02 "
print " \t jmp ( %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 "
# Fast jump table routine; faster and smaller than self-modifying code
print " \t lda %s _JMP+1,x " % ( niceName )
print " \t pha "
print " \t lda %s _JMP,x " % ( niceName )
print " \t pha "
print " \t rts \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 "
# Blitting functions
print " \n "
for shift in range ( 0 , 7 ) :
# 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 " \t ldx PARAM1 "
cycleCount + = 3
rowStartCode , extraCycles = rowStartCalculatorCode ( ) ;
print rowStartCode
cycleCount + = extraCycles
spriteChunks = layoutSpriteChunk ( pixelData , width , height , shift , xdraw , cycleCount )
for row in range ( height ) :
for chunkIndex in range ( len ( spriteChunks ) ) :
print spriteChunks [ chunkIndex ] [ row ]
print " \n "
2016-09-10 18:04:57 +00:00
2017-05-29 20:14:37 +00:00
def layoutSpriteChunk ( pixelData , width , height , shift , xdraw , cycleCount ) :
2016-12-25 20:43:14 +00:00
2017-06-20 13:37:57 +00:00
colorStreams = byteStreamsFromPixels ( pixelData , width , height , shift , bitsForColor , highBitForColor )
maskStreams = byteStreamsFromPixels ( pixelData , width , height , shift , bitsForMask , highBitForMask )
code = generateBlitter ( colorStreams , maskStreams , height , xdraw , cycleCount )
2016-12-25 20:43:14 +00:00
2017-06-20 13:37:57 +00:00
return code
2016-12-25 20:43:14 +00:00
def byteStreamsFromPixels ( pixelData , width , height , shift , bitDelegate , highBitDelegate ) :
2017-06-20 13:37:57 +00:00
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
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 ( pixelData [ row ] [ 0 ] )
byteSplits [ byteIndex ] = highBit + bitChunk
bitPos + = 7
byteStreams [ row ] = byteSplits ;
return byteStreams
2016-12-25 20:43:14 +00:00
2017-05-29 20:14:37 +00:00
def generateBlitter ( colorStreams , maskStreams , height , xdraw , baseCycleCount ) :
2017-06-20 13:37:57 +00:00
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 ] = \
" \t lda (SCRATCH0),y \n " + \
" \t eor # %% %s \n " % byteSplits [ chunkIndex ] + \
" \t sta (SCRATCH0),y \n " ;
cycleCount + = 5 + 2 + 6
else :
spriteChunks [ chunkIndex ] [ row ] = \
" \t lda # %% %s \n " % byteSplits [ chunkIndex ] + \
" \t sta (SCRATCH0),y \n " ;
cycleCount + = 2 + 6
else :
optimizationCount + = 1
# Increment indices
if chunkIndex == len ( byteSplits ) - 1 :
spriteChunks [ chunkIndex ] [ row ] + = " \n "
else :
spriteChunks [ chunkIndex ] [ row ] + = " \t iny "
cycleCount + = 2
# Finish the row
if row < height - 1 :
rowStartCode , extraCycles = rowStartCalculatorCode ( )
spriteChunks [ chunkIndex ] [ row ] + = " \t inx \n " + rowStartCode ;
cycleCount + = 2 + extraCycles
else :
spriteChunks [ chunkIndex ] [ row ] + = " \t RESTORE_AXY \n "
spriteChunks [ chunkIndex ] [ row ] + = " \t rts \t ;Cycle count: %d , Optimized %d rows. " % ( cycleCount , optimizationCount ) + " \n "
return spriteChunks
2016-08-18 19:12:11 +00:00
2016-09-10 18:04:57 +00:00
def rowStartCalculatorCode ( ) :
2017-06-20 13:37:57 +00:00
return \
" \t lda HGRROWS_H1,x \n " + \
" \t sta SCRATCH1 \n " + \
" \t lda HGRROWS_L,x \n " + \
" \t sta SCRATCH0 \n " + \
" \t ldy PARAM0 \n " + \
" \t lda DIV7_2,y \n " + \
" \t tay \n " , 4 + 3 + 4 + 3 + 3 + 4 + 2 ;
2016-12-25 20:43:14 +00:00
2016-09-10 18:04:57 +00:00
def fillOutByte ( numBits ) :
2017-06-20 13:37:57 +00:00
filler = " "
for bit in range ( numBits ) :
filler + = " 0 "
return filler
2016-08-18 19:12:11 +00:00
2016-09-10 18:04:57 +00:00
def shiftStringRight ( string , shift ) :
2017-06-20 13:37:57 +00:00
if shift == 0 :
return string
shift * = 2
result = " "
for i in range ( shift ) :
result + = " 0 "
result + = string
return result
2016-12-25 20:43:14 +00:00
def bitsForColor ( pixel ) :
2017-06-20 13:37:57 +00:00
if pixel == Colors . black :
return " 00 "
else :
if pixel == Colors . white :
return " 11 "
else :
if pixel == Colors . green or pixel == Colors . orange :
return " 01 "
2016-12-25 20:43:14 +00:00
2017-06-20 13:37:57 +00:00
# blue or magenta
return " 10 "
2016-12-25 20:43:14 +00:00
def bitsForMask ( pixel ) :
2017-06-20 13:37:57 +00:00
if pixel == Colors . black :
return " 00 "
2016-12-25 20:43:14 +00:00
2017-06-20 13:37:57 +00:00
return " 11 "
2016-12-25 20:43:14 +00:00
def highBitForColor ( pixel ) :
2017-06-20 13:37:57 +00:00
# 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 "
2016-12-25 20:43:14 +00:00
2017-06-20 13:37:57 +00:00
return highBit
2016-12-25 20:43:14 +00:00
def highBitForMask ( pixel ) :
2017-06-20 13:37:57 +00:00
return " 1 "
2016-12-25 20:43:14 +00:00
def pixelColor ( pixelData , row , col ) :
2017-06-20 13:37:57 +00:00
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
else :
if r == 255 and g > 0 and b == 0 :
color = Colors . orange
else :
if r == 255 and g == 255 and b == 255 :
color = Colors . white
else :
if r == g and r == b and r != 0 and r != 255 : # Any gray is chroma key
color = Colors . key
return color
2016-07-21 03:39:11 +00:00
2016-12-25 20:43:14 +00:00
def printHorizontalLookup ( ) :
2017-06-20 13:37:57 +00:00
disclaimer ( )
print " DIV7_2: "
for pixel in range ( 140 ) :
print " \t .byte $ %02x " % ( ( pixel / 7 ) * 2 )
2016-08-18 19:12:11 +00:00
2017-06-20 13:37:57 +00:00
print " \n \n MOD7_2: "
for pixel in range ( 140 ) :
print " \t .byte $ %02x " % ( ( pixel % 7 ) * 2 )
2017-05-30 21:06:33 +00:00
2016-07-21 03:39:11 +00:00
2016-09-10 18:04:57 +00:00
def disclaimer ( ) :
2017-06-20 13:37:57 +00:00
print '''
2016-12-21 22:03:28 +00:00
; This file was generated by HiSprite . py , a sprite compiler by Quinn Dunki .
2016-09-10 18:04:57 +00:00
; If you feel the need to modify this file , you are probably doing it wrong .
'''
2017-06-20 13:37:57 +00:00
return
2016-07-21 03:39:11 +00:00
if __name__ == " __main__ " :
2017-06-20 13:37:57 +00:00
main ( sys . argv [ 1 : ] )