今回は描画ルーチンの高速化について検証したいと思います。


  • 前回までの描画方法
前回の描画では下記のルーチンを紹介しました。

GRPDraw2x12Sep1:: call VRMAddress ; HL 描画アドレス ex de, hl ; 描画を HL→DE の流れとする push de call .draw ; L 描画 pop de set 5, d ; DE 描画アドレスを H 側に .draw ld iyl, 11 .loop ldi ldi ld a, SCRWidth - 2 add a, e ld e, a jr nc, .chk inc d .chk dec iyl jr nz, .loop ldi ldi ret
普通に ldi を使って描画している、何の変哲も無いルーチンです。プログラムサイズは34バイトとなかなかに小さいです。このルーチンのメリットとしては、やはりサイズの小ささにあるかと思います。


  • 普通に高速化した描画ルーチン
さて、続いては pop でデータを取得する方法です。ネットで検索すると縦画面シューティングをつくるというサイトでも紹介されている方法です。AILZ80ASMの特徴的な REPT LAST マクロを使ってシンプルに記述してみました。

GRPDraw2x12Sep2:: call VRMAddress ; HL 描画アドレス ex de, hl ld (WRK.Temp00), sp di ld sp, hl ex de, hl ld bc, SCRWidth - 1
REPT 12 LAST -1 pop de ; DE データを読み出す ld (hl), e inc hl ld (hl), d ; VRAMに書き出す add hl, bc ; 次のアドレスに ENDM ld bc, $2000 - SCRWidth * 11 - 1
add hl, bc ; アドレスを H に移動 ld bc, 40 - 1 REPT 12 LAST -1 pop de ; DE データを読み出す ld (hl), e inc hl ld (hl), d ; VRAMに書き出す add hl, bc ; 次のアドレスに ENDM ld sp, (WRK.Temp00) ei ret
このルーチンのメリットは、最初のルーチンよりかなり高速化されたですね。デメリットとしては、プログラムサイズは 145バイトと、そこそこ大きくなってしまった事でしょうか。それでも高速化という意味では、この方法は採用したいところです。
※ テレワークでのボイスチャットやyoutubeのゲーム実況などに。


  • かなり異常な描画ルーチン
さて、先に紹介したサイトでは、思い切ってデータを 1/2 にする等で高速化を推し進めていますが、私はもう一つの高速化を知っているのでそちらをご紹介します。下記の方法は確か 1980年代の日本ファルコムさんが PC-8801のゲームで採用していた方法です。

GRPDraw2x12Sep3:: call VRMAddress ; HL 描画アドレス ; 描画アドスの初期化 ld bc, SCRWidth ld (.put00 + 1), hl add hl, bc ld (.put01 + 1), hl add hl, bc ld (.put02 + 1), hl add hl, bc ld (.put03 + 1), hl add hl, bc ld (.put04 + 1), hl add hl, bc ld (.put05 + 1), hl add hl, bc ld (.put06 + 1), hl add hl, bc ld (.put07 + 1), hl add hl, bc ld (.put08 + 1), hl add hl, bc ld (.put09 + 1), hl add hl, bc ld (.put0A + 1), hl add hl, bc ld (.put0B + 1), hl ld bc, $2000 - SCRWidth * 11 add hl, bc ld (.put10 + 1), hl
ld bc, SCRWidth
add hl, bc ld (.put11 + 1), hl add hl, bc ld (.put12 + 1), hl add hl, bc ld (.put13 + 1), hl add hl, bc ld (.put14 + 1), hl add hl, bc ld (.put15 + 1), hl add hl, bc ld (.put16 + 1), hl add hl, bc ld (.put17 + 1), hl add hl, bc ld (.put18 + 1), hl add hl, bc ld (.put19 + 1), hl add hl, bc ld (.put1A + 1), hl add hl, bc ld (.put1B + 1), hl ; 描画する ex de, hl ld (.stack + 1), sp di ld sp, hl pop hl .put00 ld ($0000), hl pop hl .put01 ld ($0000), hl pop hl .put02 ld ($0000), hl pop hl .put03 ld ($0000), hl pop hl .put04 ld ($0000), hl pop hl .put05 ld ($0000), hl pop hl .put06 ld ($0000), hl pop hl .put07 ld ($0000), hl pop hl .put08 ld ($0000), hl pop hl .put09 ld ($0000), hl pop hl .put0A ld ($0000), hl pop hl .put0B ld ($0000), hl pop hl .put10 ld ($0000), hl pop hl .put11 ld ($0000), hl pop hl .put12 ld ($0000), hl pop hl .put13 ld ($0000), hl pop hl .put14 ld ($0000), hl pop hl .put15 ld ($0000), hl pop hl .put16 ld ($0000), hl pop hl .put17 ld ($0000), hl pop hl .put18 ld ($0000), hl pop hl .put19 ld ($0000), hl pop hl .put1A ld ($0000), hl pop hl .put1B ld ($0000), hl .stack ld sp, $0000 ei ret
プログラムはさらに長く、総容量は212バイトもあります。しかも、残念ながらこの方法は ROMでは採用できません、なぜならプログラム書き換えが必要だからです。高速化以上にこのルーチンを使用するメリットが二つあります。ひとつは、描画し始めると一気に超高速で書き換えるので、たらつきちらつき色ズレといった異常表示状態が目立たないという事です。もう一つが割り込み禁止時間が短い事です。サウンドの揺らぎが少なくなるはずです。


  • 実行速度測定
