QRL is a run-length encoding (RLE) image viewer and editor that manages compressed graphics stored at address 37000, with its machine code routines loaded starting at address 30400. The program uses a suite of USR calls into machine code blocks referenced by calculated addresses (STB, BTS, ECD, DCD, CPF, INV, TLN, WIP, LAD) that handle screen blitting, encoding, decoding, inverting, and wiping operations. A cassette-based save/load system supports three data types: RLE files, raw screens (6144 bytes from address 16384), and the BASIC program itself with its machine code payload. The WIPE routine (line 700) iterates over the screen’s bitmap in a specific band-row-column order, poking each line’s address into a parameter area before calling the USR WIP machine code routine, effectively wiping a user-specified number of pixel rows. Input handling normalizes lowercase to uppercase by checking whether the key code exceeds 90 and subtracting 32, and a debounce loop at line 410 waits for the key to be released before acting.
Program Analysis
Program Structure
The program divides into several functional regions:
- Lines 1–35: Initialization — clears memory, dimensions the display string
E$, sets color variablesC1/C2, calculates all machine code entry-point addresses, and calls the screen-setup USR routine. - Lines 40–160: Main loop — calls the “total lines” USR (
TLN) to get the current RLE file size intoL, optionally draws the menu, then polls for a keypress and dispatches on it. - Lines 190–291: Load sub-system — handles loading an RLE file to address 37000 or a raw screen to address 16384, with a shared filename-input subroutine at line 290.
- Lines 300–394: Save sub-system — handles saving an RLE file, screen (6144 bytes), or the full BASIC program with machine code payload, plus optional verify.
- Lines 400–430: Key-input routine — busy-waits for a keypress, beeps, debounces, uppercases, then jumps to the address stored in
RET. - Lines 500–510: Menu drawing subroutine — prints a three-row option grid inside a PLOT/DRAW border box.
- Lines 700–740: Wipe routine — iterates over screen memory in band/row/column order, poking the address of each pixel row into the machine code parameter area and calling the USR WIP routine.
- Lines 900–920: Post-load trampoline — reloads the machine code block and calls a buffer-clear USR before restarting.
Machine Code Interface
All machine code lives in a block starting at STB = 30400. Named entry points are calculated at line 10 as fixed offsets from STB:
| Variable | Address | Purpose |
|---|---|---|
STB | 30400 | Screen-to-buffer blit / display restore |
BTS | 30403 | Buffer-to-screen blit |
ECD | 30406 | Encode screen to RLE |
DCD | 30409 | Decode RLE to screen; returns success flag |
CPF | 30412 | Copy/flip routine (used on COPY key) |
INV | 30415 | Invert image |
TLN | 30418 | Return total line count / RLE data length |
WIP | 30421 | Wipe one pixel row at a parameterized address |
LAD | 30426 | Two-byte parameter area: target address for WIP |
The spacing of exactly 3 bytes between most entry points suggests each is a simple JP nn (3-byte absolute jump) acting as a jump table. LAD at offset 26 is not itself called; it is a data area poked by the BASIC wipe loop before calling WIP.
Key BASIC Idioms
VAL "number"throughout: A standard memory-saving technique — storing numeric literals as short string tokens rather than 5-byte floating-point constants in the BASIC line.- Computed
GO TO RET: The input dispatcher stores a return address in variableRETand jumps to it withGO TO RET, simulating a computed GOSUB return without a full subroutine stack entry. - Uppercase normalization (line 420):
IF K$>"Z" THEN LET K$=CHR$ (CODE K$-32)converts any lowercase letter to uppercase, making the key-dispatch case-insensitive without duplicating every IF branch. - Debounce at line 410:
IF K$=INKEY$ THEN GO TO 410spins until the key is physically released, preventing a single press from being acted on multiple times in the main loop. RUNas a soft reset: Used liberally (lines 250, 265, 272, 280, 304, 322, 340, 360, 700, 706, 730, 920) to restart the program cleanly rather than managing complex state recovery.
Wipe Routine Detail (Lines 700–740)
The wipe iterates over Spectrum screen memory in its natural band/character-row/pixel-row order. The address of each pixel row is computed as:
A = 16384 + (B*2048) + (S*256) + (R*32)
where B is the band (0–2), S is the pixel row within a character row (0–7), and R is the character row within the band (0–7). This is the standard ZX Spectrum screen layout formula. The low and high bytes of A are then POKEd into LAD and LAD+1 before calling USR WIP, which presumably clears that row. The loop counts down X (the user-supplied number of lines) and exits early when it reaches zero.
Save/Load Architecture
Three distinct data objects are managed:
- RLE file: 37000 to 37000+
Lbytes, whereLcomes from the machine codeTLNroutine. - Screen: 6144 bytes from 16384 (bitmap only; attributes are not saved separately).
- Program: Saved with
SAVE N$ LINE 900(auto-run at line 900) followed immediately bySAVE N$ CODE 30400,400to bundle the 400-byte machine code block on the same tape file sequence.
Line 900 is the post-load trampoline that reloads the machine code and clears buffers before restarting, ensuring the code block is always present after a tape load.
Notable Anomalies and Bugs
- Mixed case variable collision: The menu-toggle variable is set as
MENUat line 110 but tested asmenuat line 40 — in Sinclair BASIC variable names are case-sensitive for multi-character names, soMENUandmenuare distinct variables. The test at line 40 always reads the originalmenu(initialized to 1 at line 20) and will never reflect the toggled value. SimilarlyMVvsmvmay cause issues. - Line 392 verify path: When
K$="P",VERIFY N$is called withoutCODE, verifying the BASIC portion, but then falls through toVERIFY N$ CODEat line 394 which would try to verify the code block — this is actually correct for the two-part save format, though the fall-through is implicit. - Input validation in line 290: Filename length is capped at 10 characters, consistent with tape filename limits, but no check is made for invalid filename characters.
CLEAR 65368at line 125: The QUIT handler clears to near the top of the 48K RAM map and then callsSTOP, effectively exiting to a clean BASIC state.
Content
Source Code
1 REM QRL by Jack Dohany
2 REM (415)367-7781
3 REM 10-86
4 CLEAR VAL "30399"
5 REM cassette version
6 DIM E$(VAL "20"): LET C1=VAL "0": LET C2=VAL "7"
10 LET STB=VAL "30400": LET BTS=STB+VAL "3": LET ECD=STB+VAL "6": LET DCD=STB+VAL "9": LET CPF=STB+VAL "12": LET INV=STB+VAL "15": LET TLN=STB+VAL "18":: LET WIP=STB+VAL "21": LET LAD=STB+VAL "26"
20 LET menu=VAL "1": LET MV=VAL "11"
30 BORDER C1: PAPER C1: INK C2
35 RANDOMIZE USR BTS
40 LET L=USR TLN: IF NOT menu THEN GO TO VAL "100"
50 GO SUB VAL "500"
100 LET RET=VAL "110": GO TO VAL "400"
110 IF K$="M" OR K$=CHR$ VAL "13" THEN LET MENU=NOT MENU: GO TO VAL "35"
120 IF K$="L" THEN LET C$=" LOAD ": GO TO VAL "200"
125 IF K$="Q" THEN CLEAR VAL "65368": PRINT AT VAL "21",VAL "10";" OK TO RUN ": STOP
130 IF K$="S" THEN LET C$=" SAVE ": GO TO VAL "300"
135 IF K$="C" THEN RANDOMIZE USR BTS: RANDOMIZE USR CPF: GO TO VAL "40"
140 IF K$="I" THEN RANDOMIZE USR INV: GO TO VAL "35"
145 IF K$="E" THEN GO TO VAL "260"
150 IF K$="D" THEN GO TO VAL "270"
155 IF k$="W" THEN GO TO VAL "700"
160 GO TO VAL "100"
190 FOR I=VAL "0" TO VAL "300": NEXT I: BEEP VAL ".01",VAL "25": RETURN
200 INPUT "LOAD FILE OR SCREEN? "; LINE K$: LET RET=VAL "201": GO TO VAL "420"
201 IF k$="F" THEN LET W$=" RLE FILE ": GO TO VAL "220"
202 IF k$="S" THEN LET W$=" SCREEN ": GO TO VAL "240"
203 GO TO VAL "35"
220 GO SUB VAL "290"
222 LOAD n$CODE 37000: RUN
240 GO SUB VAL "290"
242 CLS : LOAD n$CODE VAL "16384"
250 RANDOMIZE USR STB: RUN
260 PRINT FLASH 1;AT mv+VAL "7",VAL "1";" Allow 10 seconds "
261 FOR i=VAL "0" TO VAL "250": NEXT i
265 RANDOMIZE USR BTS: RANDOMIZE USR ECD: RUN
270 CLS : LET GOOD=USR DCD
271 IF NOT GOOD THEN GO TO VAL "280"
272 RANDOMIZE USR STB: RUN
280 PRINT FLASH VAL "1";AT mv+VAL "7",VAL "1";" INVALID RLE FILE ": FOR i=VAL "0" TO VAL "250": NEXT i: RUN
290 INPUT ("";C$;" ";W$;'"Filename: ");N$: IF LEN n$>VAL "10" THEN GO TO VAL "290"
291 RETURN
300 INPUT "SAVE FILE, SCREEN or PROG? "; LINE K$: LET ret=VAL "301": GO TO VAL "420"
301 IF K$="F" THEN LET W$=" RLE FILE ": GO TO VAL "320"
302 IF K$="S" THEN LET W$=" SCREEN ": GO TO VAL "340"
303 IF K$="P" THEN LET W$=" PROGRAM ": GO TO VAL "360"
304 RUN
320 IF NOT L THEN GO TO VAL "280"
322 GO SUB VAL "290": IF n$="" THEN RUN
324 SAVE N$CODE 37000,L: GO TO VAL "380"
340 GO SUB VAL "290": IF n$="" THEN RUN
350 RANDOMIZE USR BTS: SAVE n$CODE VAL "30800",VAL "6144": GO TO VAL "380"
360 GO SUB VAL "290": IF n$="" THEN RUN
364 SAVE N$ LINE VAL "900": BEEP VAL ".01",VAL "10": SAVE N$CODE VAL "30400",VAL "400"
380 CLS : PRINT AT VAL "20",VAL "0";"VERIFY (Y/N) "; FLASH VAL "1";"?"
381 IF INKEY$="" THEN GO TO VAL "381"
382 IF INKEY$="y" OR INKEY$="Y" THEN GO TO VAL "390"
383 IF INKEY$="n" OR INKEY$="N" THEN RUN
384 GO TO VAL "381"
390 CLS : PRINT AT VAL "20",VAL "0";"REWIND/PLAY TO VERIFY ";N$
392 IF K$="P" THEN VERIFY N$
394 VERIFY N$CODE : RUN
400 LET K$=INKEY$: IF K$="" THEN GO TO VAL "400"
405 BEEP VAL ".01",VAL "20"
410 IF K$=INKEY$ THEN GO TO VAL "410"
420 IF K$>"Z" THEN LET K$=CHR$ (CODE K$-VAL "32")
430 GO TO RET
500 PAPER C2: INK C1: PRINT AT MV,VAL "0";E$'" MENU DECODE COPY "'E$'" LOAD ENCODE WIPE "'E$'" SAVE INVERT QUIT "'E$'" rle file:";L;TAB VAL "20"'E$'
505 PLOT VAL "3",VAL "171"-MV*VAL "8": DRAW VAL "152",VAL "0": DRAW VAL "0",-VAL "64": DRAW -VAL "152",VAL "0": DRAW VAL "0",VAL "64"
510 PAPER C1: INK C2: RETURN
700 INPUT "Wipe lines (1-192): "; LINE q$: IF q$="" THEN RUN
704 FOR i=VAL "1" TO LEN q$: IF q$(i)<"0" OR q$(i)>"9" THEN RUN
706 NEXT I: LET X=VAL Q$: IF X<VAL "1" OR X>VAL "192" THEN RUN
710 RANDOMIZE USR BTS: FOR B=VAL "2" TO VAL "0" STEP -VAL "1": FOR R=VAL "7" TO VAL "0" STEP -VAL "1": FOR S=VAL "7" TO VAL "0" STEP -VAL "1": LET A=VAL "16384"+(B*VAL "2048")+(S*VAL "256")+(R*VAL "32")
720 POKE LAD+VAL "1",INT (A/VAL "256"): POKE LAD,A-VAL "256"*PEEK (LAD+VAL "1"): RANDOMIZE USR WIP
730 LET X=X-VAL "1": IF X=VAL "0" THEN RANDOMIZE USR STB: GO TO VAL "40"
740 NEXT S: NEXT R: NEXT B: RUN
900 LOAD N$CODE
910 RANDOMIZE USR VAL "30741": REM clear buffers
920 RUN
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.
