MZ-700でアイマスMADを再生してみよう、の実験報告

私です。たけだPです。(挨拶)

MZ-700の表示能力は、40x25文字のテキスト画面だけです。
しかし他機種とは異なり、1文字毎に文字色と背景色の双方を指定することが出来ます。
この仕様のおかげで、MZ-700は不可能のないマシンとなり得るのです。

何が凄いのかティンとこない方は、是非こちらの紹介記事をご覧ください。
ティンときた! −勝手にまとめるアイマスMAD−
この試みの馬鹿らしさ加減について、実に的確にご紹介くださっています。
本当にありがとうございます。




思考実験

MZ-700は、VRAMから文字の種類と色情報を読み出して、その内容をモニタに表示します。
1文字は縦8ドットから構成されますので、
最初の水平同期で、VRAMからデータを読んで、文字パターンの一番上のラインを表示して、
次の水平同期で、また同じアドレスを読んで、文字パターンの2番目のラインを表示して、
そのまた次の水平同期で、また同じアドレスを読んで〜、
といった感じで、1文字を表示するのに8回もVRAMの同じアドレスを読み込みます。
この1文字を表示している間に、VRAMの内容を変更したらどうなるでしょうか?

VRAMのデータの内、文字の種類を書き換えて、ラスタ毎に違う文字を表示する事で
モノクロ高解像度のビットマップを実現しているのが、紅茶羊羹さんの 野球拳2000です。



さて、例えば画面いっぱいに#_(左半分を塗りつぶしたキャラグラ)を表示してみます。
ここで、1文字の表示期間中に、VRAMの色情報の方を書き換えることによって、
例えば1文字の上半分と下半分で、文字色と背景色を変えてやることが可能です。
こうすることで、1画素を4x4ドットとして、画面全体で80x50画素という中解像度の
8色カラービットマップを実現することができるはずです。

VRAMに転送する色情報を、全てオンメモリーで確保するのは容量的に厳しいですが、
大容量のEMMボードからデータを垂れ流せば、アニメーションだって可能かもしれません。
EMMボードは、アドレスを指定するクロックが勿体無いから、データを読み込んだら
自動的にアドレスが先に進む、オートインクリメント形式である必要があります。
ついでに、8253にもデータを出力してやれば、アイマスオープニングと同じ要領で
音声だって再生できるかも、もう夢が広がりまくりです。

という神の啓示を、朝、通勤中に電子工場の脇を歩きながら受信しました(笑)



理論検証

では、実際に実現可能かどうか検証してみましょう。

水平同期期間は、VRAMからデータを読んで画面を表示する水平描画期間と、
ブラウン管の電子銃を、次のラスタの左端に移動する水平帰線期間からなります。
水平描画期間中は、ビデオ回路がVRAMを占有してしまいますので、
CPUは水平帰線期間の間しかVRAMにデータを転送することができません。
うっかり水平描画期間中にVRAMにアクセスすると、次の水平帰線期間がくるまで
CPUは停止してしまうようになっています。(ここ、重要)
これは、実際には描画が行われない垂直帰線期間でも同様です。(ちょー重要)

従って、水平帰線期間中にどれだけデータを転送できるかが勝負となります。
紅茶羊羹さんの調査によると、MZ-700の水平帰線期間は62クロック相当です。
紅茶羊羹さんより、VRAMにアクセス可能な期間は63クロックとご指摘がありました。
多謝、多謝。…1クロック違えば、もうちょっと詰められるかなあ。


メモリにデータを転送する命令は色々あります。普通はLD命令系を使用しますが、
一番少ないクロック数で実行されるのは、実はPUSH HL/DE/BC命令です。
11クロックで2バイト転送できますので、1バイト辺り5.5クロックとなります。

裏レジも駆使して(裏レジを切り替えるEXX命令が4クロック)、
PUSH HL
PUSH DE
PUSH BC
EXX
PUSH HL
PUSH DE
これで59クロックとなります。
このPUSH命令×5で10バイトが、水平帰線期間中に転送できる限界となります。
野球拳2000で、アニメーションする範囲の幅が狭いのもこのためです。

ちなみに、PUSH IX/IY命令はは15クロックですので、
PUSH HL
PUSH DE
PUSH BC
PUSH IX
PUSH IY
とすると63クロック、残念ながらぎりぎり足りません。
ぎりぎり足りるけど、余裕が無さ過ぎてちょっと不安ですね。

PUSH命令で転送するデータは、VRAMにアクセスできない水平描画期間中に、
EMMボードからI/O命令で各レジスタに読み込んでおきます。
LD C, 03H
IN H,(C)
IN L,(C)
IN D,(C)
IN E,(C)
EXX
LD C, 03H
IN H,(C)
IN L,(C)
IN D,(C)
IN E,(C)
IN B,(C)
IN C,(C)
Aレジスタを経由するよりは、こうする方がクロック数を削減できます。

