Skip to content

Decode a Small Run-length Block in C64 BASIC

Expand count/value pairs into an eight-cell output with explicit input bounds, actual written length and useful partial-error results.

A row of three identical tiles can be stored as a count and a tile value. Stretching it back out is straightforward until the data ends halfway through a pair, or a run is larger than the space you reserved. This decoder stops safely and tells you what it produced.

C64 BASIC shows both retained behaviour checks passing.
The working caller completes with both saved checks equal to one.

Include and initialise

Reserve I(13) for the encoded source, B(8) for decoded output, public W/E/S/T/L and private J/K/V. Reserve lines 3000–3240, combine the helper and caller with build.py and keep END, STOP or a jump before the helper. C64/BBC use GOSUB and Spectrum GO SUB. Do not borrow private scratch for an enclosing loop.

Call 3000 at a deliberate reset boundary. It clears all source/output array cells and all public scalars. Supply source bytes afterwards. The maintained 13-byte source includes both a valid asset and deliberate malformed examples; these bytes are caller data, not a required immutable format table.

Decode a selected span

S is an inclusive zero-based source byte offset, T the exclusive end, with 0 <= S <= T <= 13. L is the available output capacity from zero to eight. Set them and call 3100. Pairs are positive count followed by byte value. S=0 refers to I(1), and decoded bytes begin at B(1).

Field Use
I(1)..I(13), S/T Caller-owned encoded bytes and selected span; preserved
B(1)..B(8), L Owned destination and supplied capacity
W / E Actual bytes produced / fresh completion or error code

E=0 means the selected span completed, E=1 invalid bounds or non-byte source data, E=2 a truncated pair, E=3 a zero count, E=4 insufficient output space. Input bounds and all 13 source values are validated before writing. W resets to zero each call. An empty span succeeds without output, even at zero capacity.

On a stream error, B contains a valid decoded prefix of W bytes. Bytes after that prefix are preserved. The routine can stop inside a run when capacity is exhausted. It does not roll back that prefix and returns no continuation cursor. Re-decoding with more capacity starts at S again. If you need an all-or-nothing asset, use a staging buffer and publish it only when E=0.

The reusable routine

Reusable numbered routines

routines.bas
3000 LET W=0: LET E=0: LET S=0: LET T=0: LET L=0
3010 FOR J=1 TO 13: LET I(J)=0: NEXT J
3020 FOR J=1 TO 8: LET B(J)=0: NEXT J: RETURN
3100 LET W=0: LET E=1
3110 IF S<0 OR S>13 OR S<>INT (S) THEN RETURN
3120 IF T<S OR T>13 OR T<>INT (T) THEN RETURN
3130 IF L<0 OR L>8 OR L<>INT (L) THEN RETURN
3140 FOR J=1 TO 13
3150 IF I(J)<0 OR I(J)>255 OR I(J)<>INT (I(J)) THEN RETURN
3160 NEXT J: LET E=0: LET J=S+1
3170 IF J>T THEN RETURN
3180 IF J+1>T THEN LET E=2: RETURN
3190 LET K=I(J): LET V=I(J+1)
3200 IF K=0 THEN LET E=3: RETURN
3210 IF W=L THEN LET E=4: RETURN
3220 LET W=W+1: LET B(W)=V: LET K=K-1
3230 IF K>0 THEN GOTO 3210
3240 LET J=J+2: GOTO 3170

The loop checks for a complete pair and rejects zero counts before emitting its run. Every store checks remaining capacity. Filling the destination exactly can succeed if that also finishes the selected stream; a full buffer alone is not proof of success.

Decode and use the returned length

The normal source begins 3,65,2,66,1,67. It expands to AAABBC: six values, with capacity eight. The custom caller offers five cells, producing AAABB and E=4. Both are expected, useful outcomes with different completeness.

Example calls from the maintained caller

