PSPで遊ぼう!
はじめに

PSPが日本で発売されてから、2年以上たちました。MPEG-4にエンコードされた動画を再生したり、ポッドキャストを自動的にダウンロードしたり、ブラウザでウェブサイトを閲覧したり、最近では1セグ放送を受信したり、とゲーム以外のことでも幅広く活用されているPSPですが、PSPはSCEの認証を受けていないバイナリを実行させることはできません。が、ファームウェアに存在するセキュリティホールを突いて起動したり、起動できるバージョンまでダウングレードしたり、と自作のバイナリを走らせることは初期のころから行われていました。世界で始めて実際にPSPの上で動作するHello Worldが公開されたのは、発売から5ヶ月ほどたったときです。呼ぶことのできるAPIも大部分が調査されていて、世界中の有志によりSDK(SoftwareDevelopmentKitの略。ソフトの開発に必要なものや、便利なものが詰まってる)が作成されています。今回は、そのSDKを使わせてもらい、お手軽にPSPでの開発の世界を体験してみる、ということを目標にこの記事を書いてみました。

現在出ているPSPの最新のファームウェアのバージョンは3.70です。ですが、当然このバージョンでは自作のバイナリを走らせることはできません。自作のバイナリを走らせるときは、大抵1.50のバージョンを使います。しかし、最近はカスタムファームウェア(以降、CFWと略称)というものがリリースされました。CFWは、通常のファームウェアの機能に加え、プログラム実行時のバイナリの検査を回避させてオリジナルのバイナリを起動させることができます。なので、今回対象にするのは、CFW3.71M33が導入されたPSP-1000(初期型。``うすちーさ''く無いほう)です。CFWを導入するためにもファームウェアのバージョンが1.5の必要がありましたが、つい最近どのバージョンからでもCFWを導入することができるパンドラバッテリーというものが見つかりました。細かい手順などの説明は省きますが、PSPのダウングレードなどに精通していないと作業は危険です。また、パンドラバッテリーの作成にバージョン1.50やCFWのPSPを必要としています。なので、親しい``そういった方面に''詳しいPSPのユーザに頼んでCFWを導入してもらうのが一番確実でしょう。ですが、CFWの導入やファームウェアのバージョンダウンといった行為はEULA(仕様許諾契約)に反しています。なので、そのようなPSPは基本的にSCEから有償、無償問わず全てのサポートを受けられなくなります。それを踏まえ、自己責任でお願いします。

SDKのインストール

PSP用のSDKは、コンパイラやライブラリ、バイナリ編集用のツールなどと多くのものを含みます。それらをまとめてPSPToolchainと呼ばれています。SDKはLinux向けに作られていますが、Windows上のCygwinでも動作します。今回はUbuntu Linux上にインストールしました。

PSPToolchainをビルド、インストール、アップグレードしてくれるスクリプトがSVNで公開されているので、チェックアウトします。

$ cd /tmp $ svn checkout svn://svn.pspdev.org/psp/trunk/psptoolchain $ cd psptoolchain

とりあえず、readme.txtを呼んでみます。どうやら、以下のものがビルドに必要なようです。

cygwinな人はたぶん関係ないのですが、ここに書いてあるもの以外でlibusbも必要になります。足りないものをインストールします。うちの場合はautomake、bison、flex、ncurses、texinfo、libusbがありませんでした。automakeはバージョン1.9でないとだめなようです。Cygwinな人はCygwinのインストーラから簡単にインストールできます。

$ sudo apt-get install automake1.9 $ sudo apt-get install bison $ sudo apt-get install flex $ sudo apt-get install libncurses-dev $ sudo apt-get install texinfo $ sudo apt-get install libusb-dev

次には、環境変数の設定が書いてあります。.bashrcに追記して、有効にするためにログインしなおします。Cygwinな人はWindowsの環境変数に追加してください。

$ echo 'export PSPDEV=/usr/local/pspdev' >> ~/.bashrc $ echo 'export PATH=\$PATH:\$PSPDEV/bin' >> ~/.bashrc

では、実際にスクリプトを実行します。

$ sudo ./toolchain-sudo.sh

