富士通のパソコン「FM TOWNS」用にDOOMを移植した話

令和3年に登場した新たなFM TOWNS用移植ゲーム「DOOM」です。まだまだ現役のTOWNSユーザーでDOOMを遊ぶためにTOWNS用LinuxWindows 95を導入したという皆様、心行くまでTownsOSネイティブ版DOOMを堪能してください。

Google ドライブ - FM TOWNS用ソフト「DOOM」(2021年7月22日更新 V1.1 L25)

旧バージョン(V1.1 L10 上記のファイルで不具合がある場合)

CPU 486以上 高速モード設定推奨(機種でいうとFM TOWNS II MX以上)、メモリ8MB(空きメモリ6MB以上・空きがあれば+4MBをキャッシュ用に使用)必要・ハードディスク必須・TownsOS V2.1 L40(WAVEサウンドライブラリ使用)以上推奨・MS-DOS 6.2を組み込んでいる環境だと10MB以上メモリが無いと空き不足で起動しないかも。
必要動作環境のためFM TOWNS マーティーでのプレイは不可能です。
エミュレータ津軽」および「うんづ」で互換BIOS使用、TownsOS V2.1 L51から起動してThe Ultimate DOOMおよびDOOM2に収録されているデモの再生、数ステージのプレイまで動作確認。ただし「うんづ」の方では効果音にノイズが乗る。

実機での動作確認済み機種

実機FM TOWNS II MA + 486DX4 100MHzで音声も含めて正常に動作するとのこと。pinさん、ありがとうございます。

実機FM TOWNS II HCでも安定動作。Type.dさん、ありがとうございます。

 

FM TOWNSエミュレータ津軽」にて互換BIOS使用、CPU速度33MHz時の動作

動画は初期V1.1 L10版なので音の遅延などが発生するけどL20以降は軽減されている、最新版のL25ではフレームレートが1割程度向上。

 

f:id:BCC:20210702210053p:plain

f:id:BCC:20210702210109p:plain

ゲームにはシェアウェア(体験)版のDOOM1.WADが最初から入っていてエピソード1のみ遊べるけど、もちろん製品版The Ultimate DOOM(DOOMU.WAD)やDOOM2(DOOM2.WAD)をDOOM.EXPと同じフォルダに入れればそちらが読み込まれるようになってます。ユーザーが簡単に読み込むWADファイルを選択といった機能は用意していない(実行ファイルへのパラメータでのみ可能)ので、WADファイルごとに別々にフォルダを作りその中にWADファイルとDOOM.EXPを入れましょう。
The Ultimate DOOMやDOOM2は今でもSteamやGOG.comで購入することができます。

各ストアから購入後、ダウンロードしたらゲームフォルダ内のbaseフォルダに入っているWADファイルのみをコピーすればOK。
・・・はい!そこ!わざわざ今時のPCゲームサイトから購入するなら、最新PC向けの各種DOOMポートに入れて遊べばよくね?ってツッコんじゃダメ!!!

f:id:BCC:20210702200246p:plain
TownsOSに登録するアイコン(DOOM.ICN)も作ってみました。アイコン画面から起動したいという人は登録しておきましょう。

 

操作方法(現バージョンではキーボード操作のみ)
矢印キー・・・メニューの操作とゲーム中の移動操作
ESC・BREAKキー・・・メニューを開く
RETURNキー・・・メニューでの決定
CTRLキー・・・攻撃
スペースキー・・・扉を開ける&スイッチを押す
SHIFT+矢印・・・ダッシュ移動
ALT+矢印・・・垂直移動
012345678・・・武器切り替え
TABキー・・・マップ表示

 

終了メッセージもTOWNS仕様

f:id:BCC:20210702203709p:plain

f:id:BCC:20210702200432p:plain



