Z80 アセンブラにとって、高速化は命題となります。その中で2バイトをまとめて処理出来る push / pop は使い方によってはかなりの高速化が期待出来るため、本来の使い方以外の使用方法がいくつも開発されてきました。今回はそんなスタック(以下、SP と略す)に纏わるお話です。


  • PC-8001 には割り込みがない
標準的な状態の PC-8001 には割り込みが存在しません。画面表示のために DMA が存在する程度です。そのため、基本的に di で割り込み禁止する必要がありません。自由気ままに、好きなタイミングで好きなように SP を変更して使用する事が出来ます。

兄弟機である PC-6001 系は割り込みがかなり使用されています。キーボードは別空間にあり、押された情報が割り込みで飛んできたりします。そのため、SP を弄るときは di による割り込み禁止が欠かせません。というか、PC-8001 だけが特殊だと言えます。MZ-80K も割り込みあったりしますし、なんで PC-8001 にはなかったのでしょうか…


  • push による画面消去
一番有名な SP 活用方法は、push による高速ゼロクリア方法でしょう。ld hl,0 とした上で push hl とすれば、2バイト操作最速でゼロクリアします。しかも勝手に SP ポインタは 2 バイト一つ前に更新されます。使わない手はありません。
;
; PC-80001 画面を初期化する
;
GRPClear:
ld (.stack), sp
ld hl, 0
ld d, %11101000 ; ATRB.WHITE
ld sp, 0xF300 + 120 * 25 ; VTEXT 最後尾
ld b, 25

; アトリビュートを初期化する
.clear ld e, 80
db 0xD5, 0xD5, 0xD5, 0xD5, 0xD5, ; PUSH DE
db 0xD5, 0xD5, 0xD5, 0xD5, 0xD5,
db 0xD5, 0xD5, 0xD5, 0xD5, 0xD5,
db 0xD5, 0xD5, 0xD5, 0xD5,
ld e, 0
push de

db 0xE5, 0xE5, 0xE5, 0xE5, 0xE5, ; PUSH HL
db 0xE5, 0xE5, 0xE5, 0xE5, 0xE5,
db 0xE5, 0xE5, 0xE5, 0xE5, 0xE5,
db 0xE5, 0xE5, 0xE5, 0xE5, 0xE5,
db 0xE5, 0xE5, 0xE5, 0xE5, 0xE5,
db 0xE5, 0xE5, 0xE5, 0xE5, 0xE5,
db 0xE5, 0xE5, 0xE5, 0xE5, 0xE5,
db 0xE5, 0xE5, 0xE5, 0xE5, 0xE5,
djnz .clear
.stack equ $ + 1
ld sp, 0
ret
push de や push hl を直接マシンオペコードで羅列しましたが、アセンブラが対応していれば REPT マクロなどを使う方が見やすいと思います。


  • シーンシーケンス処理
pop すると次々とアドレスを拾ってくる性質を利用して、デモなどでシーンの管理に使用出来たりします。サンプルを読み解きながら簡単に説明していきます。

まず、デモの流れに合わせて、呼び出しルーチンテーブルを作成します。
;
; デモ開始シーケンスデータ
;
SQNOpening::
dw TaskSNDInit ; サウンド初期化
dw TaskScreenClear ; 画面クリア
dw TaskWait100 ; 時間待ち100
dw TaskTelop2 ; 2行テロップ
dw IMGCopyR ; HL

dw TaskWait250 ; 時間待ち250
dw TaskLZeDecode ; 基本画面
dw IMGBase ; HL
dw ADRS.VTEXT ; DE
dw TaskBGMStart ; 登場効果音
dw BGMFadeIn ; HL
ここでは終了コードを -1 とします。HL にシーンテーブルアドレスを設定して、シーケンス処理を呼び出します。メインはこれだけです。

