# Gamboy + JIT
由於不太清楚如何結合 [GBIT](https://github.com/koenk/gbit),先試著理解並記錄一下程式的運作,以 [0466847](https://github.com/sysprog21/jitboy/tree/046684749b8ca3dca379f2740aede7c859f35311) 版本來記錄。
:::warning
TODO:
1. 實作直譯器,作為 JIT 的對照,後續也可比照
* 修改 `src/core.c` 的 `run_vm` 函式,比照 [gameboy-emu](https://github.com/sysprog21/gameboy-emu) 和 [GBIT](https://github.com/koenk/gbit) 的 opcode 直譯器實作方式,提供「非 JIT」(即直譯器) 的執行流程
* 在 `INTERPRETER_ONLY` 的模式,應忽略所有的 `compiled_blocks`
2. 用上述的直譯器搭配 [GBIT](https://github.com/koenk/gbit) 驗證
可能的程式碼:
```cpp
bool run_vm(gb_vm *vm)
{
uint16_t prev_pc = vm->state.last_pc;
vm->state.last_pc = vm->state.pc;
#ifdef INTERPRETER_ONLY
/* execute each opcode */
#else
/* compile next block / get cached block */
...
#endif
```
:notes: jserv
:::
## 程式原理
### main.c
```c=45
gb_vm *vm = malloc(sizeof(gb_vm));
if (!init_vm(vm, argv[optind], opt_level, true)) {
LOG_ERROR("Fail to initialize\n");
exit(1);
}
```
先分配空間給 vm,再初始化 vm (詳見 core.c)
57 ~ 137 行則是對按鍵做反應,以 `vm->state.keys.state` 記錄哪顆按鍵被按下,按下時會做 `vm->memory.mem[0xff0f] |= 0x10;` 產生 joypad interrupt。
### core.c
**`bool init_vm(gb_vm *vm, const char *filename, int opt_level, bool init_io)`**
先做記憶體初始化後將卡帶資訊印出 (見 memory.c)。
19 ~ 74 行給 `vm->state` 當中的參數初始值,以及給 `vm->memory` 中的部分 IO ports 和 Interrupt Enable Register 初始值。
87 ~ 99 行依照 init_io 的值來決定是否顯示視窗。
**`bool run_vm(gb_vm *vm)`**
110 ~ 155 行依照 program counter 指向的位置來決定執行的 function,
當 pc < 0x4000 (ROM bank 0),若對應的 compiled_blocks[0][pc] 沒有被執行過,則先編譯 pc 指到的 byte 成 opcode 後,存入 block 中(見 gbz80.c),接著將執行次數加一後執行該 function。
而如果 0x4000 <= pc < 0x8000 (ROM bank 1 ... N),會先確認第幾個 bank 後,對對應的 compiled_blocks[bank][pc - 0x4000] 做跟上面一樣的處理,而因為 compiled_blocks 的範圍是 [MAX BANK][0x4000],所以這邊的 pc 對應到的會是 pc - 0x4000。
當 pc >= 0xff80 (High RAM) 時,其實也與上方類似,不過這次對應到的是 highmem_blocks[pc - 0xff80]。
最後當 pc 在以上情況外時,代表執行 RAM 中的 function,這時以一個臨時的 block temp 存取編譯後的結果並且執行。
170 ~ 237 行會在 cpu halt 時執行。
首先檢查已執行的 clock cycles 是否 >= 下次更新的時間,如果是的話先透過 update_ioregs 檢查該不該產生中斷(見 interrupt.c)。接著檢查 mem 0xff44 是否等於 144 (畫到最後一個 row),如果還沒的話將 draw_frame 設成 true,而如果是的話檢查 draw_frame 是否為 true,如果是的話先透過 time = SDL_GetTicks() 取得 SDL library 初始化後經過的時間,接著確認 time 已經經過下一個幀的時間,或是等待到經過為止。
每更新了 60 個影格,會計算系統忙碌時間的比例更新在視窗的標題上。
當有中斷發生時,會先取消 halt 模式,將 pc 存到 stack 中,然後跳到中斷開始的地址。
檢查 LCD 狀態是否是 mode 3 且是因為等待 mode 3 而 halt,是的話取消 halt。
```
0xff41 bits 0-1
00: H-Blank
01: V-Blank
10: Searching Sprites Atts
11: Transfering Data to LCD Driver
```
檢查 LCD 是否畫到造成 halt 的行數,且是因為等待畫線而 halt,是的話取消 halt。
最後更新下次的更新時間,以及如果這時依然沒有取消 halt 的話,更新已經執行的 instruction 數。
**`static void show_statistics(gb_vm *vm)`**
計算總共執行過的 ROM function,最常執行的 function 和執行過的次數,以及所有 function 被執行過的次數,和執行過的幀數,並且印出來。
**`bool free_vm(gb_vm *vm)`**
首先執行上面的 show_statistics,接著釋放有被執行過的 block,關掉視窗,釋放記憶體。
### memory.c
**`static uint8_t get_joypad_state(gb_keys *keys, uint8_t value)`**
此函式用來更新 joypad register,運作原理可參考 [input update function](https://thomas.spurden.name/gameboy/#input_update_function0)
**`static void gb_memory_change_ram_bank(gb_memory *mem, int bank)`**
**`static void gb_memory_change_rom_bank(gb_memory *mem, int bank)`**
**`static void gb_memory_access_rtc(gb_memory *mem, int elt)`**
存取 RTC (real time clock),未完成
**`static void gb_memory_update_rtc_time(gb_memory *mem, int value)`**
更新 RTC,未完成
**`void gb_memory_write(gb_state *state, uint64_t addr, uint64_t value)`**
**`bool gb_memory_init(gb_memory *mem, const char *filename)`**
一開始先檢查 filename 是否存在,不過這裡的 filename 即是遊戲檔案(如 mario.gb),而如果沒有輸入的話在 main.c 就會直接退出了,所以這裡不會有 filename 不存在的問題。
首先會以唯讀模式打開輸入的 filename,接著將檔案中前 0x8000 的內容映射到 0x1000000 這段記憶體位址上,而 0x0000 ~ 0x7fff 正是 GB 中 ROM 的位址空間。之後再用 anonymous mapping 映射 0x8000 ~ 0xffff 的空間來做為 RAM。
最後再初始化 mem 中的各項參數。
**`bool gb_memory_free(gb_memory *mem)`**
釋放 memory init 時分配給 mem->ram_banks 時的記憶體,關閉檔案,並且用 munmap 來取消 memory init 時映射的內容。
**`void dump_header_info(gb_memory *mem)`**
印出卡帶中的各項資訊,裡面引用的位址可以參考,不過 Manufacturer 引用的位址是 Title 的後半段,可能只適用在部分遊戲上。
![](https://i.imgur.com/caJqBwl.png =400x)
### gbz80.c
**`static bool optimize_cc(GList *inst)`**
根據指令的 flag 來做最佳化,不清楚原理。
**`bool compile(gb_block *block, gb_memory *mem, uint16_t start_address, int opt_level)`**
首先建立一個叫 instructions 的 list,然後進入 for loop。
for loop 中會先用 g_new 分配空間給叫做 inst 的 gbz80_inst 結構(在 emit.h 中定義),接著依照 opcode 將指令表內的內容存入 inst 中,更新 inst->args (不知道是什麼),還有 inst->address (指令對應的位址),還有 i (下一輪要讀取的位址)。接著透過 g_list_prepend 將這次讀取的指令 append 到 instructions 中。如果讀取的指令有 INST_FLAG_ENDS_BLOCK 這個 flag 則退出 for loop。
之後透過 g_list_reverse 將 instructions 反轉,經過兩個最佳化 function(optimize_block, optimize_cc) 後,透過 emit 來執行指令,最後將 instructions 釋放掉。
### interrupt.c
**`uint64_t next_update_time(gb_state *state)`**
先透過 0xff07 和 0xff41 取得時脈以及 LCD 狀態
```
0xff07 bits 0-1
00: 4.096 khz
01: 262.144 khz
10: 65.536 khz
11: 16.384 khz
```
由於刷新一個 line 需要 114 machine cycles,所以預設的 next update time 是 state->ly_count + 114。如果預設的 time > state->inst_count + cl (時脈),將 time 改成 state->inst_count + cl。接著根據 LCD 狀態決定 time 的判斷,如果是 OAM search 時,判斷
```
if (time > state->ly_count + 20)
time = state->ly_count + 20;
```
而如果是 pixel transfer 則是
```
if (time > state->ly_count + 64)
time = state->ly_count + 64;
```
不過根據這張圖來看,應該是 + 63 (?)
![](https://i.imgur.com/6Ou2pz5.png =700x)
:::warning
針對上述 pixel transfer,可提交 pull request
:notes: jserv
:::
> 好的,待了解如何操作後更新
**`void update_ioregs(gb_state *state)`**
```c=38
if (state->inst_count > state->tima_count + cl) {
state->tima_count = state->inst_count;
if (mem[0xff07] & 0x4) {
mem[0xff05]++;
if (mem[0xff05] == 0) {
mem[0xff05] = mem[0xff06];
// timer interrupt selected
mem[0xff0f] |= 0x04;
}
}
}
```
一開始透過 0xff07 取得時脈,並做 `if (state->inst_count > state->tima_count + cl)` 判斷 0xff05 (timer counter) 該更新了沒,成立的話先 `state->tima_count = state->inst_count`,判斷 0xff07 & 0x4 (start timer),成立的話 0xff05++。而如果這時 0xff05 == 0 (timer overflow),載入 0xff06 (timer modulo),且將 0xff0f (interrupt flag) 設為 tima interrupt。
```c=56
/* reset the coincidence flag */
mem[0xff41] &= ~0x04;
// ly-register 0xff44
if (state->inst_count > state->ly_count + 114) {
state->ly_count = state->inst_count;
if (mem[0xff44] < 144)
update_line(mem);
mem[0xff44]++;
mem[0xff44] %= 153;
if (mem[0xff45] == mem[0xff44]) {
/* Set the coincidence flag */
mem[0xff41] |= 0x04;
/* Coincidence interrupt selected */
if (mem[0xff41] & 0x40)
mem[0xff0f] |= 0x02; /* stat interrupt occurs */
}
/* if-register 0xff0f */
if (mem[0xff44] == 144) {
/* VBLANK interrupt is pending */
mem[0xff0f] |= 0x01;
/* mode 1 interrupt selected */
if (mem[0xff41] & 0x10 && (mem[0xff41] & 0x03) != 1)
mem[0xff0f] |= 0x02; /* stat interrupt occurs */
/* LCDC Stat mode 1 */
mem[0xff41] &= ~0x03;
mem[0xff41] |= 0x01;
}
}
```
將 0xff41 的 coincidence flag 取消後,做 `if (state->inst_count > state->ly_count + 114)` 判斷該不該刷新 line,成立的話先 `state->ly_count = state->inst_count`,而如果 0xff44 < 144 代表畫面還沒畫完,用 update_line (lcd.c) 更新畫面。之後 0xff44++ 以及 0xff44 %= 153 (根據前人的成果可以知道這裡應是 144 + 10 = 154)。
如果 0xff45(LY compare) == 0xff44,重新立 coincidence flag,如果同時 LY coincidence interrupt 有被選擇,就會將 0xff0f 設 LCDC interrupt。
而如果 0xff44 == 144 時,將 0xff0f 設為 V-Blank interrupt。如果同時 `mem[0xff41] & 0x10 (V-Blank interrupt enable) && (mem[0xff41] & 0x03) != 1 (不在 V-Blank 期間)`,將 0xff0f 設 LCDC interrupt。最後將 0xff41 設為 During V-Blank。
```c=93
if (mem[0xff44] < 144) {
/* if not VBLANK */
if (state->inst_count - state->ly_count < 20) {
/* mode 2 interrupt selected */
if (mem[0xff41] & 0x20 && (mem[0xff41] & 0x03) != 2)
mem[0xff0f] |= 0x02; /* stat interrupt occurs */
/* LCDC Stat mode 2 */
mem[0xff41] &= ~0x03;
mem[0xff41] |= 0x02;
} else if (state->inst_count - state->ly_count < 63) {
/* LCDC Stat mode 3 */
mem[0xff41] &= ~0x03;
mem[0xff41] |= 0x03;
} else {
/* mode 0 interrupt selected */
if (mem[0xff41] & 0x08 && (mem[0xff41] & 0x03) != 0)
mem[0xff0f] |= 0x02; /* stat interrupt occurs */
/* LCDC Stat mode 0 */
mem[0xff41] &= ~0x03;
}
}
```
若 0xff44 < 144,先判斷 `if (state->inst_count - state->ly_count < 20)` 確認是否在 OAM search,是的話先依照 `if (mem[0xff41] & 0x20 && (mem[0xff41] & 0x03) != 2)` 決定要不要設 LCDC interrupt flag (與前一段類似),然後將 0xff41 設為 During searching OAM。
如果在 pixel transfer 期間,將 0xff41 設為 During Transfering Data to LCD Driver。
如果在 H-Blank 期間,先依照 `if (mem[0xff41] & 0x08 && (mem[0xff41] & 0x03) != 0)` 決定是否設 LCDC interrupt flag,接著將 0xff41 設為 During H-Blank (Entire Display Ram can be accessed)。
**`uint16_t start_interrupt(gb_state *state)`**
先確認 state->ime == true (interrupt master enable),篩選同時在 Interrupt Flag 和 Interrupt Enable 的中斷存在 interrupts 中。
如果是 V-Blank interrupt,將 ime 取消,取消 V-Blank flag,將 state->trap_reason |= REASON_INT (追蹤造成 block 中止的原因,設成因為 interrupt 而中止),最後回傳 0x40 (interrupt start address)。
其他 interrupt 的處理大致相同,只有因應不同的 interrupt 取消對應的 flag 以及回傳對應的開始位址。而 serial interrupt 的部分並未實作,可能是因為模擬器並沒有像實機具有傳送功能。
### lcd.c
**`static void render_back(uint32_t *buf, uint8_t *addr_sp)`**
**`static int render_thread_function(void *ptr)`**
先將 ptr assign 給 lcd,再將 lcd assign 給 g_lcd (兩者皆是 gb_lcd 結構)。
利用 SDL_CreateRenderer(lcd->win, -1, SDL_RENDERER_ACCELERATED) 製造 rendering context,將 rendering 結果顯示在 lcd->win,並選擇支援硬體加速的 driver。
當 !lcd->exit 時,重複執行以下動作 : 利用 SDL_LockMutex(lcd->vblank_mutex) 鎖住 vblank_mutex,利用 SDL_CondWait(lcd->vblank_cond, lcd->vblank_mutex) 將 vblank_mutex 解鎖並等待 vblank_cond 的訊號後重新鎖住 vblank_mutex,在兩個 cur_imgbuf 之間切換(這裡應該是有兩個 imgbuf 來儲存畫面,並以 cur_imgbuf 來決定目前要顯示的畫面),SDL_UnlockMutex(lcd->vblank_mutex) 解鎖 vblank_mutex,render_frame(lcd) 更新畫面。
最後用 SDL_GetRenderer(lcd->win) 取得 renderer 後用 SDL_DestroyRenderer(renderer) 來清除該 renderer。
**`static void render_frame(gb_lcd *lcd)`**
一開始透過 SDL_GetRenderer(lcd->win) 取得 renderer,接著用 `bitmapTex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 160, 144);` 創一個 SDL texture 並 assign 到 bitmapTex 中。format 表示顏色的順序,streaming 表示 texture 會經常改變。
```c=238
void *pixels = NULL;
int pitch = 0;
SDL_LockTexture(bitmapTex, NULL, &pixels, &pitch);
memcpy(pixels, imgbuf[(cur_imgbuf + 1) % 2], 160 * 144 * sizeof(uint32_t));
SDL_UnlockTexture(bitmapTex);
```
這段將 bitmapTex 鎖住並寫入下一張圖片的內容,但有疑問的地方是根據 [SDL](https://wiki.libsdl.org/SDL_LockTexture) 的內容,pitch 指的是一個 row 的長度,以這邊來說,應該是 160 * 4 bytes,但這裡卻給 0。
```c=244
SDL_RenderCopy(renderer, bitmapTex, NULL, NULL);
SDL_RenderPresent(renderer);
```
最後則是將 texture 內容複製到 renderer 中,再將螢幕透過 renderer 做更新。
## 實作直譯器
根據提示,修改 core.c 中的 run_vm,把 `/* compile next block / get cached block */` 到 `LOG_DEBUG("ioregs: STAT=%02x LY=%02x IF=%02x IE=%02x\n",` 前一行用 #else 和 #endif 分開。(109 ~ 156 行)。
gameboy-emu 中直譯器的部分應該是在 [gameboy.h](https://github.com/sysprog21/gameboy-emu/blob/master/gameboy.h) 中,`__gb_step_cpu` 中,標註 `/* Excute opcode */` 的這段,但因為兩個程式碼間有不少差距,可能需要改很久,所以這裡我先試了另一個方法,也就是參考 [gbz80.c](https://github.com/sysprog21/jitboy/blob/046684749b8ca3dca379f2740aede7c859f35311/src/gbz80.c) 裡面 `compile` 的內容。
在原本的實作中,會編譯 pc 指到的地址的內容並且存到 block 中,以便之後使用,然後更新 pc。而現在嘗試不使用 block 而是直接把 compile 的內容拿來改,如下 :
```cpp
// core.c
AAA *BBB = malloc(sizeof(AAA));
BBB->funcA = 0;
GList *instructions = NULL;
gb_memory *mem = &vm->memory;
uint16_t i = vm->state.pc;
for (;;) {
gbz80_inst *inst = g_new(gbz80_inst, 1);
uint8_t opcode = mem->mem[i];
if (opcode != 0xcb) {
*inst = inst_table[opcode];
} else {
opcode = mem->mem[i + 1];
*inst = cb_table[opcode];
}
inst->args = mem->mem + i;
inst->address = i;
i += inst->bytes;
instructions = g_list_prepend(instructions, inst);
if (inst->flags & INST_FLAG_ENDS_BLOCK)
break;
}
instructions = g_list_reverse(instructions);
if (!emit(NULL, instructions, BBB))
printf("emit failed!\n");
vm->state.pc = BBB->funcA(&vm->state);
g_list_free_full(instructions, g_free);
```
```cpp
// emit.dasc
bool emit(gb_block *block, GList *inst, AAA *BBB)
{
.
.
.
if (block != NULL) {
block->func = labels[lbl_f_start];
block->mem = buf;
block->size = sz;
block->end_address = end_address;
block->exec_count = 0;
} else {
BBB->funcA = labels[lbl_f_start];
}
.
.
.
}
```
基本上就是把原本關於 block 的內容忽略掉,但由於不知道原本 block 裡面 func 的內容怎麼傳的,所以我宣告了一個 struct AAA 在 emit.h 中,並加入 `uint16_t (*func)(gb_state *);` 這個屬性用來更新 pc。
實驗的結果是程式可以跑,但是遊玩時會遇到不少錯誤,例如跳到 goomba 上面不會打倒它反而會損血,最致命的是玩了不久後會當機直接卡死。
雖然也可能是因為我亂改的過程中忽略了一些地方造成錯誤,但目前看來還是得照 gameboy-emu 來改才能知道了。
:::warning
所以才要一開始就思考「如何驗證」,也是 GBIT 存在的緣故。按部就班來實作,及早提交 pull request
:notes: jserv
:::
[最新的變更](https://github.com/nelsonlai1/jitboy/tree/test/src),照著 gameboy-emu 的方式改,但畫面顯示不出來,觀察 pc 和 opcode 會發現陷入一個循環中,推測是中斷的部分沒處理好。
## 額外參考資料
[Everything You Always Wanted To Know About GAME BOY](http://www.emulatronia.com/doctec/consolas/gameboy/gameboy.txt)