技術 約12分で読めます

20年放置していたP/ECEをWindows 11で復活させ、自作ゲームをLLVMツールチェーンでビルドして実機で動かした

いけさん目次

押し入れから、20年ぶりにP/ECEが出てきた。

P/ECEは2001年にアクアプラスが出した、手のひらサイズの携帯ゲーム機。 この個体は20年くらい前、何かのゲームをやりたくて買ったものだ。 けっきょくそのゲームに全然ハマらず、どこかにしまい込んだまま忘れていたのを、この前たまたま発掘した。

出てきたなら何か動かしたい。 それも、せっかくなら別プロジェクトで書いた自作ゲーム「弁当脱出」を、この128×88のモノクロ液晶に載せてみたい。 ただ、24年前の携帯機にそれを持っていくのは、すんなりとはいかなかった。 当時のドライバは今のWindowsで動かず、ビルドに要るコンパイラは付属CDごと行方不明で、最終的にLLVMツールチェーンを自前で建てるはめになった。 やったことを順番に書いていく。

検証環境

項目内容
開発マシンIntel Core i7-13700H(14コア / 20スレッド)、RAM 32GB
OSWindows 11 Home(build 26200)
ビルド環境WSL2 上の Ubuntu 22.04。WSLに割り当てられたRAMは15GB、20スレッドが見える
ツールチェーンautch/llvm-s1c33 をソースからビルドした Clang/LLVM 22.1.1
対象機P/ECE(PME-001)。スペックは次の節

P/ECEとは何だったのか

P/ECE(ピース)は、ToHeartやうたわれるもので知られるアクアプラスが2001年11月30日に発売した携帯機。 価格は税抜9,800円。 ゲーム機でありながらC言語の開発環境が標準で付属し、作ったソフトをロイヤリティフリーで配布・販売してよいという、当時としてはかなり変わった製品だった。 いまでいうインディー開発機の先駆けに近い。

主なスペックはこう。

項目内容
CPUEPSON S1C33209 24MHz(32bit RISC)
メインメモリSRAM 256KB
ストレージフラッシュRAM 512KB
ディスプレイFSTN 4階調モノクロ液晶、128×88ドット
電源単三乾電池1本、またはUSBバスパワー
インターフェースUSB Type-B、赤外線通信
サイズ101×65×17mm、重量67g(電池込み92g)

補足しておくと、FSTN液晶はバックライトの無い単純なモノクロパネル。 4階調なので白・黒に加えて薄いグレーが2段階出るだけ。 音はPWM音源、つまりCPUが波形を直接吐いて鳴らすソフトウェア合成で、専用の音源チップは積んでいない。 スプライト機能もなく、画面はビットマップにベタ描きする。 ハード支援はほぼ無く、CPUパワーで全部やる潔い構成。

P/ECE本体とシステムメニュー。十字キー(オレンジ)とA/B/START/SELECT

ボタンは十字キーとA・B、それにSTART・SELECT。 このあと自作ゲームの操作をここへ割り当てることになる。

このUSBバスパワー駆動に、今回はかなり助けられた。 20年眠っていたこの個体は、もう電池では動かない。 電池の接点がやられていて、接点復活剤をかけても復活しなかった。 たぶん酸化しているので、直すなら接点そのものを交換するしかなさそうだ。 ただ、USBを挿せば普通に起動する。 なので最初からずっとPCに繋ぎっぱなしのまま、検証も開発も全部USB給電で通した。

ドライバが今のWindowsで動かない

最初の壁はドライバ。 P/ECEはPCと専用ソフトでやりとりするのにカスタムのUSBドライバを使う。 そのドライバは当時の32bit Windows向けにしか作られておらず、署名もない。 いまのWindows 11は64bitで、署名なし32bitドライバはロードできない。 そもそも付属CDはとうに行方不明だが、仮に手元にあってもこのドライバだけは使えない。

接続した時点で、Windowsのデバイスマネージャには PIECE PME-001 として現れる。 ただしドライバが当たらないのでエラー状態(問題コード28=ドライバ未インストール)になっている。 本体のUSB IDは VID_0E19 / PID_1000。 これが目印。

回避策はある。 P/ECEのUSBインターフェースを汎用ドライバのWinUSBに差し替えて、PC側ソフトが叩くDLLをWinUSB経由で再実装したものに置き換える。 OSのドライバ署名強制を切る荒業もあるが、OS全体のセキュリティを下げるので採らない。