.loop ld hl, SQNOpening ; 冒頭デモ
call Sequence ; デモ開始
jr .loop
シーケンス処理では、DEMO.SEQUENCE にシーケンスポインタを保存してから、メインに戻るアドレスを自己書き換えで jp に設定します。そして、SP にシーケンスポインタを戻して pop hl としてそのアドレスにジャンプしています。
;
; シーケンス処理
;
Sequence::
ld (DEMO.SEQUENCE), hl
pop hl
ld (WORK.Stack), hl ; 終了アドレス格納
.loop ld sp, (DEMO.SEQUENCE)
pop hl ; タスクアドレス取得
ld a, h
and l
inc a
jr z, .exit ; シーケンス終了判定
jp (hl) ; タスク処理にジャンプ
.exit ld sp, (WORK.Stack) ; SP を戻す
jp 0 ; 元の処理に戻る
ジャンプ先ではそれぞれのタスクを処理したら、無条件で Sequence.loop に戻る事で、引き続きシーケンス処理を継続していきます。ジャンプ先の処理で call や push / pop を使用したい場合は、SP の保存と初期化を行います。
;
; タスク/画面を消す
;
TaskScreenClear:
ld (DEMO.SEQUENCE), sp
ld sp, 0
call GRPSGScreenClear
jr Sequence.loop
外部パラメータが必要な場合は、シャンプ先のタスクで pop します。先のシーンテーブルを見ると、TaskLZeDecode の次に IMGbase と ADRS.VTEXT が並んでいますので、pop hl / pop de とすることで、それぞれのレジスタにパラメータとして受け取る事が出来ます。
;
; タスク/データ展開
;
TaskLZeDecode:
pop hl
pop de
ld (DEMO.SEQUENCE), sp
ld sp, 0
call LZeDecode
jr Sequence.loop
時系列の管理はこれだけです。再生速度は今回はシーンデータ内に TaskWait とすることで管理しました。


  • ワークの読み出しと保存
ワークの並びを工夫する事で、IX 等のインデックスレジスタを使用せず、まとめて複数の処理を実行する事が出来ます。例えば鼠屋さんが以下は私が作成した固定小数演算による弾の移動処理の一部です(サンプルとして提示するために処理を端折っています)。

.move ld (WORK.Stack), sp ; スタックを保存
di
ld sp, hl ; SP ワーク位置を設定
pop de ; DE Y加算値
pop bc ; BC X加算値
pop hl ; HL Y座標
add hl, de ; Y座標を更新
ex de, hl ; DEにY座標を退避
pop hl ; HL X座標
add hl, bc ; X座標を更新
push hl ; X座標を格納
push de ; Y座標を格納
ld sp, (WORK.Stack) ; スタック戻す
ei
ld l, d ; L Y座標(整数部)

この処理は X,Y の固定小数を、ワークに保存されている 16ビット移動値を加算して、また元のワークに書き戻し HL に X,Y 座標の整数値を取得する処理です。上記処理はワークの並びに依存しています。ワークはこんな感じで並べています。
;-----------------------
; 攻撃(弾)
;
BULLET:
.RANGE equ 0 ; 1 射程(0=None)
.STATUS equ .RANGE + 1 ; 1 状態(種類,攻撃Lv)
.SPD_Y equ .STATUS + 2 ; 2 移動加算Y
.SPD_X equ .SPD_Y + 2 ; 2 移動加算X
.POS_Y equ .SPD_X + 1 ; 2 現在のY位置(固定小数)
.POS_X equ .POS_Y + 2 ; 2 現在のX位置(固定小数)
.DRAW_ADRS equ .POS_X + 2 ; 2 描画アドレス
.IMAGE equ .DRAW_ADRS + 2 ; 2 現在表示中の画像アドレス
.RECT equ .IMAGE + 2 ; 2 実際の描画サイズ
.SIZE equ (.RECT + 2 + 1) / 2 * 2 ; 偶数補正

これをもしインデックスレジスタを使用したら、どれだけ処理が重くなるか分かりますでしょうか。以下は IX を使用した場合の参考例です。

.move ld h, (IX + BULLET.POS_Y + 1)
ld l, (IX + BULLET.POS_Y)
ld d, (IX + BULLET.SPD_Y + 1)
ld e, (IX + BULLET.SPD_Y)
add hl, de
ld (IX + BULLET.POS_Y + 1), h
ld (IX + BULLET.POS_Y), l
ld a, h
ld h, (IX + BULLET.POS_X + 1)
ld l, (IX + BULLET.POS_X)
ld d, (IX + BULLET.SPD_X + 1)
ld e, (IX + BULLET.SPD_X)
add hl, de
ld (IX + BULLET.POS_X + 1), h
ld (IX + BULLET.POS_X), l
ld l, a
如何にも重そうですよね…。このように、スタックを上手く使用すれば、劇的に実行速度を上げる事が出来ます。

  • サウンドドライバを SP で最適化する