その他、現バージョンV1.1 L25での仕様
・シングルプレイのみ対応
・最大フレームレートは30fps(オリジナルのIBM PC版は35fps)。fpsの違いで挙動に違いがあるかもしれない。
・メモリさえあれば初期に発売された縦型の灰色FM TOWNS(通称目玉タウンズ)でも一応動きますが、386 CPUでは画面設定を下げてもフレームレートが1桁でゲームになりません。
・デフォルト設定では「GRAPHIC DETAIL」は「LOW」、「SCREEN SIZE」は右から三段階下げている。
・効果音の多重再生は強引な方法で実装しているためか処理が重い。16ビットPCM搭載の白TOWNSなら重さが軽減されるかも。
・内蔵音源で音楽は再生されませんが、実行パラメータに-cddaをつけるとタイトル画面はCDトラック3番目、ステージクリア画面はCDトラック5番目、その他ゲーム画面などではCDトラック4番目の曲が再生されるようになります(実験的な実装で音量調整不可)。
Linux版ソースからあるバグは殆ど未修正。ただし、オプション内の「GRAPHICS DETAIL」が「LOW」にならない問題と、ゲーム終了時のメッセージで表示されてはいけない文章の表示および強制終了エラーが発生する、「The Ultimate DOOM」でエピソード2以降のスイッチが押しても視覚的な変化が起きない(反応自体はしている)バグだけは致命的なので修正済み。

・TownsOS V2.1 L31でもゲーム自体は起動するが、微妙にフレームレートが低い、効果音が鳴らないという問題あり。

 

 


ここからは、今更なんでこんな代物を作ってしまったかという駄文。
FM TOWNSとは、1989年に富士通より発売されたパソコン。
32ビットCPU「80386」(今でもPCで使われているCPUのご先祖だ)、世界初CD-ROMドライブ標準搭載、32768色同時発色、同時8音PCM音源と当時としてはなかなかのカタログスペックだったが、発売初期はハードウェアの作りこみやソフトウェアラインナップの貧弱さといったイメージの悪さ、問題点が改良されてくる90年代前半にはDOS/Vによる安価なIBM PC互換機での日本語表示の確立やその後のWindows 95の発売によって日本独自アーキテクチャPCの存在意義がなくなり、1997年にFMV-TOWNSを最後にシリーズ発売が終了となってしまった不遇の機種。
世間ではとやかく言われることの多いPCではあるものの、流石に今の水準では色々と劣るがGUIによるファイル操作や各種設定が行えた「TownsOS」や、HDD起動せずともCD-ROMから直接ゲームといったアプリをブートできた手軽さは当時小学生だった自分にはPCを操作するという行為を覚えさせてくれたマシンであった。
市販ゲームはアーケード移植は弱くとも最低でも32ビットCPUだったという点を生かして、IBM PCPC-9880386を搭載してもただの早い16ビットCPUとしか動いていなかったのと違い32ビットネイティブコードを実行(正確にはMS-DOS上でDOSエクステンダで32ビット&1MB以上のメモリ環境を使えるようにしている)できていたのとCD-ROMドライブやPCM音源搭載だったために、PCゲーム移植では処理速度は申し分なく音声周りがTOWNS版のみ豪華というタイトルも珍しくなかった。


ただ、一つだけ、一点だけ、残念なことがある。
それはFPSの金字塔DOOM」が移植されていないのだ。
1993年にid Softwareより発売された「DOOM」はテクスチャマッピング付の立体的なグラフィック描写、個々によって特質の違うモンスターや武器、優れたマップデザインによってヒットとなり当時でも各種コンソール機、更に(一応)TOWNSのライバルでもあったPC-98にも公式に移植され、その後Linux版ソースコードが公開されたことによって非公式にATMやデジカメといった珍機種、こちらも当時は公式移植されなかったX68000にまで移植されることとなり、32ビットCPUと4~8MBのメモリがあるコンピュータがあるならハッカーが移植するというネタになっているのだが、今の今まで32ビットCPU搭載・メモリも必要十分載せられるという条件を満たしているTOWNSに移植されていないのである。
Windows 95の登場によってIBM PC互換機に環境が移った際、そのPC上で動くDOOMを遊んだとき「これがTOWNSで遊べたらなぁ」という悔しい思いをしていた自分だが、悲しいことにその当時はC言語の知識はなく自力でFM TOWNSへの移植なんて夢のまた夢だった。