ところでPUSH命令でデータを転送するということは、データの転送先アドレスは
デクリメントされますので、画素データは右から左に転送していきます。
1水平同期期間で転送できるのが10バイトですので、横方向の40文字分のデータを
転送するのには、4水平同期期間×上下=8水平同期期間が必要です。
この間にもVRAMの内容はどんどん画面に表示されていきますので、実際に色が
変化する部分は、左に10文字進む毎に1ラインずつ下にずれていきます。

1行分の転送状況を図にすると、こんな感じになります。
横方向は1マスが1文字を表します。
縦方向は1マスが1ラインですので、8マスで1文字分です。



水平同期期間毎に、赤、青、緑、黄、紫、水色、黄緑、橙の部分のデータが
順番に転送されます。
ここで、白色の部分は、文字の表示期間中に色を変更できない画素です。
この部分については、1つ前のフレームで事前にデータを転送しておきます。



これにも4水平同期期間が必要ですので、1行の範囲を隙間なく描画するには
合計で12水平同期期間が必要ということになります。
また、画素のサイズも、4x1ドットから4x4ドットとまちまちになっています。
プログラムが4水平同期で1ループする構造のため12水平同期としていますが、
実際には11水平同期期間で転送できることがお判り頂けるかと思います。
紅茶羊羹さんによって、これを詰めることで更なる描画範囲拡大を図った
動画が公開されています。(後述)


さて、MZ-700では1垂直同期期間は262水平同期期間となりますので、
262/12=21行分の描画が可能という結論になります。
ということは、1つ前のフレームで事前に転送しておくのに必要な期間は
21×4=84水平同期期間です。
これは垂直帰線期間を超えますので、前のフレームの描画中に、もう次の
フレームのデータを転送し始めるということになってしまいますが、
これは画面の上側から転送するのであれば、書き換えられるVRAMの内容は、
そのフレームでは既に表示し終わっていますので、特に問題とはなりません。

実際のプログラムでは、4水平同期期間で画面の右から左までデータを転送する
ところまでで、プログラムをループさせます。
したがって、ループの最後でJP命令の10クロックが必要です。

また、PUSH命令でデータを転送するためには、ループの先頭で、VRAMの右端の
アドレスをSPに読み込む必要があります。
更に細かい話をすると、262-12*21=10水平同期期間だけ、データを転送しない
端数のラインがありますので、その辺りの例外処理(取得したSPが0000Hの場合、
別処理で10ライン空回しする)の判定も必要です。
メモリ上にテーブルを用意、IYをポインタにしてアドレスを取得していますが、
LD SP,IY
POP HL
BIT 7,H
JP Z,1300H
LD SP,HL
これだけ処理を縮めてやっても、やはり結構なクロック数となります。

このため、ループの先頭と最後の水平同期期間、つまり画面の右端と左端では
実際には8バイトしか転送しないようになっています。
最後の水平描画期間では、IN命令が2回分減りますので、その分のクロックで
IYの値を2回インクリメントしています。
(EMMボードから、画素データと一緒にSPも読み込むというのも試みましたが、
実機検証時に、EMMボードのデータ化けが問題となったため断念しています。)

さて、これで時間内に画素データを転送できることは確認できましたが、
実際には画面の表示とデータ転送の同期を取ってやらないといけません。
I/Oで水平同期信号を取得して、信号の立上がり・立下りを監視する?
勿論、そんな真っ当に処理するようなクロック数の余裕なんてありません。

ここで、水平描画期間中にVRAMにアクセスすると、CPUが次の水平帰線期間まで
停止してしまうという、MZ-700の困った仕様が威力を発揮します。
水平描画期間中にPUSHするデータをレジスタに読み込んでおいて、そのまま
PUSH命令を実行するだけで、勝手に水平帰線期間まで待機してくれますので、
プログラムで同期を取る必要がありません。
要は、VRAMへのデータ転送中に、次のラインの水平描画期間に突入することが
ないようにクロック数を調整しておけば、もうそれでOKということです。
画面表示しない垂直帰線期間中でも、CPUが停止してしまうという困った仕様が、
意外なところで功を奏しています。

エミュレータ版でのプログラムの内容はこちらになります。



中間色表現

72x42とまあまあの解像度を実現することができましたが、色数が8色では
アニメーションをするにもいまいちです。
その代わり、フレームレートは毎秒60コマと相当に高いですので、これを生かして
色表現を豊かにしてみます。

連続する3フレームにわたって、時間方向に色をディザリングしてやると、
RGBの各原色について、無点灯、1フレームだけ点灯、2フレームだけ点灯、
3フレーム全てで点灯、と4階調を表現することができます。
これで、4×4×4で合計64色を表現することが可能となります。



