This should have been a really easy thing to do! It is in effect a simple two-state machine with hardly any complicated conditions or special cases. Yet I found it something akin to getting cats to march in a parade; each one running off in its own direction! Change one little thing, and something else breaks.. Even now I cant be 100% sure that Ive caught them all, although Im reasonably sure that it produces a legal
representation of the uncompressed data.
Originally written in assembler, I re-wrote it from scratch in SBASIC to get a better understanding of whats going on. Afterall, in most cases the compression of a sprite only has to be performed once, so speed is not as critical as for decompression. (DERLE, the corresponding de-compression routine (and also an assembler version of DoRLE, called ENRLE) may be found here.)
There are many ways to peel an apple. Equally, there are many ways to do RLE compression. Below is one way.
I wrapped the routine in a harness to more easily test it against a variety of targets during development. The top level simply goes through some directory, taking each sprite file found. This brings it to the next level, which takes a suitable sprite and performs RLE compression on its pattern data. The program will only compress a solid sprite, ie a sprite without a bitmask or alpha channel. It doesnt do a detailed analysis of a sprite, so unusual or malformed sprites will produce nonsense or nothing. I leave it as an excercise for the interested programmer to devise a program to compress a complete (complex) GD2 sprite!
The same compressor, DoRLE, can also be used to compress the mask data. Bitmasks are usually compressed with the same item size as the pattern, while alpha data is best compressed with an item size of one.
To invoke the program use either LRUN it in a classic three window job #0, or EW/EX it.
10 rem This harness compresses all solid, mode 11 rem 16..64 sprites in a directory 12 : 13 rem Alter from_dir and to_dir as appropriate. 14 rem No serious error checking: You must use 15 rem correct parameters! 16 rem Overwrites same name in target! 17 : 18 fdr$ = 'ram1_' 19 tdr$ = 'ram1_32_' 20 : 21 IF JOBID = 0 THEN 22 cw = 1 23 ELSE 24 cw = FOPEN("con"): BORDER#cw; 1, 5 25 END IF 26 CLS#cw 27 : 28 fdv$ = fdr$(1 TO 5) 29 IF LEN(fdr$) > 5: dr$ = fdr$(6 TO): ELSE : dr$ = '' 30 dl% = LEN(dr$) + 1 31 ch = FOP_DIR(fdr$) 32 fl = FLEN(#ch) 33 pos = -64 34 : 35 REPeat lp 36 pos = pos + 64: IF pos >= fl: EXIT lp 37 BGET#ch\ pos + 5, t%: IF t% = 255: NEXT lp 38 GET#ch\ pos + 14, nm$ 39 nl% = LEN(nm$) 40 IF nl% = 0: NEXT lp 41 IF NOT ('spr' INSTR nm$) = (nl% - 2): NEXT lp 42 fnm$ = fdv$ & nm$ 43 tnm$ = tdr$ & nm$(dl% TO) 44 PRINT#cw; fnm$, 45 PRINT#cw; SprComp(fnm$, tnm$) 46 END REPeat lp 47 : 48 CLOSE#ch 49 BEEP 2000, 2: PRINT#cw; 'Done!' 50 IF JOBID: BGET#cw; k% 51 : 52 : 53 rem + ------------------------------------------------------------------------ + 54 rem |< SprComp >| 55 rem + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 56 rem | RLE-compress a solid GD2 sprite | 57 rem | | 58 rem | Given the filename of a suitable sprite in fnm$, this routine | 59 rem | compresses it and writes the sprite to tnm$ (overwrite!) fnm$ can | 60 rem | equal tnm$, if you like. | 61 rem | | 62 rem | Returns the compressed size (+ve) or an error: | 63 rem | -7 => File not found | 64 rem | -8 => Already compressed | 65 rem | -19 => Wrong mode or not a solid sprite, etc | 66 rem | -5 => The sprite did not compress | 67 rem | If tnm$ is in use, the program just crashes! | 68 rem + ------------------------------------------------------------------------ + 69 rem | V0.01, pjw, 2019 Mar 20 | 70 rem + ------------------------------------------------------------------------ + 71 : 72 DEFine FuNction SprComp(fnm$, tnm$) 73 LOCal ch, fl, sz, spu, spc, md%, ct% 74 : 75 rem Check for valid sprite 76 ch = FOP_IN(fnm$): IF ch < 0: RETurn ch 77 fl = FLEN(#ch) 78 WGET#ch; md%, ct% 79 LGET#ch\ 16, spu: CLOSE#ch: rem (re-use this LOCal) 80 IF spu: RETurn -19: rem Only pattern supported 81 IF (ct% && 64) = 64: RETurn -8: rem Already compressed 82 IF ct% <> 0: RETurn -19: rem Only single, simple sprite allowed 83 : 84 rem Get colour mode and work out compression code 85 SELect ON md% 86 = 528: md% = 1: rem GD2-16 87 = 544, 545: md% = 2: rem GD2-32/33 88 = 576: md% = 4: rem GD2-64 89 = REMAINDER : RETurn -19: rem Unsupported mode 90 END SELect 91 : 92 rem Create suitable buffer and load sprite 93 spu = ALCHP(fl + fl) 94 spc = spu + fl 95 LBYTES fnm$, spu 96 : 97 rem Copy sprite header and patch for compressed 98 POKE$ spc, PEEK$(spu, 24) 99 POKE spc + 3, PEEK(spc + 3) + 64: rem Flag RLE pattern 100 : 101 sz = DoRLE(spu + 24, spc + 24, fl - 24, md%) 102 IF sz > 0: SBYTES_O tnm$, spc, sz + 24 103 RECHP spu 104 RETurn sz 105 END DEFine SprComp 106 : 107 : 108 : 109 rem + ------------------------------------------------------------------------ + 110 rem |< Do RLE >| 111 rem + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 112 rem | Standard sprite RLE compression | 113 rem | | 114 rem | The compressed data always consists of one byte and one or more items. | 115 rem | If the leading byte x is in the range 0..127 then x + 1 uncompressed | 116 rem | items follow. Otherwise only one item follows, which represents 257 - x | 117 rem | times that item in the uncompressed data. | 118 rem | | 119 rem | Given the item size z% = 1, 2, or 4, this routine compresses the data | 120 rem | at address fad to the buffer at address tad, according to the | 121 rem | description above. The compressed data is flagged with the literal | 122 rem | "RLE<item size>" followed by a longword giving the uncompressed data | 123 rem | size, osz, followed by the compressed data. | 124 rem | | 125 rem | Returns: The size of the compressed data (+ 8b for the header). | 126 rem | NB The target buffer is assumed to be of the same size as the | 127 rem | uncompressed data! In the event the compressed data would exceed the | 128 rem | buffer, the error Buffer Full (-5) is returned instead. The contents of | 129 rem | the buffer is then undefined. | 130 rem + ------------------------------------------------------------------------ + 131 rem | V0.01, pjw, 2019 Mar 20 | 132 rem | V0.02, pjw, 2020 Dec 11, bug fix: same loop didnt terminate properly | 133 rem + ------------------------------------------------------------------------ + 134 : 135 DEFine FuNction DoRLE(fad, tad, osz, z%) 136 LOCal lp, dl, sl, er, fbe, tbe, fpt, tpt 137 LOCal c%, adr, cit$(4), nit$(4) 138 : 139 rem Set header 140 POKE$ tad, 'RLE' 141 POKE tad + 3, z% + 48 142 POKE_L tad + 4, osz 143 : 144 rem Init 145 fbe = fad + osz: rem From Buffer End 146 tbe = tad + osz: rem To Buffer End 147 fpt = fad: rem From running PoinTer 148 tpt = tad + 8: rem To running PoinTer 149 : 150 rem RUN 151 cit$ = PEEK$(fpt, z%): rem Current ITem 152 REPeat lp 153 er = diff: IF er < 0: EXIT lp: rem Different Loop 154 er = same: IF er < 0: EXIT lp: rem Same Loop 155 END REPeat lp 156 IF er = -5: RETurn er: rem Buffer overrun 157 RETurn tpt - tad: rem Return compressed data size 158 END DEFine DoRLE 159 : 160 DEFine FuNction diff 161 adr = tpt: tpt = tpt + 1: c% = -1 162 REPeat dl 163 fpt = fpt + z%: IF fpt > fbe: POKE adr, c%: RETurn -10 164 nit$ = PEEK$(fpt, z%) 165 IF nit$ = cit$ THEN 166 rem End of differences 167 IF c% > -1: POKE adr, c%: ELSE : tpt = adr 168 EXIT dl 169 ELSE 170 IF tpt > tbe: RETurn -5 171 POKE$ tpt, cit$ 172 tpt = tpt + z%: c% = c% + 1 173 cit$ = nit$ 174 IF c% = 127 THEN 175 rem Reset counter 176 POKE adr, 127 177 adr = tpt: tpt = tpt + 1: c% = -1 178 END IF 179 END IF 180 END REPeat dl 181 RETurn 0 182 END DEFine diff 183 : 184 DEFine FuNction same 185 c% = 2 186 REPeat sl 187 fpt = fpt + z% 188 IF fpt > fbe THEN 189 POKE tpt, 257 - c%: tpt = tpt + 1: POKE$ tpt, cit$ 190 RETurn -10 191 END IF 192 nit$ = PEEK$(fpt, z%) 193 IF nit$ <> cit$ THEN 194 rem End of same run 195 IF c% > 1 THEN 196 POKE tpt, 257 - c%: tpt = tpt + 1: POKE$ tpt, cit$ 197 tpt = tpt + z%: IF tpt > tbe: RETurn -5 198 ELSE 199 IF c% = 1: fpt = fpt - z% 200 END IF 201 cit$ = nit$ 202 EXIT sl 203 ELSE 204 c% = c% + 1 205 IF c% = 128 THEN 206 rem Reset counter 207 POKE tpt, 129: tpt = tpt + 1: POKE$ tpt, cit$ 208 tpt = tpt + z%: IF tpt > tbe: RETurn -5 209 c% = 0 210 END IF 211 END IF 212 END REPeat sl 213 RETurn 0 214 END DEFine same 215 : 216 :