そんな悔しい思いをしてから24年後。捨てていなかったOSやフリコレ(大量のフリーソフトをCDに焼いて公式に市販されていたもの。パソコン通信しかなく繋ぐだけで1分数十円の料金がかかるのでTOWNSユーザーには重宝された。)といった当時の物や21世紀になってからオークションで購入していたはソフトはまだ手元に残っていたものの、所有していたFM TOWNS 2FとFM TOWNS II MXをどちらもCDドライブが故障して使えなくなってしまい、エミュレータにしても「うんづ」というある程度完成度が高いものが存在するものの、既に10年以上更新もされておらずCDイメージを直接読み込めない(仮想CDソフトは今のPCになるべく入れたくない)ということでCDをその都度交換して読み込むのが嫌いな自分は触らなくなっていたのだが、物置を整理していた時に紛失していたと思われていた残りのソフトが見つかり、その中にFM TOWNSの標準C言語開発環境である「High C コンパイラ V1.7」や後述する割り込み用ライブラリ「HIS」(ちょもらんま氏作)を収録していたOh! FM TOWNS誌付録のCD「天晴 Vol.2」があったのだ。

f:id:BCC:20210702204542j:plain

f:id:BCC:20210702204603j:plain

更にTOWNS用エミュレータが「うんづ」しかないという状況だったのが、FM TOWNSユーザーでフライト系のゲームや実用ソフト・ライブラリを製作していた山川機長(元YS11)氏が新たなるエミュレータ「津軽」を開発していて、しかもこのエミュレータはCDイメージを直接読み込み可能な上にFM TOWNS実機が無くてもCD・FD・HDDブートまで可能な互換BIOSまで作られていたのだ。実機がなくともOSや開発環境を仮想TOWNSにインストールして構築することができてしまう。なんたる偶然の巡り合わせ。
当時は叶えらなかった雪辱を晴らすよい機会、そしてもしFM TOWNSDOOMが存在していたらどんな仕上がりになっていたのかという興味本位で移植を開始したのだった。

他にも移植しようとした物好きな外国人がいたものの、印象としてかなり移植を実現するのは困難な雰囲気だったので、おそらく無理だろうと思って自分が取り掛かったわけなんだけど、本当にこれがいばらの道なのなんだって。
当たり前なんだけど、今更FM TOWNSの開発環境に関する資料なんて探しても殆ど現存しない。ソフトこそ見つかったけど、当時でも情報源として唯一といっていいくらいだったOh! FM TOWNSといった参考文集とかは流石に残っていなかった。
High CのCD内にテキストファイルでマニュアルがあるものの、本当にただのテキストファイルで見易い目次が用意されておらず知りたい情報があっても延々と上から下まで読んでくしかないわ、サンプルプログラムにしても説明不足な点が多かったりやりたいことに限ってサンプルなんて存在しないから結局自分で試行錯誤するしかないという。
まともなデバッグ機能や統合開発環境も無い中80~90年代にCで規模の大きいプログラム組んでた人は本当に凄すぎる・・・小学生だったから理解できないというのもあるけど、あの頃やってたらまず間違いなく挫折する。

