While I haven't actually written the Assembly code for it yet, I am working on a sprite rendering system test of some variety, to use in making games down the line. Mostly posting this here as a to-do, and to share the design docs I have so far. I'm (currently) targeting a 300kHz Custom setup with a 256x128 (8-bit by 7-bit) 512-color (9-bit) monitor, with 64kB of RAM and a 4MB harddrive.
The actual sprite format is as follows, fairly simple:
- Number of addresses used for sprite (on the disk - they're always going to be hotloaded, for memory efficiency reasons)
- Image metadata: first 8 bits are width, next 7 are height, next 8 are how many color IDs the image uses, while the last 9 are just ignored
- List of color IDs, one per line, with each line being the actual 9-bit monitor color ID
- List of pixel data, 4 pixels per line in a 1-dimensional array, each pixel being an 8-bit color ID. Color ID 0 is transparent.
The pseudocode I wrote - of no particular language, it's just to articulate to myself what I would actually need to implement - for the actual render loop itself is as follows:
* Loop through in some form: estimated 21 cycles per loop tick that DOES NOT DRAW, otherwise 24 * Store both X and Y, use them in the loop. * X *must* be a power of 2. As must Y. * That way, it's possible to evenly divide things & whatnot. * Need to have a variable for "currentItem", a 0-3 int. Used in figuring out which part of the current line it's on. * Every time currentItem rises above 3, it increments the main counter, going down to the next line. * -=LOOP LAYOUT=- * PRE-LOOP: line = getdata(disk, registers, counter) //X=0, Y=0, counter=beginningOfImage, currentItem=0, registers[0:]=0, registers[4] = X,Y offset for extended SETDATA * splice(line, registers[2], (currentItem*8), ((currentItem+1)*8)-1, (3-currentItem)*8, 1) //put the color ID into register 2 for comparison * color = getdata(disk, global, id+1+registers[2]) * if color != 0 //make sure the pixel isn't transparent * splice(color, registers[0], 22, 31, 9, 1) //splice the new 9-bit color in that we got from the color ID list. goes from last 9 bits to first 9 bits. * splice(X, registers[0], 23, 31, 17, 1) //splice the 8-bit X position in that we got from the incrementation. goes from last 8 bits to bits[9:17]. * splice(Y, registers[0], 24, 31, 24, 1) //splice the 7-bit Y position in that we got from the incrementation. goes from last * setdata(monitor, registers, 0, true, 4) //draw the pixel, with an X/Y offset in register 4 (the actual position the sprite was requested to be drawn at) * endif * currentItem += 1 * if currentItem >= 4 * currentItem = 0 * counter += 1 * line = getdata(disk, registers, counter) * endif * x += 1 * if x>= width * y += 1 * x = 0 * endif * if y>= height * breakFunc * endif
Next off, an example image's code, plus the image itself. The formatting isn't...quite right for use in-game, but it gives a rough idea of what an image file looks like.
44 //# of addresses. Equal to 1 + numCols + ((X*Y)/4). ADDR 0 0b00001000_0010000_00001011_000000000 //X = 8, Y = 16, C = 11. ADDR 1 019 //Colors[01] = 019, dark orange (30). ADDR 2 030 //Colors[02] = 030, orange (30). ADDR 3 093 //Colors[03] = 093, orange-grey (30). ADDR 4 163 //Colors[04] = 163, dark lime-grey (90). ADDR 5 236 //Colors[05] = 236, lime-grey (90). ADDR 6 027 //Colors[06] = 027, black-yellow (60). ADDR 7 100 //Colors[07] = 100, dark yellow (60). ADDR 8 109 //Colors[08] = 109, mild yellow (60). ADDR 9 054 //Colors[09] = 054, yellow (60). ADDR 10 124 //Colors[10] = 124, lime (90). ADDR 11 180 //Colors[11] = 180, lighter lime (90). ADDR 12 0b00000000_00000000_00000000_00000000 //00, 00, 00, 00: 00,00 to 03,00 all transparent. ADDR 13 0b00000000_00000000_00000000_00000000 //00, 00, 00, 00: 04,00 to 07,00 all transparent. ADDR 14 0b00000000_00000000_00000111_00000111 //00, 00, 07, 07: 00,01 to 03,01 have helmet. ADDR 150 0b00000000_00000000_00000000_00000000 //00, 00, 00, 00: 04,01 to 07,01 all transparent. ADDR 16 0b00000000_00000110_00001001_00000111 //00, 06, 09, 07: 00,02 to 03,02 have helmet. ADDR 17 0b00000111_00000000_00000000_00000000 //07, 00, 00, 00: 04,02 to 07,02 have helmet. ADDR 18 0b00000000_00000110_00001000_00001011 //00, 06, 08, 11: 00,03 to 03,03 have helmet. ADDR 19 0b00001010_00000000_00000000_00000000 //10, 00, 00, 00: 04,03 to 07,03 have helmet. ADDR 20 0b00000000_00000110_00000001_00001000 //00, 06, 01, 10: 00,04 to 03,04 have helmet & arm. ADDR 21 0b00001010_00000000_00000000_00000000 //10, 00, 00, 00: 04,04 to 07,04 have helmet. ADDR 22 0b00000000_00000001_00000010_00000001 //00, 01, 02, 01: 00,05 to 03,05 have arm. ADDR 23 0b00000001_00000000_00000000_00000000 //01, 00, 00, 00: 04,05 to 07,05 have arm. ADDR 24 0b00000000_00000001_00000011_00000011 //00, 01, 03, 03: 00,06 to 03,06 have arm. ADDR 25 0b00000100_00000101_00000100_00000101 //04, 05, 04, 05: 04,06 to 07,06 have cannon. ADDR 26 0b00000000_00000001_00000001_00000001 //00, 01, 01, 01: 00,07 to 03,07 have arm. ADDR 27 0b00000100_00000100_00000100_00000100 //04, 04, 04, 04: 04,07 to 07,07 have cannon. ADDR 28 0b00000000_00000001_00000010_00000010 //00, 01, 02, 02: 00,08 to 03,08 have body. ADDR 29 0b00000001_00000000_00000000_00000000 //01, 00, 00, 00: 04,08 to 07,08 have body. ADDR 30 0b00000000_00000001_00000011_00000001 //00, 01, 03, 01: 00,09 to 03,09 have leg. ADDR 31 0b00000001_00000000_00000000_00000000 //01, 00, 00, 00: 04,09 to 07,09 have leg. ADDR 32 0b00000000_00000001_00000010_00000001 //00, 01, 02, 01: 00,10 to 03,10 have leg. ADDR 33 0b00000010_00000001_00000000_00000000 //02, 01, 00, 00: 04,10 to 07,10 have leg. ADDR 34 0b00000000_00000001_00000010_00000001 //00, 01, 02, 01: 00,11 to 03,11 have leg. ADDR 35 0b00000010_00000001_00000000_00000000 //02, 01, 00, 00: 04,11 to 07,11 have leg. ADDR 36 0b00000000_00000001_00000010_00000001 //00, 01, 02, 01: 00,12 to 03,12 have leg. ADDR 37 0b00000010_00000001_00000000_00000000 //02, 01, 00, 00: 04,12 to 07,12 have leg. ADDR 38 0b00000001_00000011_00000001_00000001 //01, 03, 01, 01: 00,13 to 03,13 have foot. ADDR 39 0b00000010_00000001_00000000_00000000 //02, 01, 00, 00: 04,13 to 07,13 have foot. ADDR 40. 0b00000001_00000011_00000001_00000011 //01, 03, 01, 03: 00,14 to 03,14 have foot. ADDR 41. 0b00000001_00000000_00000000_00000000 //01, 00, 00, 00: 04,14 to 07,14 have foot. ADDR 42. 0b00000001_00000001_00000001_00000001 //01, 01, 01, 01: 00,15 to 03,15 have foot. ADDR 43. 0b00000001_00000001_00000000_00000000 //01, 01, 00, 00: 04,15 to 07,15 have foot. ADDR 44.
It's not a terribly efficient system - my estimates show the example character taking ~2.5k cycles to render, which at 60 FPS is already half the frame, or even at 20 FPS, roughly 1/6th the frame. Even if a "room" was always 256x128 pixels, it could still easily take several seconds to render in all the entities, terrain, etc, to speak nothing of the time it might take to render large motile objects. Go bigger than that (ah-la old Metroid games, Super Mario Bros, etc), and your performance simply disappears as soon as the room needs to scroll. I KNOW there has to be a way to improve its efficiency, I'm just...currently a bit too daft to find it. >.<
Some other ideas I can think of off the top of my head would be to use a list of individual pixels, if I had a max sprite size limit of 16x16, it would be feasible to make one that supports 2 pixels per line, in the form of CCCCCCCCXXXXYYYYCCCCCCCCXXXXYYYY, only rendering those individual pixels. It'd cut down on some of the performance issues, since it outright skips transparent pixels, as well as the need to increment X and Y. It'd be (in some cases) more space efficient (my math says the test character would take up only 38 addresses instead of the current 45), though it might lose out on some speed per loop cycle, it just compensates with having less cycles overall. Image files would also likely read nicer.
Will update in replies if I make any breakthroughs.