toolchain.shは、デフォルトでは/usr/localの下に書き込むのでroot権限が必要です。が、toolchain.shを実行するためには上の環境変数の定義が必要です。一般ユーザで操作していた場合はrootの環境変数には定義されていないため、toolchain.shの実行に失敗します。なので、toolchain-sudo.shを実行しましょう。toolchain-sudo.shは環境変数を定義してからtoolchain.shを実行してくれます。俺は普段からrootだぜ!って人や、俺はcygwinだから関係ねーぜ!って人はtoolchain.shでもたぶん大丈夫です。

toolchain.shの実行には時間がかかるので、気長に待ちましょう。Athlon64X2 2300+、VMwareServerの上で走らせてるUbuntu 64bitで1時間半ほどかかりました。

無事に終わったようなら、ためしにpsp-gcc -vとか入れてみましょう。うちでは下のように出てきました。

Using built-in specs. Target: psp コンフィグオプション: ../configure --prefix=/usr/local/pspdev --target=psp-enable-languages=c,c++ --with-newlib --enable-cxx-flags=-G0 スレッドモデル: single gcc バージョン 4.1.0 (PSPDEV 20060507)

どうやらCだけじゃなく、C++も使えるみたいですね。

サンプルを走らせてみる

バージョン情報だけ見てても寂しいので、実際にサンプルをビルドしてみます。デフォルトでは、/usr/local/pspdev/psp/sdk/samplesの下にサンプルがあります、が、/usr/localの下に書き込めるのはrootだけなので、samplesごと適当な場所にコピーしておきましょう。

$ cp -R /usr/local/pspdev/psp/sdk/samples/ ~/src/psp

ためしに、gu/reflectionをビルドしてみます。

$ cd ~/src/psp/samples/gu/reflection $ make

XMBのスクリーンショット リフレクションのサンプル 成功すると、同じディレクトリにEBOOT.PBPというファイルが生成されています。それを、メモリースティックデュオ(以下、MSDと略称)にコピーします。MSDのルートをms:/とすると、EBOOT.PBPをms:/PSP/GAME/Test/EBOOT.PBPと、適当にディレクトリ作ったりして配置します。できたらそのMSDをPSPに挿します。XMBのゲームの項目の、メモリースティックを選択すると、“Reflection Sample”というアイコンがあるので、そこで決定を押します。PSPのロゴが出た後、鏡の上を箱が飛び跳ねる画面が映ります。

セルシェーディングのサンプル ほかのサンプルも見てみましょう。gu/celshadingを同じように実行してみます。どうやらトゥーンシェーディングのデモのようですね。ローポリゴンならアイマスみたいなものも描画できるかもしれません。外でやるには多大な勇気が必要ですが。

コントローラ入力のサンプル 見るだけではつまらないので、フィードバックがあるものも見てみることにします。controller/basicを実行してみます。アナログパッドの状態とそれぞれのボタンの状態を得ることができます。アナログパッドのセンターは127ですので、この画面ではアナログパッドのY方向に入力があるように見えます、が、実は触ってません。実は、このPSPのアナログパッドは半分壊れてますので、普通のPSPだとちゃんと動作するはずです。

とりあえず動くものを作ってみる

サンプルを実行するだけでは面白くないので、そろそろHello Worldを作ってみます。

#include <pspkernel.h> #include <pspdebug.h> PSP_MODULE_INFO("Hello! PSP World", 0, 1, 1); PSP_MAIN_THREAD_ATTR(THREAD_ATTR_USER); int main(){ pspDebugScreenInit(); pspDebugScreenPrintf("Hello! PSP World"); return 0; }

ビルドしてEBOOT.PBPを生成します。Makifile用のテンプレートもSDKに用意されているのですが、今回は直接コマンドを実行することにします。

$ psp-gcc -I/usr/local/pspdev/psp/sdk/include -Wall -L/usr/local/pspdev/psp/sdk/lib helloworld.c -lpspdebug -lpspdisplay -lpspge -lpspsdk -lc -lpspuser $ psp-fixup-imports a.out $ mksfo 'Hello PSP World' PARAM.SFO $ psp-strip a.out $ pack-pbp EBOOT.PBP PARAM.SFO NULL NULL NULL NULL NULL a.out NULL

Hello, world! では、同じように実行してみましょう。文字が左上に出ました。

実は、このサンプルで呼んでいるpspDebugScreenPrintfはデバッグ用にSDKについている関数です。ちゃんと、PSPのAPIを呼ぶ物も作ってみます。とりあえず、PSPのグラフィックユニットでスプライトを使ってグラデーションを表示させてみましょう。

