前提として、SDカードの技術情報が必要なのですが、こちらはまた別に書きます。
PMVフォーマットのベースは、ベタのビットマップファイルです。 というかむしろ、VRAMのイメージそのものです。 スクリーンモード2のセミグラフィックモードの対応は、エンコーダ側で変換を行っています。ファイルフォーマットPMVフォーマットでは、ムービーファイルの容量の低減と再生時の負荷低減を目指して、 run-lengthとフレーム差分の2種類の方法で圧縮をかけています。 この2つの圧縮アルゴリズムはフレーム内に混在可能です。 現在のエンコーダは、どちらも同じ圧縮率になる場合は、 再生時の負荷が低い(後述)フレーム差分を優先します。
バッファはオプションの設定により、 21.5kB(0x6a00-0xbfff)または29.5kB(0x6a00-0xdfff)使います。
PMVファイルは、ヘッダとボディから構成されます。ボディは圧縮していますが、可能な限り軽量にするため、CRCなどのエラーチェックは一切使いません。 ファイルの全体構成を以下の図に示します。
ヘッダはPC-6001のテープフォーマットや拙作のMP6フォーマットにあわせ、16バイトとします。定義は以下のようになります。現在のプレイヤーではversion, compression, imagetype, reservedは使っていません。
圧縮技術struct PMVHeader {
char magic[4]; // "PMVt"
unsigned char version; // verion, 1
unsigned char compression; // compression method, 1
unsigned char imagetype; // 0: gray scale bmp, 1: pbm, // 2: pbm w/o header, 3: semi-graphic(6)
unsigned char fps; // frames per second
unsigned short width; // frame width (pixels)
unsigned short height; // frame height (pixels)
unsigned short framesize; // maximum compressed frame size (bytes)
unsigned char reserved[2]; // for future use
} __attribute__((packed));
考え方
圧縮の実際SDカードからのロード時間を短縮するため、ボディには圧縮をかけています。
今回ちょっと悩んだのが圧縮方法です。Deflateアルゴリズムのように、 比較的展開側の負荷を低減する方法は確立していますが、 それでもPC-6001には荷が重過ぎます。 そもそも標準のDeflateアルゴリズムははハフマン符号テーブルと 32kBのバッファを要求します。 また、ハフマン符号がビット単位の処理になるため、Z80では効率的に処理できません。
そこで、「バイト単位での処理」「展開用のバッファを要求しない」 ということをを基本にしました。 すると、おのずから方策は限られてきます。
まずはRLE(Run-Length Encoding)による圧縮を行いました。 要は、連続する同じバイトはまとめるということです。 モノクロの画像データは連続する白または黒が結構あるため、 これでもそこそこ圧縮が可能となります。
次に、動画における「連続するフレームは似たような画像である」という性質に着目します。 直前のフレームと同じデータなら、その部分の情報は記録しなくていいわけです。
では具体的に見ていきましょう。
まずは、データ内に特別な値である「マジック」を用意します。
読み込んだバイトがマジックでないなら、それは純粋にデータです。 現状マジックは1種類しかないので、 256種類のデータのうち255種類は普通のデータとして処理します。
1バイト目にマジックを見つけたら、専用の処理に入ります。2バイト目が0xffならフレームエンド、0xfeならマジックそのものです。 それ以外の場合、0x00-0x03ならフレーム差分、0x04-0xfdならRLE圧縮と解釈します。フレーム差分、RLEの場合は3バイト目を読みこみます。
<magic> 00 <length>: same as the previous frame (0 <= length <= 255)
<magic> 01 <length>: same as the previous frame (256 <= length <= 511)
<magic> 02 <length>: same as the previous frame (512 <= length <= 767)
<magic> 03 <length>: same as the previous frame (768 <= length <= 1023)
<magic> 04-fd <byte>: run-length for a byte (4 <= length <= 253)
<magic> fe: <magic>
<magic> ff: end of a frame
other: byte
プレイヤーフレーム差分の場合は2バイト目と3バイト目を合わせて長さとします。これにより、最大1023バイトまでの前フレームと同じバイト列を3バイトで表現できます。RLEの場合は3バイト目は連続しているデータです。上記のように、253バイトまでの連続する同じバイト列を3バイトで表現できます。
マジックはいくつかのムービーソースから出現頻度の低い値を統計的に算出し、現在は0x55を使っています。
プレイヤーはSDカードからPMVファイルのヘッダをロードし、ファイルの正当性(最初の4バイト, "PMVt")をチェックします。 その後画面サイズを元に画面モードを設定し、fps値から1フレームにかけられる時間を算出し、保存します。
バッファリング
pmvプレイヤーは、 最低でも1画面分の情報をメモリに読み込んでから画面に展開します。 そのためにヘッダのframesizeを使います。 フレームの描画実行中はロードしないため、 描画しようとしたときにframesize分をロードしていないときは、 ロードを実行します。 バーストモード時は、 framesizeにかかわらず常に読み込める限り読み込んでから描画します。
画面スイッチング圧縮フォーマットから考えれば、 理論的には実際に必要な1フレーム分のデータ量は、 1画面のVRAMの量を超すことがありえる(実際はまずありえませんが)ので、 エンコード時に正しくframesizeを設定しておくことが必要です。 エンコーダは実行時に圧縮したすべてのフレームの中での最大値を検出し、 これをframesizeに設定します。 このため、プレイヤーはこのframesizeの値以上のバッファがあれば再生が可能です。 実際には、現在のSDカードドライバはセクタ単位のアクセスを行うので、 バッファ量は512バイト境界で切り上げが必要です。
バッファはラウンドロビンで使います。バッファを大きく取るほど、バースト再生時のパフォーマンスがあがりますが、バーストモードでない場合は意味がありません。
バーストモードの場合、 ファイルをすべて読み終えた後はバッファにデータが残っていますので、 終了する前にこれら読み込み済みのフレームをすべて描画してから終了します。
画面描画デフォルトでは、プレイヤーはフレームごとに、 PC-6001のスクリーン2(0xe000-0xf9ff)とスクリーン3(0xc000-0xd9ff)を交互に描画し、 描画し終わってから画面を切り替えることによってちらつきを抑えます (昔のゲームなどにもよく使われたテクニックですね)。 一方、画面スイッチオフの指定がある場合はスクリーン2のみに描画します。 128x96程度の解像度だと、これでもそれほどちらつきません。 64x48ではデフォルト(スイッチあり)のほうがむしろムダですね。
描画自体はいたってシンプルで、バッファに読んであるデータを展開して、 VRAMに転送するだけです。 その際、上述した2種類の圧縮アルゴリズムにより、マジックを読んだ際には 展開を行います。
フレーム差分圧縮がかかっているデータは、 画面スイッチングを使わない場合は書き換えを行わず、 VRAMの描画対象メモリポインタを進めるのみです。 解像度が128x96だと書き換え量が最大1.5kBなので、 画面スイッチを行わなくてもそれほどスキャンラインは見えません。 そのため、似たようなフレームが続くと、 ほとんどメモリポインタを進めるだけで1フレームの描画が終わってしまうため、 圧縮効果と高速描画の両方が期待できます。
バッファ量256x192(6kB)だとさすがに重いので、 ちらつきを抑えるために画面スイッチが必要です。 現在のフレーム差分圧縮は1フレーム前の情報しか使わないため、 画面スイッチによって2フレーム前の情報が残っている場合は利用できません。 そのため、メモリポインタを進めるだけではなく、 前フレームからのコピー(LDIR)を行っています。
これは、2フレーム前の情報を元に圧縮すればいいだけなのですが、 ひとつのムービーファイルで対応するのはデコード時の負荷が厳しいので、 今のところ対応していません。 まぁ、ムービーファイルそのものを別にしてしまえば、 対応はそれほど難しいものではありません。
fpsはフレーム間の描画にかけられる時間を算出するのに使います。 フレーム描画時にPC-6001の2msecタイマを読んで、 時間が余っていればbusy waitします。 wait終了時の時刻を元に次のフレームまでのタイミングを設定するので、 いったん遅れたら無理に取り戻すことはしません。 それをしてしまうと、SDカードからの読み込みで遅れた分を取り戻そうとして、 最高速で描画してしまうためです。
プレイヤーが使えるバッファ量は、以下のようになります。 バーストモード時はこの量を最大限取って実行します。 通常モード時はムービーヘッダのframesizeのバッファがあればいいので、 バッファを大きくしてもあまり意味はありません。
プログラム実行モード | シングルスクリーン | デュアルスクリーン | |
mp6 (ROMあり) | 29.5kB | 21.5kB | |
p6 (ROMなし) | 16.5kB | 8.5kB |
その他
あと何があったかな。続く...?
匿名
画面が真っ暗、でもカーソルは出てる状況。
探して、ここにたどり着きました。
パスワード入力で、復活!
修理に出す寸前でした。ホントにありがとう!