用語を補足しておく。

用語意味
WinUSBMicrosoft純正の汎用USBドライバ。専用ドライバを書かなくても、ユーザー空間のアプリからUSB機器を直接叩けるようにする仕組み
Zadig任意のUSB機器のドライバをWinUSBなどに差し替えるツール
pieceif.dllP/ECE付属ソフトがUSB通信に使う純正DLL。当時のドライバ前提のままだと動かないので、WinUSB版に差し替える

差し替えにはautchさんの pieceif-libusb を使う。 2026年6月リリースのv2.0系はWinUSBを直接叩く実装で、libusb-1.0.dll のような外部DLLも要らず、pieceif.dll 単体を上書きするだけで済む。 全体の流れはこう。

flowchart TD
  A[P/ECEをUSB接続] --> B[Windowsは PIECE PME-001 を認識<br/>だがドライバ無しでエラー]
  B --> C[ZadigでWinUSBドライバに差し替え]
  C --> D[pieceif-libusb の pieceif.dll を上書き]
  D --> E[isd / WinIsd で本体と通信できる]

ZadigでデバイスをWinUSB化する。 PIECE PME-001 を選び、USB IDが 0E19 1000 であることを確認して WinUSB をインストールするだけ。

ZadigでP/ECE(PME-001)にWinUSBドライバを当てる

成功すると、さっきまでエラーだったデバイスが正常になり、紐づくサービスが WinUSB に変わる。 あとは pieceif.dll をpieceif-libusbのものに差し替えれば、PC側の転送ツールが本体と話せるようになる。

まず同梱ゲームを焼いて、通信が通るか確かめる

自作を載せる前に、転送経路が本当に通っているかを確かめておく。 P/ECEの開発環境(公式サイトから今もダウンロードできる)には、転送ツール WinIsd.exe(GUI)と isd.exe(CLIのモニタ)が入っている。 作業フォルダを指定して .pex(P/ECEの実行ファイル)を選び、本体へ送るだけ。

WinIsdで同梱のblock.pexを本体へ転送

転送した同梱のブロック崩しが本体で起動した。 ここで通信経路の正しさが確定する。

同梱のブロック崩しがP/ECE実機で動作

余談だが、本体のフラッシュには昔のデータが残っていた。 テキストリーダー(textread.pex)と、1から11まで番号を振ったtxtファイル。 20年前のわたしが何か文章を入れて持ち歩いていたらしいが、まったく記憶にない。 中身は開かずにそっとしておいた。

でも本当にやりたいのは自作を動かすこと

同梱ゲームが動くのはゴールではなく前提確認。 本題は、別プロジェクトで書いていた自作ゲーム「弁当脱出」をP/ECEに載せること。

弁当脱出は、もともと「フロッピーディスクに収まるサイズでゲームを作る」という主旨のコンテストの話を聞いて、サクッと作れないかと思って書いたもの。 上限はたぶんFD1枚ぶんの1.44MBで、Win32の生API・C言語で組んだ、テキストとメーター主体のゲームだ。 ただ実際のexeは87KBほどしかなく、制限の6%程度。 だいぶ余裕を残している。 内容は、地震で閉じ込められた状態から、持ってきた弁当をリソースに地上を目指す純ターン制サバイバル。 重要なのは、ゲームロジックの部分がWin32に一切依存していないこと。 描画(GDI)と入力(ウィンドウメッセージ)だけがWindows依存で、満腹・中毒・鮮度・イベントといった計算はただのC。 つまりロジックはそのまま流用でき、描画と入力の層だけをP/ECE向けに書き直せばいい。

問題は、それをビルドする手段。

C33コア向けコンパイラという壁

P/ECEのCPUはEPSONのS1C33コア。 自作ソフトをビルドするには、このコア向けのコンパイラが要る。 付属SDKには pcc33.exe というドライバ(コンパイラ本体を呼ぶラッパー)は入っているが、肝心のバックエンド(gcc33等のコンパイラ実体)は付属CDからしか手に入らない。 本体だけは残っていたが、CDはパッケージごとどこかへ消えていた。