コンパイルエラーで止まったり、コンパイルが通ったと思ったら起動中にフリーズするとかそもそもタイトル画面すら表示してくれないという状況が作業開始から2週間ほど経ったが、とある人のアドバイスがヒントとなりHigh C コンパイラのオプションを変更すると悩んでいたことの多くが解決するということがわかりようやくタイトル画面がお目見え。
ここから更にゲームが安定して動作するまでにちょっと手間取ったり、効果音を多重再生する処理の実装に悩んだが作業開始から1カ月である程度完成することとなった。
パフォーマンス的には同じ画面設定・音声出力ありのIBM PC版を同CPU・L2キャッシュなしで動かしたときよりも若干フレームレートが低いかもしれないが、遊べなくはないくらいのレベル。33MHz駆動だとSega Super 32X版相当だ(わかりづらい)。
FM TOWNS版はほぼオリジナルのCソースのままでBSP(バイナリ空間分割)部分の処理方法は別のソースに置き換えたものの他に大きく改良したところはなく、アセンブラ化しているのは固定小数点計算の乗除算部分のみに対して、IBM PC版は直接ビデオカード上のフレームバッファを操作したり天井/床描画関数をアセンブラ化しているといった最適化を施していたりと有利な条件がある中では割とよくできているかな。


ところで、なんで世界中のプログラマーが隙あらばDOOMを移植しているかといえば”プログラムに環境依存となる部分が本当に少ない、かつ環境依存部にしてもまとまっている”からで、見た目のインパクトに対して実装コストが低いのである。
ソースコード一覧を見ると何十とCファイルがあるが、このうちファイル名先頭に"i_"と表記されているもの、つまりi_main.c・i_net.c・i_sound.c・i_system.c・i_video.cの4~5ファイル書き換えればほぼそのマシン用のDOOMは完成する(実際にはWADファイルを読み込むd_main.cや各ソースの標準関数を使用しているところもコンパイラによって書き換えることになるがそれは抜き)。それぞれのソースにある関数もそれほど多くない。
細かいアルゴリズムとかは抜きにして大雑把にプログラムのメインループを書くと

void D_DoomLoop(void)
{


while(1)
{
I_StartTic (); //ループ最初の処理。入力されたキー情報をD_PostEvent()に送る。
D_ProcessEvents (); //ゲームのイベント処理
M_Ticker (); //
G_Ticker(); //ゲームの進行状況処理
D_Display (); //グラフィック処理。最終的にI_FinishUpdate ()でグラフィックを画面に表示
}

 

//1/35秒単位で何フレーム進んでいるか取得するI_GetTime()は各場所で使われている
}

 

とこのようになっておりネットワークとサウンド対応を後回しにすれば、I_StartTic ()・I_FinishUpdate ()・I_GetTime()が重要となってくる。