わんつー。

インターレースを考慮すると、連続2フレームに留めるべきだったかもしれません。
その場合は各原色で3階調、全体で27色ということになります。

また、表示する文字種を市松模様のキャラグラにする手法も試してみました。
縦横の両方向について、1ドット毎に文字色と背景色が交互に表示されますので、
1画面で中間色が表現できます。
このとき、文字部分と背景部分について、両方とも無点灯、片側だけ点灯、
両方とも点灯、の3階調となりますので、全体で27色です。
ただし、横方向の解像度は半減してしまいますので、再生する動画の内容を
選ぶ必要が出てきます。

うっうー。



音楽再生

音楽再生は、技術的にはアイマスオープニングのものと同様です。



MZ-700では8253というタイマICを音源として使用しています。
このICは色々な動作モードを持っていますが、通常は指定した周期で連続的に
出力信号をON/OFFするモードを使用して、これで方形波を発生させてピーという
音を発生させています。

これに対して、最初に出力をONまたはOFFにして、外部からトリガをかけたり、
周期を指定したりすることで動作を開始するモードもあります。
ということは、トリガをかけたり、周期を指定したりしなければ、信号をずっと
ONまたはOFFに固定することが出来る訳です。
これを利用して、1bitのPCM音源として利用しています。
ON/OFFの切り替えは水平同期毎に行っていますので、周波数は262×60フレームで
15.72KHzとなります。

アイマスオープニングとの相違点は、アイマスオープニングではデータレコーダで
音声テープを再生、リアルタイムでその信号の±をサンプリングするのに対して、
こちらはEMMボードからデータを読み込んでいる点だけです。

ちなみに、紅茶羊羹さんからは、折角EMMボードからデータを読むのだから、
ON/OFFではなくカウント値を取得して、8253をPWM駆動したらどうかとのご提案を
頂いております。
エミュレータでは、音声合成部のサンプル周期の関係でうまくいきませんでしたが、
実際に氏が実機で検証された音声データを聴いてみたところ、相当にクリアな
音声となっており、ざーざー感は殆どありませんでした。
プログラム的には、単にデータを出力するI/Oポートを変更するだけですので、
アニメーションとあわせて追試してみたいところです。
実機検証とあわせて、実際にやってみました。(後述)



データ生成

ニコニコ動画から、気に入った動画を落としてきます。
元ネタがアイマスばかりのような気がしますが、まるくんの影響なので気にしないでください。

落としてきた動画を、EMMボードの容量に収まる時間(約100秒)に編集します。
中間色表現で3フレームを1コマとして扱いますので、AviUtlで20FPSに変換して、
288x168のサイズで連番ビットマップに出力します。

これを、VB6で作成した適当なツールでEMMデータに変換します。
1画素の範囲の色を平均化して、データを転送する順番に出力してやるだけです。
このとき、画素のサイズが場所によってまちまちな事に注意が必要です。



これとは別に、音声データを用意します。
15.72KHzでモノラルのwavファイルを出力して、これを1bit化します。
あとは、動画データと音声データをマージして、バイナリ変換することで
EMMボードのデータは出来上がりです。



エミュレータでの検証

時間方向へのディザリングによる中間色表現の検証動画。



市松模様を使用した中間色表現の検証動画。
解像度が足らないのは弩アップで誤魔化しています。



何だかひんそーなキャラにネタが集中しているような気もしますが、
多分気のせいです、趣味です、どっちやねん、気にしないでください。



ネタに走りたがる、オチが付かないと気がすまないのは性分ですので
もう今更気にしないでください。ちんこうP、凄いよね、尊敬しています。



実機検証への道

実機検証は、MZ業界のハード開発の権威、 Oh! 石さんにお願いしました。
1chip MZ-700 の開発などで有名な方ですね)

EMMボードはSIMMとAVRプロセッサで構成されていますが、AVRは事前にSDカードから
SIMMにデータをロードしておくのに使用するだけで、再生には一切寄与しません。
また、MZ-700側からボードを見ると、当時の純正EMMボードとI/Oポートの割付が
全く同じで、単純に容量が16MBに増えただけの完全上位互換となっています。
これはつまり、当時のメモリチップの容量と価格に目を瞑れば、当時の技術でも
今回の動画再生実験は十分実現可能だったということを意味します。


折角実機で検証するのだから、エミュレータではうまく再現できなかった、
紅茶羊羹さんの新しい音声再生処理も取り込むことになりました。

この手法では、8253のモード0を使用します。
この動作モードでは、カウント値を指定すると、出力信号がOFFになります。
指定されたカウント値だけクロックが入力されると、出力信号がONとなります。
そして、再度カウント値を指定するまでは、どれだけクロック入力があっても
出力信号は変わりません。

