Recent

Author Topic: ZX02 implementation in Pascal  (Read 1734 times)

geraldholdsworth

  • Sr. Member
  • ****
  • Posts: 284
ZX02 implementation in Pascal
« on: January 06, 2026, 05:01:49 pm »
Has anyone managed to convert the ZX02 compressor and decompressor in Pascal?
(this is the ZX0 method, optimised for 6502 processors)

It's driving me insane, as I don't know enough about 6502 to comprehend what is going on, and the code that AI has chucked at me (without asking, I might add) doesn't work (expected). The documentation for how the compression and decompression works makes no sense (when I apply it to the data set I have been given). My only hope is chucking the 6502 code I have been given (which works) into a virtual simulator and switching on the trace output to see what it is doing.

There are implementation available in C, but I also don't understand that enough to adapt it.

Thaddy

  • Hero Member
  • *****
  • Posts: 18729
  • To Europe: simply sell USA bonds: dollar collapses
Re: ZX02 implementation in Pascal
« Reply #1 on: January 06, 2026, 06:12:22 pm »
Can you send me the C implementation? I can probably adapt it.
I can also test it on real hardware since my C64 is still working. (although that is a 6510, that does not matter)
I have working interface to transfer the files over serial.
« Last Edit: January 06, 2026, 06:16:06 pm by Thaddy »
If Europe sells their USA bonds the USD will collapse. Europe can affort that given average state debts. The USA can't affort that. Just an advice...

geraldholdsworth

  • Sr. Member
  • ****
  • Posts: 284
Re: ZX02 implementation in Pascal
« Reply #2 on: January 06, 2026, 06:16:58 pm »
I think this is it:
Code: [Select]
/*
 * (c) Copyright 2021 by Einar Saukas. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * The name of its author may not be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "zx02.h"
#include "memory.h"

#define MAX_SCALE 50

int offset_ceiling(zx02_state *s, int index) {
    return index > s->offset_limit     ? s->offset_limit
           : index < s->initial_offset ? s->initial_offset
                                       : index;
}

int elias_gamma_bits(zx02_state *s, int value) {
    int bits = 1;
    // Return a really big number to limit range to valid values
    if (value < 1 || value > 0x100)
        return 1<<20;
    // Optimization: don't send last 2 bits on limit value:
    if (s->elias_short_code && value == 0x100)
        bits = -1;
    while (value >>= 1)
        bits += 2;
    return bits;
}

int elias_gamma_bits_1(zx02_state *s, int value) {
    if (value == 1)
        return elias_gamma_bits(s, 256);
    else if (value > 256)
        return 1<<20;
    else
        return elias_gamma_bits(s, value - 1);
}

int offset_bits(zx02_state *s, int value) {
    if (s->zx1_mode)
        return (value > 127) ? 17 : 9;
    else
        return 8 + elias_gamma_bits(s, value / 128 + 1);
}

// Build a new list with the given chain
BLOCK *new_chain(BLOCK *chain) {

    // Get chain size
    size_t size = 0;
    for(BLOCK *ptr = chain; ptr; ptr=ptr->chain)
        size++;

    if(!size)
        return NULL;

    BLOCK *ret = calloc(size, sizeof(BLOCK));
    if(!ret) {
        fprintf(stderr, "Error: Insufficient memory\n");
        exit(1);
    }

    size = 0;
    for(BLOCK *ptr = chain; ptr; ptr=ptr->chain) {
        memcpy(&ret[size], ptr, sizeof(BLOCK));
        ret[size].chain = &ret[size] + 1;
        ret[size].unused_chain = 0;
        size++;
    }
    ret[size-1].chain = 0;

    return ret;
}

void optimize(zx02_state *s) {
    BLOCK **last_literal;
    BLOCK **last_match;
    BLOCK **optimal;
    int *match_length;
    int *best_length;
    int best_length_size;
    int bits;
    int index;
    int offset;
    int length;
    int bits2;
    int dots = 2;

    if (s->initial_offset >= s->input_size)
        s->initial_offset = s->input_size - 1;

    int max_offset = offset_ceiling(s, s->input_size - 1);

    /* allocate all main data structures at once */
    last_literal = (BLOCK **)calloc(max_offset + 1, sizeof(BLOCK *));
    last_match = (BLOCK **)calloc(max_offset + 1, sizeof(BLOCK *));
    optimal = (BLOCK **)calloc(s->input_size, sizeof(BLOCK *));
    match_length = (int *)calloc(max_offset + 1, sizeof(int));
    best_length = (int *)calloc(s->input_size, sizeof(int));
    if (!last_literal || !last_match || !optimal || !match_length || !best_length) {
        fprintf(stderr, "Error: Insufficient memory\n");
        exit(1);
    }
    if (s->input_size > 1)
        best_length[1] = 1;

    // Allocate initial block memory
    struct block_mem_t *mem = block_mem_new();

    /* start with fake block */
    assign(mem, &last_match[s->initial_offset],
           allocate(mem, -1, s->skip - 1, s->initial_offset, NULL));

    printf("[");

    /* process remaining bytes */
    for (index = s->skip; index < s->input_size; index++) {
        best_length_size = 1;
        max_offset = offset_ceiling(s, index);
        for (offset = 1; offset <= max_offset; offset++) {
            /* Check that we have a matching byte at this offset */
            if (index != s->skip && index >= offset &&
                s->input_data[index] == s->input_data[index - offset]) {
                /* copy from last offset, only if code at this offset was a literal */
                if (last_literal[offset]) {
                    length = index - last_literal[offset]->index;
                    bits = last_literal[offset]->bits + 1 + elias_gamma_bits(s, length);
                    assign(mem, &last_match[offset],
                           allocate(mem, bits, index, offset, last_literal[offset]));
                    if (!optimal[index] || optimal[index]->bits > bits)
                        assign(mem, &optimal[index], last_match[offset]);
                }
                /* copy from new offset */
                match_length[offset]++;
                if (best_length_size < match_length[offset]) {
                    bits = optimal[index - best_length[best_length_size]]->bits +
                           elias_gamma_bits_1(s, best_length[best_length_size]);
                    do {
                        best_length_size++;
                        bits2 = optimal[index - best_length_size]->bits +
                                elias_gamma_bits_1(s, best_length_size);
                        if (bits2 <= bits) {
                            best_length[best_length_size] = best_length_size;
                            bits = bits2;
                        } else {
                            best_length[best_length_size] =
                                best_length[best_length_size - 1];
                        }
                    } while (best_length_size < match_length[offset]);
                }
                length = best_length[match_length[offset]];
                bits = optimal[index - length]->bits + offset_bits(s, offset) +
                       elias_gamma_bits_1(s, length);
                if (!last_match[offset] || last_match[offset]->index != index ||
                    last_match[offset]->bits > bits) {
                    assign(mem, &last_match[offset],
                           allocate(mem, bits, index, offset, optimal[index - length]));
                    if (!optimal[index] || optimal[index]->bits > bits)
                        assign(mem, &optimal[index], last_match[offset]);
                }
            } else {
                /* copy literals */
                match_length[offset] = 0;
                if (last_match[offset]) {
                    length = index - last_match[offset]->index;
                    bits = last_match[offset]->bits + 1 + elias_gamma_bits(s, length) +
                           length * 8;
                    assign(mem, &last_literal[offset],
                           allocate(mem, bits, index, 0, last_match[offset]));
                    if (!optimal[index] || optimal[index]->bits > bits)
                        assign(mem, &optimal[index], last_literal[offset]);
                }
            }
        }

        /* indicate progress */
        if (index * MAX_SCALE / s->input_size > dots) {
            printf(".");
            fflush(stdout);
            dots++;
        }
    }

    printf("]\n");

    // Get new chain to return
    s->optimal = new_chain(optimal[s->input_size - 1]);

    // Free all memory
    block_mem_free(mem);
    free(last_literal);
    free(last_match);
    free(optimal);
    free(match_length);
    free(best_length);
}
Source: https://github.com/dmsc/zx02

