メガドライブのスプライト情報の配置をPrologで最適化した話

> ![シャマシュ](https://upload.wikimedia.org/wikipedia/commons/5/55/Milkau_Oberer_Teil_der_Stele_mit_dem_Text_von_Hammurapis_Gesetzescode_369-2.jpg)
> [シャマシュ](https://ja.wikipedia.org/wiki/%E3%82%B7%E3%83%A3%E3%83%9E%E3%82%B7%E3%83%A5)
>
> シャマシュ（Shamash, šamaš）は、メソポタミアの太陽神[1]。シュメール語ではウトゥ（Ud）と呼ばれる。シャマシュはアッカド語で「太陽」、ウトゥはシュメール語で「太陽」または「日」の意[2]。
> 
> シュメールにおける原初の5都市のうち、天から与えられた4番目の都市シッパル、ほかラルサにおいても都市神を担い、両都市に神殿「エバッバル」を持つ[3]。シュメール人は太陽を白色と見ており、エバッバルは「白く輝く神殿[4]」の意を含み別名「白い家」とも呼ばれていた[5][※ 1]。
> 
> 元来は女神とされていたが、アッカドのシャマシュにシュメールのウトゥが取り込まれていく信仰過程で、性別が反転し男神に変化していった[1]。 


今年はメガドライブミニが発売され、新作としてダライアスが発売されて話題になりました。ダライアスが好きすぎて自分で移植してしまったものをM2さんが著作権問題の手続きをしっかり行ってくれて発売にこぎつけたといいます。

そんな話を聞いていると自分としてはスペースハリアー２ではなくスペースハリアーがメガドライブに欲しかったなと思うわけで作り始めてみました。

メガドライブは16Bitのマシンであり、うまく調整すれば高速に動作しますが、VRAMの容量は64kbyteととても小さいので、通常のゲームづくりでは、ステージ間や、シーン切替時に画面の描画を止めて一気にロードするのが簡単です。
しかし描画の内容を懲り始めると動的ローディングが必要になります。
DMA転送機能を使うことCPUとは別に並列に独立してデータ転送を行うことが出来ます。
しかしこのDMA転送は128kbyte境界をまたいで行うことが出来ません。
スプライト情報の中でDMA転送する箇所に128kbyte境界をまたぐデータが存在すると、キャラ化けが起きてしまいます。
128kbyte境界をまたいでも大丈夫にした関数を使えば問題は解決できますが、速度的に不利になります。
できればスプライト情報は128kbyteの壁をまたがないよう配置したい。
128kbyteの境界をまたがないような制約を付けたリストデータ生成ができれば高速に転送が可能になります。
制約でリストの再配置をするにはPrologで楽にかけるはず。
そこで、Prologでスプライト情報の再配置プログラムを作ってみました。

## データ構造の解析

制約をつくるにはSGDK 1.3.4 のスプライト情報の解析が必要です。

```Prolog
spriteNumber(dt(_,W,H),N) :- N is floor((W+3)/4)*floor((H+3)/4). % スプライト数
```

まずスプライトのサイズは以上のような計算で求められます。8x8のタイルを1x1〜4x4までのサイズであれば1つのスプライトで扱えますがそれ以上のサイズになるとスプライトの個数が増えます。

```Prolog
head(dt(_,W,H),SIZE) :- spriteNumber(dt(_,W,H),N),SIZE is 48*N+40. % ヘッダサイズ
```

ヘッダのサイズはスプライトのサイズに４８をかけたものに40バイト足したものとなります。

```Prolog
size(dt(_,W,H),SIZE) :- head(dt(_,W,H),HD),SIZE is HD+32*W*H+66.
```

全体のサイズはヘッダサイズに加えて、幅と高さに32をかけたタイルサイズ+66バイトになります。

```Prolog
s(dt(N,W,H),dt(N,W,H,HD,WH,SIZE)):- WH is 32*W*H,head(dt(N,W,H),HD), size(dt(N,W,H),SIZE).
```

制約の計算に必要な情報は、ヘッダ情報サイズHDからタイルサイズWH分でありデータのサイズSIZEとして求められます。


## 初期リスト生成

元のデータのリストは db.pl に保存し読み込みます:

```Prolog
:- consult('db.pl').
```

この読み込んだデータをリストLsとして読み込み、制約情報を付けたRsに変換し、サイズが小さいものから順番に並べ替えRs2に変換、サイズの大きいものからのリストRs3に変換します:

```Prolog
gen(Rs3):-
  findall(dt(A,W,H),dt(A,W,H),Ls),!,
  maplist(s,Ls,Rs),
  predsort(ck,Rs,Rs2),
  reverse(Rs2,Rs3).
ck(R,dt(_,_,_,_,_,SIZE),dt(_,_,_,_,_,SIZE2)) :-
  compare(R,SIZE,SIZE2),
  R \= '='.
ck(<,_,_).
```

サイズで並び替えるのは、大きいものの間に小さいものを詰め込むイメージで配置するのが効率的だからです。

## 制約解消

```Prolog
rd(_,[],[]). % 空なら終了
rd(Addr,Ls,[B|Rs]):-
  select(B,Ls,Ls1), % 1個取り出し
  B=dt(_,_,_,HD,WH,SIZE), % データ取り出し
  HDAM is (Addr+HD) div (128 * 1024), % データ転送開始位置の128kbyteごとの番号を取得
  HDAM is (Addr+HD+WH) div (128 * 1024), % データ転送終了位置の128kbyteごとの番号を取得
  Addr2 is Addr+SIZE,
  rd(Addr2,Ls1,Rs).
```

制約の解消はリストから1つ取り出して、データ部分が128kbyte境界にないかチェックしデータがなくなるまで続けます。
境界検査は、壁に区切られた部屋の番号が同じであると考えました。

非決定的に select でリストを取り出し制約検査で失敗したらselectをやり直すトラックバックを繰り返すことで制約条件にあったリストが生成できるというわけです。

## 実装全体

以下に実装全体を示します:

```Prolog
:- consult('db.pl').
start(0x3969a0).

spriteNumber(dt(_,W,H),N) :- N is floor((W+3)/4)*floor((H+3)/4).
head(dt(_,W,H),SIZE) :- spriteNumber(dt(_,W,H),N),SIZE is 48*N+40.
size(dt(_,W,H),SIZE) :- head(dt(_,W,H),HD),SIZE is HD+32*W*H+66.
s(dt(N,W,H),dt(N,W,H,HD,WH,SIZE)):- WH is 32*W*H,head(dt(N,W,H),HD), size(dt(N,W,H),SIZE).

gen(Rs3):-
  findall(dt(A,W,H),dt(A,W,H),Ls),!,
  maplist(s,Ls,Rs),
  predsort(ck,Rs,Rs2),
  reverse(Rs2,Rs3).
ck(R,dt(_,_,_,_,_,SIZE),dt(_,_,_,_,_,SIZE2)) :-
  compare(R,SIZE,SIZE2),
  R \= '='.
ck(<,_,_).
rd(_,[],[]).
rd(Addr,Ls,[B|Rs]):-
  select(B,Ls,Ls1),
  B=dt(_,_,_,HD,WH,SIZE),
  HDAM is (Addr+HD) div (128 * 1024),
  HDAM is (Addr+HD+WH) div (128 * 1024),
  Addr2 is Addr+SIZE,
  rd(Addr2,Ls1,Rs).
w(B,Addr,Addr2):-
  B=dt(N,W,H,_,_,Size),
  format('SPRITE ~w "gfx/~w.png" ~w ~w NONE\n# ~16r ~w\n',[N,N,W,H,Addr,B]),
  Addr2 is Addr+Size.
:- gen(Rs),start(Addr),!,rd(Addr,Rs,Rs1),!,foldl(w,Rs1,Addr,_).
:- halt.
```

使い方はデータベースに読み込みスプライトデータを保存しておき、リソースの開始位置を `start(0x3969a0).` のように書いてプログラムを実行することで、再配置されたスプライト情報がテキストで吐き出されるので、リソースファイルに保存するようにして使います。

## まとめ

Prolog を使って制約を解消しメガドライブのスプライト情報の再配置を行いDMA転送の最適化を行う手法を解説しました。
Prolog は非決定的な計算を素直に書き下すことができるので DFS をするためにわざわざ手続き的な記述をすることなくわかりやすく書くことが出来ます。
しかしただ闇雲に入れ替えを繰り返すのは遅かったので、大きい順に並べ替える工夫をすることで高速化をしました。
実用上十分高速なのでCに書き換えなくても問題は解決できました。

メガドライブ自体のプログラムはC言語で書いてゴリゴリに高速化する必要があります。
しかし、開発環境はずっと高速でデータ量も少ないのでPrologを開発時に利用するのは便利で有効なのではないかと思います。

64色パレットの制約や、転送タイミングの制約などもPrologにより最適化できそうだと思っていたのですが、参考程度にしかPrologを使えず悔しかったのですが、再配置はうまくいったので良かったです。


> ![Mace](https://upload.wikimedia.org/wikipedia/commons/6/6c/Various_Indo-Persian_maces.jpg)
> [Mace](https://en.wikipedia.org/wiki/Mace_(bludgeon))
> 
> A mace is a blunt weapon, a type of club or virge that uses a heavy head on the end of a handle to deliver powerful strikes. A mace typically consists of a strong, heavy, wooden or metal shaft, often reinforced with metal, featuring a head made of stone, bone, copper, bronze, iron, or steel.
>
> The head of a military mace can be shaped with flanges or knobs to allow greater penetration of plate armour. The length of maces can vary considerably. The maces of foot soldiers were usually quite short (two or three feet, or sixty to ninety centimetres). The maces of cavalrymen were longer and thus better suited for blows delivered from horseback. Two-handed maces could be even larger.
>
> Maces are rarely used today for actual combat, but many government bodies (for instance, the British House of Commons and the U.S. Congress), universities and other institutions have ceremonial maces and continue to display them as symbols of authority. They are often paraded in academic, parliamentary or civic rituals and processions.