# 2020q3 Gameboy-emu contributed by < `Rsysz` > ###### tags: `sysprog` > [Gameboy 模擬器](https://hackmd.io/@sysprog/BJvOWzFqv) > [GitHub](https://github.com/Rsysz/gameboy-emu) ## Intro 首先根據老師給予的 [jitboy](https://github.com/sysprog21/jitboy) 內的描述,了解到當前模擬器有兩種較常見的實作方向。 * 一是模擬 `Game boy` 的環境,就能讓舊有的 `ROM` 順利執行,但缺點就是因 `CPU instruction` 不兼容,因此須將指令逐行翻譯,採用靜態編譯,因此 `branch miss` 將無法避免 * 第二種便是透過 `JITcompiler` 達成動態編譯,也就是在執行期才對下段指令編譯,因更好的效能而為多數模擬器所採用,相對於前者更消耗資源 根據老師制定的目標,我所實作的 [gameboy-emu](https://github.com/Rsysz/gameboy-emu) 採用第一種方式,目標在達成 **portable** 與優化模擬器效能。 ![](https://i.imgur.com/RYvVr6w.png) ## Gameboy 根據 [Gameboy Overview](https://thomas.spurden.name/gameboy/) 內的解說與 [Gameboy CPU (LR35902) instruction set](https://www.pastraiser.com/cpu/gameboy/gameboy_opcodes.html) 的 `CPU instruction` - [x] TODO: 研讀 [git-commit](https://chris.beams.io/posts/git-commit/) 使用 `git rebase` 合併多餘 `commit` 並優化 `commit message`,並依檔案增減修改數量拆分為多個 `commit` 分次提交 `pull request` ```cpp gameboy.h __gb_execute_cb >> cpu.h __cpu_execute_cb gameboy.h __gb_step_cpu >> cpu.h cpu_step gameboy.h __gb_draw_line >> cpu.h __gb_draw_line gameboy.h __gb_write >> mmu.h mmu_write gameboy.h __gb_read >> mmu.h mmu_read ``` [github log](https://github.com/Rsysz/gameboy-emu/commits/master) 不知道這樣的改動是否OK? > 這樣的修改還是難以 code review,先從 CPU 執行狀態開始搬動,一開始大概兩個 commits。注意,你提交的 pull request 可能會有相依性,你應該等待 review 並做出對應的變動,然後 rebase 再提交。 > :notes: jserv ## Refactor 因目前 `pull request` 的分支與 `master` 分支部分有所不同,在這裡單獨介紹 `master` 部分的改動 從 `gameboy.h` 中將 ```cpp __gb_step_cpu() __gb_execute_cb() ``` 拆離至 `cpu` 部分 ```cpp __gb_read() __gb_write() ``` 拆離至 `mmu` 部分 參考 [mGBA](https://github.com/mgba-emu/mgba/tree/master/src/sm83) 將 `cpu instruction` 以 `macro` 展開型式表達 以 `computed-goto` 建構 `dispatch_table` 修改 `__gb_step_cpu` 用以快速跳躍至指定 `instruction` 整合 `gbit` 指令除錯器進 `bench.c` 整合 `bios` 檔案於開機時顯示 ## Experiment 目前打算參考 `jitboy` `instr_table` 部分對 `cpu.c` 程式碼重構,並接著加入 `BIOS` 顯示功能,因此先做了一個小實驗,驗證想法可行性 ```cpp #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> typedef struct cpu_instr_s { void *execute; enum { NONE, REG_A, REG_B, REG_C, REG_D, REG_E, REG_H, REG_L /* = 7 */, REG_AF, REG_BC, REG_DE, REG_HL, REG_SP, IMM8, IMM16 /* = 14 */, MEM_BC, MEM_DE, MEM_HL, MEM_16, MEM_8, MEM_C, MEM_INC_DE, MEM_INC_HL, MEM_DEC_HL /*=22*/, CC_Z, CC_C, CC_NZ, CC_NC, MEM_0x00, MEM_0x08, MEM_0x10, MEM_0x18, MEM_0x20, MEM_0x28, MEM_0x30, MEM_0x38, BIT_0, BIT_1, BIT_2, BIT_3, BIT_4, BIT_5, BIT_6, BIT_7, TARGET_1, TARGET_2, WAIT_LY, WAIT_STAT3 } op1, op2; uint8_t *args; uint16_t address; uint8_t bytes; uint8_t cycles, alt_cycles; enum { INST_FLAG_NONE = 0x00, INST_FLAG_PRESERVE_CC = 0x01, INST_FLAG_PERS_WRITE = 0x02, INST_FLAG_USES_CC = 0x04, INST_FLAG_AFFECTS_CC = 0x08, INST_FLAG_ENDS_BLOCK = 0x10, INST_FLAG_SAVE_CC = 0x20, INST_FLAG_RESTORE_CC = 0x40 } flags; } cpu_instr; void NOP() { printf("__func__"); } static const cpu_instr inst_table[1] = { [0] = {NOP, NONE, NONE, 0, 0, 1, 1, 1, 0} }; int main() { struct cpu_instr_s test = inst_table[0]; void(*opcode_function)() = test.execute; opcode_function(); getchar(); return 0; } ``` :::warning TODO: 1. 使用 `clang-format -i *.[ch]` 對原始程式碼進行縮排調整,強制設定縮排為 4 個空白,如此才能確保原始程式碼和 HackMD 展現的效果一致。 2. 避免使用 **不雅** 詞彙 3. 研讀 [Threaded Code](http://www.complang.tuwien.ac.at/forth/threaded-code.html) 和 [Fast VMs without assembly](http://www.emulators.com/docs/nx25_nostradamus.htm),以理解上述手法的原理 4. 使用 computed-goto 時,要確定已在 `Makefile` 加入 `CFLAGS += -fno-gcse -fno-crossjumping`,如此才能讓 gcc 產生更好的程式碼 >不好意思,老師我想請問應該要在哪個部分使用 `computed-goto` 呢? 目前是想說在 `gb_step_cpu` 內並將每個迴圈前面確認 `interrupt flags` 部分跟後方確認 `reg.lcy` 狀態每次 `run` 完指令都重複做一次 不知道這樣的想法對不對呢,另外我把指令拆離成獨立的 `function` 那內部也不用 `switch-case` 了 >這樣還需要使用`computed-goto` 嗎 :notes: jserv ::: 前面提的大概像這樣,另外 `Tail_handler` 外部是 `while(!gb->gb_frame)` 所以就像是沒有使用 `_gb_read` 或 `_gb_write` 的指令可以不用檢查直接繼續執行嗎? ```cpp static const void *dispatch_table[] = { &&NOP, &&... } #define DISPATCH() \ goto* dispatch_table[(gb->gb_halt ? 0x00 : __gb_read(gb, gb->cpu_reg.pc++))] __gb_step_cpu(struct gb_s *gb) { ... NOP: ... DISPATCH(); EI: gb->gb_ime = 1; goto Tail_handler Tail_handler: ... } ``` 可是現在我把這些cpu指令都寫成function的話,這種改寫還有必要嗎? 另外如果要改的話我是直接把function塞在 `label` 裡面嗎? 目前已改用 `computed-goto` 的 `dispatch_table` 做跳躍與 `macro` 展開的 `instruction function`,並能正常運行,但我的寫法是把 `table` 當成 `switch-case` 的用法在作跳躍,也就是每個 `cycle` 還是要到 `while` 的邊界判斷一次,想請問是否有更好的改法呢? > 這是為何你需要讀書,讀書可以讓我們有充分的視野和思緒來做判斷。上面的參考文獻讀過後,你應該知道取捨。快去讀書! > :notes: jserv ![](https://i.imgur.com/nRM5dKX.png) 引入 `gbit` 程式碼並修改 `bench.c` 對全指令偵錯並將偵錯版本上傳為新分支 `Debug` ![](https://i.imgur.com/QnFuw4u.png) 已將 `Debug` 分支與原始程式碼合併至 `master`,並修改 `bench.c` 為測試全指令並輸出測試時間