PythonによるNESエミュレータ開発3

時間があればちょっとずつ続けてます。

とりあえず画面がでるようにはなりました。速度は全然追いつかないですけど。

pynes

CPU

基本的には変化なし。細かいバグが多くて大変・・・・

わずかでも速くしたいところなので、あまり構造を壊さず速度を上げられないかな、と思ってPrefetch cueを実装してみました。Prefetch cueはCPUの非常に基本的な最適化で、基本的故に単純、実装しても負荷になることはないだろう、ってことですな。

エミュレーターでは(とくにPythonでは)ハードウェアでいうメモリが遅いうんぬんとは別の理由で、メモリアクセスはかなり重い処理になります。

python code
  1. def memory(self, addr):
  2.   if addr < "RAMの範囲":
  3.     self.ram[addr]
  4.   elif addr < "メモリマップドIOの範囲":
  5.     self.io.read(addr)
  6.   #else:
  7.   # .
  8.   # .
  9.   # .
  10.  

さらにページングされている場合、C言語ならポインタでいけるんですけど、Pythonではそうはいかないので、いちいち長ったらしく書くか、ページング用にメソッドを書いてそれをはさむことになります。

そう、関数の呼び出しが非常に多い部分なのです。実際、profileモジュールでデータを取ってみてもメモリ読み込み書き込みが結構なウェイトをしめてました。

そこでPrefetch用のクラスを作って、別スレッドで現在のPC+いくらかをとるようにしてみました。これは現在のオペコードを実行している間に実行されます。これで次のオペコードを実行する前にオペコードとオペランドを取得でき、メモリ読み込み関数を呼ぶ回数が減ります。ただし、本物のPrefetch cueと同じでジャンプ命令が多いとあんまり効果がありません。