まずはゲームの経過フレーム数を返す「I_StartTic ()」
本来ならば1/35秒(これはIBM PCの標準リフレッシュレートが70Hzに由来。70fps動作では重いので半分の35fpsにされている)で、公式に公開されているLinuxDOOMソースコードでは時計の時刻を取得してゲームを立ち上げた時と現在のマイクロ秒単位の時差から導き出しているのだが、FM TOWNSは最小でも10ミリ秒単位でしか時計が取得できず、この方法では最大33fpsと中途半端、また実装してテストした限りでは途中でゲームが止まったりと動作が不安定となってしまっていた。
ではどうしたかというと、発掘したOh! FM TOWNS 1995年8月号付録のCD「天晴 Vol.2」に収録されていた割り込みライブラリ「HIS」を使用して、1/60秒毎に発生する垂直同期が入ると自動的に用意した関数に飛ぶようにして偶数毎にI_StartTic ()関数が返す値を増やしていく、最大35fps動作を諦めて30fpsに落とす(#define TICRATEを35→30に変更)ことにした。

void VSYNChandler( void ) //VSync(垂直同期が発生した場合自動的にこの関数が読み込まれる)
{


static int VSyncfrag = 1;

VSyncfrag = 1 - VSyncfrag;

 

if(VSyncfrag)
ticcount++;

 

/******** VSYNC割り込み原因クリアレジスタへの書き込み ********/
_outb( VSYNCclear, 0 );


}

//
// I_GetTime
// Returns time in 1/30th second tics.
//
int I_GetTime(void)
{
return ticcount;
}

 

時計を利用してフレーム数を図っていた時と違い安定性はよく、486ベースのTOWNSマシンではオリジナルの最大フレームレートである35fpsを維持し続けることは難しいと思われるので細かい挙動の違いはあっても大きな問題はないはず。
なお、今回使用したライブラリ「HIS」に関しては、再配布可能(改変のみ許可が必要)ということが説明文に記載されていたので付属のTOWNS版DOOMソースコードフォルダ内に置いておいた。
もし、FM TOWNS用ゲームソフトなどをHigh Cを使って新規に開発したいという人がいたら、大分楽になると思うのでありがたく使わせていただきましょう。

 

次は入力処理を行う「I_StartTic ()」

//
// I_StartTic
//
void I_StartTic (void)
{


event_t ev;
unsigned int keyscan, encode, rc;

 

I_ReadMouse();

 

//
// keyboard events
//

while(1)
{
keyscan = KYB_read( 1, &encode );

if ( (keyscan >> 8 ) == 0xff )
{
break;
}
else if( (keyscan >> 7 ) == 0)
{
ev.type = ev_keydown;
}
else
{
ev.type = ev_keyup;
}

rc = keyscan & 127;
ev.data1 = keyboard_scan[rc];
D_PostEvent(&ev);

}

}

TOWNS版ではキーボードバッファに溜まっている文字を最後まで読み取っていき、文字がなくなったらwhile文を抜けるという処理を行っている。D_PostEvent()に入力されるべき文字式はIBM PC用のものでTOWNS用キーボードの異なっているため、char型の配列で用意したもの(keyboard_scan[128])から読み取って送るようにしている。

 

最後に肝心要のグラフィック表示を行う「I_FinishUpdate ()」。
DOOMエンジン内ではscreen[0]という64K(320*200)バイトのバイト配列が最終的に完成した映像のフレームバッファとなっており、これをどうにかしてそのままVRAMに転送するないしその機種にあうVRAMデータ構成にしてから転送すればよい。
FM TOWNSではTBIOSと呼ばれるAPI/デバイスドライバに相当する機能がOSに用意されていて、これを経由してグラフィック描写を行うのだが高機能なのはいいんだけど逆に単純な機能がなく(1点だけドットを打ちたくても複数点描画の関数を呼び出す)これが当時ゲームを作るうえでのボトルネックになっていたのかもしれない。
ただ、幸いなことにDOOMではフレームバッファ映像をメインメモリ上に作成しており、FM TOWNSDOOMではEGB_putBlockというメインメモリの配列データをVRAMに転送する関数を使ってそのまま送るだけで表示することができた。最初は自前で一点一点書き込もうとしたが表示が崩れてうまくいかず速度的にも相当遅かったが、この関数一つ使うだけの方法のほうが簡単でしかも実用的に表示できる速度だった。

//Write in screens[0] to VRAM
DWORD( para+0 ) = screens[0]; //データ格納領域アドレス
WORD( para+4 ) = getds(); //データ格納領域セレクタ(データセグメントレジスタ取得)
WORD( para+6 ) = 0; //書き込み開始X座標
WORD( para+8 ) = 19; //書き込み開始Y座標
WORD( para+10 ) = SCREENWIDTH - 1; //書き込み終了X座標
WORD( para+12 ) = SCREENHEIGHT + 18; //書き込み終了Y座標
EGB_putBlock( work, 0, para );

ただし、DOOMの画面は320*200ドット 1670色中256色なのに対し、FM TOWNSの画面モードは大雑把に分けてフレームバッファに対して解像度が広すぎる640*480ドットの4096色中16色・1670色中256色、もしくはフレームバッファと色階調の異なる320*240ドット 3万色くらいしか使える画面モードがなくゲーム画面にピッタリあうのが用意されていない。
3万色モードを使って1670色中256色→3万色の色変換行ってから転送すればいいが、DOOMはパレットの色を変更して画面効果を出しているので色々と書き換えないといけない、その色変換に発生する余分な処理は避けたい。
ではどうしたかというと、FM TOWNSのグラフィックコントローラには画面表示を2~16で整数倍する機能があるので、640*480ドット 1670色中256色モードにしてこの画面を縦横2倍表示設定にすると実質320*240ドット表示となった。縦40ドット余分だが、フレームバッファの転送位置を縦+20ドットにすることで上下で対象の大きさの黒帯となり違和感のない画面となった。

//Screen set to Mode12(640*480,256 Colors, 1 Page only)
EGB_resolution( work, 0, 12 );

 

///x2 zoom, 640*480->320*240
EGB_displayStart( work, 2, 2, 2 );

 

最初は表示していない領域に次のフレーム画面を描画して、描画が終わったら表示領域を切り替えるデュアルバッファ方式を採用していたのだが、FM TOWNSの仕様(表示領域切り替え時は必ず垂直同期待ちが発生する?)かこの切り替え動作が妙に重くてデュアルバッファ方式で表示すると殆どフレームレートが出なかったので表示領域に直接描画するシングルバッファ方式にしている。ティアリングが出ることがあるが実用的に遊べるフレームレート出ないと意味がない。


環境依存部分ではないが、最適化や特定のコンパイラで問題になると思われるm_fixed.c内のFixedMulとFixedDiv2関数について。
DOOMが発売された1993年頃のCPUでは浮動小数点演算ユニットがパイプライン化されていないため遅い、そもそもCPUによっては内蔵されていなくてハードウェアで演算できないという問題があったため、DOOMエンジン内の小数点計算には整数32ビットの上部下部のビットをそれぞれ16.16の整数部と小数部に分けた固定小数点(fixed_t)と考えて計算される方法が採用されている。
固定小数点の乗算では32ビット*32ビット=64ビットの結果64ビットを右16ビットシフトを行い64ビットの中間部分を32ビットに切り離して最終的な計算結果(除算は先にシフトしてから実際の除算)を出していて、DOOMのCソースでは

fixed_t FixedMul( fixed_t a, fixed_t b )
{
return ((long long) a * (long long) b) >> FRACBITS;
}

 

fixed_t FixedDiv2 ( fixed_t a,  fixed_t b )

{

double c;


c = ((double)a) / ((double)b) * FRACUNIT;


return (fixed_t) c;

}

とlong long型(64ビット)で計算しているものの、FM TOWNSのHigh Cコンパイラはlong long型に非対応なのだ。
本来ならばアセンブラを使って乗算命令であるimul命令を使って計算すると二つの32ビットレジスタに上部32ビット・下部32ビットの結果が出てくるのでシフト命令で16ビットシフトするだけの単純なアセンブリコードを書けばいいんだけど、肝心のTOWNS用アセンブラを持っていなかった。では、どうしたかというと、これまた山川機長がかつて製作していた固定小数点用ライブラリ「TORNARD」(フリコレ10収録)内にある乗除算関数に値を渡して計算することとした。同じ16.16固定小数点でTORNARDのソースコードを確認してもDOOMのオリジナルコードとほぼ同じだったので問題なし。
・・・だったけど、最終的にFM TOWNS用NASMがあるということを教えてもらい、自分でFixedMulとFixedDivをアセンブル

section .text
align 4
global FixedMul
FixedMul:
mov eax, [esp+4]
imul dword [esp+8]
shrd eax, edx, 16
ret

 

align 4
global FixedDiv2
FixedDiv2:
mov eax, [esp+4]
cdq
shld edx,eax,16
sal eax,16
idiv dword[esp+8]
ret

 

TORNARDのコードよりもごくわずか(数クロック・バイトくらい)に短くなった。乗除算は当時のCPUでは重たい処理で、ゲームのループ中に何度も読みだされる箇所となっているため、効率的かつ効果的に最適化するならここはできる限り早くしておいたほうがいいだろう。