そこで持ち出したのが、autchさんの piece-toolchain-llvm。 S1C33をターゲットにしたClang/LLVMのツールチェーンで、ランタイム(newlib / picolibc / compiler-rt)までソースから作る。 当時のSDKバイナリを一切使わずにビルドできるのが大きい。 代わりに、LLVMをソースからフルビルドする必要がある。

LLVMツールチェーンを建てる(ここでメモリが先に音を上げる)

ビルドは検証環境のWSL2(Ubuntu 22.04)でやる。 リポジトリを取得し、サブモジュール(LLVMフォーク、picolibc、newlib)を引いてくる。 LLVMのサブモジュールだけで2.4GBある。

LLVM本体のビルドはおおよそこうなる(実際はリポジトリ付属のMakefileに沿う)。

cmake -G Ninja -S llvm/llvm -B build \
  -DCMAKE_BUILD_TYPE=Debug \
  -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD="S1C33" \
  -DLLVM_DEFAULT_TARGET_TRIPLE="s1c33-none-piece" \
  -DLLVM_ENABLE_PROJECTS="clang;lld" \
  -DLLVM_USE_LINKER=mold \
  -DCMAKE_C_COMPILER_LAUNCHER=ccache
make            # llvm + sysroot

20スレッド(-j20)で回して、25分ほどで3295ステップ中2400まで進んだところで死んだ。

c++: fatal error: Killed signal terminated program cc1plus
compilation terminated.

死因はメモリ不足。 OOMキラーがコンパイラのプロセスを殺していた。 さっき出した15GBのRAMに対して、Debugビルドの巨大なソース(PassBuilder.cppASTMatchFinder.cpp は1ファイルで3〜4GB食う)が20並列で同時にコンパイルされ、メモリを使い切った。

用語を補足しておく。

用語意味
OOM(Out Of Memory)メモリ枯渇。Linuxのカーネルが、メモリを食い過ぎたプロセスを強制終了する(OOMキラー)。Killed signal terminated がその痕跡
-j20makeやninjaの並列ジョブ数。コア数ぶん並列にコンパイルすると速いが、その数だけメモリも同時に要る

24年前の携帯機のために最新のLLVMを建てたら、母艦のRAMのほうが先に音を上げる、という構図。

ちなみに、これだけ遅いならGPUで殴れないのか、とも思った。 だがこれは無理で、コンパイラの処理は分岐とポインタ追跡だらけ。GPUが得意な「大量データに同じ演算」とは正反対で、clangをGPUに載せる実用的な道は無い。 速くしたいならccacheやdistccでCPU側を工夫するしかなく、そもそも今回-jを絞ったのもメモリ都合だった。

対処は単純で、並列数をメモリ量から逆算して絞る。 重いファイルが1個3〜4GBなら、15GB ÷ 4GB ≒ 3-j3。 重要なのは、すでにビルド済みのオブジェクト(2400ステップぶん)はそのまま再利用できること。 ゼロからではなく、残りを -j3 で焼き直すだけで済む。

ninja -C build -j3 clang llc lld llvm-objdump llvm-objcopy

これでLLVM本体(clang 22.1.1)は通った。 ただしもう一段詰まる。 続くランタイム(picolibc)のビルドで llvm-ar が無いと言われる。 Makefileの llvm ターゲットは clang llc lld llvm-objdump llvm-objcopy しかビルドせず、ランタイムが必要とする llvm-ar が抜けていた。 これは単体で足せばいい。

ninja -C build llvm-ar llvm-ranlib llvm-nm
make -C tools/crt all     # picolibc / newlib / compiler-rt

ここまで来てようやく、C33向けのclangとCランタイムが揃った。

描画と入力をP/ECEのAPIへ移植する

ツールチェーンができたので、本題の移植に入る。 方針は最初に書いたとおり、ロジックは流用し、描画と入力を書き直す。

画面: 128×88 にレイアウトし直す

元のゲームはWindowsのウィンドウに大量のテキストを並べていた。 P/ECEは128×88しかない。 P/ECEのカーネルフォントは全角10×10ドットなので、横はせいぜい12文字、縦は8行。 全画面をこの制約に合わせて組み直す。

描画はフレームバッファ方式。 unsigned char vbuff[128*88] を1ピクセル1バイトで持ち、そこへ直接描いて pceLCDTrans() で液晶に転送する。 テキストはカーネルの pceFontPutStr / pceFontPrintf、メーターのバーは自前で矩形を塗る関数を書いた。