I'm currently going through the 6502 code and doing a literal translation (where I can). It's not pretty as it has labels and gotos.

geraldholdsworth

  • Sr. Member
  • ****
  • Posts: 284
Re: ZX02 implementation in Pascal
« Reply #3 on: January 07, 2026, 08:47:19 pm »
Literal translation direct from 6502 code (so messy):
Code: Pascal  [Select][+][-]
  1. program Project1;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. uses
  6.  {$IFDEF UNIX}
  7.  cthreads,
  8.  {$ENDIF}
  9.  Classes,
  10.   SysUtils
  11.  { you can add units after this };
  12.  
  13. var
  14.  A           : Word=0;  //We could just use Byte, but we need 9 bits
  15.  X           : Word=0;
  16.  Y           : Word=0;
  17.  bitr        : Word=$80;
  18.  C           : Byte=0;
  19.  decompressed: array of Byte=();
  20.  compressed  : array of Byte=();
  21.  inptr       : Cardinal=$180;
  22.  outptr      : Cardinal=0;
  23.  offset      : Cardinal=0;
  24.  pointer     : Cardinal=0;
  25.  F           : TFileStream=nil;
  26.  i           : Integer;
  27.  OK          : Boolean=True;
  28.  
  29. label
  30.   decode_literal, dzx0s_new_offset, dzx0s_copy, done;
  31.  
  32. //Arithmetic shift left C<-b<-0
  33. procedure ASL(var b: Word);
  34. begin
  35.  b:=b<<1;
  36.  C:=b div $100;
  37.  b:=b mod $100;
  38. end;
  39.  
  40. //Logical shift right 0->b->C
  41. procedure LSR(var b: Word);
  42. begin
  43.  b:=b mod $100;
  44.  C:=b AND 1;
  45.  b:=b>>1;
  46. end;
  47.  
  48. //Rotate left one bit C<-b<-C
  49. procedure ROL(var b: Word);
  50. begin
  51.  b:=(b<<1)OR C;
  52.  C:=b div $100;
  53.  b:=b mod $100;
  54. end;
  55.  
  56. //Get the next byte from the compressed stream
  57. function get_byte(var ok: Boolean): Word;
  58. begin
  59.  Result:=0;
  60.  if inptr<Length(compressed) then
  61.  begin
  62.   Result:=compressed[inptr];
  63.   inc(inptr);
  64.   ok    :=True;
  65.  end else ok:=False;
  66. end;
  67.  
  68. //Put a byte into the next location in the decompressed stream
  69. procedure save_byte(b: Word);
  70. begin
  71.  if outptr>=Length(decompressed) then SetLength(decompressed,outptr+1);
  72.  decompressed[outptr]:=b mod $100;
  73.  inc(outptr);
  74. end;
  75.  
  76. //Get an encoded elias number
  77. function get_elias: Boolean;
  78. label
  79.   elias_start, elias_get, elias_skip1;
  80. begin
  81.  Result:=True;
  82.  X:=1;
  83.  goto elias_start;
  84.  
  85. elias_get:
  86.  A:=X;
  87.  ASL(bitr);
  88.  ROL(A);
  89.  X:=A;
  90.  
  91. elias_start:
  92.  ASL(bitr);
  93.  if bitr<>0 then goto elias_skip1;
  94.  A   :=get_byte(Result);
  95.  ROL(A);
  96.  bitr:=A;
  97.  
  98. elias_skip1:
  99.  if C=1 then goto elias_get;
  100. end;
  101.  
  102. //Execution starts here
  103. begin
  104.  A     :=0;    //Accumulator
  105.  X     :=0;    //X register
  106.  Y     :=0;    //Y register
  107.  C     :=0;    //C flag (carry)
  108.  bitr  :=$80;
  109.  inptr :=$180; //Compressed data starts here
  110.  outptr:=$0;
  111.  OK    :=True; //In theory, this should never go False
  112.  //Load the compressed file in
  113.  F     :=TFileStream.Create('prelude',fmOpenRead or fmShareDenyNone);
  114.  SetLength(compressed,F.Size);
  115.  F.Read(compressed[0],F.Size);
  116.  F.Free;
  117.  
  118. decode_literal:
  119.  OK:=get_elias;
  120.  if not OK then goto done;
  121.  while X<>0 do
  122.  begin
  123.   A:=get_byte(OK);
  124.   if not OK then goto done;
  125.   save_byte(A);
  126.   dec(X);
  127.  end;
  128.  ASL(bitr);
  129.  if C=1 then goto dzx0s_new_offset;
  130.  OK:=get_elias;
  131.  if not OK then goto done;
  132.  
  133. dzx0s_copy:
  134.  A:=outptr-offset-(1-C);
  135.  if C<0 then C:=0 else C:=1;
  136.  pointer:=A;
  137.  while X<>0 do
  138.  begin
  139.   A:=decompressed[pointer];
  140.   inc(pointer);
  141.   save_byte(A);
  142.   dec(X);
  143.  end;
  144.  ASL(bitr);
  145.  if C=0 then goto decode_literal;
  146.  
  147. dzx0s_new_offset:
  148.  offset:=(offset mod $100)OR Y<<8;
  149.  A:=get_byte(OK);
  150.  if not OK then goto done;
  151.  LSR(A);
  152.  if C=1 then
  153.  begin
  154.   if A=$7F then goto done;
  155.   offset:=(offset mod $100)OR A<<8;
  156.   A:=get_byte(OK);
  157.   if not OK then goto done;
  158.  end;
  159.  offset:=(offset AND $FF00)OR A;
  160.  OK:=get_elias;
  161.  if not OK then goto done;
  162.  inc(X);
  163.  C:=X div $100; //In 6502, INX does not affect the C flag, so this can be removed
  164.  X:=X mod $100;
  165.  if C=0 then goto dzx0s_copy;
  166.  
  167. done:
  168.  //Display the output
  169.  WriteLn(#$1B'[91m'+#$1B'[7m'+StringOfChar('=',63)+#$1B'[0m');
  170.  WriteLn(#$1B'[1mCompressed length  :'+#$1B'[93m 0x'+IntToHex(Length(compressed),4)+#$1B'[0m');
  171.  WriteLn(#$1B'[1mDecompressed length:'+#$1B'[93m 0x'+IntToHex(Length(decompressed),4)+#$1B'[0m');
  172.  WriteLn(#$1B'[91m'+#$1B'[7m'+StringOfChar('-',63)+#$1B'[0m');
  173.  WriteLn(#$1B'[1m$00 $01 $02 $03 $04 $05 $06 $07 $08 $09 $0A $0B $0C $0D $0E $0F'+#$1B'[0m');
  174.  WriteLn(#$1B'[94m'+StringOfChar('-',63)+#$1B'[0m');
  175.  for i := 0 to High(decompressed) do
  176.  begin
  177.   Write('$'+IntToHex(decompressed[i],2)+' ');
  178.   if i mod 16=15 then WriteLn('');
  179.  end;
  180.  Writeln;
  181.  //Save the output to a file
  182.  F:=TFileStream.Create('output',fmCreate or fmShareDenyNone);
  183.  F.Write(decompressed[0],Length(decompressed));
  184.  F.Free;
  185. end.
It works, but would be nice for something a wee bit tidier.
Still haven't got a clue how it works, mind.

MathMan

  • Sr. Member
  • ****
  • Posts: 472
Re: ZX02 implementation in Pascal
« Reply #4 on: January 08, 2026, 01:37:48 pm »
Fascinating  ;)

Do you by any chance have a compressed file, that could be used for testing?

BTW: only had a glimpse look at it, but think it is a variant of coded sub-string expression.

Cheers,
MathMan

Edit: Did a little reading inbetween. As guessed it is indeed a variant of the Lempel-Ziv-Welch family of compressors. It is based on LZSS with certain fine-tuning of parameters to accomodate 'low-end' CPU like Z80, 6502 etc. If you still want to understand how it is working look here for an explanation on LZSS

https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Storer%E2%80%93Szymanski
« Last Edit: January 08, 2026, 02:05:24 pm by MathMan »

geraldholdsworth

  • Sr. Member
  • ****
  • Posts: 284
Re: ZX02 implementation in Pascal
« Reply #5 on: January 08, 2026, 02:44:36 pm »
This is a file I've been working with (unzip it first - the system wouldn't let me attach it as is). The first 0x180 bytes are uncompressed, so these can be skipped over.

geraldholdsworth

  • Sr. Member
  • ****
  • Posts: 284
Re: ZX02 implementation in Pascal
« Reply #6 on: January 08, 2026, 03:24:16 pm »
Edit: Did a little reading inbetween. As guessed it is indeed a variant of the Lempel-Ziv-Welch family of compressors. It is based on LZSS with certain fine-tuning of parameters to accomodate 'low-end' CPU like Z80, 6502 etc. If you still want to understand how it is working look here for an explanation on LZSS

I think my main problem of understanding was that I couldn't relate the Elias codes in the given file to the description of how Elias encoding worked.

MathMan

  • Sr. Member
  • ****
  • Posts: 472
Re: ZX02 implementation in Pascal
« Reply #7 on: January 09, 2026, 12:26:37 am »
Working on it, but will take longer than anticipated.

This thing is side effected no end, damn! Probably coming from the 6502 implementation base, where the author tried to minimze memory footprint to the extreme.

I could already simplify several things, but the decompression part has one nasty little item in it that defies translation to a more comprehensible while/repeat/for semantic. Will have to spend more time on analysing the algorithm. More, once there is more ...

jamie

  • Hero Member
  • *****
  • Posts: 7544
Re: ZX02 implementation in Pascal
« Reply #8 on: January 09, 2026, 01:27:11 am »
I understand the 6502 ASM better than anything else that has been presented here %)

I wrote a couple of IDE/ASM for the 6502/6510 etc systems years ago.

If I feel compelled to convert it, I may :D

Jamie
The only true wisdom is knowing you know nothing

geraldholdsworth

  • Sr. Member
  • ****
  • Posts: 284
Re: ZX02 implementation in Pascal
« Reply #9 on: January 09, 2026, 07:15:42 am »
I've tidied up the code a little bit, and also added some error checking. It's starting to look a litle better - I've managed to get rid of the labels and gotos...but the solution isn't much better, I feel.
Code: Pascal  [Select][+][-]
  1. program Project1;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. uses
  6.  {$IFDEF UNIX}
  7.  cthreads,
  8.  {$ENDIF}
  9.  Classes,
  10.  SysUtils
  11.  { you can add units after this };
  12.  
  13. type
  14.   TByteArray = array of Byte;
  15.  
  16. function ZX02Decompress(compressed: TByteArray;start: Cardinal=0): TByteArray;
  17. //Translated from the 6502 code as used in Repton 3 Redux
  18. //Thank you to Matthew Atkinson for the source code
  19. //See also: https://github.com/dmsc/zx02
  20. var
  21.  A      : Word=0;
  22.  X      : Byte=0;
  23.  bitr   : Word=$80;
  24.  C      : Byte=0;
  25.  inptr  : Cardinal=0;
  26.  outptr : Cardinal=0;
  27.  //Arithmetic shift left C<-A<-0
  28.  procedure ASL;
  29.  begin
  30.   bitr:=bitr<<1;
  31.   C   :=bitr div $100;
  32.   bitr:=bitr mod $100;
  33.  end;
  34.  //Rotate left one bit C<-A<-C
  35.  procedure ROL;
  36.  begin
  37.   A:=(A<<1)OR C;
  38.   C:=A div $100;
  39.   A:=A mod $100;
  40.  end;
  41.  //Get the next byte from the compressed stream, and update the pointer
  42.  function GetByte: Boolean;
  43.  begin
  44.   if inptr<Length(compressed)then
  45.   begin
  46.    A:=compressed[inptr]mod$100;               //Ensure it is 8 bits
  47.    inc(inptr);
  48.    Result:=True;
  49.   end
  50.   else
  51.   begin
  52.    A:=$FF00;                                  //>$FF Indicates error condition
  53.    Result:=False;
  54.   end;
  55.  end;
  56.  //Put a byte into the next location in the decompressed stream
  57.  procedure SaveByte(b: Word);
  58.  begin
  59.   if outptr>=Length(Result)then SetLength(Result,outptr+1);
  60.   Result[outptr]:=b mod$100;                  //b should not be more than $FF
  61.   inc(outptr);
  62.  end;
  63.  //Get an encoded elias number
  64.  function GetElias: Boolean;
  65.  var
  66.   Lfirst: Boolean=True;
  67.  begin
  68.   X     :=1;
  69.   Lfirst:=True;
  70.   repeat
  71.    if not Lfirst then
  72.    begin
  73.     A:=X mod$100;
  74.     ASL;
  75.     ROL;
  76.     X:=A;
  77.    end;
  78.    Lfirst:=False;
  79.    ASL;
  80.    if bitr=0 then
  81.     if GetByte then
  82.     begin
  83.      ROL;
  84.      bitr:=A;
  85.     end;
  86.   until(C=0)or(A>$FF);
  87.   Result:=A<$100;                             //Error state
  88.  end;
  89. //Function body starts here
  90. var
  91.  action : String='decode_literal';
  92.  Y      : Byte=0;
  93.  offset : Cardinal=0;
  94.  pointer: Cardinal=0;
  95. begin
  96.  //Initialise
  97.  A   :=0;  //Accumulator
  98.  X   :=0;  //X register
  99.  Y   :=0;  //Y register
  100.  C   :=0;  //C flag (carry)
  101.  bitr:=$80;
  102.  //A starting position other than zero has been specified.
  103.  if start>0 then //Copy the uncompressed data across first
  104.  begin
  105.   //Set our output container
  106.   SetLength(Result,start);
  107.   //Copy the data across
  108.   for inptr:=0 to start-1 do Result[inptr]:=compressed[inptr];
  109.   //Set the starting points
  110.   inptr :=start;
  111.   outptr:=start;
  112.  end
  113.  else
  114.  begin //Otherwise, just set them to zero
  115.   inptr :=$0;
  116.   outptr:=$0;
  117.  end;
  118.  //Start the decompression
  119.  repeat
  120.   //No compression - just copy from src to dest
  121.   if action='decode_literal' then
  122.   begin
  123.    if not GetElias then exit(nil);            //Error
  124.    while X<>0 do
  125.    begin
  126.     if not GetByte then exit(nil);            //Error
  127.     SaveByte(A);
  128.     dec(X);
  129.    end;
  130.    ASL;
  131.    if C=1 then action:='get_new_offset'
  132.    else
  133.    begin
  134.     if not GetElias then exit(nil);           //Error
  135.     action:='copybytes';
  136.    end;
  137.   end;
  138.   //Copy a series of bytes N number of times
  139.   if action='copybytes' then
  140.   begin
  141.    pointer:=(outptr-offset-(1-C));
  142.    while X<>0 do
  143.    begin
  144.     if pointer>=Length(Result) then exit(nil);//Error
  145.     SaveByte(Result[pointer]);
  146.     inc(pointer);
  147.     dec(X);
  148.    end;
  149.    ASL;
  150.    if C=0 then action:='decode_literal' else action:='get_new_offset';
  151.   end;
  152.   //Get new offset
  153.   if action='get_new_offset' then
  154.   begin
  155.    offset:=(offset mod $100)OR Y<<8;
  156.    if not GetByte then exit(nil);             //Error
  157.    A:=A mod $100;
  158.    C:=A AND 1;
  159.    A:=A>>1;
  160.    if C=1 then
  161.    begin
  162.     if A=$7F then exit;//Reached the end of the compressed data
  163.     offset:=(offset mod $100)OR A<<8;
  164.     if not GetByte then exit(nil);            //Error
  165.    end;
  166.    offset:=(offset AND $FF00)OR A;
  167.    if not GetElias then exit(nil);            //Error
  168.    inc(X);
  169.    X:=X mod $100;
  170.    if C=0 then action:='copybytes' else action:='';
  171.   end;
  172.  until action='';
  173.  if A<>$7F then SetLength(Result,0);          //Error
  174. end;
  175.  
  176. //Main program +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  177. var
  178.  decompressed: TByteArray=();
  179.  compressed  : TByteArray=();
  180.  F           : TFileStream=nil;
  181.  filename    : String='';
  182.  outputfile  : String='';
  183.  startofdata : Cardinal=0;
  184. begin
  185.  //Get the file specified
  186.  filename  :=ParamStr(1);
  187.  //And the optional output file
  188.  outputfile:=ParamStr(2);
  189.  //Starting point
  190.  startofdata:=StrToIntDef('$'+ParamStr(3),0);
  191.  //If no output file specified, then make our own
  192.  if outputfile='' then outputfile:=ExtractFilePath(filename)+'-decompressed';
  193.  //If no input file, can't do any decompression
  194.  if filename<>'' then
  195.   //Needs to exist too
  196.   if FileExists(filename) then
  197.   begin
  198.    //Load the compressed file in
  199.    try
  200.     F:=TFileStream.Create(filename,fmOpenRead or fmShareDenyNone);
  201.     SetLength(compressed,F.Size);
  202.     F.Read(compressed[0],F.Size);
  203.     F.Free;
  204.    except //File error has occurred.
  205.     on E:Exception do WriteLn(#13#10#$1B'[91m'#$1B'[1mError: '+E.Message
  206.                              +#$1B'[0m');
  207.    end;
  208.    //Decompress the data
  209.    decompressed:=ZX02Decompress(compressed,startofdata);
  210.    //Display the result
  211.    if Length(decompressed)<Length(compressed) then //Was it a success?
  212.     WriteLn(#13#10#$1B'[91m'#$1B'[1m'
  213.            +'Decompression failed. Likely reason: not a valid ZX02 file.'
  214.            +#$1B'[0m') //No
  215.    else
  216.    begin //Yes
  217.     WriteLn(#13#10#$1B'[92m'#$1B'[1mDecompression success.'+#$1B'[0m');
  218.     WriteLn(#$1B'[1mCompressed length  :'
  219.            +#$1B'[93m 0x'+IntToHex(Length(compressed)  ,4)+#$1B'[0m');
  220.     WriteLn(#$1B'[1mDecompressed length:'
  221.            +#$1B'[93m 0x'+IntToHex(Length(decompressed),4)+#$1B'[0m');
  222.     //Save the output to a file
  223.     try
  224.      F:=TFileStream.Create(outputfile,fmCreate or fmShareDenyNone);
  225.      F.Write(decompressed[0],Length(decompressed));
  226.      F.Free;
  227.     except //File error has occurred.
  228.      on E:Exception do WriteLn(#13#10#$1B'[91m'#$1B'[1mError: '+E.Message
  229.                               +#$1B'[0m');
  230.     end;
  231.    end; //Errors due to wrong input
  232.   end else WriteLn(#13#10#$1B'[91m'#$1B'[1mFile "'+filename+'" does not exist.'
  233.                   +#$1B'[0m')
  234.  else WriteLn(#13#10#$1B'[93m'#$1B'[1m'
  235.              +'Syntax: <input file>[<output file>[<start>]]'+#$1B'[0m');
  236. end.

creaothceann

  • Sr. Member
  • ****
  • Posts: 268
Re: ZX02 implementation in Pascal
« Reply #10 on: January 09, 2026, 09:31:24 am »
I get a range check error at "pointer:=(outptr-offset-(1-C));" with the file provided above.

Anyway, the main program could be clearer:

Code: Pascal  [Select][+][-]
  1. program Elias;
  2. uses
  3.         Classes, SysUtils;
  4.  
  5. {$MinEnumSize 4}
  6.  
  7. type
  8.         TByteArray = array of Byte;
  9.  
  10.  
  11. // Translated from the 6502 code as used in Repton 3 Redux
  12. // Thank you to Matthew Atkinson for the source code
  13. // See also: https://github.com/dmsc/zx02
  14. function ZX02_Decompress(Compressed : TByteArray;  Start : DWord = 0) : TByteArray;
  15. type
  16.         TAction = (none, CopyBytes, DecodeLiteral, GetNewOffset);
  17. var
  18.         Action : TAction = DecodeLiteral;
  19.         A      : DWord   =   0;  // accumulator
  20.         X      : DWord   =   0;  // index register
  21.         Y      : DWord   =   0;  // index register
  22.         BitR   : DWord   = $80;
  23.         carry  : DWord   =   0;  // carry flag
  24.         Offset : DWord   =   0;
  25.         Ptr    : DWord   =   0;
  26.         PtrIn  : DWord   =   0;
  27.         PtrOut : DWord   =   0;
  28.  
  29.         procedure ASL;  // arithmetic shift left: carry <-- A <-- 0
  30.         begin
  31.                 BitR  := BitR << 1;
  32.                 carry := BitR div $100;
  33.                 BitR  := BitR mod $100;
  34.         end;
  35.  
  36.  
  37.         procedure ROL;  // rotate left one bit: carry <-- A <-- carry
  38.         begin
  39.                 A     := (A << 1) OR carry;
  40.                 carry := A div $100;
  41.                 A     := A mod $100;
  42.         end;
  43.  
  44.  
  45.         function GetByte : Boolean;  // get the next byte from the compressed stream and update the pointer
  46.         begin
  47.                 if (PtrIn < SizeUInt(Length(Compressed))) then begin
  48.                         A := Compressed[PtrIn];
  49.                         Inc(PtrIn);
  50.                         Result := True;
  51.                 end else begin
  52.                         A      := $FF00;  // >$FF indicates error condition
  53.                         Result := False;
  54.                 end;
  55.         end;
  56.  
  57.  
  58.         procedure SaveByte(b : Word);  // put a byte into the next location in the decompressed stream
  59.         begin
  60.                 if (PtrOut >= SizeUInt(Length(Result))) then SetLength(Result, PtrOut + 1);
  61.                 Result[PtrOut] := b mod $100;  // b should not be more than $FF
  62.                 Inc(PtrOut);
  63.         end;
  64.  
  65.  
  66.         function GetElias : Boolean;  // get an encoded elias number
  67.         var
  68.                 LFirst : Boolean = True;
  69.         begin
  70.                 X      := 1;
  71.                 LFirst := True;
  72.                 repeat
  73.                         if not LFirst then begin
  74.                                 A := X mod $100;
  75.                                 ASL;
  76.                                 ROL;
  77.                                 X := A;
  78.                         end;
  79.                         LFirst := False;
  80.                         ASL;
  81.                         if (BitR = 0) then  if GetByte then begin
  82.                                 ROL;
  83.                                 BitR := A;
  84.                         end;
  85.                 until (carry = 0) or (A > $FF);  // error state
  86.                 Result := (A < $100);
  87.         end;
  88.  
  89. begin
  90.         // initialization
  91.         if (Start > 0) then begin
  92.                 // copy uncompressed data
  93.                 SetLength(Result, Start);
  94.                 for PtrIn := 0 to (Start - 1) do  Result[PtrIn] := Compressed[PtrIn];
  95.                 // set the starting points
  96.                 PtrIn  := Start;
  97.                 PtrOut := Start;
  98.         end;
  99.         // decompression
  100.         repeat
  101.                 // no compression - just copy from src to dest
  102.                 if (Action = DecodeLiteral) then begin
  103.                         if not GetElias then exit(NIL);  // error
  104.                         while (X <> 0) do begin
  105.                                 if not GetByte then exit(NIL);  // error
  106.                                 SaveByte(A);
  107.                                 Dec(X);
  108.                         end;
  109.                         ASL;
  110.                         if (carry = 1) then begin
  111.                                 Action := GetNewOffset;
  112.                         end else begin
  113.                                 if not GetElias then exit(NIL);  // error
  114.                                 Action := CopyBytes;
  115.                         end;
  116.                 end;
  117.                 // copy a series of bytes N number of times
  118.                 if (Action = CopyBytes) then begin
  119.                         Ptr := (PtrOut - Offset - (1 - carry));
  120.                         while (X <> 0) do begin
  121.                                 if (Ptr >= SizeUInt(Length(Result))) then exit(NIL);  // error
  122.                                 SaveByte(Result[Ptr]);
  123.                                 Inc(Ptr);
  124.                                 Dec(X);
  125.                         end;
  126.                         ASL;
  127.                         if (carry = 0)
  128.                                 then Action := DecodeLiteral
  129.                                 else Action := GetNewOffset;
  130.                 end;
  131.                 // get new offset
  132.                 if (Action = GetNewOffset) then begin
  133.                         Offset := (Offset mod $100) OR (Y << 8);
  134.                         if not GetByte then exit(NIL);  // error
  135.                         A     := A mod $100;
  136.                         carry := A AND 1;
  137.                         A     := A >> 1;
  138.                         if (carry = 1) then begin
  139.                                 if (A = $7F) then exit;  // end of compressed data
  140.                                 Offset := (Offset mod $100) OR (A << 8);
  141.                                 if not GetByte then exit(NIL);  // error
  142.                         end;
  143.                         Offset := (Offset AND $FF00) OR A;
  144.                         if not GetElias then exit(NIL);  // error
  145.                         Inc(X);
  146.                         X := X mod $100;
  147.                         if (carry = 0)
  148.                                 then Action := CopyBytes
  149.                                 else Action := none;
  150.                 end;
  151.         until (Action = none);
  152.         if (A <> $7F) then SetLength(Result, 0);  // error
  153. end;
  154.  
  155.  
  156. // main program
  157.  
  158. var
  159.         Compressed     : TByteArray  = ();
  160.         Decompressed   : TByteArray  = ();
  161.         F              : TFileStream = NIL;
  162.         InputFileName  : String      = '';
  163.         OutputFileName : String      = '';
  164.         StartOfData    : DWord       = 0;
  165.  
  166. {$R *.res}
  167.  
  168. begin
  169.         InputFileName  :=                   ParamStr(1);      // get  input file (needs to exist)
  170.         OutputFileName :=                   ParamStr(2);      // get output file (optional)
  171.         StartOfData    := StrToIntDef('$' + ParamStr(3), 0);  // get starting point
  172.         if (InputFileName = '')          then begin  WriteLn(#13#10#$1B'[93m'#$1B'[1m' + 'Syntax: <input file>[<output file>[<Start>]]' + #$1B'[0m');  Halt(-1);  end;
  173.         if not FileExists(InputFileName) then begin  WriteLn(#13#10#$1B'[91m'#$1B'[1m' + 'File "' + InputFileName + '" does not exist.' + #$1B'[0m');  Halt(-2);  end;
  174.         if (OutputFileName = '')         then begin  OutputFileName := ExtractFilePath(InputFileName) + '-decompressed';                                          end;
  175.         // load data
  176.         try
  177.                 F := TFileStream.Create(InputFileName, fmOpenRead or fmShareDenyNone);
  178.                 SetLength(Compressed,    F.Size);
  179.                 F.Read   (Compressed[0], F.Size);
  180.                 F.Free;
  181.         except
  182.                 on E:Exception do begin
  183.                         WriteLn(#13#10#$1B'[91m'#$1B'[1mError: ' + E.Message + #$1B'[0m');
  184.                         exit;
  185.                 end;
  186.         end;
  187.         // decompress
  188.         Decompressed := ZX02_Decompress(Compressed, StartOfData);
  189.         // display result
  190.         if (Length(Decompressed) < Length(Compressed)) then begin
  191.                 WriteLn(#13#10#$1B'[91m'#$1B'[1mDecompression failed. Likely reason: not a valid ZX02 file.'+#$1B'[0m');
  192.                 Halt(-3);
  193.         end;
  194.         WriteLn(#13#10#$1B'[92m'#$1B'[1mDecompression success.'+#$1B'[0m');
  195.         WriteLn(#$1B'[1m' + '  compressed length:' + #$1B'[93m 0x' + IntToHex(Length(  Compressed), 4) + #$1B'[0m');
  196.         WriteLn(#$1B'[1m' + 'decompressed length:' + #$1B'[93m 0x' + IntToHex(Length(Decompressed), 4) + #$1B'[0m');
  197.         // save
  198.         try
  199.                 F := TFileStream.Create(OutputFileName, fmCreate OR fmShareDenyNone);
  200.                 F.Write(Decompressed[0], Length(Decompressed));
  201.                 F.Free;
  202.         except
  203.                 on E:Exception do begin
  204.                         WriteLn(#13#10#$1B'[91m'#$1B'[1mError: ' + E.Message + #$1B'[0m');
  205.                         Halt(-4);
  206.                 end;
  207.         end;
  208. end.
« Last Edit: January 09, 2026, 09:38:07 am by creaothceann »

geraldholdsworth

  • Sr. Member
  • ****
  • Posts: 284
Re: ZX02 implementation in Pascal
« Reply #11 on: January 09, 2026, 03:05:22 pm »
I get a range check error at "pointer:=(outptr-offset-(1-C));" with the file provided above.

Did you specify the start as 0x180? The file provided has uncompressed data for the first 0x180 bytes, then the compressed stuff starts.

EDIT: Actually, that shouldn't matter as it should trap it as an error and exit. I've just tried it with no start offset and it, correctly, reports that it is not a compressed file. No range check errors.
« Last Edit: January 09, 2026, 03:14:23 pm by geraldholdsworth »

creaothceann

  • Sr. Member
  • ****
  • Posts: 268
Re: ZX02 implementation in Pascal
« Reply #12 on: January 09, 2026, 07:38:58 pm »
It only works if I disable all debugging options. While understandable (the 6502 silently truncates values to 8- or 9-bit) it's unfortunate for debugging...

geraldholdsworth

  • Sr. Member
  • ****
  • Posts: 284
Re: ZX02 implementation in Pascal
« Reply #13 on: January 09, 2026, 07:59:45 pm »
I made pointer a Cardinal so that it doesn't go below zero. However, it can be set as an Integer, and then an extra check is required to make sure it is not negative (i.e., in range of the array it's being used as an index for). So, in effect, I was just being lazy. :-[

Or, of course, just use the abs (?) keyword to make sure it is positive.

cdbc

  • Hero Member
  • *****
  • Posts: 2631
    • http://www.cdbc.dk
Re: ZX02 implementation in Pascal
« Reply #14 on: January 09, 2026, 08:21:44 pm »
Hi
Quote
I made pointer a Cardinal so that it doesn't go below zero.
Warning: The above will only work on 32bit, NOT 64bit...!
A safer approach is to make it a 'PtrUInt' instead of a 'Cardinal', because a 'PtrUInt' is the Unsigned Integer of pointer/register-size of the current platform. The same goes for 'PtrInt' => pointer/register-sized Signed Integer
Regards Benny
If it ain't broke, don't fix it ;)
PCLinuxOS(rolling release) 64bit -> KDE6/QT6 -> FPC Release -> Lazarus Release &  FPC Main -> Lazarus Main

 

TinyPortal © 2005-2018