#include <pspkernel.h> #include <pspgu.h> #include <pspdisplay.h> PSP_MODULE_INFO("HelloPSP", 0, 0, 2); PSP_MAIN_THREAD_ATTR(THREAD_ATTR_USER); struct Color8888{ unsigned char r; unsigned char g; unsigned char b; unsigned char a; }; struct Color8888 cornerColor[4]={{0xFF,0xFF,0xFF,0xFF},{0xFF,0,0,0xFF},{0,0,0xFF,0xFF},{0,0,0,0xFF}};/*グラデーション生成用の、4隅の色*/ /*GUに渡す頂点の構造体*/ struct Vertex{ unsigned short u, v; unsigned int color; short x, y, z; }; /*Color8888を32bitの整数に*/ unsigned int toInt(struct Color8888 c){ return (unsigned int)(((unsigned int)(c.a&0xFF)<<24)|((unsigned int)(c.b&0xFF)<<16)|((unsigned int)(c.g&0xFF)<<8)|(unsigned int)(c.r&0xFF)); } /*4隅の色からグラデーションをつくって、メモリに書き出す。今回は単純に、RGBでグラデーション作成*/ void createImage(unsigned int* pixel){ int x,y; struct Color8888 tmp,c[2]; for(y=0;y<272;++y){ tmp.r=(unsigned char)(cornerColor[2].r*(float)y/272+cornerColor[0].r*(float)(272-y)/272+0.5); tmp.g=(unsigned char)(cornerColor[2].g*(float)y/272+cornerColor[0].g*(float)(272-y)/272+0.5); tmp.b=(unsigned char)(cornerColor[2].b*(float)y/272+cornerColor[0].b*(float)(272-y)/272+0.5); tmp.a=(unsigned char)(cornerColor[2].a*(float)y/272+cornerColor[0].a*(float)(272-y)/272+0.5); c[0] = tmp; pixel[y*512]=toInt(tmp); tmp.r=(unsigned char)(cornerColor[3].r*(float)y/272+cornerColor[1].r*(float)(272-y)/272+0.5); tmp.g=(unsigned char)(cornerColor[3].g*(float)y/272+cornerColor[1].g*(float)(272-y)/272+0.5); tmp.b=(unsigned char)(cornerColor[3].b*(float)y/272+cornerColor[1].b*(float)(272-y)/272+0.5); tmp.a=(unsigned char)(cornerColor[3].a*(float)y/272+cornerColor[1].a*(float)(272-y)/272+0.5); c[1] = tmp; pixel[y*512+479]=toInt(tmp); for(x=1; x<479; ++x){ tmp.r=(unsigned char)(c[1].r*(float)x/480+c[0].r*(float)(480-x)/480+0.5); tmp.g=(unsigned char)(c[1].g*(float)x/480+c[0].g*(float)(480-x)/480+0.5); tmp.b=(unsigned char)(c[1].b*(float)x/480+c[0].b*(float)(480-x)/480+0.5); tmp.a=(unsigned char)(c[1].a*(float)x/480+c[0].a*(float)(480-x)/480+0.5); pixel[y*512+x]=toInt(tmp); } } sceKernelDcacheWritebackAll();/*CPUキャッシュの内容をメモリに書き出す*/ } int main(){ static unsigned int __attribute__((aligned(16)))displayList[4*65536];/*GUに渡すコマンドを入れる*/ static unsigned int __attribute__((aligned(16)))pixel[512*512];/*描画に使用するテクスチャ*/ struct Vertex* v;/*描画に使用する頂点*/ sceGuInit(); sceGuStart(GU_DIRECT, displayList);/*コマンドを入れるメモリの指定*/ sceGuDrawBuffer(GU_PSM_8888, (void*)0, 512);/*描画用のVRAMののアドレスを指定*/ sceGuDispBuffer(480, 272, (void*)(512*272*4)/*これより前にはDrawBufferがある*/, 512);/*表示用のVRAMのアドレスを指定*/ sceGuOffset(0,0);/*仮想表示領域の原点の指定*/ sceGuViewport(480/2,272/2,480,272);/*仮想表示領域の一部をビューポートに指定*/ sceGuScissor(0, 0, 480, 272);/*ビューポートのどこを実際に表示するか*/ sceGuEnable(GU_SCISSOR_TEST);/*sceGuScissorの設定を有効にする*/ sceGuEnable(GU_TEXTURE_2D);/*スプライトに貼るため、テクスチャを有効にする*/ sceGuFinish();/*初期化終わり*/ sceGuSync(0, GU_SYNC_FINISH);/*コマンドの送信完了を待つ*/ sceDisplayWaitVblankStart();/*垂直同期みたいなもの*/ sceGuDisplay(GU_TRUE);/*ディスプレイをつける*/ createImage(pixel);/*スプライトに貼るためのグラデーションの作成*/ sceGuStart(GU_DIRECT, displayList);/*描画開始*/ sceGuTexMode(GU_PSM_8888, 0, 0, GU_FALSE);/*テクスチャの設定*/ sceGuTexImage(0, 512, 512, 512, pixel);/*テクスチャをVRAMにコピー*/ sceGuTexFunc(GU_TFX_REPLACE, GU_TCC_RGBA);/*テクスチャの貼り付け方を指定*/ sceGuTexFilter(GU_NEAREST, GU_NEAREST);/*拡大、縮小の方法を指定*/ v=(struct Vertex*)sceGuGetMemory(2*sizeof(struct Vertex));/*頂点を表現するためのメモリを、コマンドリストから取得。特に開放する必要は無いみたい*/ v[0].u=0;v[0].v=0; v[1].u=480;v[1].v=272;/*テクスチャUVの設定*/ v[0].x=0;v[0].y=0;v[0].z=0; v[1].x=480;v[1].y=272;v[1].z=0;/*スプライトの頂点の設定*/ v[0].color=0;v[1].color=0;/*頂点色の設定。スプライトだと単色になる*/ sceGuDrawArray(GU_SPRITES, GU_TEXTURE_16BIT|GU_COLOR_8888|GU_VERTEX_16BIT|GU_TRANSFORM_2D, 2, 0, v);/*スプライトの描画*/ sceGuFinish();/*描画終わり*/ sceGuSync(0, GU_SYNC_FINISH);/*描画完了まで待つ*/ sceGuSwapBuffers();/*描画バッファと表示バッファを入れ替えて、描画バッファの内容を表示する*/ return 0; }