日本語はそのまま出せた。 pceFontPutStr はShift-JISを解釈して全角を描く(戻り値が「最後のバイトがShift-JIS第1バイトだったらそのコード」になっている時点で、内部でSJISを見ているのが分かる)。 ただしソースの文字コードに一工夫いる。 Clangはソースを基本UTF-8として扱うので、表示文字列はビルド時にShift-JISへ変換してから渡す。 変換はWindows系のSJISである CP932 を指定する(厳格な SHIFT_JIS だと、em dash のような一部の文字で変換が止まる)。

bento_sjis.c: bento_piece.c
	iconv -f UTF-8 -t CP932 $< -o $@

入力: 数字キーをカーソル選択に置き換える

元のゲームは数字キーやEnter/X/Cで操作していた。 P/ECEに数字キーは無い。 十字キーとA・B、START・SELECTだけ。 そこで操作系をカーソル選択に作り替える。

プレイ中の行動(進む・押す・休む・弁当を食べる・拾い物を使う)を1本のコマンド一覧にまとめ、十字キーの上下でカーソルを動かし、Aで実行する。 ボタン入力は押しっぱなし状態(PAD_*)と押した瞬間(TRG_*)の両方がカーネルから取れるので、メニュー移動には押した瞬間のほうを使う(連続で動かないように)。

P/ECEアプリの骨格はこのコールバック3つ。

#include <piece.h>
static unsigned char vbuff[128*88];

void pceAppInit(void){              // 起動時
  pceLCDSetBuffer(vbuff);
  /* 初期描画 */
  pceAppSetProcPeriod(1);
}
void pceAppProc(int cnt){           // 周期的に呼ばれる本体
  int p = pcePadGet();              // ボタン状態+トリガー
  /* 入力で状態を更新し、変化があれば再描画して pceLCDTrans() */
}
void pceAppExit(void){}             // 終了時

ロジック側(弁当8品の腐敗・毒、満腹の天井、ランダムイベント等)は原作のCをほぼそのまま持ってきた。 変えたのは wchar_tchar にしたことと、描画・入力の差し替えだけ。

ビルドは、SJISへ変換したソースをclangに通し、ppack.pex に固める。 コンパイル時に「文字エンコーディングが不正」という警告が大量に出るが、これはSJISバイトがUTF-8として不正なだけで、Clangはバイトをそのまま通す(autch自身のサンプルアプリも同じ作りでビルドが通る)。 日本語のバイトはバイナリに正しく入る。

.pex を焼いて、実機で起動する

できた bento.pex を本体へ書き込む。 GUIのWinIsdでもいいが、今回は isd.exe のCLIモニタにコマンドを流し込んで焼いた。 このモニタは本体のフラッシュファイルシステム(PFFS)を直接いじれる。

=            PFFSの一覧表示
=w bento.pex ファイルをPFFSへ書き込む

=w で書き込むと、本体の一覧に bento.pex が並ぶ。

+ 14: 14132 block.pex
+ 15:  4931 bento.pex      ← 書き込み完了
    18 sectors free

あとは本体のシステムメニューから起動するだけ。

起動直後。 タイトルと、つかみの一言が出る。

弁当脱出の起動画面。「明日も仕事か」「[A]はじめる」

行き先を選び、弁当を詰め、地震で閉じ込められたあとにルール説明が入る。 満腹0で餓死、中毒100で死、弁当は腐ると毒になる。

ルール説明画面。満腹・中毒・弁当の基本ルール

そして脱出本編。 画面上にターン数と脱出距離、満腹と中毒のメーター、下にコマンドカーソルと弁当の鮮度バー。 カーソルをAで実行するたびにターンが進み、満腹が減り、弁当が腐っていく。

弁当脱出のプレイ画面。上にターン数と脱出距離・満腹/中毒メーター、下にコマンドカーソルと弁当の鮮度バー

無事に……とはいかず、判断を誤って餓死した。

餓死の結末画面。「餓死した」「[START]もう一度」

エミュレータを通さず、いきなり実機に焼いた一発目で表示崩れゼロ。 128×88に収まるよう設計した甲斐はあった。


描画はビットマップ直焼きなので、適当なドット絵でも出せばもうちょっとグラフィカルに行けるかもしれない。 FDの範囲内で収まりそうなら、今度試してみるか。