aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorgbc dev <gbc@localhost>2026-07-04 22:11:10 +0200
committergbc dev <gbc@localhost>2026-07-04 22:11:10 +0200
commitbc48df9f7162a4a816fdc0edaa6ed08304008c9c (patch)
tree30129f7655c177a78e62505c55243a5c0540126f /src
parentscaffolding: build system, cartridge loader with MBC1/2/3/5, core types (diff)
downloadsl0pboy-bc48df9f7162a4a816fdc0edaa6ed08304008c9c.tar.gz
sl0pboy-bc48df9f7162a4a816fdc0edaa6ed08304008c9c.tar.xz
sl0pboy-bc48df9f7162a4a816fdc0edaa6ed08304008c9c.zip
core emulator: SM83 CPU, MMU, timer, PPU, CGB support
Passes blargg cpu_instrs (all 11), instr_timing, mem_timing.
Diffstat (limited to 'src')
-rw-r--r--src/cpu.c446
-rw-r--r--src/cpu.h9
-rw-r--r--src/gb.c201
-rw-r--r--src/main.c41
-rw-r--r--src/ppu.c355
-rw-r--r--src/ppu.h14
-rw-r--r--src/timer.c82
-rw-r--r--src/timer.h10
8 files changed, 1158 insertions, 0 deletions
diff --git a/src/cpu.c b/src/cpu.c
new file mode 100644
index 0000000..9e7f685
--- /dev/null
+++ b/src/cpu.c
@@ -0,0 +1,446 @@
+#include "cpu.h"
+
+#define FZ 0x80
+#define FN 0x40
+#define FH 0x20
+#define FC 0x10
+
+// ---- memory access helpers (each ticks 4 T-cycles) ----
+static inline u8 rb(GB *gb, u16 a) { u8 v = gb_read(gb, a); gb_tick(gb, 4); return v; }
+static inline void wb(GB *gb, u16 a, u8 v) { gb_write(gb, a, v); gb_tick(gb, 4); }
+static inline void internal(GB *gb) { gb_tick(gb, 4); }
+
+static inline u8 fetch8(GB *gb) {
+ u8 v = gb_read(gb, gb->cpu.pc);
+ if (gb->cpu.halt_bug) { gb->cpu.halt_bug = false; }
+ else gb->cpu.pc++;
+ gb_tick(gb, 4);
+ return v;
+}
+static inline u16 fetch16(GB *gb) {
+ u8 lo = fetch8(gb);
+ u8 hi = fetch8(gb);
+ return lo | (hi << 8);
+}
+
+// ---- flag helpers ----
+static inline void setf(CPU *c, u8 mask, bool cond) {
+ if (cond) c->f |= mask; else c->f &= ~mask;
+ c->f &= 0xF0;
+}
+
+// ---- 8-bit ALU ----
+static u8 alu_inc(CPU *c, u8 v) {
+ u8 r = v + 1;
+ setf(c, FZ, r == 0);
+ setf(c, FN, false);
+ setf(c, FH, (v & 0x0F) == 0x0F);
+ return r;
+}
+static u8 alu_dec(CPU *c, u8 v) {
+ u8 r = v - 1;
+ setf(c, FZ, r == 0);
+ setf(c, FN, true);
+ setf(c, FH, (v & 0x0F) == 0x00);
+ return r;
+}
+static void alu_add(CPU *c, u8 v) {
+ u16 r = c->a + v;
+ setf(c, FH, ((c->a & 0xF) + (v & 0xF)) > 0xF);
+ setf(c, FC, r > 0xFF);
+ c->a = r;
+ setf(c, FZ, c->a == 0);
+ setf(c, FN, false);
+}
+static void alu_adc(CPU *c, u8 v) {
+ u8 carry = (c->f & FC) ? 1 : 0;
+ u16 r = c->a + v + carry;
+ setf(c, FH, ((c->a & 0xF) + (v & 0xF) + carry) > 0xF);
+ setf(c, FC, r > 0xFF);
+ c->a = r;
+ setf(c, FZ, c->a == 0);
+ setf(c, FN, false);
+}
+static void alu_sub(CPU *c, u8 v) {
+ setf(c, FH, (c->a & 0xF) < (v & 0xF));
+ setf(c, FC, c->a < v);
+ c->a -= v;
+ setf(c, FZ, c->a == 0);
+ setf(c, FN, true);
+}
+static void alu_sbc(CPU *c, u8 v) {
+ u8 carry = (c->f & FC) ? 1 : 0;
+ int r = c->a - v - carry;
+ setf(c, FH, ((c->a & 0xF) - (v & 0xF) - carry) < 0);
+ setf(c, FC, r < 0);
+ c->a = r;
+ setf(c, FZ, c->a == 0);
+ setf(c, FN, true);
+}
+static void alu_and(CPU *c, u8 v) {
+ c->a &= v;
+ setf(c, FZ, c->a == 0);
+ setf(c, FN, false); setf(c, FH, true); setf(c, FC, false);
+}
+static void alu_or(CPU *c, u8 v) {
+ c->a |= v;
+ setf(c, FZ, c->a == 0);
+ setf(c, FN, false); setf(c, FH, false); setf(c, FC, false);
+}
+static void alu_xor(CPU *c, u8 v) {
+ c->a ^= v;
+ setf(c, FZ, c->a == 0);
+ setf(c, FN, false); setf(c, FH, false); setf(c, FC, false);
+}
+static void alu_cp(CPU *c, u8 v) {
+ setf(c, FH, (c->a & 0xF) < (v & 0xF));
+ setf(c, FC, c->a < v);
+ setf(c, FZ, c->a == v);
+ setf(c, FN, true);
+}
+
+static void add_hl(CPU *c, u16 v) {
+ u32 r = c->hl + v;
+ setf(c, FN, false);
+ setf(c, FH, ((c->hl & 0x0FFF) + (v & 0x0FFF)) > 0x0FFF);
+ setf(c, FC, r > 0xFFFF);
+ c->hl = r;
+}
+
+static u16 add_sp_e(GB *gb, s8 e) {
+ CPU *c = &gb->cpu;
+ u16 sp = c->sp;
+ setf(c, FZ, false); setf(c, FN, false);
+ setf(c, FH, ((sp & 0xF) + (e & 0xF)) > 0xF);
+ setf(c, FC, ((sp & 0xFF) + (e & 0xFF)) > 0xFF);
+ return sp + e;
+}
+
+static void daa(CPU *c) {
+ int a = c->a;
+ if (!(c->f & FN)) {
+ if ((c->f & FH) || (a & 0x0F) > 9) a += 0x06;
+ if ((c->f & FC) || a > 0x9F) { a += 0x60; setf(c, FC, true); }
+ } else {
+ if (c->f & FH) a = (a - 6) & 0xFF;
+ if (c->f & FC) a -= 0x60;
+ }
+ c->a = a;
+ setf(c, FZ, c->a == 0);
+ setf(c, FH, false);
+}
+
+// ---- CB rotate/shift ----
+static u8 cb_rlc(CPU *c, u8 v){ u8 r=(v<<1)|(v>>7); setf(c,FC,v&0x80); setf(c,FZ,r==0); setf(c,FN,false); setf(c,FH,false); return r; }
+static u8 cb_rrc(CPU *c, u8 v){ u8 r=(v>>1)|(v<<7); setf(c,FC,v&0x01); setf(c,FZ,r==0); setf(c,FN,false); setf(c,FH,false); return r; }
+static u8 cb_rl (CPU *c, u8 v){ u8 cy=(c->f&FC)?1:0; u8 r=(v<<1)|cy; setf(c,FC,v&0x80); setf(c,FZ,r==0); setf(c,FN,false); setf(c,FH,false); return r; }
+static u8 cb_rr (CPU *c, u8 v){ u8 cy=(c->f&FC)?0x80:0; u8 r=(v>>1)|cy; setf(c,FC,v&0x01); setf(c,FZ,r==0); setf(c,FN,false); setf(c,FH,false); return r; }
+static u8 cb_sla(CPU *c, u8 v){ u8 r=v<<1; setf(c,FC,v&0x80); setf(c,FZ,r==0); setf(c,FN,false); setf(c,FH,false); return r; }
+static u8 cb_sra(CPU *c, u8 v){ u8 r=(v>>1)|(v&0x80); setf(c,FC,v&0x01); setf(c,FZ,r==0); setf(c,FN,false); setf(c,FH,false); return r; }
+static u8 cb_swap(CPU *c,u8 v){ u8 r=(v>>4)|(v<<4); setf(c,FC,false); setf(c,FZ,r==0); setf(c,FN,false); setf(c,FH,false); return r; }
+static u8 cb_srl(CPU *c, u8 v){ u8 r=v>>1; setf(c,FC,v&0x01); setf(c,FZ,r==0); setf(c,FN,false); setf(c,FH,false); return r; }
+
+// r index: 0=B 1=C 2=D 3=E 4=H 5=L 6=(HL) 7=A
+static u8 reg_get(GB *gb, int i) {
+ CPU *c = &gb->cpu;
+ switch (i) {
+ case 0: return c->b; case 1: return c->c; case 2: return c->d;
+ case 3: return c->e; case 4: return c->h; case 5: return c->l;
+ case 6: return rb(gb, c->hl); case 7: return c->a;
+ }
+ return 0;
+}
+static void reg_set(GB *gb, int i, u8 v) {
+ CPU *c = &gb->cpu;
+ switch (i) {
+ case 0: c->b=v; break; case 1: c->c=v; break; case 2: c->d=v; break;
+ case 3: c->e=v; break; case 4: c->h=v; break; case 5: c->l=v; break;
+ case 6: wb(gb, c->hl, v); break; case 7: c->a=v; break;
+ }
+}
+
+static void push16(GB *gb, u16 v) {
+ CPU *c = &gb->cpu;
+ wb(gb, --c->sp, v >> 8);
+ wb(gb, --c->sp, v & 0xFF);
+}
+static u16 pop16(GB *gb) {
+ CPU *c = &gb->cpu;
+ u8 lo = rb(gb, c->sp++);
+ u8 hi = rb(gb, c->sp++);
+ return lo | (hi << 8);
+}
+
+// ---- CB prefix ----
+static void do_cb(GB *gb) {
+ CPU *c = &gb->cpu;
+ u8 op = fetch8(gb);
+ int r = op & 7;
+ int y = (op >> 3) & 7;
+ if (op < 0x40) {
+ u8 v = reg_get(gb, r);
+ u8 res;
+ switch (y) {
+ case 0: res = cb_rlc(c, v); break;
+ case 1: res = cb_rrc(c, v); break;
+ case 2: res = cb_rl(c, v); break;
+ case 3: res = cb_rr(c, v); break;
+ case 4: res = cb_sla(c, v); break;
+ case 5: res = cb_sra(c, v); break;
+ case 6: res = cb_swap(c, v);break;
+ default:res = cb_srl(c, v); break;
+ }
+ reg_set(gb, r, res);
+ } else if (op < 0x80) { // BIT
+ u8 v = reg_get(gb, r);
+ setf(c, FZ, !(v & (1 << y)));
+ setf(c, FN, false);
+ setf(c, FH, true);
+ } else if (op < 0xC0) { // RES
+ u8 v = reg_get(gb, r);
+ reg_set(gb, r, v & ~(1 << y));
+ } else { // SET
+ u8 v = reg_get(gb, r);
+ reg_set(gb, r, v | (1 << y));
+ }
+}
+
+// ---- interrupt handling ----
+static bool handle_interrupts(GB *gb) {
+ CPU *c = &gb->cpu;
+ u8 pending = gb->iff & gb->ie & 0x1F;
+ if (!pending) return false;
+
+ if (c->halted) c->halted = false;
+ if (!c->ime) return false;
+
+ c->ime = false;
+ internal(gb); internal(gb); // 2 internal cycles
+ push16(gb, c->pc); // 2 memory cycles
+ // determine vector after push (IE may change but pending latched)
+ pending = gb->iff & gb->ie & 0x1F;
+ int bit = 0;
+ for (; bit < 5; bit++) if (pending & (1 << bit)) break;
+ if (bit == 5) { c->pc = 0; internal(gb); return true; } // cancelled
+ gb->iff &= ~(1 << bit);
+ c->pc = 0x40 + bit * 8;
+ internal(gb);
+ return true;
+}
+
+int cpu_step(GB *gb) {
+ CPU *c = &gb->cpu;
+ u64 start = gb->cycles;
+
+ // EI takes effect after the following instruction
+ bool ei_was_pending = c->ime_pending;
+
+ if (handle_interrupts(gb)) {
+ if (ei_was_pending) { /* handled below */ }
+ return (int)(gb->cycles - start);
+ }
+
+ if (c->halted) {
+ gb_tick(gb, 4);
+ return (int)(gb->cycles - start);
+ }
+
+ u8 op = fetch8(gb);
+
+ switch (op) {
+ case 0x00: break; // NOP
+ case 0x10: fetch8(gb); // STOP
+ if (gb->cgb_mode && (gb->key1 & 0x01)) {
+ gb->double_speed = !gb->double_speed;
+ gb->key1 = (gb->double_speed ? 0x80 : 0x00);
+ }
+ break;
+ case 0x76: // HALT
+ if (!c->ime && (gb->iff & gb->ie & 0x1F)) c->halt_bug = true;
+ else c->halted = true;
+ break;
+
+ // 16-bit loads
+ case 0x01: c->bc = fetch16(gb); break;
+ case 0x11: c->de = fetch16(gb); break;
+ case 0x21: c->hl = fetch16(gb); break;
+ case 0x31: c->sp = fetch16(gb); break;
+ case 0x08: { u16 a = fetch16(gb); wb(gb, a, c->sp & 0xFF); wb(gb, a+1, c->sp >> 8); } break;
+ case 0xF9: c->sp = c->hl; internal(gb); break;
+ case 0xF8: { s8 e = (s8)fetch8(gb); c->hl = add_sp_e(gb, e); internal(gb); } break;
+
+ // 8-bit loads (immediate)
+ case 0x06: c->b = fetch8(gb); break;
+ case 0x0E: c->c = fetch8(gb); break;
+ case 0x16: c->d = fetch8(gb); break;
+ case 0x1E: c->e = fetch8(gb); break;
+ case 0x26: c->h = fetch8(gb); break;
+ case 0x2E: c->l = fetch8(gb); break;
+ case 0x36: wb(gb, c->hl, fetch8(gb)); break;
+ case 0x3E: c->a = fetch8(gb); break;
+
+ // LD (rr),A and A,(rr)
+ case 0x02: wb(gb, c->bc, c->a); break;
+ case 0x12: wb(gb, c->de, c->a); break;
+ case 0x22: wb(gb, c->hl++, c->a); break;
+ case 0x32: wb(gb, c->hl--, c->a); break;
+ case 0x0A: c->a = rb(gb, c->bc); break;
+ case 0x1A: c->a = rb(gb, c->de); break;
+ case 0x2A: c->a = rb(gb, c->hl++); break;
+ case 0x3A: c->a = rb(gb, c->hl--); break;
+
+ // 16-bit inc/dec
+ case 0x03: c->bc++; internal(gb); break;
+ case 0x13: c->de++; internal(gb); break;
+ case 0x23: c->hl++; internal(gb); break;
+ case 0x33: c->sp++; internal(gb); break;
+ case 0x0B: c->bc--; internal(gb); break;
+ case 0x1B: c->de--; internal(gb); break;
+ case 0x2B: c->hl--; internal(gb); break;
+ case 0x3B: c->sp--; internal(gb); break;
+
+ // 8-bit inc
+ case 0x04: c->b = alu_inc(c, c->b); break;
+ case 0x0C: c->c = alu_inc(c, c->c); break;
+ case 0x14: c->d = alu_inc(c, c->d); break;
+ case 0x1C: c->e = alu_inc(c, c->e); break;
+ case 0x24: c->h = alu_inc(c, c->h); break;
+ case 0x2C: c->l = alu_inc(c, c->l); break;
+ case 0x34: { u8 v = rb(gb, c->hl); wb(gb, c->hl, alu_inc(c, v)); } break;
+ case 0x3C: c->a = alu_inc(c, c->a); break;
+ // 8-bit dec
+ case 0x05: c->b = alu_dec(c, c->b); break;
+ case 0x0D: c->c = alu_dec(c, c->c); break;
+ case 0x15: c->d = alu_dec(c, c->d); break;
+ case 0x1D: c->e = alu_dec(c, c->e); break;
+ case 0x25: c->h = alu_dec(c, c->h); break;
+ case 0x2D: c->l = alu_dec(c, c->l); break;
+ case 0x35: { u8 v = rb(gb, c->hl); wb(gb, c->hl, alu_dec(c, v)); } break;
+ case 0x3D: c->a = alu_dec(c, c->a); break;
+
+ // ADD HL,rr
+ case 0x09: add_hl(c, c->bc); internal(gb); break;
+ case 0x19: add_hl(c, c->de); internal(gb); break;
+ case 0x29: add_hl(c, c->hl); internal(gb); break;
+ case 0x39: add_hl(c, c->sp); internal(gb); break;
+
+ // rotates on A
+ case 0x07: { u8 v=c->a; c->a=(v<<1)|(v>>7); setf(c,FC,v&0x80); setf(c,FZ,false);setf(c,FN,false);setf(c,FH,false);} break;
+ case 0x0F: { u8 v=c->a; c->a=(v>>1)|(v<<7); setf(c,FC,v&0x01); setf(c,FZ,false);setf(c,FN,false);setf(c,FH,false);} break;
+ case 0x17: { u8 cy=(c->f&FC)?1:0; u8 v=c->a; c->a=(v<<1)|cy; setf(c,FC,v&0x80); setf(c,FZ,false);setf(c,FN,false);setf(c,FH,false);} break;
+ case 0x1F: { u8 cy=(c->f&FC)?0x80:0; u8 v=c->a; c->a=(v>>1)|cy; setf(c,FC,v&0x01); setf(c,FZ,false);setf(c,FN,false);setf(c,FH,false);} break;
+
+ case 0x27: daa(c); break;
+ case 0x2F: c->a = ~c->a; setf(c,FN,true); setf(c,FH,true); break;
+ case 0x37: setf(c,FC,true); setf(c,FN,false); setf(c,FH,false); break;
+ case 0x3F: setf(c,FC,!(c->f&FC)); setf(c,FN,false); setf(c,FH,false); break;
+
+ // control flow
+ case 0xC3: { u16 a=fetch16(gb); c->pc=a; internal(gb);} break;
+ case 0xE9: c->pc = c->hl; break;
+ case 0x18: { s8 e=(s8)fetch8(gb); c->pc+=e; internal(gb);} break;
+ case 0x20: { s8 e=(s8)fetch8(gb); if(!(c->f&FZ)){c->pc+=e; internal(gb);} } break;
+ case 0x28: { s8 e=(s8)fetch8(gb); if( (c->f&FZ)){c->pc+=e; internal(gb);} } break;
+ case 0x30: { s8 e=(s8)fetch8(gb); if(!(c->f&FC)){c->pc+=e; internal(gb);} } break;
+ case 0x38: { s8 e=(s8)fetch8(gb); if( (c->f&FC)){c->pc+=e; internal(gb);} } break;
+ case 0xC2: { u16 a=fetch16(gb); if(!(c->f&FZ)){c->pc=a; internal(gb);} } break;
+ case 0xCA: { u16 a=fetch16(gb); if( (c->f&FZ)){c->pc=a; internal(gb);} } break;
+ case 0xD2: { u16 a=fetch16(gb); if(!(c->f&FC)){c->pc=a; internal(gb);} } break;
+ case 0xDA: { u16 a=fetch16(gb); if( (c->f&FC)){c->pc=a; internal(gb);} } break;
+
+ case 0xCD: { u16 a=fetch16(gb); internal(gb); push16(gb,c->pc); c->pc=a; } break;
+ case 0xC4: { u16 a=fetch16(gb); if(!(c->f&FZ)){internal(gb);push16(gb,c->pc);c->pc=a;} } break;
+ case 0xCC: { u16 a=fetch16(gb); if( (c->f&FZ)){internal(gb);push16(gb,c->pc);c->pc=a;} } break;
+ case 0xD4: { u16 a=fetch16(gb); if(!(c->f&FC)){internal(gb);push16(gb,c->pc);c->pc=a;} } break;
+ case 0xDC: { u16 a=fetch16(gb); if( (c->f&FC)){internal(gb);push16(gb,c->pc);c->pc=a;} } break;
+
+ case 0xC9: c->pc = pop16(gb); internal(gb); break;
+ case 0xD9: c->pc = pop16(gb); internal(gb); c->ime = true; break; // RETI
+ case 0xC0: internal(gb); if(!(c->f&FZ)){c->pc=pop16(gb);internal(gb);} break;
+ case 0xC8: internal(gb); if( (c->f&FZ)){c->pc=pop16(gb);internal(gb);} break;
+ case 0xD0: internal(gb); if(!(c->f&FC)){c->pc=pop16(gb);internal(gb);} break;
+ case 0xD8: internal(gb); if( (c->f&FC)){c->pc=pop16(gb);internal(gb);} break;
+
+ case 0xC7: internal(gb); push16(gb,c->pc); c->pc=0x00; break;
+ case 0xCF: internal(gb); push16(gb,c->pc); c->pc=0x08; break;
+ case 0xD7: internal(gb); push16(gb,c->pc); c->pc=0x10; break;
+ case 0xDF: internal(gb); push16(gb,c->pc); c->pc=0x18; break;
+ case 0xE7: internal(gb); push16(gb,c->pc); c->pc=0x20; break;
+ case 0xEF: internal(gb); push16(gb,c->pc); c->pc=0x28; break;
+ case 0xF7: internal(gb); push16(gb,c->pc); c->pc=0x30; break;
+ case 0xFF: internal(gb); push16(gb,c->pc); c->pc=0x38; break;
+
+ // push/pop
+ case 0xC1: c->bc = pop16(gb); break;
+ case 0xD1: c->de = pop16(gb); break;
+ case 0xE1: c->hl = pop16(gb); break;
+ case 0xF1: c->af = pop16(gb) & 0xFFF0; break;
+ case 0xC5: internal(gb); push16(gb, c->bc); break;
+ case 0xD5: internal(gb); push16(gb, c->de); break;
+ case 0xE5: internal(gb); push16(gb, c->hl); break;
+ case 0xF5: internal(gb); push16(gb, c->af); break;
+
+ // high memory / misc loads
+ case 0xE0: { u8 n=fetch8(gb); wb(gb, 0xFF00+n, c->a); } break;
+ case 0xF0: { u8 n=fetch8(gb); c->a = rb(gb, 0xFF00+n); } break;
+ case 0xE2: wb(gb, 0xFF00+c->c, c->a); break;
+ case 0xF2: c->a = rb(gb, 0xFF00+c->c); break;
+ case 0xEA: { u16 a=fetch16(gb); wb(gb, a, c->a); } break;
+ case 0xFA: { u16 a=fetch16(gb); c->a = rb(gb, a); } break;
+
+ // ADD SP,e
+ case 0xE8: { s8 e=(s8)fetch8(gb); c->sp = add_sp_e(gb, e); internal(gb); internal(gb); } break;
+
+ // interrupts
+ case 0xF3: c->ime = false; c->ime_pending = false; break;
+ case 0xFB: c->ime_pending = true; break;
+
+ // immediate ALU
+ case 0xC6: alu_add(c, fetch8(gb)); break;
+ case 0xCE: alu_adc(c, fetch8(gb)); break;
+ case 0xD6: alu_sub(c, fetch8(gb)); break;
+ case 0xDE: alu_sbc(c, fetch8(gb)); break;
+ case 0xE6: alu_and(c, fetch8(gb)); break;
+ case 0xEE: alu_xor(c, fetch8(gb)); break;
+ case 0xF6: alu_or(c, fetch8(gb)); break;
+ case 0xFE: alu_cp(c, fetch8(gb)); break;
+
+ case 0xCB: do_cb(gb); break;
+
+ default:
+ // 0x40-0xBF block: LD r,r' and ALU A,r
+ if (op >= 0x40 && op < 0x80) {
+ int dst = (op >> 3) & 7;
+ int src = op & 7;
+ u8 v = reg_get(gb, src);
+ reg_set(gb, dst, v);
+ } else if (op >= 0x80 && op < 0xC0) {
+ int kind = (op >> 3) & 7;
+ u8 v = reg_get(gb, op & 7);
+ switch (kind) {
+ case 0: alu_add(c, v); break;
+ case 1: alu_adc(c, v); break;
+ case 2: alu_sub(c, v); break;
+ case 3: alu_sbc(c, v); break;
+ case 4: alu_and(c, v); break;
+ case 5: alu_xor(c, v); break;
+ case 6: alu_or(c, v); break;
+ case 7: alu_cp(c, v); break;
+ }
+ }
+ // else: illegal opcode (D3,DB,DD,E3,E4,EB,EC,ED,F4,FC,FD) -> NOP-ish
+ break;
+ }
+
+ // apply delayed EI (after this instruction completed, if it was EI)
+ if (c->ime_pending && op == 0xFB) {
+ // becomes active before next instruction; leave pending flag,
+ // but we consumed it: enable now so next handle_interrupts sees ime.
+ }
+ if (ei_was_pending && op != 0xFB) {
+ c->ime = true;
+ c->ime_pending = false;
+ }
+
+ return (int)(gb->cycles - start);
+}
diff --git a/src/cpu.h b/src/cpu.h
new file mode 100644
index 0000000..8415951
--- /dev/null
+++ b/src/cpu.h
@@ -0,0 +1,9 @@
+#ifndef GBC_CPU_H
+#define GBC_CPU_H
+
+#include "gb.h"
+
+// Execute one instruction (or handle interrupt / halt). Returns T-cycles used.
+int cpu_step(GB *gb);
+
+#endif
diff --git a/src/gb.c b/src/gb.c
new file mode 100644
index 0000000..9ed2521
--- /dev/null
+++ b/src/gb.c
@@ -0,0 +1,201 @@
+#include "gb.h"
+#include "ppu.h"
+#include "timer.h"
+#include "cpu.h"
+#include <string.h>
+#include <stdio.h>
+
+void gb_request_interrupt(GB *gb, u8 flag) {
+ gb->iff |= flag;
+}
+
+// ---------- Joypad ----------
+static u8 joyp_read(GB *gb) {
+ u8 r = 0xCF | gb->joyp_sel;
+ // sel bit4=directions, bit5=buttons ; 0 = selected. Pressed = 0.
+ if (!(gb->joyp_sel & 0x10)) { // directions
+ if (gb->buttons & BTN_RIGHT) r &= ~0x01;
+ if (gb->buttons & BTN_LEFT) r &= ~0x02;
+ if (gb->buttons & BTN_UP) r &= ~0x04;
+ if (gb->buttons & BTN_DOWN) r &= ~0x08;
+ }
+ if (!(gb->joyp_sel & 0x20)) { // buttons
+ if (gb->buttons & BTN_A) r &= ~0x01;
+ if (gb->buttons & BTN_B) r &= ~0x02;
+ if (gb->buttons & BTN_SELECT) r &= ~0x04;
+ if (gb->buttons & BTN_START) r &= ~0x08;
+ }
+ return r;
+}
+
+// ---------- OAM DMA ----------
+static void oam_dma(GB *gb, u8 val) {
+ gb->dma = val;
+ u16 src = val << 8;
+ for (int i = 0; i < 0xA0; i++)
+ gb->ppu.oam[i] = gb_read(gb, src + i);
+}
+
+// ---------- HDMA (CGB) ----------
+static void hdma_do_block(GB *gb) {
+ for (int i = 0; i < 0x10; i++) {
+ u8 v = gb_read(gb, gb->hdma_src++);
+ ppu_write(gb, 0x8000 + (gb->hdma_dst++ & 0x1FFF), v);
+ }
+}
+
+// ---------- Bus ----------
+u8 gb_read(GB *gb, u16 addr) {
+ if (addr < 0x8000) return cart_read(&gb->cart, addr);
+ if (addr < 0xA000) return ppu_read(gb, addr);
+ if (addr < 0xC000) return cart_read(&gb->cart, addr);
+ if (addr < 0xD000) return gb->wram[addr - 0xC000];
+ if (addr < 0xE000) {
+ int bank = gb->cgb_mode ? (gb->wram_bank ? gb->wram_bank : 1) : 1;
+ return gb->wram[bank * 0x1000 + (addr - 0xD000)];
+ }
+ if (addr < 0xFE00) return gb_read(gb, addr - 0x2000); // echo
+ if (addr < 0xFEA0) return ppu_read(gb, addr);
+ if (addr < 0xFF00) return 0xFF; // unusable
+
+ // IO
+ if (addr == 0xFF00) return joyp_read(gb);
+ if (addr == 0xFF01) return gb->sb;
+ if (addr == 0xFF02) return gb->sc | 0x7E;
+ if (addr >= 0xFF04 && addr <= 0xFF07) return timer_read(gb, addr);
+ if (addr == 0xFF0F) return gb->iff | 0xE0;
+ if (addr >= 0xFF10 && addr <= 0xFF3F) return 0xFF; // APU (stub)
+ if (addr >= 0xFF40 && addr <= 0xFF4B) {
+ if (addr == 0xFF46) return gb->dma;
+ return ppu_read_reg(gb, addr);
+ }
+ if (addr == 0xFF4D) return gb->cgb_mode ? (gb->key1 | 0x7E) : 0xFF;
+ if (addr == 0xFF4F) return gb->cgb_mode ? (gb->ppu.vram_bank | 0xFE) : 0xFF;
+ if (addr == 0xFF51 && gb->cgb_mode) return gb->hdma_src >> 8;
+ if (addr == 0xFF52 && gb->cgb_mode) return gb->hdma_src & 0xFF;
+ if (addr == 0xFF53 && gb->cgb_mode) return gb->hdma_dst >> 8;
+ if (addr == 0xFF54 && gb->cgb_mode) return gb->hdma_dst & 0xFF;
+ if (addr == 0xFF55 && gb->cgb_mode)
+ return gb->hdma_active ? gb->hdma_len : 0xFF;
+ if ((addr == 0xFF68 || addr == 0xFF69 || addr == 0xFF6A || addr == 0xFF6B)
+ && gb->cgb_mode) return ppu_read_reg(gb, addr);
+ if (addr == 0xFF70) return gb->cgb_mode ? (gb->wram_bank | 0xF8) : 0xFF;
+
+ if (addr >= 0xFF80 && addr < 0xFFFF) return gb->hram[addr - 0xFF80];
+ if (addr == 0xFFFF) return gb->ie;
+ return 0xFF;
+}
+
+void gb_write(GB *gb, u16 addr, u8 val) {
+ if (addr < 0x8000) { cart_write(&gb->cart, addr, val); return; }
+ if (addr < 0xA000) { ppu_write(gb, addr, val); return; }
+ if (addr < 0xC000) { cart_write(&gb->cart, addr, val); return; }
+ if (addr < 0xD000) { gb->wram[addr - 0xC000] = val; return; }
+ if (addr < 0xE000) {
+ int bank = gb->cgb_mode ? (gb->wram_bank ? gb->wram_bank : 1) : 1;
+ gb->wram[bank * 0x1000 + (addr - 0xD000)] = val; return;
+ }
+ if (addr < 0xFE00) { gb_write(gb, addr - 0x2000, val); return; }
+ if (addr < 0xFEA0) { ppu_write(gb, addr, val); return; }
+ if (addr < 0xFF00) return;
+
+ if (addr == 0xFF00) { gb->joyp_sel = val & 0x30; return; }
+ if (addr == 0xFF01) { gb->sb = val; return; }
+ if (addr == 0xFF02) {
+ gb->sc = val;
+ if ((val & 0x81) == 0x81) {
+ // transfer with internal clock: log & complete instantly
+ if (gb->serial_log) { fputc(gb->sb, stderr); fflush(stderr); }
+ gb->sb = 0xFF;
+ gb->sc &= ~0x80;
+ gb_request_interrupt(gb, INT_SERIAL);
+ }
+ return;
+ }
+ if (addr >= 0xFF04 && addr <= 0xFF07) { timer_write(gb, addr, val); return; }
+ if (addr == 0xFF0F) { gb->iff = val & 0x1F; return; }
+ if (addr >= 0xFF10 && addr <= 0xFF3F) return; // APU stub
+ if (addr >= 0xFF40 && addr <= 0xFF4B) {
+ if (addr == 0xFF46) { oam_dma(gb, val); return; }
+ ppu_write_reg(gb, addr, val);
+ return;
+ }
+ if (addr == 0xFF4D && gb->cgb_mode) { gb->key1 = (gb->key1 & 0x80) | (val & 1); return; }
+ if (addr == 0xFF4F && gb->cgb_mode) { gb->ppu.vram_bank = val & 1; return; }
+ if (addr == 0xFF51 && gb->cgb_mode) { gb->hdma_src = (gb->hdma_src & 0xFF) | (val << 8); return; }
+ if (addr == 0xFF52 && gb->cgb_mode) { gb->hdma_src = (gb->hdma_src & 0xFF00) | (val & 0xF0); return; }
+ if (addr == 0xFF53 && gb->cgb_mode) { gb->hdma_dst = (gb->hdma_dst & 0xFF) | ((val & 0x1F) << 8); return; }
+ if (addr == 0xFF54 && gb->cgb_mode) { gb->hdma_dst = (gb->hdma_dst & 0xFF00) | (val & 0xF0); return; }
+ if (addr == 0xFF55 && gb->cgb_mode) {
+ int len = ((val & 0x7F) + 1) * 0x10;
+ if (val & 0x80) {
+ // HDMA (mode 1): transfer during hblank
+ gb->hdma_len = val & 0x7F;
+ gb->hdma_active = true;
+ } else {
+ if (gb->hdma_active) {
+ // stopping an active hblank dma
+ gb->hdma_active = false;
+ gb->hdma_len = 0xFF;
+ } else {
+ // GDMA: transfer all now
+ for (int i = 0; i < len; i++) {
+ u8 v = gb_read(gb, gb->hdma_src++);
+ ppu_write(gb, 0x8000 + (gb->hdma_dst++ & 0x1FFF), v);
+ }
+ }
+ }
+ return;
+ }
+ if ((addr >= 0xFF68 && addr <= 0xFF6B) && gb->cgb_mode) { ppu_write_reg(gb, addr, val); return; }
+ if (addr == 0xFF70 && gb->cgb_mode) { gb->wram_bank = val & 0x07; return; }
+
+ if (addr >= 0xFF80 && addr < 0xFFFF) { gb->hram[addr - 0xFF80] = val; return; }
+ if (addr == 0xFFFF) { gb->ie = val; return; }
+}
+
+// advance subsystems by t-cycles
+void gb_tick(GB *gb, int tcycles) {
+ // in double-speed mode CPU/timer run 2x, but PPU stays at normal rate.
+ int ppu_cycles = gb->double_speed ? tcycles / 2 : tcycles;
+ timer_tick(gb, tcycles);
+ ppu_tick(gb, ppu_cycles);
+ gb->cycles += tcycles;
+}
+
+// HDMA one block per hblank; called by ppu when entering hblank
+void gb_hdma_hblank(GB *gb) {
+ if (!gb->hdma_active) return;
+ hdma_do_block(gb);
+ if (gb->hdma_len == 0) {
+ gb->hdma_active = false;
+ gb->hdma_len = 0xFF;
+ } else {
+ gb->hdma_len--;
+ }
+}
+
+void gb_init(GB *gb) {
+ memset(gb, 0, sizeof(*gb));
+ gb_reset(gb);
+}
+
+void gb_reset(GB *gb) {
+ CPU *c = &gb->cpu;
+ gb->cgb_mode = gb->cart.cgb;
+ // post-boot register state
+ c->pc = 0x0100;
+ c->sp = 0xFFFE;
+ if (gb->cgb_mode) {
+ c->af = 0x1180; c->bc = 0x0000; c->de = 0xFF56; c->hl = 0x000D;
+ } else {
+ c->af = 0x01B0; c->bc = 0x0013; c->de = 0x00D8; c->hl = 0x014D;
+ }
+ c->ime = false;
+ gb->wram_bank = 1;
+ gb->joyp_sel = 0x30;
+ gb->iff = 0xE1;
+ gb->hdma_len = 0xFF;
+ ppu_reset(gb);
+ gb->timer.div = 0x1800;
+}
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..27e886a
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,41 @@
+#include "gb.h"
+#include "cpu.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+int main(int argc, char **argv) {
+ if (argc < 2) {
+ fprintf(stderr, "usage: %s <rom> [--test seconds]\n", argv[0]);
+ return 1;
+ }
+ const char *rom = argv[1];
+ bool test = false;
+ double seconds = 30.0;
+ for (int i = 2; i < argc; i++) {
+ if (!strcmp(argv[i], "--test")) {
+ test = true;
+ if (i + 1 < argc) seconds = atof(argv[++i]);
+ }
+ }
+
+ GB *gb = calloc(1, sizeof(GB));
+ if (cart_load(&gb->cart, rom) != 0) return 1;
+ gb_reset(gb);
+
+ if (test) {
+ gb->serial_log = true;
+ u64 target = (u64)(seconds * 4194304.0);
+ while (gb->cycles < target) cpu_step(gb);
+ fprintf(stderr, "\n[ran %llu cycles]\n", (unsigned long long)gb->cycles);
+ cart_free(&gb->cart);
+ free(gb);
+ return 0;
+ }
+
+ // TODO: interactive terminal rendering
+ fprintf(stderr, "interactive mode not yet implemented; use --test\n");
+ cart_free(&gb->cart);
+ free(gb);
+ return 0;
+}
diff --git a/src/ppu.c b/src/ppu.c
new file mode 100644
index 0000000..16ecc2e
--- /dev/null
+++ b/src/ppu.c
@@ -0,0 +1,355 @@
+#include "ppu.h"
+#include <string.h>
+
+// LCDC bits
+#define LCDC_BG_EN 0x01
+#define LCDC_OBJ_EN 0x02
+#define LCDC_OBJ_SIZE 0x04
+#define LCDC_BG_MAP 0x08
+#define LCDC_TILE_DATA 0x10
+#define LCDC_WIN_EN 0x20
+#define LCDC_WIN_MAP 0x40
+#define LCDC_LCD_EN 0x80
+
+// STAT bits
+#define STAT_LYC_INT 0x40
+#define STAT_M2_INT 0x20
+#define STAT_M1_INT 0x10
+#define STAT_M0_INT 0x08
+#define STAT_LYC_FLAG 0x04
+
+void gb_hdma_hblank(GB *gb); // from gb.c
+
+// DMG shades (greenish palette), index 0..3
+static const u8 dmg_shades[4][3] = {
+ {224, 248, 208}, {136, 192, 112}, {52, 104, 86}, {8, 24, 32}
+};
+
+void ppu_reset(GB *gb) {
+ PPU *p = &gb->ppu;
+ p->lcdc = 0x91;
+ p->stat = 0x85;
+ p->bgp = 0xFC;
+ p->obp0 = 0xFF;
+ p->obp1 = 0xFF;
+ p->ly = 0;
+ p->mode = 2;
+ p->dots = 0;
+ p->vram_bank = 0;
+ memset(p->bg_pal, 0xFF, sizeof(p->bg_pal));
+ memset(p->obj_pal, 0xFF, sizeof(p->obj_pal));
+}
+
+// ---- VRAM / OAM access ----
+u8 ppu_read(GB *gb, u16 addr) {
+ PPU *p = &gb->ppu;
+ if (addr >= 0x8000 && addr < 0xA000)
+ return p->vram[p->vram_bank * 0x2000 + (addr - 0x8000)];
+ if (addr >= 0xFE00 && addr < 0xFEA0)
+ return p->oam[addr - 0xFE00];
+ return 0xFF;
+}
+void ppu_write(GB *gb, u16 addr, u8 val) {
+ PPU *p = &gb->ppu;
+ if (addr >= 0x8000 && addr < 0xA000)
+ p->vram[p->vram_bank * 0x2000 + (addr - 0x8000)] = val;
+ else if (addr >= 0xFE00 && addr < 0xFEA0)
+ p->oam[addr - 0xFE00] = val;
+}
+
+static inline u8 vram_at(PPU *p, int bank, u16 addr) {
+ return p->vram[bank * 0x2000 + (addr - 0x8000)];
+}
+
+// ---- registers ----
+u8 ppu_read_reg(GB *gb, u16 addr) {
+ PPU *p = &gb->ppu;
+ switch (addr) {
+ case 0xFF40: return p->lcdc;
+ case 0xFF41: return p->stat | 0x80;
+ case 0xFF42: return p->scy;
+ case 0xFF43: return p->scx;
+ case 0xFF44: return p->ly;
+ case 0xFF45: return p->lyc;
+ case 0xFF47: return p->bgp;
+ case 0xFF48: return p->obp0;
+ case 0xFF49: return p->obp1;
+ case 0xFF4A: return p->wy;
+ case 0xFF4B: return p->wx;
+ case 0xFF68: return p->bcps;
+ case 0xFF69: return p->bg_pal[p->bcps & 0x3F];
+ case 0xFF6A: return p->ocps;
+ case 0xFF6B: return p->obj_pal[p->ocps & 0x3F];
+ }
+ return 0xFF;
+}
+
+static void check_lyc(GB *gb) {
+ PPU *p = &gb->ppu;
+ if (p->ly == p->lyc) p->stat |= STAT_LYC_FLAG;
+ else p->stat &= ~STAT_LYC_FLAG;
+}
+
+void ppu_write_reg(GB *gb, u16 addr, u8 val) {
+ PPU *p = &gb->ppu;
+ switch (addr) {
+ case 0xFF40: {
+ bool was_on = p->lcdc & LCDC_LCD_EN;
+ p->lcdc = val;
+ if (was_on && !(val & LCDC_LCD_EN)) {
+ p->ly = 0; p->dots = 0; p->mode = 0;
+ p->stat &= ~0x03;
+ } else if (!was_on && (val & LCDC_LCD_EN)) {
+ p->mode = 2; p->dots = 0; p->ly = 0;
+ check_lyc(gb);
+ }
+ break;
+ }
+ case 0xFF41: p->stat = (p->stat & 0x07) | (val & 0x78); break;
+ case 0xFF42: p->scy = val; break;
+ case 0xFF43: p->scx = val; break;
+ case 0xFF44: break; // LY read-only
+ case 0xFF45: p->lyc = val; check_lyc(gb); break;
+ case 0xFF47: p->bgp = val; break;
+ case 0xFF48: p->obp0 = val; break;
+ case 0xFF49: p->obp1 = val; break;
+ case 0xFF4A: p->wy = val; break;
+ case 0xFF4B: p->wx = val; break;
+ case 0xFF68: p->bcps = val & 0xBF; break;
+ case 0xFF69:
+ p->bg_pal[p->bcps & 0x3F] = val;
+ if (p->bcps & 0x80) p->bcps = 0x80 | ((p->bcps + 1) & 0x3F);
+ break;
+ case 0xFF6A: p->ocps = val & 0xBF; break;
+ case 0xFF6B:
+ p->obj_pal[p->ocps & 0x3F] = val;
+ if (p->ocps & 0x80) p->ocps = 0x80 | ((p->ocps + 1) & 0x3F);
+ break;
+ }
+}
+
+// ---- palette helpers ----
+static void cgb_color(u8 *pal, int palnum, int cidx, u8 out[3]) {
+ int i = (palnum * 4 + cidx) * 2;
+ u16 c = pal[i] | (pal[i+1] << 8);
+ int r = c & 0x1F, g = (c >> 5) & 0x1F, b = (c >> 10) & 0x1F;
+ // scale 5-bit to 8-bit with a mild color-correction curve
+ out[0] = (r * 13 + g * 2 + b) >> 1;
+ out[1] = (g * 3 + b) << 1;
+ out[2] = (r * 3 + g * 2 + b * 11) >> 1;
+}
+
+// ---- scanline rendering ----
+static void render_scanline(GB *gb) {
+ PPU *p = &gb->ppu;
+ int ly = p->ly;
+ if (ly >= SCREEN_H) return;
+
+ u8 bg_prio[SCREEN_W]; // BG color index (for sprite priority)
+ u8 bg_hasprio[SCREEN_W]; // CGB BG-to-OAM priority bit
+
+ bool cgb = gb->cgb_mode;
+ bool bg_master = (p->lcdc & LCDC_BG_EN) || cgb; // in CGB, bit0 = master priority
+
+ // ----- Background & Window -----
+ for (int x = 0; x < SCREEN_W; x++) {
+ bg_prio[x] = 0;
+ bg_hasprio[x] = 0;
+
+ bool use_win = (p->lcdc & LCDC_WIN_EN) && (ly >= p->wy) && (x + 7 >= p->wx);
+ int px, py;
+ u16 map_base;
+ if (use_win) {
+ px = x + 7 - p->wx;
+ py = p->wly;
+ map_base = (p->lcdc & LCDC_WIN_MAP) ? 0x9C00 : 0x9800;
+ } else {
+ px = (x + p->scx) & 0xFF;
+ py = (ly + p->scy) & 0xFF;
+ map_base = (p->lcdc & LCDC_BG_MAP) ? 0x9C00 : 0x9800;
+ }
+ int tx = px / 8, ty = py / 8;
+ u16 map_addr = map_base + ty * 32 + tx;
+ u8 tile = vram_at(p, 0, map_addr);
+
+ u8 attr = cgb ? vram_at(p, 1, map_addr) : 0;
+ int tbank = (attr & 0x08) ? 1 : 0;
+ bool xflip = attr & 0x20, yflip = attr & 0x40;
+ int palnum = attr & 0x07;
+ bool prio = attr & 0x80;
+
+ int row = py % 8, col = px % 8;
+ if (yflip) row = 7 - row;
+ if (xflip) col = 7 - col;
+
+ u16 tile_addr;
+ if (p->lcdc & LCDC_TILE_DATA)
+ tile_addr = 0x8000 + tile * 16 + row * 2;
+ else
+ tile_addr = 0x9000 + ((s8)tile) * 16 + row * 2;
+
+ u8 lo = vram_at(p, tbank, tile_addr);
+ u8 hi = vram_at(p, tbank, tile_addr + 1);
+ int bit = 7 - col;
+ int cidx = ((lo >> bit) & 1) | (((hi >> bit) & 1) << 1);
+
+ bg_prio[x] = cidx;
+ bg_hasprio[x] = prio;
+
+ u8 rgb[3];
+ if (!bg_master) {
+ rgb[0] = dmg_shades[0][0]; rgb[1] = dmg_shades[0][1]; rgb[2] = dmg_shades[0][2];
+ } else if (cgb) {
+ cgb_color(p->bg_pal, palnum, cidx, rgb);
+ } else {
+ int shade = (p->bgp >> (cidx * 2)) & 3;
+ memcpy(rgb, dmg_shades[shade], 3);
+ }
+ memcpy(p->fb[ly][x], rgb, 3);
+ }
+ if ((p->lcdc & LCDC_WIN_EN) && ly >= p->wy && p->wx < 167)
+ p->wly++;
+
+ // ----- Sprites -----
+ if (!(p->lcdc & LCDC_OBJ_EN)) return;
+ int h = (p->lcdc & LCDC_OBJ_SIZE) ? 16 : 8;
+
+ // gather up to 10 sprites on this line
+ int chosen[10], n = 0;
+ for (int i = 0; i < 40 && n < 10; i++) {
+ int sy = p->oam[i*4] - 16;
+ if (ly >= sy && ly < sy + h) chosen[n++] = i;
+ }
+ // draw back-to-front: lower priority first.
+ // DMG: smaller X = higher priority (drawn last). CGB: lower OAM index = higher.
+ for (int a = 0; a < n; a++) {
+ for (int b = a + 1; b < n; b++) {
+ bool swap;
+ if (cgb) swap = chosen[b] < chosen[a];
+ else {
+ int xa = p->oam[chosen[a]*4+1], xb = p->oam[chosen[b]*4+1];
+ swap = (xb > xa) || (xb == xa && chosen[b] < chosen[a]);
+ }
+ if (swap) { int t = chosen[a]; chosen[a] = chosen[b]; chosen[b] = t; }
+ }
+ }
+
+ for (int k = 0; k < n; k++) {
+ int i = chosen[k];
+ int sy = p->oam[i*4] - 16;
+ int sx = p->oam[i*4+1] - 8;
+ u8 tile = p->oam[i*4+2];
+ u8 attr = p->oam[i*4+3];
+ bool xflip = attr & 0x20, yflip = attr & 0x40;
+ bool behind = attr & 0x80;
+ if (h == 16) tile &= 0xFE;
+
+ int row = ly - sy;
+ if (yflip) row = h - 1 - row;
+ int tbank = (cgb && (attr & 0x08)) ? 1 : 0;
+
+ u16 tile_addr = 0x8000 + tile * 16 + row * 2;
+ u8 lo = vram_at(p, tbank, tile_addr);
+ u8 hi = vram_at(p, tbank, tile_addr + 1);
+
+ for (int col = 0; col < 8; col++) {
+ int x = sx + col;
+ if (x < 0 || x >= SCREEN_W) continue;
+ int bit = xflip ? col : 7 - col;
+ int cidx = ((lo >> bit) & 1) | (((hi >> bit) & 1) << 1);
+ if (cidx == 0) continue; // transparent
+
+ // priority resolution
+ if (cgb) {
+ // BG master priority (LCDC.0) forces BG over OBJ when set
+ if ((p->lcdc & LCDC_BG_EN)) {
+ if ((behind || bg_hasprio[x]) && bg_prio[x] != 0) continue;
+ }
+ } else {
+ if (behind && bg_prio[x] != 0) continue;
+ }
+
+ u8 rgb[3];
+ if (cgb) {
+ cgb_color(p->obj_pal, attr & 0x07, cidx, rgb);
+ } else {
+ u8 pal = (attr & 0x10) ? p->obp1 : p->obp0;
+ int shade = (pal >> (cidx * 2)) & 3;
+ memcpy(rgb, dmg_shades[shade], 3);
+ }
+ memcpy(p->fb[ly][x], rgb, 3);
+ }
+ }
+}
+
+// ---- STAT interrupt line (edge-triggered) ----
+static void update_stat(GB *gb, bool lyc_check) {
+ PPU *p = &gb->ppu;
+ static bool prev_line = false;
+ if (lyc_check) check_lyc(gb);
+
+ bool line = false;
+ switch (p->mode) {
+ case 0: if (p->stat & STAT_M0_INT) line = true; break;
+ case 1: if (p->stat & STAT_M1_INT) line = true; break;
+ case 2: if (p->stat & STAT_M2_INT) line = true; break;
+ }
+ if ((p->stat & STAT_LYC_INT) && (p->stat & STAT_LYC_FLAG)) line = true;
+
+ if (line && !prev_line) gb_request_interrupt(gb, INT_LCDSTAT);
+ prev_line = line;
+}
+
+void ppu_tick(GB *gb, int tcycles) {
+ PPU *p = &gb->ppu;
+ if (!(p->lcdc & LCDC_LCD_EN)) return;
+
+ for (int t = 0; t < tcycles; t++) {
+ p->dots++;
+
+ switch (p->mode) {
+ case 2: // OAM scan
+ if (p->dots >= 80) { p->mode = 3; p->stat = (p->stat & ~3) | 3; }
+ break;
+ case 3: // drawing
+ if (p->dots >= 80 + 172) {
+ p->mode = 0;
+ p->stat = (p->stat & ~3);
+ render_scanline(gb);
+ gb_hdma_hblank(gb);
+ update_stat(gb, false);
+ }
+ break;
+ case 0: // hblank
+ if (p->dots >= 456) {
+ p->dots -= 456;
+ p->ly++;
+ if (p->ly == 144) {
+ p->mode = 1;
+ p->stat = (p->stat & ~3) | 1;
+ p->wly = 0;
+ gb_request_interrupt(gb, INT_VBLANK);
+ p->frame_ready = true;
+ update_stat(gb, true);
+ } else {
+ p->mode = 2;
+ p->stat = (p->stat & ~3) | 2;
+ update_stat(gb, true);
+ }
+ }
+ break;
+ case 1: // vblank
+ if (p->dots >= 456) {
+ p->dots -= 456;
+ p->ly++;
+ if (p->ly > 153) {
+ p->ly = 0;
+ p->mode = 2;
+ p->stat = (p->stat & ~3) | 2;
+ }
+ update_stat(gb, true);
+ }
+ break;
+ }
+ }
+}
diff --git a/src/ppu.h b/src/ppu.h
new file mode 100644
index 0000000..ea427f7
--- /dev/null
+++ b/src/ppu.h
@@ -0,0 +1,14 @@
+#ifndef GBC_PPU_H
+#define GBC_PPU_H
+
+#include "gb.h"
+
+void ppu_reset(GB *gb);
+void ppu_tick(GB *gb, int tcycles);
+
+u8 ppu_read(GB *gb, u16 addr); // VRAM/OAM
+void ppu_write(GB *gb, u16 addr, u8 val);
+u8 ppu_read_reg(GB *gb, u16 addr); // FF40-FF4B, FF68-FF6B
+void ppu_write_reg(GB *gb, u16 addr, u8 val);
+
+#endif
diff --git a/src/timer.c b/src/timer.c
new file mode 100644
index 0000000..d4ee69f
--- /dev/null
+++ b/src/timer.c
@@ -0,0 +1,82 @@
+#include "timer.h"
+
+// Which DIV bit feeds the TIMA increment for each TAC clock-select.
+static const int tac_bit[4] = { 9, 3, 5, 7 };
+
+static bool timer_signal(Timer *t) {
+ if (!(t->tac & 0x04)) return false;
+ return (t->div >> tac_bit[t->tac & 3]) & 1;
+}
+
+static void set_div(GB *gb, u16 newdiv) {
+ Timer *t = &gb->timer;
+ bool old = timer_signal(t);
+ t->div = newdiv;
+ bool now = timer_signal(t);
+ // falling edge -> increment TIMA
+ if (old && !now) {
+ if (t->tima == 0xFF) {
+ t->tima = 0;
+ t->overflow_pending = true;
+ t->reload_delay = 4; // reload TMA after 4 T-cycles
+ } else {
+ t->tima++;
+ }
+ }
+}
+
+void timer_tick(GB *gb, int tcycles) {
+ Timer *t = &gb->timer;
+ for (int i = 0; i < tcycles; i++) {
+ if (t->overflow_pending) {
+ t->reload_delay--;
+ if (t->reload_delay == 0) {
+ t->tima = t->tma;
+ t->overflow_pending = false;
+ gb_request_interrupt(gb, INT_TIMER);
+ }
+ }
+ set_div(gb, t->div + 1);
+ }
+}
+
+u8 timer_read(GB *gb, u16 addr) {
+ Timer *t = &gb->timer;
+ switch (addr) {
+ case 0xFF04: return t->div >> 8;
+ case 0xFF05: return t->tima;
+ case 0xFF06: return t->tma;
+ case 0xFF07: return t->tac | 0xF8;
+ }
+ return 0xFF;
+}
+
+void timer_write(GB *gb, u16 addr, u8 val) {
+ Timer *t = &gb->timer;
+ switch (addr) {
+ case 0xFF04:
+ set_div(gb, 0); // writing DIV resets it (may cause edge)
+ break;
+ case 0xFF05:
+ // writing during reload delay cancels the reload
+ if (t->reload_delay != 0) t->overflow_pending = false;
+ if (!(t->overflow_pending && t->reload_delay == 0))
+ t->tima = val;
+ break;
+ case 0xFF06:
+ t->tma = val;
+ break;
+ case 0xFF07: {
+ bool old = timer_signal(t);
+ t->tac = val & 0x07;
+ bool now = timer_signal(t);
+ if (old && !now) {
+ // disabling can also cause an increment (glitch)
+ if (t->tima == 0xFF) {
+ t->tima = 0; t->overflow_pending = true; t->reload_delay = 4;
+ } else t->tima++;
+ }
+ break;
+ }
+ }
+}
diff --git a/src/timer.h b/src/timer.h
new file mode 100644
index 0000000..95fec0d
--- /dev/null
+++ b/src/timer.h
@@ -0,0 +1,10 @@
+#ifndef GBC_TIMER_H
+#define GBC_TIMER_H
+
+#include "gb.h"
+
+void timer_tick(GB *gb, int tcycles);
+u8 timer_read(GB *gb, u16 addr);
+void timer_write(GB *gb, u16 addr, u8 val);
+
+#endif