Skip to content

Read Bits from a Small Data Block in C64 BASIC

Extract up to eight bits at a time from a four-byte block, keeping a cursor and rejecting a field that would cross the end.

A door needs one open/closed flag. A facing direction might need two bits. Giving each a whole byte is convenient, but a packed record can hold several small fields together. This reader pulls those fields back out in the order they were written.

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

Include and initialise

Reserve B(4), public V/O/C/N and private I/J/K/P/T. Reserve numbered lines 3000–3220, combine routines.bas with your caller using build.py, and put END, STOP or a jump before the helper. C64/BBC use GOSUB; Spectrum uses GO SUB. Never use the helper’s private scratch as an enclosing FOR variable.

Call 3000 once when beginning a new block. It clears all four bytes, C/N and V/O. Supply the bytes afterwards. B(1) is the first byte; its leftmost, most significant bit comes first. C counts consumed bits from zero to 32. It is a bit offset, not a byte index.

Read one field

Set N to the number of bits, from zero to eight, then call 3100. If the whole field fits, O=1, V contains its unsigned value, and C advances by N. A request for zero bits succeeds, returns zero and leaves C unchanged, including at C=32.

Field Use
B(1)..B(4) Caller-owned bytes; preserved by every read
C Retained consumed-bit count, 0..32
N Requested width, 0..8
V / O Fresh returned value / valid flag

The helper validates C, N and every source byte as integers in range, then checks C+N before extracting anything. Invalid input or an incomplete field returns V=0,O=0 and preserves C and B. Zero is also a valid field value: always check O. Calls do no input, display, timing or allocation.

The reusable routine

Reusable numbered routines

routines.bas
3000 LET V=0: LET O=0: LET C=0: LET N=0: FOR I=1 TO 4: LET B(I)=0: NEXT I: RETURN
3100 LET V=0: LET O=0
3110 IF C<0 OR C>32 OR C<>INT (C) THEN RETURN
3120 IF N<0 OR N>8 OR N<>INT (N) THEN RETURN
3130 IF C+N>32 THEN RETURN
3140 FOR I=1 TO 4
3150 IF B(I)<0 OR B(I)>255 OR B(I)<>INT (B(I)) THEN RETURN
3160 NEXT I
3170 LET O=1
3180 IF N=0 THEN RETURN
3190 FOR J=1 TO N
3200 LET I=INT (C/8)+1: LET K=C-8*INT (C/8): LET P=2^(7-K)
3210 LET T=INT (B(I)/P): LET T=T-2*INT (T/2): LET V=2*V+T: LET C=C+1
3220 NEXT J: RETURN

INT(C/8)+1 locates the source array element. The remainder selects its bit. Multiplying V by two before adding each bit keeps the written order, including fields crossing a byte boundary. The routine uses arithmetic rather than assuming a shared bitwise operator across three BASIC dialects.

Read a retained record

The normal caller reads widths 4,4,8,8,8 from bytes 171,205,239,1. The values are 10,11,205,239,1 and the final cursor is 32. Full trace checks compare every returned value and byte, beyond the caller’s two visible summary checks.

Example calls from the maintained caller

demo.bas, excerpt
90 GOSUB 3000: LET AA=0: LET BB=0
100 LET B(1)=171: LET B(2)=205: LET B(3)=239: LET B(4)=1: LET SS=0
110 LET N=4
120 GOSUB 3100: GOSUB 2000: LET SS=SS+V
130 LET N=4
140 GOSUB 3100: GOSUB 2000: LET SS=SS+V
150 LET N=8
160 GOSUB 3100: GOSUB 2000: LET SS=SS+V
170 LET N=8
180 GOSUB 3100: GOSUB 2000: LET SS=SS+V
190 LET N=8
200 GOSUB 3100: GOSUB 2000: LET SS=SS+V
210 IF C=32 THEN LET AA=1
220 IF SS=466 THEN LET BB=1
230 PRINT "BITS 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

Line 2000, the mailbox and AA/BB/SS/JJ/KK (Z/Q on Spectrum) belong to the diagnostic. A game supplies N, calls 3100, checks O and uses V. It retains C until deliberately starting another record.

Customise the fields

The custom caller reads four complete bytes instead. Width is a property of each field: your format can mix flags, directions and small counters. Both sides must agree on widths and most-significant-bit-first order. A field can hold only values representable by its chosen width.

This fixed four-byte example is useful for small records. Longer files need wider storage/offsets and a refill policy. Byte-aligned data can use direct array access with far less interpreter work. A hot native reader may use a shift register or bit reservoir; that needs explicit refill and end-of-input rules.

Mistake and repair

The wrong caller resets C before each field. Every request succeeds, but it keeps reading the beginning of the block. Both summary checks fail: the cursor never reaches 32 and the value sum is wrong. The repair retains the cursor between calls. Its source and loaded program match the normal caller exactly.

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

A successful call visits at most eight bits, with division, powers and integer arithmetic per bit. Validation scans the four source numbers. No cycle budget is inferred from this BASIC implementation. The public values are BASIC numbers and array cells, not eight packed machine-code bytes.

Maths for Games explains binary place values, division/remainders and choosing a bit width. The Craft helps judge whether saving a few record bytes is worth a slower, less convenient interface. The matching writer follows the same bit order, but owns the destination and has a different failure contract.

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 377 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-bit-reader commodore-64 --tools /path/to/extracted/releases and inspect with python3 scripts/check-basic-packing.py bounded-bit-reader.

Explore further

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