こんな感じ。

これを使えば、PWM駆動で高音質な音声再生が出来るんじゃねえ?という訳です。
もう1bit PCMの高音質なオーディオ機器と同じ世界ですね(笑)
MZ-700では、CPUクロックを4分周した信号がクロックとして入っていますので、
1水平同期あたりのカウント値は57クロック。
したがって、15.7KHzで57段階のPWM駆動が出来る計算となります。
こちらのエミュレータ上での動画開発と平行して、紅茶羊羹さんの方で音声再生の
実機検証が進められていましたが、もうガチで高音質で魂消ました。

ただ、これを動画再生と組み合わせるまでがもう大変。
音声再生の手法上、カウント値の設定はより均等な周期で行われる必要があります。
しかし、元のコードで、モード切替でON/OFFしていたところを、カウント値を
設定する処理に変更しただけでは、8クロックも周期に誤差が生じてしまいます。
これをNOPの2発入れて調整しても、まだ±1クロックの誤差が残りました。
実機検証での音質もいまいちな感じ。
(元データの音量が小さかったため、ノイズに埋もれたという要素もありましたが)

で、もう意地になって、全水平同期で均等な間隔でカウント値が設定されるように
プログラムのロジックを見直しました。
均等な周期を保障するには、やはり水平描画期間中のVRAMアクセスによるWAITを
活用する必要があります。その上で、8バイトしかデータを出力しないときでも、
10バイト出力するときと同じだけクロックを消費するようにダミー命令を入れて、
それから8253にカウント値を設定するようにします。
また、全水平同期において、水平描画期間中のVRAMアクセスを発生させるためには、
EMMからのデータ取得も、ループ末端のジャンプも、空き水平同期の例外処理判定も、
全て水平描画期間以内で完了する必要があります。

これを実現するために、既にクロック数をかなり切り詰めた元々のプログラムから、
ループの最初の水平同期だけで更に30クロックも切り詰める必要がありました。
画像データを画素順に並べるために、EXXを無駄打ちしていたのを改めるのをはじめ、
I/Oアドレスを格納するCレジスタの有効範囲を確認してLD C,03Hを削ったり、
BITとJP Zで条件分岐するのをやめて、ジャンプ先もデータテーブルから取得して
JP (IX)で通常ループと例外処理に分岐したり。
ニーモニック表と睨めっこして、もう思いつく限りの手動最適化を掛けています。

最終的なプログラムの内容はこちらになります。
今時のコンパイラって、こういう最適化を全部自動でやってくれるんですよね。
凄いよね、コンパイラ屋さんって。
OS屋さんにも憧れるけど、本当に凄いのはコンパイラ屋さんだと思います。



そして実機の世界より

という訳で、Oh!石さんの実機検証の動画がアップされました。



エミュレータ上ではポケモンフラッシュ状態でしたが、実機で更にビデオカメラ撮影だと
いい感じに再生されていますね。
音声のクリアさ加減も、おもわずちびりそうです。



市松模様だとガチで60FPS出せますので、今度は元ネタも60FPSでやってみたいところ。



最後にオチを付けることも忘れてはいけません(笑)



更なる発展へ

で、速攻で紅茶羊羹さんからも追撃がきました(笑)
紅茶羊羹さんは、Oh!石さんとはまた違った方式で擬似EMM回路を実装して、
実機上での検証を行われています。
敢えて単純化すると、間にFIFOバッファを挟んで、PCのプリンタ出力からMZ-700に
データを垂れ流すような感じです。この方式ではランダムアクセスは出来ませんが、
今回のようなストリーム再生だったらシーケンシャルなアクセスだけですから、
これでも十分ですね。少なくともプログラムからは同じように見えています。



技術的な見所は2点。
私の元プログラムのループを展開することで、更にクロック消費をおさえて
その分で描画範囲を拡大したのと、24bitカラーを時間方向に誤差拡散することで
ちらつきのない(砂嵐度は高くなりましたが)中間色表現をしているところ。
私も頑張って最適化しましたが、本物のZ80職人の手に掛かるとここまでいけるんだなあ。



他機種も凄いんですよ



元々このプロジェクトは、MZ-700の永遠のライバル、PC-6001上で実現された
この動画への応戦という意味もありました。
いやだって、コメントとかタグがもの凄い勢いで煽るもんだから。



で、更にPC-8001からの刺客も。



今度はベーシックマスターレベル3やJr.の参戦が。
うわははは、いやもう、すげえすげえ。

こうなったら、私もスパカセで再参戦するしか(ry



EmuZ-700 謎WIPページに戻る
TAKEDA, toshiya's HOME PAGEに戻る