ファミコンエミュレータの話 gucchan (KG: arch, Jun Murai Lab.) gucchan@sfc.wide.ad.jp 自己紹介 - ぐっちゃん - 学部2年。村井研(ARCH) - 興味分野: 高い所でも低い所でもなんでも - 今やっている事: 一言で言うとネットワーク機能のないOSにIPを喋らせる事 おしながき - エミュレータについての概要 ゲームソフト吸い出し機と著作権法 ROMの構造 6502アセンブリ言語入門 エミュレータの基本と実際の解析手法 CPUエミュレーション PPUエミュレーション タイミング調整の話 エミュレータを作る時は 最近のエミュレータネタ ゲームエミュレータとは なぜファミコン? - 解析が進んでドキュメントも整理されている 画面レンダリングとクロックのタイミング図、デバッグ用ROMなど - 他のエミュレータに比べて実装が楽 ROMのサイズも小さいのでデバッグしやすい (スーパーマリオは40KB) 様々な言語での既存実装が大量にある (開発時動いてるものしか信用できない) スペック - CPU 6502(RP2A03), 8bit - WRAM2KB, ビデオRAM 2KB) - 画面解像度 NTSC/PAL 256x240 - 最大発色数 52色 - 昭和58年(1983年) 7月15日に発売 - 当時のメーカー希望小売価格は14,800円 ROMの入手 - ROMが無ければ意味がない ゲームエミュレータはゲーム機と同じ働きをするものなので、当然ゲームソフ トに相当するROMが無ければ意味がない ROMの入手1 - インターネットからの入手 違法にアップロードされたROMをダウンロードする方法。 当然ながらダウンロードも違法行為(2010年1月1日より施行)。 ROMの入手2 - 吸い出し機から入手 吸い出し機と呼ばれるカートリッジから中身を抽出してPCに転送する機械を使っ て購入したカートリッジを吸い出す。マジコンにもこの機能は存在する。 → それ法律的に大丈夫なのか? 著作権法における複製 - 著作権法 (最終校正: 平成二七年六月二四日法律第四六号) 著作権の目的となつている著作物(以下この款において単に「著作物」という。)は、 個人的に又は家庭内その他これに準ずる限られた範囲内において使用すること(以下「私 的使用」という)を目的とするときは、 次に掲げる場合を除き、その使用する者が複製することができる 。(第三十条) 一、公共の使用に供する事を目的として設置されている自動複製機器を用いて複製 二、技術的保護手段の回避 三、著作権を侵害する自動公衆送信を受信して行うデジタル方式の録音・録画を 事実を知りながら行った場合 著作権法における複製 - 著作権法 (最終校正: 平成二七年六月二四日法律第四六号) 著作権の目的となつている著作物(以下この款において単に「著作物」という。)は、 個人的に又は家庭内その他これに準ずる限られた範囲内において使用すること(以下「私 的使用」という)を目的とするときは、 次に掲げる場合を除き、その使用する者が複製することができる 。(第三十条) 一、公共の使用に供する事を目的として設置されている自動複製機器を用いて複製 二、技術的保護手段の回避 三、著作権を侵害する自動公衆送信を受信して行うデジタル方式の録音・録画を 事実を知りながら行った場合 技術的な保護手段を用いておらず、かつ 私的範囲内での複製なら問題ない ROMの入手 - 自作した吸い出し機から入手 吸い出し機は高いので秋葉原からパーツを仕入れて来てTTLで回路を制作。 ロジックアナライザで確認するとROMが流れている 吸い出し機の仕組み - アドレスピン、データピン 基本的に欲しいROMのアドレスをカートリッジのアドレスピンに流すとそれに対 応する1byteのデータが8本(=1本1bitで1byte)データピンに流れてくる仕組み。 ^ÇÇêÉëë=éáå ① a~í~=éáå ② 吸い出し機の仕組み - PRG-ROM, CHR-ROM ファミコンのカートリッジにはゲームプログラム本体(PRG-ROM)と グラフィックデータ(CHR-ROM)が存在する iNES Header (16byte) PRG-ROM (32KB) CHR-ROM (8KB) 吸い出し機の仕組み - コントロールピン コントロールピンを利用してPRG-ROMを取り出すのか CHR-ROMを取り出すのかを指定できる。(他の制御信号もある) ^ÇÇêÉëë=éáå `çåíêçä=éáå ① ② a~í~=éáå ③ CHR-ROM - たった8KBのデータにグラフィックが収まっている 8x8のタイルが512個びっしり敷き詰められている。 ただのバイナリにしか見えない。C++で可視化プログラムを作ってみる事に。 CHR-ROMの可視化 void PPU::pattern_table_debug() { uint16_t base = 0x0000; uint8_t pal[4] = { 0x31, 0x21, 0x11, 0x01 }; int w = 0, h = 0; for(int k = 1; k <= (0x2000 / 16); k++) { for(int y = 0; y < 8; y++) { for(int x = 0; x < 8; x++) { uint8_t c = ((((vm.chr(base + y + 8) >> (7 - x)) & 1) << 1) | ((vm.chr(base + y) >> (7 - x)) & 1)) & 3; screen_buff[(NES_SCREEN_HEIGHT * (y + h)) + (x + w)] = pal[c]; } } w += 8; if((k & 0x1f) == 0) h += 7; base += 16; } } } CHR-ROMの可視化2 初代ドンキーコ●グの グラフィックデータ 初代パック●ンの グラフィックデータ 改変マリオ(拡大) jçÇáÑáÉÇ kçêã~ä CHR-ROMをいじるだけでこのような改変が可能になる マッパー 初期はPRG32KB+CHR8KBの容量だったがゲームの規模が大きくなるにつ れ、容量が足らなくなるため、ROM容量を拡張したものが出てくる 初期のPRG 32KB + CHR 8KBのROMをMapper 0(NROM)と言う - ROM Bank-switching PRG0 PRG1 Select=PRG0 Select=PRG0 Controller Bus PRG0 PRG2 CHR Bus Bus PPU 基盤情報を調べる - NesCartDB (bootgod.dydns.org:7777) 全世界のカートリッジのタイトル、基盤情報などを収集、公開している データベース。スーパーマリオブラザーズはMapper0(32K+8K)である事が分かる ご開帳 アセンブリ言語入門 機械語に変換 ② コンパイラ #include <stdio.h> int main(void) { printf(“hello,world\n”); return 0; } ① Cコード (高級言語) ③ 変換された機械語 CPUは機械語しか 理解する事ができない アセンブリ言語入門 - CPUは機械語しか理解できない。しかし... 人間には分かりにくい! アセンブリも同じだが、CPUの種類によっても機械語は異なってくる。 CPUの技術資料とにらめっこしながらプログラムを読み解かないといけない Hello,Worldはな、 55 48 89 e5 bf d4 05... C言語喋れないんか? アセンブリ言語入門 - アセンブリ言語と機械語 (x86_64) 機械語 アセンブリ この表示では一対一になっており、0x55=push %rbpという命令を表し、 0xc3=retqという命令を表している。機械語より命令の意味が分かりやすくなった。 6502アセンブリ言語入門 - 基本構文 äÇ~ オペコード AÅMUM I ñ オペランドE演算の対象などF - レジスタ 一時的に保存できる変数のようなもの。 変数のようで記憶できるデータ量は非常に少ない。 しかし、CPUの中に存在するものなので非常に高速にアクセスできる。 6502では、a, x, y, s, p, pcの6種類しかない! pcだけ16bitでそれ以外は8bitなので全部でも7byteしか記憶できない! 6502アセンブリ言語入門 - レジスタの役割 Aレジスタ: 演算を行う時に使う Xレジスタ: カウンタ、スタックの内容へのアクセス Yレジスタ: カウンタ Sレジスタ: スタックポインタ (後ほど解説) Pレジスタ: 演算結果やCPUのステータス情報が格納される PCレジスタ: 現在実行しているプログラムの位置が格納される 6502アセンブリ言語入門 - アドレッシング オペランドの場所を指し示す表現方法。 6502には様々な13種類のアドレッシングが存在する。2つの例を示す。 ~ÇÅ @ANM ^に加算 数値MñNMを=EfããÉÇá~íÉ=~ÇÇêÉëëáåÖF äÇ~ AÅMUM I ^にロード ñ jbjxMñ`MUMHñzの値を=E^ÄëçäìíÉ=fåÇÉñ=uF 6502アセンブリ言語入門 - スタック pushとpopの2つの操作が可能なデータ構造で、 スタック領域という関数内だけで使われる変数やアドレス情報を退避させるた めに使う。ファミコンにはたった256byteしかない。 6502アセンブリ言語入門 - 簡単なサンプル (5回繰り返し) ldx #$08 decrement: dex stx $0200 cpx #$03 bne decrement stx $0201 brk uレジスタにUを入れる uレジスタの内容をN減らす uレジスタの内容をMñOMM番地に格納 uレジスタとPを比較 もしPじゃなかったら戻る uレジスタの内容をMñOMN番地に格納 6502アセンブリ言語入門 - Easy6502 http://skilldrick.github.io/easy6502/ ブラウザ上で6502のコードを書いて実行できる。 メモリやレジスタを確認しながら書けるのでアセンブリの勉強に最適 簡単なハード解析の例 - サードパーティー製のファミコンコントローラ 任天堂純正ではない互換品のコントローラはなぜ ファミコンでコントローラとして動くのか 簡単なハード解析の例 - 回路のトレース どこがGNDに接続されていて、この配線ならこのボタンの役割が 適当なのかな....などと考察しながら回路をトレースしていく。 簡単なハード解析の例 - ロジックアナライザによる解析 コントローラーぐらいだと安いUSBロジアナでも解析可能。デジタル波形を みながらどのタイミングでこの信号が出てるから...などと言いながら先ほどの 推測と照らし合わせる 簡単なハード解析の例 - ゲームパッドのタイミング波形とピンアサイン こうして得られた結果を元に仕様を作成 同じ形式とピンアサインで信号を流せばよい。 source: http://www.ece383.com/datasheets/nes_driver_walkthrough.html CPU - アドレスバス、データバス、コントロールバス 必要なデータの場所を指定 → アドレスバス (単方向) 読み出し(R)なのか書き込み(W)なのか → コントロールバス (単方向) アドレスバスで指定したアドレスに書込/読込む → データバス (双方向) UÄáí=a^q^=_rp Rlj `mr R^j j~ééÉê `çåíêçääÉê UÄáí=`lkqRli=_rp NSÄáí=^aaRbpp=_rp fLl CPU - メモリマップ メインメモリの領域のうち、どの範囲が何に割り当てられているかを示した図 I/O命令をメインメモリの操作命令で扱う事ができる (Memory-Mapped I/O) 0x10000 PRG-ROM 32KB 0x8000 0x6000 0x4020 Mirrors (0x0000~0x07ff) SRAM Expansion ROM 0x0800 I/O Registers RAM 0x2000 0x0200 WRAM 0x0000 4KB Stack 256byte Zero Page 256byte 0x0100 0x0000 CPU - 割り込み(Interrupt) 実行中の処理を中断して強制的に指定された処理を実行させる(キー入力など) - NMI = 強制的に割り込み処理を実行 - IRQ = IRQ禁止フラグが立っていなければ処理を実行 - RESET = ゲームの一番最初、リセットボタンが押された時に実行 - BRK = ソフトウェア割り込み CPU - 割り込みベクタと復帰アドレス 各割り込み毎に割り当てられた割り込みベクタからアドレスを読み取って 割り込み処理本体(ISR)を実行する。 ただこの時に元の状態に戻るためにスタックに処理途中のアドレスを格納する CPUエミュレーション - CPUの基本的な動き (Instruction Cycle) 基本的にこの動きを超高速にひたすら繰り返す。これをプログラム上で行う。 IF (Instruction Fetch) ID (Instruction Decode) EX (EXecute) WB (Write Back) 1: 命令を読み込む 2: 命令を解釈する 3: 命令を実行する (この後pcを進める) 4: 結果をキャッシュに書き込む CPUエミュレーション - 実際のエミュレーション // 1命令実行 void CPU::run() { uint16_t opcode = vm.read(++pc); switch(opcode) { case 0xa1: op_lda(addr_indirectx()); break; case 0xa5: op_lda(addr_zero()); break; case 0xa9: op_lda(addr_imm()); break; case 0xea: op_nop(); break; // 同じようなコードがひたすら続く } } // 命令とアドレッシングの例 void CPU::op_lda(uint16_t addr) { reg_a = vm.read(addr); updateFlag(reg_a); } uint16_t CPU::addr_imm() { return pc++; } 1 10の総和を求める メモリダンプ CPUトレース 機械語サンプル PPU - PPU(Picture Processing Unit) ファミコンのグラフィックを統括しているチップ。 CPUとは別にアドレス空間を持っている。DMA転送が可能。 このチップがファミコンの最も重要な部分といっても過言ではない。 少ないメモリで当時としては驚きのグラフィックを作り出す事ができた。 メモリの仕様効率を上げるために複数のデータ構造を使って画面を描画 PPU - データ構造 複数のデータ構造を使いながらゲーム画面を描画していく パターンテーブル (パターンデータ) パレットテーブル (色の管理) ネームテーブル (敷き詰めるパターンの番号) 属性テーブル (色の管理) スプライトRAM (座標情報) ゲーム画面 PPU - キャラクタ 色と座標情報を持たないグラフィックデータ(CHR-ROMの内容) 8x8のタイルで各ピクセルのデータ量は2bit。つまり、1キャラクタ16バイト。 2bitなので4つの状態を表す事ができる。 - ブロック キャラクタ(8x8)が4つ集まって 16x16になったデータ CHR1 CHR2 CHR3 CHR4 PPU - パターンテーブル キャラクタパターンを保存するテーブル → なぜ色と座標情報を持たないのか? メモリの節約のため。キャラクタは金型に近いものでパレットを切り替える事で 同じキャラクタで別の絵を作る事ができる (マリオとルイージは同じキャラクタから作られ、パレットだけを切り替えている) PPU - キャラクタにはどうやって色を付ける? 1つの画面に最大4つのパレットを使う事ができる。 実際には共通の背景色が各パレットに1個ずつあるため、12色が使用可能。 パレットの番号は属性テーブルという別の1byteのデータによってブロック単位で 決められる。 属性テーブル (パレット番号の管理) CHR1 CHR2 CHR3 CHR4 参照されるパレット PPU - ファミコンは本来RGB出力をしていない - 16*4=64色あるが無彩色3相は輝度信号の影響を受けないため64-12=52色が表現可 - エミュレータではRGBルックアップテーブル定義して - 属性テーブルとパターンテーブルから算出した値がパレットへのインデックス値 色相4bit (2^4=16色) 輝度2bit (2^2=4色) PPU - ファミコンの画面はBG(背景)とスプライトの二重レイヤ構成 - スプライトはマリオなどの動いているもの、動かないものは背景になっている - 座標の書き換えを最小限にしてメモリと負荷を削減 スプライト有効時 スプライト無効時 - スプライトはハードウェアの関係上横に8個しか配置できない PPU - VBlank(垂直帰線時間) スキャンラインは240本。左から右へスキャンラインを240本描画した後に、 最初のスキャンラインに戻るまでの時間をVBlank(垂直帰線時間)と呼ばれている この間にのみVRAMをCPUは弄る事ができる (VBlank外で弄ると画面が乱れる) PPU - スプライトと背景の描画プロセス 画面を描画する時に、まず1スキャンラインずつ背景を描画する前に その描画している予定のラインにスプライトが配置されるかどうかをチェック。 配置されるようであればスプライトも出力するという 絵と絵を重ねる処理をハードウェア的に実行 PPU - こういうのはハードウェアの制約的に不可能 PPU - ネームテーブル 256 x 240の画面の中に敷き詰める背景データを指定する。 タイミング調整 - エミュレートしたファミコン(のCPUだけ) タイミング調整をしていないので本来1.48MHz動作のはずが仮想2.13GHz動作に タイミング調整 - 1画面描画する分のCPU,PPUのクロック数が決まっている - そのクロック分ハードウェアをエミュレーションして残りの時間は待機させる - この調整方法でリフレッシュレートを60Hzに調整 エミュレーションに 掛かるクロック分の時間(秒) 1/60秒 画面更新 1/60秒 - エミュレーション時間 (空き時間) 1/60秒 画面更新 1/60秒 画面更新 1/60秒 画面更新 命令を実行するのに必要なクロック数を記録していく必要性 t (s) タイミング調整 - 右のようなオペコードと対応している命令実行に必要なクロック数テーブルを用意 - 命令を実行する度にクロック数を記録 void CPU::run() { uint16_t opcode = vm.read(++pc); switch(opcode) { // 命令実行 } executed_clock += cpu_cycles[opcode]; } const uint8_t cpu_cycles[0x100] = { 7, 6, 2, 8, 3, 3, 5, 5, 3, 2, 2, 2, 4, 4, 6, 6, 2, 5, 2, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 6, 7, 6, 6, 2, 8, 3, 3, 5, 5, 4, 2, 2, 2, 4, 4, 6, 6, 2, 5, 2, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 6, 7, 6, 6, 2, 8, 3, 3, 5, 5, 3, 2, 2, 2, 3, 4, 6, 6, 2, 5, 2, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 6, 7, 6, 6, 2, 8, 3, 3, 5, 5, 4, 2, 2, 2, 5, 4, 6, 6, 2, 5, 2, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 6, 7, 2, 6, 2, 6, 3, 3, 3, 3, 2, 2, 2, 2, 4, 4, 4, 4, 2, 5, 2, 6, 4, 4, 4, 4, 2, 4, 2, 5, 5, 4, 5, 5, 2, 6, 2, 6, 3, 3, 3, 3, 2, 2, 2, 2, 4, 4, 4, 4, 2, 5, 2, 5, 4, 4, 4, 4, 2, 4, 2, 4, 4, 4, 4, 4, 2, 6, 2, 8, 3, 3, 5, 5, 2, 2, 2, 2, 4, 4, 6, 6, 2, 5, 2, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 6, 7, 2, 6, 3, 8, 3, 3, 5, 5, 2, 2, 2, 2, 4, 4, 6, 6, 2, 5, 2, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 6, 7 }; エミュレータを作る時は - すでにある実装を探す 動いているエミュレーターの実装が一番の参考になる。 GithubでNES(ファミコンの英語名)などと検索するとたくさんヒットする - 解析された資料を探す 基本的にリバースエンジニアリングされた情報を元に作るので まとめられた資料を探す。 有名エミュレータの作者などのサイトにも結構まとめてある。 - エミュレーションするハードのソフトを作る 命令する側(ゲームプログラム)と命令されてその処理を行う側(エミュレータ)の 双方を同時に把握する事によって理解力を深める事ができる エミュレータを作る時は すでにある参考にした実装 LiteNES (C実装) https://github.com/NJUOS/LiteNES nes (Golang実装) https://github.com/fogleman/nes 参考にした解析資料 Nesdev Wiki http://wiki.nesdev.com/w/index.php/Nesdev_Wiki NES Specifications, Everynes - Nocash NES Specs http://problemkaputt.de/everynes.htm 最近のエミュレータネタ 10年近く開発されているDSエミュレータがFinalに DeSmuME(デスミューミ)はフランス人のYopyop156氏が開発。 フランスの法改正のため開発が一時的に中断され、オープンソースに。 ほぼ全てのDSソフトが動作可能になっただけでなく、 Wi-Fi通信も可能になった。 全サイズ: 79MB ソースコードサイズ: 24万7248行 最近のエミュレータネタ 高校生が開発したiOS向けGBAエミュレータが要請で削除 アメリカのRiley Testut氏が開発したiOS向けのGBAエミュレータ「GBA4iOS」は デベロッパー証明書を悪用してGithubからダウンロードして インストールする事ができた。 これに任天堂の弁護団がGithub上からの削除を要請している事が分かり、 削除された。
© Copyright 2024 Paperzz