さて、実際に動かしてどの程度高速化したかを検証してみます。まずは描画テストルーチンを簡単に組んでみます。

org $8000 call InitSystem ld hl, 0 ld de, Bullet.L0 call GRPDraw2x12Sep1 ld hl, $0103 ld de, Bullet.L1 call GRPDraw2x12Sep2 ld hl, $0206 ld de, Bullet.L0 call GRPDraw2x12Sep3 jr $

表示状態
ほぼ同一条件で同じデータを描画しただけのサンプルです。データアドレスの .L0 は左詰、.L1 は右詰ですが、データとして大差はありません。実行するとこんな画面表示になります。※画面は抜粋
さて、実行速度の測定です。あまり知られていないと思うのですが、実は PC6001VW4 には実行からブレークポインタで停止するまでの処理時間を表示する機能があります。それが state 命令です。コンソール画面から state on とすると実行速度表示モードになります

ということで、まずはアセンブルした結果の lst と sym を PC6001VW4 に読み込んでから、u 0x8000 として、正しくプログラムが読めている事を確認します。続いて b コマンドで HL レジスタに値を入れているラインにブレークポインタを貼っていきます。ブレークを貼り終わってから、もう一度 u コマンドを実行すると、ブレークが貼られたアドレスは赤文字になります。
ブレークポインタを貼った
r pc 0x8000 として開始アドレスを設定、state on の状態で g で実行します(本当は g 0x8000 とコマンド打ちたいw)。最初のサンプル描画直後で停止した状態を下記に示します。
サンプル1の実行結果
実行速度は 2772 と出ました。Instruction とはマシンオペコード別のサイクル数です。それに M1wait とあるのは、命令をフェッチした際に発生するメモリウェイトです。この二つのステート数が加算されたのが実際の実行時間となります。なお、PC6001VW4 のステート表示は、実行時に発生した割り込み処理の実行時間も含まれています。そのため、割り込み処理時間がなるべく短くなるように、割り込み処理にはなるべく何もぶら下げないようにしておきます。それでも処理時間は加算されてしまうのですが、まあ、誤差範囲という事で…。

続いてサンプル2の pop データ取得描画での実行時間です。
サンプル2の実行結果
実行時間は 1563 です。やっぱり速いですね!Z80 で 2バイトを一気に処理できる pop を駆使するとこんな速度が出たりするのです。では、お待ちかね(?)、サンプル3 プログラム書き換え描画の実行速度です。
サンプル3の実行結果
実行時間は 1581 です。データの取得にスタックを使い、描画は LD (Adr), HL を使うという反則級のプログラムです…が、思ったほどは速くなっていません。ちらつきや色ズレが気になる時に使用すると良いかもですが、実測するとこんなもんなんですね💦

なお、このプログラム、実は PC-8801 だと劇的に高速化するんです。理由は同じアドレスに R,G,B と3回も描画するハードウェアの仕様に因ります。前半のプログラム書き換えでアドレスが決まったら、後はポート出力で R,G,B と切り替えて描画処理を合計3回通すだけで済むためです。PC-6001 だと L, H のアドレスが 0x2000 も離れているため、アドレス計算は都度し直さないといけないので、処理速度が速くならなかったというワケなんです。L と H をバンク切り替えとかで同じアドレスに持ってこれれば…とも思いましたが、残念ながら動きませんでした…。

ということで、上記プログラムより高速に描画する処理が作れたらご一報ください💦💦💦

上記プログラムを収めたサンプルを提示しておきますー
P6DrawTest.zip
※ アセンブルすると 32KB というどでかいバイナリが出来てしまいます。テストなのでご容赦を…