同じようにビルドします。さっきとコンパイルオプションが微妙に変わっています。

$ psp-gcc -I/usr/local/pspdev/psp/sdk/include -Wall -L/usr/local/pspdev/psp/sdk/lib hello\_gu.c -lpspgu -lpspge -lpspdisplay -lc -lpspuser $ psp-fixup-imports a.out $ mksfo 'PSP GU Test' PARAM.SFO $ psp-strip a.out $ pack-pbp EBOOT.PBP PARAM.SFO NULL NULL NULL NULL NULL a.out NULL

GUを使って、Hello, world! 無事に表示されました。紙の会誌ではグレースケールなのでわかりませんでしたが、ちゃんとグラデーションになっています。今回レンダリングしてるのはスプライトですが、もちろん普通の3角形のポリゴンもレンダリングできますし、インデックスバッファを使ってごにょごにょ、みたいな事もできます。使うテクスチャを直接生成していますが、当然ファイルから読み込んで表示することもできます。が、標準では画像ファイルを直接扱うことができません。幸い、libpngといったようなライブラリもPSPに移植されているので、それを使うことができます。

ここでは触れていませんが、WAVファイルを鳴らすこともできます。liboggやlibvorbisも移植されているので、OGGな音を鳴らすこともできるはずです。ほかにも、WiFiを使ってアドホックでほかのPSPと通信、やインターネットに接続、なんてこともできます。が、WiFiを使うにはカーネルモードで実行しなければならないのですが、現在カーネルモードの解析はまだ始まったばかりです。さすがにお手軽に使う、というわけにはいかないのではないでしょうか。でも、今の状態でもちょっとしたゲームを作るくらいならできます。すでにオリジナルなゲームを作ってる人もいるみたいです。さぁ、ぜひPSPでオリジナルなゲームを……、え?言いだしっぺの法則?ほら、今はR-TYPE TACTICSで忙しいですし(ry

本格的に開発を始めようと思うとSDKのドキュメントが必要になります。けど、ドキュメント自体はありません。必要な情報は、すべてヘッダファイルにコメントとして書いてあるので、doxygenなどのツールを使うとドキュメントにできるみたいです。


それでは、ぜひPSPでの開発を楽しんでみてください。