💾

X68000 ZX0圧縮を試す & C言語とアセンブラのリンク

2022/12/25に公開

ZX0圧縮とは

ZX0は、新しめの可逆圧縮展開プログラムです。アルゴリズムはLZSSとLZ77をベースにしたもので、高速な展開速度と、展開プログラムの小ささが特徴です。展開プログラムは実装が容易で、Z80といった8bitCPUでも動かすことが出来ます
ただし、展開速度と圧縮ファイルサイズと展開プログラムの小ささと引き換えに、圧縮時間はかなり長い(遅い)です。圧縮用のプログラムはWindowsで実行します。

そんな特徴を持つZX0形式のドキュメントを見ていたところ、68000マシン語のデコーダ実装がありましたので、X68000で動かしてみました。

C言語からアセンブリコードを呼び出す

68000のマシン語はとてもわかりやすく、書きやすいのですが、それでもC言語を使った方が開発効率はよいです。ただ、コード最適化と実行ファイルのサイズとのトレードオフになりますから、C言語とマシン語をいい感じに組み合わせるのが良いでしょう。
C言語からマシン語のコードを呼び出す方法をすっかり忘れてしまっていたので、調べた内容をメモとして残しておきます。これが最適な書き方ではないと思いますので、参考程度に。

C言語側(呼び出し側)

#include <stdio.h>

// マシン語側の関数定義
int AsAddFunc(int a, int b);

void main()
{
    int r;

    // 2個のINTの引数を渡して、INTの戻り値を受け取る
    r = AsAddFunc(1, 2);
    printf("result:%d\n", r);
}

アセンブリ側(呼び出され側)

        ; AsAddFuncという関数を外部から参照できるように宣言
        ; 関数名は_から始める規約
        .xdef   _AsAddFunc

        ; 2個のINTの引数を受け取る時のオフセットをアセンブラに計算してもらう定義
        ; ここがなぜ8なのか & linkの役割については68000関連のマシン語の資料を参照してください
        .offset 8
inParam1        ds.l    1
inParam2        ds.l    1

        .text

	; 2個の引数を加算して返すだけの処理	
_AsAddFunc:
        link    a6,#0
        ; ・movemではなくてmoveでいいのですが、お約束の記述。
        ; ・XCではd0, d1, d2 / a0, a1, a2は壊しても良いそうです(GCCは不明)が、
        ;  無難に退避しておきます。
        movem.l d1,-(sp)

        ; 引数取り出し
        ; d0が第一引数、d1が第二引数
        move.l inParam1(a6),d0
        move.l inParam2(a6),d1
        ; d0が戻り値
        add.l   d1,d0

        movem.l (sp)+,d1
        unlk    a6
        rts

        .end

C言語からの引数はスタックに積んで、戻り値はd0に設定するようです。link/unlk(a6)を使わずにsp相対で引数を参照してもいいと思います。
これをコンパイルするには、C言語ソースをmain.c、アセンブリソースをAsAddFunc.sというファイル名にした場合は、

gcc -O4 -osample.x main.c AsAddFunc.s

だけでOKです。

もう一例。
C言語側で用意した配列を、マシン語を使って0x00で埋めるというものです。実用性はありません。

C言語側(呼び出し側)

#include <stdio.h>

void FillZeroAry(unsigned char* addr, unsigned short size);

#define ARRAY_SIZE (4)

void main()
{
    unsigned char array[ARRAY_SIZE];
    int i;

    for (i = 0; i < ARRAY_SIZE; i++) array[i] = 0xff;
    FillZeroAry(array, ARRAY_SIZE);
    for (i = 0; i < ARRAY_SIZE; i++) printf("array[%d] = %02x\n", i, array[i]);
}

アセンブリ側(呼び出され側)

        .xdef   _FillZeroAry

        .offset 8
inParam1        ds.l    1
paddingW        ds.w    1
inParam2        ds.w    1

        .text

        ; 領域を00で埋める
        ; 第一引数:領域先頭アドレス(INT*)
        ; 第二引数:サイズ(WORD)
_FillZeroAry:
        link    a6,#0
        movem.l d0/a0,-(sp)

        ; destination address
        movea.l inParam1(a6),a0
        ; size(word)
        move.w  inParam2(a6),d0
        subq.w  #1,d0
@@:
        move.b  #0,(a0)+
	dbra    d0,@b

        movem.l (sp)+,d0/a0
        unlk    a6
        rts

        .end