前回紹介したサウンドドライバも、ワークの読み出しと保存で最適化する事が出来ます。ガッツリ本気で最適化してみました。
SNDPlayer:
; アドレスがなければ終了
ld l, (ix + MML.ADDRESS)
ld h, (ix + MML.ADDRESS + 1)
ld a, h
or l
ret z
この部分は SP で

SNDPlayer:
ld (.retads), sp
ld sp, hl ; SP = +2 MML.ADDRES

; アドレスがなければ終了
pop de ; SP = +4 MML.LENGTH
ld a, d ; DE MMLデータポインタ
or e
jr z, .exit ; データが無ければ終了する
こんな事になります。pop de 一発ですね。発音継続処理に至っては
        ; 発音継続確認
dec (ix + MML.LENGTH)
jp m, .cmd ; マイナスならMML解析に
ret nz ; プラスなら今のまま継続
ld (ix + MML.FREQ), a ; ゲームタイムで音を消す
ld (ix + MML.FREQ + 1), a
ret
この処理が SP 最適化をかけると
        ; 発音継続確認
pop hl ; H = 初期音長, L = 現音長
dec l ; 現在の音長 -1
jp m, .cmd ; マイナスならコマンド解析
push hl ; 音長を保存
jr nz, .exit ; 音長最後で無ければ終了
push de ; dummy
ld h, l ; HL = 0
push hl ; MML.FREQ をゼロに

; SNDTimer に戻る
.retads equ $ + 1
.exit ld sp, 0 ; SP を元に戻して
ret ; 終了する
パッと見では動作が理解しづらくなってきました。もちろん、こんな事が出来るのは、ワークエリアの並び方をアクセスしやすいように変更しているためです。さらにルーチンの並びを、最も処理が通る部分は、なるべくジャンプが発生しないように一直線で処理が進むようにしています。最も処理が通る場所とは、サウンドドライバで言えば音長継続処理ですね。

さらに音階処理でカウンタ値の取得と設定は、アライメントの高速化テクニックも用いて、計算量を減らしています。
        ; 音階
ld l, h ; 音長を初期化
push hl ; 音長を保存
push de ; MMLポインタを保存
add a, a ; カウンタ連番を2倍して
add a, FRQTBL % 256 ; テーブル下位8bit加算
ld l, a
ld h, FRQTBL / 256 ; HL カウンタ値の位置
ld a, (hl)
inc l
ld h, (hl)
ld l, a ; HL カウンタ値
push hl ; MML.FREQ に保存
jr .exit
BGM開始処理なんてもう…
SNDMusicStart:
ld (.retads), sp
ld sp, SND.CHEnd
ld de, 0x0800 ; 音長初期値 8
ld c, (hl)
inc hl
ld b, (hl) ; BC CH3 MML アドレス
inc hl
push bc ; CH3 + MML.REPEAT
push de ; CH3 + MML.LENGTH
push bc ; CH3 + MML.ADDRESS
push de ; CH2 + MML.FREQ (dummy)
ld c, (hl)
inc hl
ld b, (hl) ; BC CH2 MML アドレス
inc hl
push bc ; CH2 + MML.REPEAT
push de ; CH2 + MML.LENGTH
push bc ; CH2 + MML.ADDRESS
push de ; CH2 + MML.FREQ (dummy)
ld c, (hl)
inc hl
ld b, (hl) ; BC CH1 MML アドレス
push bc ; CH1 + MML.REPEAT
push de ; CH1 + MML.LENGTH
push bc ; CH1 + MML.ADDRESS
.retads equ $ + 1
ld sp, 0 ; SP を元に戻して
ret ; 終了する
もはや push の羅列にしか見えません

この最適化済みサウンドドライバサンプルはこちらに置いておきます。ご自由にお使いください。全部じゃなければ Twitter等で内容を紹介して頂いても OKです。
※ 丸ごと転載は勘弁してください

以上、参考になれば幸いです。