bsnes ソースコードリーディング
bsnes のソースコードを読んだ記録をしていく
- なんで読むの?
- SNES エミュレータ実装で拡張チップ SA-1 は実装してなかったから。CX 系も知れたら、くらいか
- なんで higan じゃないの?
- スーファミ以外のコードが入ってくるとコードパスがわかりにくいような予感がするから
- どこまで読む?
- 多分 main から各コンポーネント (CPU、PPU、SMP、DSP) の呼び出し場所
- nall, ruby など bsnes 上に構築されたコンポーネントも読み解く
コードと直接関係ありそうなところを抜き出したディレクトリ構成
.
├── bsnes
├── docs
├── extras
├── hiro
├── libco
├── nall
├── ruby
├── shaders
└── sourcery
まずはひと目でわかるところ
- bsnes
- bsnes source code
- README.md がある
- docs
- ドキュメント類があるが、Manifest (ゲームのメタ情報) 上書き方法が書いてある感じ
- extras
- settings.bml のみ。bsnes の設定保存場所かな
ここから下が bsnes 独特な場所。higan の wiki から持ってくる
hiro
原文
hiro is the cross-platform GUI toolkit that higan uses. Unlike other cross-platfrom GUI toolkits, it uses native widgets on each supported platform: GTK+ or Qt on Linux/BSD, Cocoa on macOS, and the standard controls on Windows.
Depends on:
- nall
deepl
hiroは、higanが使用するクロスプラットフォームGUIツールキットです。他のクロスプラットフォームGUIツールキットとは異なり、サポートされる各プラットフォームのネイティブウィジェットを使用します。Linux/BSDではGTK+やQt、macOSではCocoa、Windowsでは標準のコントロールが使用されます。
クロスプラットフォームGUIを担保するライブラリ。とはいえ、各プラットフォームに向けた記述がされているのでボリュームは多め。
mac だと cocoa ディレクトリ配下あたりがスタートかな
libco
原文
libco is a cooperative multithreading library, which allows higan's code to describe each system component separately, but to run nearly as fast as if every component were woven together.
deepl 翻訳
libco は協調型マルチスレッドライブラリで、higan のコードが各システムコンポーネントを個別に記述しながら、各コンポーネントが織りなすようにほぼ高速に動作することを可能にしています。
「cooperative multithreading」のところにリンクが張ってあるが byuu.org はリンク切れなので、internet archive を漁る
ぐわー、すごい量書いてる。一旦後回し
nall
原文
nall is byuu's alternative to the C++ standard library. As well as providing standard functionality like string processing and data structures, it also provides code shared between byuu's various projects, like cryptography algorithms, audio and image processing, and cross-platform filesystem handling.
deepl 翻訳
nall は byuu の C++ 標準ライブラリの代替となるものです。文字列処理やデータ構造のような標準的な機能を提供するだけでなく、暗号アルゴリズムや音声・画像処理、クロスプラットフォームのファイルシステム処理のような、byuuの様々なプロジェクトで共有されるコードも提供しています。
ふむ、utils 的なやつ。HTTP client/server とかもあったりする。これも量が凄い
ruby
原文
ruby is the interface between a hiro application and platform-specific APIs for emulator output and input. It displays emulated video through OpenGL or Direct3D with shaders; it plays emulated audio through ALSA, OSS, or WASAPI; and it reads gamepad, keyboard and mouse input.
Depends on:
- hiro
- nall
deepl 翻訳
rubyは、hiroアプリケーションと、エミュレータの出力や入力のためのプラットフォーム固有のAPIとの間のインターフェイスです。OpenGLやDirect3Dのシェーダーによるエミュレーション映像の表示、ALSAやOSS、WASAPIによるエミュレーション音声の再生、ゲームパッドやキーボード、マウスの入力の読み込みを行います。
video, audio, input というディレクトリがある。GUI と責務を分けたのは確かに。
shaders
これは higan のドキュメントにはないが、まんまシェーダーが入っているところ
sourcery
これも higan のドキュメントにはない。
sourcery.cpp を見た感じ、bml を input として、hpp と cpp を生成しているように見える。ビルド周りで使うのかな?
ビルドスクリプト
何はともあれ大事なビルドスクリプト
ビルドする際にはこういうコマンドでやる (mac 想定)
git clone git@github.com:bsnes-emu/bsnes.git
# clang: error: the clang compiler does not support '-march=native' が出るので local=false つける
make -C bsnes local=false
GitHub Actions workflow に build.yml があって助かった
build.yml
ビルドログはこんな感じ。めちゃくちゃ Makefile キレイな気がする
make 時のビルドログ
Compiling hiro/hiro.cpp ...
Compiling ../ruby/ruby.cpp ...
Compiling ../libco/libco.c ...
Compiling emulator/emulator.cpp ...
Compiling filter/filter.cpp ...
Compiling lzma/lzma.cpp ...
Compiling sfc/interface/interface.cpp ...
Compiling sfc/system/system.cpp ...
Compiling sfc/controller/controller.cpp ...
Compiling sfc/cartridge/cartridge.cpp ...
Compiling sfc/memory/memory.cpp ...
Compiling sfc/cpu/cpu.cpp ...
Compiling sfc/smp/smp.cpp ...
Compiling sfc/dsp/dsp.cpp ...
Compiling sfc/ppu/ppu.cpp ...
Compiling sfc/ppu-fast/ppu.cpp ...
Compiling sfc/expansion/expansion.cpp ...
Compiling sfc/coprocessor/coprocessor.cpp ...
Compiling sfc/slot/slot.cpp ...
Compiling gb/Core/apu.c ...
Compiling gb/Core/camera.c ...
Compiling gb/Core/rumble.c ...
Compiling gb/Core/display.c ...
Compiling gb/Core/gb.c ...
Compiling gb/Core/joypad.c ...
Compiling gb/Core/mbc.c ...
Compiling gb/Core/memory.c ...
Compiling gb/Core/printer.c ...
Compiling gb/Core/random.c ...
Compiling gb/Core/rewind.c ...
Compiling gb/Core/save_state.c ...
Compiling gb/Core/sgb.c ...
Compiling gb/Core/sm83_cpu.c ...
Compiling gb/Core/symbol_hash.c ...
Compiling gb/Core/timing.c ...
Compiling processor/arm7tdmi/arm7tdmi.cpp ...
Compiling processor/spc700/spc700.cpp ...
Compiling processor/wdc65816/wdc65816.cpp ...
Compiling target-bsnes/bsnes.cpp ...
Compiling target-bsnes/program/program.cpp ...
Compiling target-bsnes/input/input.cpp ...
Compiling target-bsnes/presentation/presentation.cpp ...
Compiling target-bsnes/settings/settings.cpp ...
Compiling target-bsnes/tools/tools.cpp ...
Compiling target-bsnes/resource/resource.cpp ...
Linking out/bsnes ...
rm -rf out/bsnes.app
mkdir -p out/bsnes.app/Contents/MacOS/
mkdir -p out/bsnes.app/Contents/MacOS/Database/
mkdir -p out/bsnes.app/Contents/MacOS/Firmware/
mkdir -p out/bsnes.app/Contents/MacOS/Shaders/
mkdir -p out/bsnes.app/Contents/Resources/
mv out/bsnes out/bsnes.app/Contents/MacOS/bsnes
cp Database/* out/bsnes.app/Contents/MacOS/Database/
cp -r ../shaders/* out/bsnes.app/Contents/MacOS/Shaders/
cp target-bsnes/resource/bsnes.plist out/bsnes.app/Contents/Info.plist
sips -s format icns target-bsnes/resource/bsnes.png --out out/bsnes.app/Contents/Resources/bsnes.icns
/Users/xxxxx/tmp/bsnes/bsnes/target-bsnes/resource/bsnes.png
/Users/xxxxx/tmp/bsnes/bsnes/out/bsnes.app/Contents/Resources/bsnes.icns
これらのコードを起点に読んでいけばいつかたどり着くかな
main エントリ的な場所
bsnes/target-bsnes/bsnes.cpp
auto nall::main(Arguments arguments)
の箇所
-
new SuperFamicom::Interface
の呼び出し-
bsnes/sfc/interface/interface.hpp
に定義されている -
Emulator::Interface
を継承 -
bsnes/emulator/interface.hpp
に定義がある - ここでいう interface はプログラミング的なものより UI とかに近いニュアンス
-
-
program.create();
を呼び出し
bsnes/target-bsnes/program/program.cpp
- 各種Settings の作成、
Application::onMain({&Program::main, this});
でonMain
にコールバック仕掛ける
bsnes/target-bsnes/bsnes.cpp
に戻り、Application::run
の実行
hiro/cocoa/application.cpp
auto pApplication::run()
が発火
bsnes/target-bsnes/program/program.cpp
-
Application::onMain({&Program::main, this});
からのProgram::main
-
emulator->run()
が呼び出されているのが分かる - emulator は先述した中身は
new SuperFamicom::Interface
したもの -
bsnes/sfc/interface/interface.cpp
にあるInterface::run()
はsystem.run()
を呼び出している -
system.run()
?
bsnes/sfc/system/system.cpp
-
SuperFamicom::system
。この Run では scheduler mode を Run に変えて、scheduler.enter()
scheduler は bsnes/sfc/sfc.hpp
-
struct Scheduler
にauto enter()
がある -
co_active()
の呼び出しがある -
libco.h
に定義がある
ここは Thread だな
迷子中。power あたりを見る
bsnes/target-bsnes/presentation/presentation.cpp
ROM ファイルが drop されたときには Presentation::onDrop
が呼ばれる
その中で program.load()
bsnes/target-libretro/program.cpp
の Program::load()
の中で emulator->load()
の呼び出し
ちょっと飛ばすが System::load
が呼ばれて、各種コンポーネントの load()
実行
- cpu
- smp
- ppu
- dsp
- cartridge
cartridge の region に応じて CPU Frequency が変わる
if(cartridge.region() == "NTSC") {
information.region = Region::NTSC;
information.cpuFrequency = Emulator::Constants::Colorburst::NTSC * 6.0;
}
if(cartridge.region() == "PAL") {
information.region = Region::PAL;
information.cpuFrequency = Emulator::Constants::Colorburst::PAL * 4.8;
}
Colorburst は bsnes/emulator/emulator.hpp
に定義されている
Program::load()
に戻り、ゲームに応じた Hacks がある
で emulator->power()
bsnes/sfc/interface/interface.cpp
の Interface::power
に行き、system.power()
なので、
bsnes/sfc/system/system.cpp
の System::power(bool reset)
ここで各コンポーネントの power(reset)
呼び出し
CPU の例
auto CPU::power(bool reset) -> void {
WDC65816::power();
Thread::create(Enter, system.cpuFrequency());
coprocessors.reset();
PPUcounter::reset();
PPUcounter::scanline = {&CPU::scanline, this};
// (snip)
}
拡張チップもここで power()
呼び出し
今回は SA1 に興味があるので SA1 を見る
auto SA1::power() -> void {
double overclock = max(1.0, min(4.0, configuration.hacks.sa1.overclock / 100.0));
WDC65816::power();
create(SA1::Enter, system.cpuFrequency() * overclock);
WDC65816 を 2 個持つ感じなのね。SA1 は動作周波数が違う感じ
create は bsnes/sfc/sfc.hpp
にある
auto create(auto (*entrypoint)() -> void, uint frequency_) -> void {
if(!thread) {
thread = co_create(Thread::Size, entrypoint);
} else {
thread = co_derive(thread, Thread::Size, entrypoint);
}
frequency = frequency_;
clock = 0;
}
System::power(bool reset)
の続き
scheduler.active = cpu.thread;
で scheduler の active を cpu.thread に指定。これで大体の準備はおしまい
各種 CPU, PPU などが先述した co_create
に指定されている ::Enter()
を呼び出す
内部では cpu.main()
などの呼び出し
auto CPU::Enter() -> void {
while(true) {
scheduler.synchronize();
cpu.main();
}
}
scheduler.synchronize でタイミングを合わせていると思われるが、bsnes/sfc/sfc.hpp
の frequency との兼ね合いもちょっと謎なのよね
60 fps ぴったりに制御している場所
60fps ぴったりにしているところがわからなくて苦戦してた。設定でデフォルト有効がゆえか〜
bsnes でちゃんと 60 fps になっている理由がやっと分かった( ˊᵕˋ ; ) Audio の Synchronize が On になってるからで、これを切ったら百数 fps で動き出したので納得。
Synchronize 設定で律速してるのね。コードだけ読むんじゃなくて触ってみないと気づかない典型
これでなんとなく分かった。
byuu さんの scheduler に書かれている。bsnes は Relative Schedulers の方。
しかし、 cooperative multithreading といい、byuu さんは凄いなぁ。
ここ以降は各コンポーネントに派生して読んでいけそうなので一旦 close
higan のおすすめもされたし。