demo.bas, excerpt
100 GOSUB 3000: LET AA=0: LET BB=0
110 LET I(1)=3: LET I(2)=65: LET I(3)=2: LET I(4)=66
120 LET I(5)=1: LET I(6)=67: LET I(7)=0: LET I(8)=90
130 LET I(9)=2: LET I(10)=68: LET I(11)=5: LET I(12)=69
140 LET I(13)=1
150 FOR JJ=1 TO 8: LET B(JJ)=99: NEXT JJ
160 LET S=0: LET T=6: LET L=8: GOSUB 3100
170 GOSUB 2000: LET SS=0: FOR JJ=1 TO W: LET SS=SS+B(JJ): NEXT JJ
180 IF W=6 THEN LET AA=1
190 IF SS=394 THEN LET BB=1
200 LET S=12: LET T=13: LET L=8: GOSUB 3100: GOSUB 2000
210 LET S=6: LET T=8: LET L=8: GOSUB 3100: GOSUB 2000
220 LET S=0: LET T=0: LET L=0: GOSUB 3100: GOSUB 2000
230 PRINT "RLE RESULTS";AA;BB
240 POKE MB+995,AA: POKE MB+996,BB: POKE MB+997,GD: POKE MB+998,WC-256*INT (WC/256): POKE MB+999,INT (WC/256): POKE MB+1000,239: STOP

The full diagnostic also exercises truncated and zero runs, empty spans, invalid bounds and output exhaustion. Line 2000, AA/BB/SS/JJ/KK (Z/Q on Spectrum) and the mailbox are observers. Your game calls 3100 and accepts only the first W cells, applying its own policy to E.

Customise capacity and data

Counts describe repeats, not byte offsets. Values can be tiles, pixels or any byte-valued token. Runs of one work but cost two encoded bytes, so alternating input may grow. There is no zero terminator or literal-packet escape in this small local codec.

L never exceeds the allocated eight cells. A larger decoder needs corresponding arrays, limits and tests, and perhaps wider counters. A streaming decoder must retain pair/run progress explicitly. If assets never change, build-time expansion may remove runtime decode work entirely.

Mistake and repair

The wrong caller replaces W with L after decoding. It advertises eight output cells although only six were produced; two old values become part of the alleged asset. The length and consumed-data checks both fail. The repair preserves the decoder’s returned W and consumes exactly that prefix. The retained trace checks E as well as the output. Repaired source and loaded program match the normal caller.

C64 BASIC shows two failed checks from the deliberate caller mistake.
The wrong caller runs to completion, but both saved behaviour checks are zero.

Cost and Maths for Games

Validation scans 13 source values. Decoding visits the selected pairs and performs at most eight output stores. A count of 255 still cannot overrun the reserved destination. This bounded interpreter routine is not a measured frame-time guarantee.

Maths for Games connects run lengths, sums and bounds. Compare encoded size 2*number_of_runs with the original length before choosing compression. The Craft helps decide whether partial output is useful or whether loading should commit only a complete asset.

Machine and measured size

C64 BASIC V2 recognises only the first two significant characters of numeric variable names. Short names make the ownership visible; check for collisions before including another component. Array element zero exists but is unused here. The diagnostic uses a mailbox at $C000, outside the normal BASIC heap.

The numbered helper occupies 470 tokenised program bytes on this interpreter, including line headers. This excludes the caller, test tables, numeric scalar/array storage, interpreter stacks and ROM. Tokenised size is not machine-code size or a hardware-cycle count. No real-time or whole-game performance budget is inferred from the diagnostic. The caller’s AA/BB, JJ/KK and RAM mailbox are observation machinery, not required game state.

Execution evidence

Working, custom, deliberately wrong and exact repaired callers executed through C64’s stock BASIC interpreter under Emu198x 0.44.2. Build198x 0.2.9 tokenises C64 PRG and Spectrum TAP programs. C64 enters stock RUN, Spectrum uses ROM tape loading, and BBC enters every numbered source line through MOS/BASIC. Actual loaded program bytes, complete state packets, positive completion/count/guards and visible result pixels are checked. After the positive completion marker, the observer advances two complete display frames before capturing, so a partially scanned final character cannot masquerade as a finished picture. Firmware is not distributed.

An independent mathematical/order model checks the trace and rejects altered source, configuration, loaded program, state, completion and image evidence. Host exhaustive checks complement the native boundary/sequential examples; those host ranges are not claimed as natively exhaustive. The supplied diagnostic callers deliberately end with STOP; the interpreter report is expected. The wrong caller prints 0,0 and the normal/custom/repaired callers print 1,1.

Public CI checks retained evidence; it does not execute restricted firmware. Refresh with python3 scripts/verify-basic-packing.py bounded-rle commodore-64 --tools /path/to/extracted/releases and inspect with python3 scripts/check-basic-packing.py bounded-rle.

Explore further

Compare the assembly technique. The BASIC game-flow routine makes session boundaries explicit. Complete source and native evidence.