ちょっと解せない(私の理解が足りてなさそう)なのは、引数サイズをWORD(ushort)にしても、アセンブリコード側ではLONG(int)としてオフセットを取らないといけないようです。引数がWORDサイズの時は下位16bitから取り出すようですね。

@@や@bはHASの拡張疑似命令です。

圧縮サイズ比較

真面目に圧縮率比較をするのであれば、様々なファイルを使って検証すべきなのですが、ここでは画像データを使って試してみました。

X68000にはちょっと荷が重そうな絵です。これ自体はフルカラーフォーマットのPNGですが、X68000でも表示できるように減色してグラフィックRAM上に展開します。

X68000のグラフィックRAMは512×512ドット / 16Bitカラーで表示できるので、524288Byte = 512KBのメモリを持っているということになります。これをそのままファイルに落とし込むとなると、X68000で扱える標準的な2HDフロッピーディスクは約1.2MBなので、フロッピー1枚には画像ファイルを2つまで入れられるということになりますね。

まず、グラフィックRAMの内容をそのままファイルに書き出したものと、それをZX0で圧縮したファイル、それから、その他の形式で圧縮したものも並べてみました。

ZX0形式への変換は、Windowsのコマンドプロンプトから

zx0 元のファイル名

とするだけです。とてもとても時間がかかります。

形式 SIZE 比率
オリジナル 524,288 1.00
PIC 488,561 0.93
ZX0 410,252 0.78
LZH 388,012 0.74
ZIP 370,994 0.71
PNG 319,813 0.61

X68000で有名なPICの圧縮率が低いのですが、このツタンカーメンの画像が、PICが得意とするアニメ調の画像と真逆だからであって、PICの性能不足ではありません。得手不得手の話ですね。

この表の中では、PNGの圧縮率が意外でした。強いですね。X68000上でPNG画像を表示するフリーウェアpngl.xがありますので、後ほど試してみます。

ZX0の展開実験

ソースコード一式はgithubに置いてあります。
https://github.com/moriyan6001/X68K_ZX0/tree/develop

元々のZX0の68000デコードルーチンが、HASでは文法の違いで通らなかったので修正してあります。また、C言語側からデコードルーチン(zx0_decompress)を呼ぶ時は、間に別の呼び出し処理を挟んでいます。

まずはオリジナルの生データをグラフィックRAMに流し込んだ時の動画から。C言語のfread()を使ってFDからグラフィックRAMへ直接流し込みしています。実機ではなくエミュレータを使っています。X68K EXPERT 10MHz設定です。


1.5秒くらいですね。

続いて、ZX0ファイルをグラフィックRAMに展開している動画です。

6.5秒くらい。圧縮率78%で展開時間は4.3倍という、速くも遅くもないなんともビミョーな結果ですが、他の方式の圧縮展開と比べたら充分に速い方だと思います。

LZH → 生データファイルの展開時間は11.9秒。

ZIP → 生データファイルの展開時間は14.3秒。

圧縮率はZX0よりもLZHやZIPの方が少しばかり高いのですが、展開時間はZX0の方が倍くらい速いようです。

せっかくなので、PNGファイルの展開もしてみました。zennのgif動画仕様の都合で途中で止めていますが、pngl.xを使ったPNG表示は36秒くらいかかりました。

稲妻走るPICは15秒くらいでした。

繰り返しますが、PICの性能が低いのではなく、PICが苦手とする絵だからです。

ここではファイル読込の速度を無視したかったので、RAMディスクを使っています。フロッピーディスクやハードディスクからの読込速度(≒ファイルサイズ)も重要な要素なのですが、結局のところファイルサイズは小さい方がよいよねという話かなと思います。ただ、前述の表でのPNGのように、ファイルサイズが小さくても展開速度が遅かったらサイズのアドバンテージが帳消しになるわけで、展開速度とファイルサイズのトレードオフですね。

今の時代(※これを書いてるのは2022年)は、X68000で使えるSCSI等々の機器の容量が無限大と言える状況なので、ファイルサイズを気にする必要はなかったりしますが、フロッピー1枚にどれだけ詰め込められるか挑戦するというのもホビーパソコンの楽しみ方の一つかと思います。
また、X68000があまり使われなくった頃から今の時代までの間に、プログラミン技術やアルゴリズムも進化したり新しい手法が発明されたりしているので、それをX68000に取り込むというのも楽しみ方の一つかもしれませんね。今時のドット絵をX68000で動かしてみたり、今時の豪華なMIDI機器をX68000に繋げてみるのも面白そうです。

Discussion