Prefetch-cueを入れてみると、まぁコードの内容によりますが、psycoを入れて1frame 0.075くらいまではいきました。PPUをいれると全然なんですけど(笑

PPU

なんとか表示できるまでにきました。正直つらいです(笑 特にスクロールに関してはloopyの文書の文書が重要、ということをしらなかったのではじめはサッパリでした。

描画部分はpygameです。pygameで描画する場合、更新したRectだけupdateするってのが常套手段なわけですが、エミュの場合はどうも・・・。というわけで今は毎回全体を描画しています。これも結構重い処理になるなあ。

ほとんどスクロールしないゲームの場合、もしかしたら32x32くらいに区切ってスプライトが動いたところだけ更新するようにしたら、結構軽いのかもなあ。


というわけで、PPUをつめてパッド入力あたりを書けばマッパー0のゲームなら動き出しそうな感じがします。ちなみにマッパーは全然です。とりあえずPythonで書いてみることが目的なので実際に使うことは想定して無いですし。

卒論の試問会も終わり、家探しの旅も終わり、つかの間の落ち着きが戻ってきました。なんか資格とかをとらないといけないらしいので、それをちょっとずつ勉強しつつ、こっちもちょっとずつ進めていきたいなーと思ってます。

07.27.08/12am

PythonによるNESエミュレータ開発2

実は既に結構挫折気味。

やっぱりPythonではちょっと厳しいかもしれない。

とりあえず一番ややこしいPPU周りの情報を調べて、ちょろちょろ書き始めたあたりでいったんCPU部分のパフォーマンスを調べてみました。

かなり厳しいものがあります。CPU部分のコードはPython的な書き方で書いてたんですが、これじゃ話にならない。まず、アクセサなんてものはつかっちゃいけないのだ。

以下環境はOS:WinXP,CPU:Athlon64 3000+,Memory:1G,Python2.4です。

CPUのレジスタ関連の実装

NESのCPUである6502のレジスタはPCは16bit、それ以外は8bit。8bitの値なんてものはCならunsigned charで一発なんだけど、Pythonにはそんなものない。足し算したらどんどん大きくなるし、引き算したらどんどん小さくなる。ので

python code
  1. class Py6502(object):
  2.   def __init__(self):
  3.     self.A = 0
  4.     self.X = 0
  5.     self.Y = 0
  6.     self.PC = 0
  7.     # .
  8.     # .
  9.     # .
  10.  

見たいなクラスにするとして、足し算するときなどは必ずself.A = (self.A + x) & 0xffみたいにして8bitに収めないといけない。ここで

python code
  1. def _create_n_bit_property(name, mask):
  2.   rname = "_"+name
  3.   result = {
  4.     name:property(lambda self: getattr(self, rname),
  5.                   lambda self,v : setattr(self, rname, v & mask),
  6.                   )
  7.   }
  8.   return result
  9.  
  10. class ForceNbitType(type):
  11.   def __new__(cls, class_name, class_bases, classdict):
  12.     names = classdict.get("__16bit__")
  13.     for name in names:
  14.       classdict.update(_create_n_bit_property(name, 0xffff))
  15.     names = classdict.get("__8bit__")
  16.     for name in names:
  17.       classdict.update(_create_n_bit_property(name, 0xff))
  18.     cls = type.__new__(cls, class_name, class_bases, classdict)
  19.     return cls
  20.  

のようなmetaclassをつくって

python code
  1. class Py6502(object):
  2.   __metaclass__ = ForceNbitType
  3.   __8bit__ : "A", "X", "Y"
  4.   __16bit__ : "PC",
  5.   def __init__(self):
  6.     self._A = 0
  7.     self._X = 0
  8.     self._Y = 0
  9.     self._PC = 0
  10.     # .
  11.     # .
  12.     # .
  13.  

とすればself.A += 10とかしてもちゃんと8bitに収まる。

非常に上手くかけるんですが、こんなのやってらんない。遅すぎる。1Frame分(28000サイクル程度)の実行に0.5秒(笑

getattr

getattrは遅い。getattrの真価は第3引数でdefault値が指定できること(だと思う)。今回のように確実に属性が存在することが分かっているならself.__getattribute__(name)を使うと結構違ってくる。ちなみにPythonのソースではこんな感じ。

c code
  1. static PyObject *
  2. slot_tp_getattro(PyObject *self, PyObject *name)
  3. {
  4.     static PyObject *getattribute_str = NULL;
  5.     return call_method(self, "__getattribute__", &getattribute_str,
  6.                "(O)", name);
  7. }
  8.  
  9. static PyObject *
  10. slot_tp_getattr_hook(PyObject *self, PyObject *name)
  11. {
  12.     PyTypeObject *tp = self->ob_type;
  13.     PyObject *getattr, *getattribute, *res;
  14.     static PyObject *getattribute_str = NULL;
  15.     static PyObject *getattr_str = NULL;
  16.  
  17.     if (getattr_str == NULL) {
  18.         getattr_str = PyString_InternFromString("__getattr__");
  19.         if (getattr_str == NULL)
  20.             return NULL;
  21.     }
  22.     if (getattribute_str == NULL) {
  23.         getattribute_str =
  24.             PyString_InternFromString("__getattribute__");
  25.         if (getattribute_str == NULL)
  26.             return NULL;
  27.     }
  28.     getattr = _PyType_Lookup(tp, getattr_str);
  29.     if (getattr == NULL) {
  30.         /* No __getattr__ hook: use a simpler dispatcher */
  31.         tp->tp_getattro = slot_tp_getattro;
  32.         return slot_tp_getattro(self, name);
  33.     }
  34.     getattribute = _PyType_Lookup(tp, getattribute_str);
  35.     if (getattribute == NULL ||
  36.         (getattribute->ob_type == &PyWrapperDescr_Type &&
  37.          ((PyWrapperDescrObject *)getattribute)->d_wrapped ==
  38.          (void *)PyObject_GenericGetAttr))
  39.         res = PyObject_GenericGetAttr(self, name);
  40.     else
  41.         res = PyObject_CallFunction(getattribute, "OO", self, name);
  42.     if (res == NULL && PyErr_ExceptionMatches(PyExc_AttributeError)) {
  43.         PyErr_Clear();
  44.         res = PyObject_CallFunction(getattr, "OO", self, name);
  45.     }
  46.     return res;
  47. }
  48.  

上が__getattribute__、下がgetattr。ま、こんなことやったって関数呼び出しのオーバーヘッドが一番ツライのでgetterは使わず、setterは必要があるときだけ使うように。これで結構はやくなる。

実行ループ

CPUのエミュレータなんてのはだいたい同じようなパターンで割り込みを除いて簡単に書くと

python code
  1.   def step_execute(self, clocks):
  2.     while (self.passed_clocks < clocks):
  3.       opecode = read()# opecodeを取得
  4.       # 実行
  5.       count = CLOCK[opecode] # 実行に必要なクロック数を取得
  6.       self.passed_clocks += count
  7.     self.passed_clocks -= clocks
  8.  

なんてのになるわけで、ここが激しくループするわけで。ここはガリガリにちょっとでも節約できるものは節約。

python code
  1.   def step_execute(self, clocks):
  2.     read = self.memory.read
  3.     get_method_by_opecode = self.get_method_by_opecode
  4.     while (self.passed_clocks < clocks):
  5.       old = self._PC; self._PC += 1; self._PC &= 0xffff
  6.       opecode = read(old)
  7.       method = get_method_by_opecode(opecode)
  8.       count = method()
  9.       count = count != None and count or CYCLES[opecode]
  10.       self.passed_clocks += count
  11.     self.passed_clocks -= clocks
  12.  

javascriptで.(ドット)演算が遅いとかいうのは最近(?)よく言われていることで、それはPythonにも当然当てはまる。なのでループ前にだせるものはローカルに出しておく。(これが簡単にできるのがRubyよりもPythonがいい部分だよなあ)

あと、実行の部分。ここはCなら関数テーブルかswitch(コンパイラによるけど大差ないと思う)になるんだけど、あいにくPythonにはswitchがないので関数テーブル的なものかif opecode == 0x01: ... elif opecode == 0x02:... elif...かになる。

関数テーブル的なものは

python code
  1. def ope_0x01(self):
  2.   #code
  3. def ope_0x02(self):
  4.   #code
  5.  
  6. self.__getattribute__("ope_"+hex(opecode))()
  7.  

となる。両方やってみたところ、大差はなかったので、関数テーブル的なほうに。

結局

そんな感じでいじってみてかつpsycoを入れて1Frame:0.15程度(PPUやAPUは中身がないので、最後までつくったらもっと遅くなる)。ほかにもいじれそうなところはあって、そこをいじれば0.1は切れそうな感じがしてます。

けど、そこまでやるとPythonである意味がないのも確か。ぶっちゃけ、エミュレータは確実にCが向いている。Javaで書かれているエミュレータもレジスタの値をセットするたびにA & 0xffみたいなことをやっていて、どーもめんどくさい。

じゃぁLLでエミュレーターを書く意味ってなんだ、というと・・・うーん(笑 自己満足以外なにもないでしょうねえ。というわけで自己満足のために、今後もヒマができれば、ちょっとづつ書き進めてみようかなあ。

07.27.08/12am

PythonによるNESエミュレータ開発1

正月だというのにひたすらCPUの命令を実装・・・。ここはひたすら地道な作業が続く。やっぱりこういう単純作業は苦手だ・・・

実装の際にはInfoNES6502 Emulation Packageのソースコード、そしてNES on FPGAのCPUのページに大変お世話になりました。特にNES on FPGAのページは非常に分かりやすくまとめられていて助かりました。

実装方針は前にも書いたとおり「できるだけPythonっぽく」。着々とエミュレーターっぽくないソースが出来上がってきています(笑 エミュレータのソースコードというとそれはもう、C言語でマクロ全開、register指定、inline(もしくはstatic)で出力されるコードを極限まで速く、なんて感じですが全く逆です。分かりやすさ優先。とりあえず書いてみて、ダメそうなら諦めるか頑張ってチューニングするか考えよう。

今日までで、CPUの全命令は実装しました(したつもり)。ただ、Indexed Absolute AddressingのX,Y、そしてIndirect Indexed Addressingでページクロスしたとき1クロック余計に発生する、という件は今のところスルー。

メモリ周りもIO以外はだいたい作ったので、今日の時点で単純なコードなら実行可能に。早速アセンブラを書いてNESASMで.NESファイルを作成。それを読み込んで動かしてみました。とりあえず動くことは動く・・・が全命令が正しく実装されているかはまだ分からないです(笑

というわけで次はデバッグ環境を整えて、命令のテストをしないとなあ。

余談。いやー卒論という重圧がないのは非常にプログラミングがはかどる。別にギリギリなわけでもないのに「卒論完全に出来上がってないのにこんなことしてていいのかなーって感じちゃう」的なものがないし。

07.27.08/12am

About

Author:yuin(http://inforno.net/)

文学部文化学科卒という生粋の文系趣味プログラマ。

主にRuby、Javascript、PHP、JAVA,Python,C,Scala,Schemeなどを使っています。今はPythonな感じかもしれない。今後作曲活動なども復活するかもしれない。

Pages