diff --git a/Makefile b/Makefile index 28b921e..03e7c06 100644 --- a/Makefile +++ b/Makefile @@ -3,36 +3,36 @@ BWSPRITE = apple-sprite9x11.png all: cpbg.dsk fonttest.dsk -rowlookup.s: quicksprite.py - python quicksprite.py -a mac65 -p 6502 -r > rowlookup.s +rowlookup.s: asmgen.py + python asmgen.py -a mac65 -p 6502 -r > rowlookup.s -collookupbw.s: quicksprite.py - python quicksprite.py -a mac65 -p 6502 -s hgrbw -c > collookupbw.s +collookupbw.s: asmgen.py + python asmgen.py -a mac65 -p 6502 -s hgrbw -c > collookupbw.s -collookupcolor.s: quicksprite.py - python quicksprite.py -a mac65 -p 6502 -c > collookupcolor.s +collookupcolor.s: asmgen.py + python asmgen.py -a mac65 -p 6502 -c > collookupcolor.s -bwsprite.s: quicksprite.py collookupbw.s rowlookup.s $(BWSPRITE) - python quicksprite.py -a mac65 -p 6502 -s hgrbw $(BWSPRITE) -n bwsprite -m -b > bwsprite.s +bwsprite.s: asmgen.py collookupbw.s rowlookup.s $(BWSPRITE) + python asmgen.py -a mac65 -p 6502 -s hgrbw $(BWSPRITE) -n bwsprite -m -b > bwsprite.s -colorsprite.s: quicksprite.py collookupcolor.s rowlookup.s $(COLORSPRITE) - python quicksprite.py -a mac65 -p 6502 -s hgrcolor $(COLORSPRITE) -n colorsprite -m > colorsprite.s +colorsprite.s: asmgen.py collookupcolor.s rowlookup.s $(COLORSPRITE) + python asmgen.py -a mac65 -p 6502 -s hgrcolor $(COLORSPRITE) -n colorsprite -m > colorsprite.s -bwtest.dsk: quicksprite.py bwtest.s bwsprite.s +bwtest.dsk: asmgen.py bwtest.s bwsprite.s atasm -obwtest.xex bwtest.s -Lbwtest.var -gbwtest.lst atrcopy bwtest.dsk boot -b bwtest.xex --brun 6000 -f -colortest.dsk: quicksprite.py colortest.s bwsprite.s +colortest.dsk: asmgen.py colortest.s bwsprite.s atasm -ocolortest.xex colortest.s -Lcolortest.var -gcolortest.lst atrcopy colortest.dsk boot -b colortest.xex --brun 6000 -f -cpbg-sprite-driver.s: quicksprite.py $(BWSPRITE) - python quicksprite.py -a mac65 -p 6502 -s hgrbw -m -k -d -g -f fatfont128.dat -o cpbg $(BWSPRITE) $(COLORSPRITE) +cpbg-sprite-driver.s: asmgen.py $(BWSPRITE) + python asmgen.py -a mac65 -p 6502 -s hgrbw -m -k -d -g -f fatfont128.dat -o cpbg $(BWSPRITE) $(COLORSPRITE) cpbg.xex: cpbg.s cpbg-sprite-driver.s atasm -mae -ocpbg.xex cpbg.s -Lcpbg.var -gcpbg.lst -cpbg.dsk: quicksprite.py cpbg.xex +cpbg.dsk: asmgen.py cpbg.xex atrcopy cpbg.dsk boot -b cpbg.xex --brun 6000 -f fonttest.dsk: fonttest.s fatfont.s diff --git a/README.rst b/README.rst index 1839b16..ced20e3 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ =========== -Quicksprite +AsmGen =========== @@ -8,8 +8,8 @@ Quicksprite Abstract ======== -Quicksprite - sprite compiler and related utilities for Apple ][ hi-res and -Atari 8-bit software sprites +AsmGen - code generator for sprites, fonts, and images for Apple ][ hi-res and +Atari 8-bit computers This program creates 6502 (or 65c02) code that draws sprites using unrolled loops for each shifted shape. By removing data shifts, image lookups, and loops @@ -34,10 +34,17 @@ Prerequisites * pypng +Credits +======= + +The sample font is modified from Michael Pohoreski's excellent tutorial on +Apple II fonts: https://github.com/Michaelangel007/apple2_hgr_font_tutorial + + Disclaimer ========== -Quicksprite, the 6502 sprite compiler sponsored by the Player/Missile Podcast +AsmGen, the 6502 code generator sponsored by the Player/Missile Podcast Copyright (c) 2017 Rob McMullen (feedback@playermissile.com) This program is free software; you can redistribute it and/or modify @@ -58,7 +65,7 @@ with this program; if not, write to the Free Software Foundation, Inc., Generated Code License ---------------------- -While the code for quicksprite itself is licensed under the GPLv3, the code it +While the code for AsmGen itself is licensed under the GPLv3, the code it produces is licensed under the the Creative Commons Attribution 4.0 International (CC BY 4.0), so you are free to use the generated code for commercial or non-commercial purposes. diff --git a/quicksprite.py b/asmgen.py similarity index 88% rename from quicksprite.py rename to asmgen.py index a5c707c..21929f3 100755 --- a/quicksprite.py +++ b/asmgen.py @@ -11,6 +11,8 @@ import png # package name is "pypng" on pypi.python.org import numpy as np + + def slugify(s): """Simplifies ugly strings into something that can be used as an assembler label. @@ -1242,16 +1244,130 @@ class FastScroll(Listing): self.out() +class RLE(Listing): + def __init__(self, assembler, data): + Listing.__init__(self, assembler) + self.raw = np.asarray(data, dtype=np.uint8) + rle = self.calc_rle() + #c1 = self.compress_pcx(rle) + #c2 = self.compress_high_bit_swap(rle) + c3 = self.compress_run_copy(rle) + + def calc_rle(self): + """ run length encoding. Partial credit to R rle function. + Multi datatype arrays catered for including non Numpy + returns: tuple (runlengths, startpositions, values) """ + ia = self.raw + n = len(ia) + if n == 0: + return ([], [], []) + else: + y = np.array(ia[1:] != ia[:-1]) # pairwise unequal (string safe) + i = np.append(np.where(y), n - 1) # must include last element posi + z = np.diff(np.append(-1, i)) # run lengths + p = np.cumsum(np.append(0, z))[:-1] # positions + return(z, p, ia[i]) + + def compress_pcx(self, rle): + run_lengths, pos, values = rle + + # Max size will be the same size as the original data. If compressed is + # greater than original, abort + compressed = np.empty(len(self.raw), dtype=np.uint8) + + compressed_size = 0 + for i in range(len(run_lengths)): + p, r, v = pos[i], run_lengths[i], values[i] + if v < 0x80: + num_tokens = 1 + else: + num_tokens = 2 + print("%d: run %d of %d (%d)" % (p, r, v, num_tokens)) + compressed_size += num_tokens + print("compressed size: %d" % compressed_size) + return compressed_size + + def compress_high_bit_swap(self, rle): + run_lengths, pos, values = rle + + # Max size will be the same size as the original data. If compressed is + # greater than original, abort + compressed = np.empty(len(self.raw), dtype=np.uint8) + + compressed_size = 0 + high_bit_set = False + for i in range(len(run_lengths)): + p, r, v = pos[i], run_lengths[i], values[i] + changed = high_bit_set + if r == 1: + if v < 0x80 and not high_bit_set: + num_tokens = 1 + elif v > 0x80 and high_bit_set: + num_tokens = 1 + elif v == 0x80: + num_tokens = 2 + else: + high_bit_set = not high_bit_set + num_tokens = 2 + else: + num_tokens = 2 + print("%d: run %d of %d (%d)%s" % (p, r, v, num_tokens, "" if changed == high_bit_set else " high bit = %s" % high_bit_set)) + compressed_size += num_tokens + print("compressed size: %d" % compressed_size) + return compressed_size + + def compress_run_copy(self, rle): + run_lengths, pos, values = rle + + # Max size will be the same size as the original data. If compressed is + # greater than original, abort + compressed = np.empty(len(self.raw), dtype=np.uint8) + + compressed_size = 0 + copy_start = -1 + for i in range(len(run_lengths)): + p, r, v = pos[i], run_lengths[i], values[i] + if r < 3: + if copy_start < 0: + copy_start = p + elif copy_start - p + r > 127: + num = p - copy_start + compressed_size += num + 1 + print("%d: copy %d (%d)" % (copy_start, num, num + 1)) + copy_start = p + else: + if copy_start >= 0: + num = p - copy_start + compressed_size += num + 1 + print("%d: copy %d (%d)" % (copy_start, num, num + 1)) + copy_start = -1 + while r > 2: + num = min(r, 128) + print("%d: run %d of %d (2)" % (p, num, v)) + p += num + r -= num + compressed_size += 2 + if r > 0: + copy_start = p + if copy_start >= 0: + num = p - copy_start + compressed_size += num + 1 + print("%d: copy %d (%d)" % (copy_start, num, num + 1)) + print("compressed size: %d" % compressed_size) + return compressed_size + + if __name__ == "__main__": disclaimer = '''; AUTOGENERATED FILE; DO NOT EDIT! ; -; This file was generated by quicksprite.py, a sprite compiler sponsored by -; the Player/Missile Podcast (based on HiSprite by Quinn Dunki). +; This file was generated by asmgen.py, a 6502 code generator sponsored by +; the Player/Missile Podcast. (The sprite compiler is based on HiSprite by +; Quinn Dunki). ; -; The code produced by quicksprite is licensed under the Creative Commons -; Attribution 4.0 International (CC BY 4.0), so you are free to use the code -; in this file for commercial or non-commercial purposes. (The code generator -; itself is licensed under the GPLv3.) +; The code produced by asmgen is licensed under the Creative Commons +; Attribution 4.0 International (CC BY 4.0), so you are free to use the code in +; this file for any purpose. (The code generator itself is licensed under the +; GPLv3.) ''' 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.") @@ -1265,8 +1381,9 @@ if __name__ == "__main__": 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("-i", "--image", default="line", choices=["line", "color","bw"], help="Screen format used for full page image conversion (default: %(default)s)") - parser.add_argument("-l", "--scroll", default=1, type=int, help="Unrolled loop to scroll screen (default: %(default)s)") + parser.add_argument("-l", "--scroll", default=0, type=int, help="Unrolled loop to scroll screen (default: %(default)s)") parser.add_argument("--merge", type=int, nargs="*", help="Merge two HGR images, switching images at the scan line") + parser.add_argument("--rle", action="store_true", default=False, help="Create run-length-encoded version of data (assumed to be an image)") parser.add_argument("-n", "--name", default="", help="Name for generated assembly function (default: based on image filename)") parser.add_argument("-k", "--clobber", action="store_true", default=False, help="don't save the registers on the stack") parser.add_argument("-d", "--double-buffer", action="store_true", default=False, help="add code blit to either page (default: page 1 only)") @@ -1310,31 +1427,37 @@ if __name__ == "__main__": sys.exit(0) for pngfile in options.files: - try: - reader = png.Reader(pngfile) - pngdata = reader.asRGB8() - except RuntimeError, e: - print "%s: %s" % (pngfile, e) - sys.exit(1) - except png.Error, e: - print "%s: %s" % (pngfile, e) - sys.exit(1) + if pngfile.lower().endswith(".png"): + try: + reader = png.Reader(pngfile) + pngdata = reader.asRGB8() + except RuntimeError, e: + print "%s: %s" % (pngfile, e) + sys.exit(1) + except png.Error, e: + print "%s: %s" % (pngfile, e) + sys.exit(1) - name = options.name if options.name else os.path.splitext(pngfile)[0] - slug = slugify(name) + name = options.name if options.name else os.path.splitext(pngfile)[0] + slug = slugify(name) - w, h = pngdata[0:2] - if w == 280 and h == 192: - # Full screen conversion! - Image(pngdata, name, options.image.lower()) + w, h = pngdata[0:2] + if w == 280 and h == 192: + # Full screen conversion! + Image(pngdata, name, options.image.lower()) + else: + sprite_code = Sprite(slug, pngdata, assembler, screen, options.xdraw, options.mask, options.backing_store, options.clobber, options.double_buffer, options.damage, options.processor) + listings.append(sprite_code) + if options.output_prefix: + r = RowLookup(assembler, screen) + luts[r.slug] = r + c = ColLookup(assembler, screen) + luts[c.slug] = c else: - sprite_code = Sprite(slug, pngdata, assembler, screen, options.xdraw, options.mask, options.backing_store, options.clobber, options.double_buffer, options.damage, options.processor) - listings.append(sprite_code) - if options.output_prefix: - r = RowLookup(assembler, screen) - luts[r.slug] = r - c = ColLookup(assembler, screen) - luts[c.slug] = c + data = np.fromfile(pngfile, dtype=np.uint8) + if options.rle: + listings.append(RLE(assembler, data)) + listings.extend([luts[k] for k in sorted(luts.keys())]) diff --git a/setup.py b/setup.py index 7060c02..ea2a16a 100644 --- a/setup.py +++ b/setup.py @@ -8,15 +8,15 @@ except ImportError: with open("README.rst", "r") as fp: long_description = fp.read() -scripts = ["quicksprite.py"] +scripts = ["asmgen.py"] -setup(name="quicksprite", +setup(name="asmgen", version="1.0", author="Rob McMullen", author_email="feedback@playermissile.com", - url="https://github.com/robmcmullen/quicksprite", + url="https://github.com/robmcmullen/asmgen", scripts=scripts, - description="Sprite compiler for Apple ][ and Atari 8-bit", + description="6502 code generator for Apple ][ and Atari 8-bit", long_description=long_description, license="GPL", classifiers=[