Quazim0t0 commited on
Commit
e2299ad
·
verified ·
1 Parent(s): ddffc01

Upload 2 files

Browse files
Files changed (2) hide show
  1. gb_console.py +373 -0
  2. gb_sm83.py +237 -0
gb_console.py ADDED
@@ -0,0 +1,373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ gb_console.py -- the full DMG console around the SM83 core: MMU with MBC1/MBC5
3
+ banking, the complete PPU (background + window + sprites, per-dot pixel emission,
4
+ variable-length mode 3 with the SCX fine-scroll penalty, STAT/LYC and VBlank
5
+ interrupt lines, OAM DMA), the DIV/TIMA timer, joypad, and serial-out capture.
6
+
7
+ The PPU's pixel datapath runs through the units API: tile-row decode, palette
8
+ apply, and the sprite-priority mux are gb_units units (golden fns or verified
9
+ neural nets); the rest is address wiring and scheduling -- the orchestrator the
10
+ README describes as standard cycle-accurate emulator engineering.
11
+
12
+ Simplifications (documented, identical in golden and neural runs so comparisons
13
+ stay meaningful): OAM DMA copies instantly at the write; no sprite-fetch mode-3
14
+ stalls (mode 3 length = 172 + SCX%8); no OAM/VRAM access blocking by mode.
15
+ """
16
+ import numpy as np
17
+ from gb_sm83 import SM83, IF_VBLANK, IF_STAT, IF_TIMER
18
+
19
+ DOTS_PER_LINE, LINES_TOTAL, LINES_VIS = 456, 154, 144
20
+
21
+
22
+ class PPU:
23
+ def __init__(self, bus, units, render=True):
24
+ self.bus = bus; self.u = units; self.render = render
25
+ self.fb = np.zeros((LINES_VIS, 160), np.uint8)
26
+ self.ly = 0; self.lx = 0; self.mode = 2
27
+ self.win_line = 0; self.stat_line = 0
28
+ self.frame = 0; self.frame_done = False
29
+ self.line_sprites = []
30
+ self.bg_cache = None; self.win_cache = None
31
+
32
+ # ---- per-line setup ----
33
+ def oam_scan(self):
34
+ io = self.bus.io; oam = self.bus.oam
35
+ h = 16 if io[0x40] & 0x04 else 8
36
+ sel = []
37
+ for i in range(40):
38
+ sy = int(oam[i * 4])
39
+ if sy <= self.ly + 16 < sy + h:
40
+ sel.append(i)
41
+ if len(sel) == 10: break
42
+ # DMG priority: lower X wins, ties by OAM order (stable sort)
43
+ self.line_sprites = sorted(sel, key=lambda i: int(oam[i * 4 + 1]))
44
+
45
+ def sprite_pixel(self, x):
46
+ """Winning sprite (idx, palette#, bg-priority) at screen x, or None."""
47
+ io = self.bus.io; oam = self.bus.oam; vram = self.bus.vram
48
+ h = 16 if io[0x40] & 0x04 else 8
49
+ for i in self.line_sprites:
50
+ sx = int(oam[i * 4 + 1])
51
+ if not (sx <= x + 8 < sx + 8):
52
+ continue
53
+ sy, tile, attr = int(oam[i * 4]), int(oam[i * 4 + 2]), int(oam[i * 4 + 3])
54
+ row = self.ly + 16 - sy
55
+ if attr & 0x40: row = h - 1 - row # Y flip
56
+ if h == 16: tile = (tile & 0xFE) | (row >> 3); row &= 7
57
+ base = tile * 16 + row * 2
58
+ idx = self.u.tilerow(int(vram[base]), int(vram[base + 1]))
59
+ col = x + 8 - sx
60
+ if attr & 0x20: col = 7 - col # X flip
61
+ ci = idx[col]
62
+ if ci != 0:
63
+ return ci, (attr >> 4) & 1, (attr >> 7) & 1
64
+ return None
65
+
66
+ def bg_pixel(self, x):
67
+ io = self.bus.io; vram = self.bus.vram
68
+ lcdc = int(io[0x40])
69
+ use_win = (lcdc & 0x20) and self.ly >= int(io[0x4A]) and x >= int(io[0x4B]) - 7
70
+ if use_win:
71
+ wy = self.win_line; wx = x - (int(io[0x4B]) - 7)
72
+ map_base = 0x1C00 if lcdc & 0x40 else 0x1800
73
+ ty, tx, fy, fx = wy >> 3, wx >> 3, wy & 7, wx & 7
74
+ key = ("w", tx, ty)
75
+ cache = self.win_cache
76
+ else:
77
+ sy = (self.ly + int(io[0x42])) & 0xFF; sx = (x + int(io[0x43])) & 0xFF
78
+ map_base = 0x1C00 if lcdc & 0x08 else 0x1800
79
+ ty, tx, fy, fx = sy >> 3, sx >> 3, sy & 7, sx & 7
80
+ key = ("b", tx, ty)
81
+ cache = self.bg_cache
82
+ if cache is None or cache[0] != key:
83
+ tid = int(vram[map_base + ty * 32 + tx])
84
+ if lcdc & 0x10: base = tid * 16 + fy * 2
85
+ else: base = 0x1000 + (tid - 256 if tid > 127 else tid) * 16 + fy * 2
86
+ row = self.u.tilerow(int(vram[base]), int(vram[base + 1]))
87
+ cache = (key, row, fy)
88
+ if use_win: self.win_cache = cache
89
+ else: self.bg_cache = cache
90
+ elif cache[2] != fy:
91
+ tid = int(vram[map_base + ty * 32 + tx])
92
+ if lcdc & 0x10: base = tid * 16 + fy * 2
93
+ else: base = 0x1000 + (tid - 256 if tid > 127 else tid) * 16 + fy * 2
94
+ cache = (key, self.u.tilerow(int(vram[base]), int(vram[base + 1])), fy)
95
+ if use_win: self.win_cache = cache
96
+ else: self.bg_cache = cache
97
+ return cache[1][fx], use_win
98
+
99
+ def emit_pixel(self, x):
100
+ io = self.bus.io
101
+ lcdc = int(io[0x40])
102
+ bg_ci = 0; used_win = False
103
+ if lcdc & 0x01:
104
+ bg_ci, used_win = self.bg_pixel(x)
105
+ sp = self.sprite_pixel(x) if lcdc & 0x02 else None
106
+ if sp is not None and self.u.sprmux(bg_ci, sp[0], sp[2]):
107
+ shade = self.u.palette(sp[0], int(io[0x49 if sp[1] else 0x48]))
108
+ else:
109
+ shade = self.u.palette(bg_ci, int(io[0x47])) if lcdc & 0x01 else 0
110
+ self.fb[self.ly, x] = shade
111
+ return used_win
112
+
113
+ # ---- STAT machinery ----
114
+ def update_stat(self):
115
+ io = self.bus.io
116
+ coinc = int(self.ly == int(io[0x45]))
117
+ io[0x41] = (io[0x41] & 0xF8) | (coinc << 2) | self.mode
118
+ line = ((int(io[0x41] >> 6) & 1) and coinc) or \
119
+ (((io[0x41] >> 5) & 1) and self.mode == 2) or \
120
+ (((io[0x41] >> 4) & 1) and self.mode == 1) or \
121
+ (((io[0x41] >> 3) & 1) and self.mode == 0)
122
+ line = int(bool(line))
123
+ if line and not self.stat_line:
124
+ self.bus.req_if(IF_STAT)
125
+ self.stat_line = line
126
+
127
+ def set_mode(self, m):
128
+ if m != self.mode:
129
+ self.mode = m
130
+ self.update_stat()
131
+
132
+ # ---- the dot clock ----
133
+ def tick(self, dots):
134
+ io = self.bus.io
135
+ if not (io[0x40] & 0x80): # LCD off
136
+ self.ly = 0; self.lx = 0; self.mode = 0
137
+ io[0x44] = 0
138
+ return
139
+ if not self.render:
140
+ self.tick_fast(dots)
141
+ return
142
+ for _ in range(dots):
143
+ lx, ly = self.lx, self.ly
144
+ if ly < LINES_VIS:
145
+ if lx == 0:
146
+ self.oam_scan(); self.set_mode(2)
147
+ self.bg_cache = self.win_cache = None
148
+ self.win_used = False
149
+ elif lx == 80:
150
+ self.set_mode(3)
151
+ scx_pen = int(io[0x43]) & 7
152
+ px = lx - 80 - scx_pen
153
+ if self.render and 80 <= lx and 0 <= px < 160:
154
+ if self.emit_pixel(px):
155
+ self.win_used = True
156
+ if lx == 80 + scx_pen + 172:
157
+ self.set_mode(0)
158
+ if not self.render:
159
+ # window line counter must advance even without rendering
160
+ lcdc = int(io[0x40])
161
+ if (lcdc & 0x20) and ly >= int(io[0x4A]) and int(io[0x4B]) - 7 < 160:
162
+ self.win_used = True
163
+ self.lx += 1
164
+ if self.lx == DOTS_PER_LINE:
165
+ self.lx = 0
166
+ if ly < LINES_VIS and getattr(self, "win_used", False):
167
+ self.win_line += 1
168
+ self.ly += 1
169
+ if self.ly == LINES_VIS:
170
+ self.set_mode(1); self.bus.req_if(IF_VBLANK)
171
+ elif self.ly == LINES_TOTAL:
172
+ self.ly = 0; self.win_line = 0
173
+ self.frame += 1; self.frame_done = True
174
+ io[0x44] = self.ly
175
+ self.update_stat()
176
+
177
+ def tick_fast(self, dots):
178
+ """No pixel emission: jump between mode boundaries instead of per-dot.
179
+ LY/mode/STAT/VBlank/window-line behavior identical to the per-dot path."""
180
+ io = self.bus.io
181
+ while dots:
182
+ if self.lx == 0 and self.ly < LINES_VIS:
183
+ self.set_mode(2)
184
+ scx_pen = int(io[0x43]) & 7
185
+ m3_end = 80 + scx_pen + 172
186
+ if self.ly < LINES_VIS:
187
+ nxt = 80 if self.lx < 80 else (m3_end if self.lx < m3_end else DOTS_PER_LINE)
188
+ else:
189
+ nxt = DOTS_PER_LINE
190
+ step = min(dots, nxt - self.lx)
191
+ self.lx += step; dots -= step
192
+ if self.ly < LINES_VIS:
193
+ if self.lx == 80: self.set_mode(3)
194
+ elif self.lx == m3_end and self.mode == 3:
195
+ self.set_mode(0)
196
+ lcdc = int(io[0x40])
197
+ if (lcdc & 0x20) and self.ly >= int(io[0x4A]) and int(io[0x4B]) - 7 < 160:
198
+ self.win_line += 1
199
+ if self.lx == DOTS_PER_LINE:
200
+ self.lx = 0; self.ly += 1
201
+ if self.ly == LINES_VIS:
202
+ self.set_mode(1); self.bus.req_if(IF_VBLANK)
203
+ elif self.ly == LINES_TOTAL:
204
+ self.ly = 0; self.win_line = 0
205
+ self.frame += 1; self.frame_done = True
206
+ io[0x44] = self.ly
207
+ self.update_stat()
208
+
209
+
210
+ class Bus:
211
+ def __init__(self, rom, units, render=True):
212
+ self.rom = np.frombuffer(bytes(rom), dtype=np.uint8)
213
+ cart = self.rom[0x147]
214
+ self.mbc = 5 if cart in (0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E) else \
215
+ (1 if cart in (0x01, 0x02, 0x03) else 0)
216
+ self.rom_bank = 1; self.ram_bank = 0; self.ram_on = False
217
+ self.cart_ram = np.zeros(0x8000, np.uint8)
218
+ self.vram = np.zeros(0x2000, np.uint8)
219
+ self.wram = np.zeros(0x2000, np.uint8)
220
+ self.oam = np.zeros(0xA0, np.uint8)
221
+ self.hram = np.zeros(0x7F, np.uint8)
222
+ self.io = np.zeros(0x80, np.uint8)
223
+ self.ie = 0
224
+ self.io[0x40] = 0x91; self.io[0x41] = 0x85; self.io[0x47] = 0xFC
225
+ self.io[0x00] = 0xCF; self.io[0x0F] = 0xE1
226
+ self.divt = 0xAB00
227
+ self.serial = []
228
+ self.keys = set() # subset of {"right","left","up","down","a","b","select","start"}
229
+ self.ppu = PPU(self, units, render)
230
+ self.cycles = 0
231
+
232
+ def req_if(self, bit): self.io[0x0F] |= bit
233
+ def clear_IF(self, bit): self.io[0x0F] &= ~bit & 0xFF
234
+ def io_IE(self): return self.ie
235
+ def io_IF(self): return int(self.io[0x0F])
236
+
237
+ # ---- timer: divt is the t-cycle counter; DIV is bits 8..15. TIMA increments on
238
+ # falling edges of the TAC-selected bit, i.e. once per 2^(bit+1) t-cycles ----
239
+ TAC_BITS = {0: 9, 1: 3, 2: 5, 3: 7}
240
+ def timer_tick(self, t):
241
+ old = self.divt
242
+ self.divt += t
243
+ tac = int(self.io[0x07])
244
+ if tac & 4:
245
+ period = 1 << (self.TAC_BITS[tac & 3] + 1)
246
+ n = self.divt // period - old // period
247
+ if n:
248
+ tima = int(self.io[0x05]) + n
249
+ if tima > 0xFF:
250
+ tma = int(self.io[0x06])
251
+ tima = tma + (tima - 0x100) % max(1, 0x100 - tma)
252
+ self.req_if(IF_TIMER)
253
+ self.io[0x05] = tima
254
+
255
+ def tick(self, m):
256
+ self.cycles += m
257
+ self.timer_tick(4 * m)
258
+ self.ppu.tick(4 * m)
259
+
260
+ # ---- address decode ----
261
+ def read(self, a):
262
+ self.tick(1)
263
+ return self._read(a)
264
+ def _read(self, a):
265
+ a &= 0xFFFF
266
+ if a < 0x4000:
267
+ return int(self.rom[a])
268
+ if a < 0x8000:
269
+ bank = self.rom_bank % max(1, len(self.rom) // 0x4000)
270
+ return int(self.rom[bank * 0x4000 + (a - 0x4000)])
271
+ if a < 0xA000: return int(self.vram[a - 0x8000])
272
+ if a < 0xC000:
273
+ if not self.ram_on: return 0xFF
274
+ return int(self.cart_ram[(self.ram_bank * 0x2000 + (a - 0xA000)) % 0x8000])
275
+ if a < 0xE000: return int(self.wram[a - 0xC000])
276
+ if a < 0xFE00: return int(self.wram[a - 0xE000])
277
+ if a < 0xFEA0: return int(self.oam[a - 0xFE00])
278
+ if a < 0xFF00: return 0xFF
279
+ if a == 0xFF00:
280
+ sel = int(self.io[0x00]); low = 0x0F
281
+ if not sel & 0x10: # d-pad selected (low = pressed)
282
+ for bit, k in enumerate(("right", "left", "up", "down")):
283
+ if k in self.keys: low &= ~(1 << bit)
284
+ if not sel & 0x20: # buttons selected
285
+ for bit, k in enumerate(("a", "b", "select", "start")):
286
+ if k in self.keys: low &= ~(1 << bit)
287
+ return (sel & 0x30) | 0xC0 | low
288
+ if a == 0xFF04: return (self.divt >> 8) & 0xFF
289
+ if a == 0xFFFF: return self.ie
290
+ if a < 0xFF80: return int(self.io[a - 0xFF00])
291
+ return int(self.hram[a - 0xFF80])
292
+
293
+ def write(self, a, v):
294
+ self.tick(1)
295
+ self._write(a, v)
296
+ def _write(self, a, v):
297
+ a &= 0xFFFF; v &= 0xFF
298
+ if a < 0x8000:
299
+ if self.mbc == 5:
300
+ if a < 0x2000: self.ram_on = (v & 0x0F) == 0x0A
301
+ elif a < 0x3000: self.rom_bank = (self.rom_bank & 0x100) | v
302
+ elif a < 0x4000: self.rom_bank = (self.rom_bank & 0xFF) | ((v & 1) << 8)
303
+ elif a < 0x6000: self.ram_bank = v & 0x0F
304
+ elif self.mbc == 1:
305
+ if a < 0x2000: self.ram_on = (v & 0x0F) == 0x0A
306
+ elif a < 0x4000:
307
+ v &= 0x1F; self.rom_bank = (self.rom_bank & 0x60) | (v if v else 1)
308
+ elif a < 0x6000: self.rom_bank = (self.rom_bank & 0x1F) | ((v & 3) << 5)
309
+ return
310
+ if a < 0xA000: self.vram[a - 0x8000] = v; return
311
+ if a < 0xC000:
312
+ if self.ram_on:
313
+ self.cart_ram[(self.ram_bank * 0x2000 + (a - 0xA000)) % 0x8000] = v
314
+ return
315
+ if a < 0xE000: self.wram[a - 0xC000] = v; return
316
+ if a < 0xFE00: self.wram[a - 0xE000] = v; return
317
+ if a < 0xFEA0: self.oam[a - 0xFE00] = v; return
318
+ if a < 0xFF00: return
319
+ if a == 0xFF04: self.divt = 0; return
320
+ if a == 0xFF46: # OAM DMA (instant copy)
321
+ self.io[0x46] = v
322
+ src = v << 8
323
+ for i in range(0xA0):
324
+ self.oam[i] = self._read(src + i)
325
+ return
326
+ if a == 0xFF02 and v & 0x80: # serial transfer start
327
+ self.serial.append(int(self.io[0x01]))
328
+ self.io[0x02] = v & 0x7F
329
+ return
330
+ if a == 0xFF41: # STAT: low 3 bits read-only
331
+ self.io[0x41] = (self.io[0x41] & 0x07) | (v & 0xF8)
332
+ self.ppu.update_stat(); return
333
+ if a == 0xFF44: return # LY read-only
334
+ if a == 0xFFFF: self.ie = v; return
335
+ if a < 0xFF80: self.io[a - 0xFF00] = v; return
336
+ self.hram[a - 0xFF80] = v
337
+
338
+
339
+ class Console:
340
+ def __init__(self, rom, units, render=True):
341
+ self.bus = Bus(rom, units, render)
342
+ self.cpu = SM83(self.bus, units)
343
+ def run_frame(self, max_steps=5_000_000):
344
+ self.bus.ppu.frame_done = False
345
+ for _ in range(max_steps):
346
+ self.cpu.step()
347
+ if self.bus.ppu.frame_done:
348
+ return
349
+ raise RuntimeError("frame did not complete")
350
+ def serial_text(self):
351
+ return bytes(self.bus.serial).decode("ascii", "replace")
352
+
353
+ # ---- state snapshot, for switching units mid-run ----
354
+ def snapshot(self):
355
+ c, b = self.cpu, self.bus
356
+ return dict(
357
+ regs=(c.A, c.B, c.C, c.D, c.E, c.H, c.L, c.fZ, c.fN, c.fH, c.fC,
358
+ c.SP, c.PC, c.IME, c.ei_pending, c.halted),
359
+ bank=(b.rom_bank, b.ram_bank, b.ram_on, b.divt),
360
+ vram=b.vram.copy(), wram=b.wram.copy(), oam=b.oam.copy(),
361
+ hram=b.hram.copy(), io=b.io.copy(), ie=b.ie, cart_ram=b.cart_ram.copy(),
362
+ ppu=(b.ppu.ly, b.ppu.lx, b.ppu.mode, b.ppu.win_line, b.ppu.stat_line,
363
+ b.ppu.frame))
364
+ def restore(self, s):
365
+ c, b = self.cpu, self.bus
366
+ (c.A, c.B, c.C, c.D, c.E, c.H, c.L, c.fZ, c.fN, c.fH, c.fC,
367
+ c.SP, c.PC, c.IME, c.ei_pending, c.halted) = s["regs"]
368
+ b.rom_bank, b.ram_bank, b.ram_on, b.divt = s["bank"]
369
+ b.vram[:] = s["vram"]; b.wram[:] = s["wram"]; b.oam[:] = s["oam"]
370
+ b.hram[:] = s["hram"]; b.io[:] = s["io"]; b.ie = s["ie"]
371
+ b.cart_ram[:] = s["cart_ram"]
372
+ (b.ppu.ly, b.ppu.lx, b.ppu.mode, b.ppu.win_line, b.ppu.stat_line,
373
+ b.ppu.frame) = s["ppu"]
gb_sm83.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ gb_sm83.py -- the COMPLETE SM83 (LR35902) core: all 256 base + 256 CB opcodes,
3
+ stack/CALL/RET/RST, 16-bit arithmetic, EI/DI/RETI/HALT and the full interrupt
4
+ sequence, at M-CYCLE bus granularity.
5
+
6
+ Two properties carry the methodology through:
7
+ 1. Every data transformation goes through the units API (gb_units.GoldenUnits or
8
+ gb_units.NeuralUnits) -- decode, ALU, rotates, BIT/RES/SET, DAA, all of it.
9
+ One step() implementation serves both, so golden-vs-neural comparison tests
10
+ the units, never two different orchestrators.
11
+ 2. 16-bit arithmetic is COMPOSED from the verified 8-bit ADC/SBC by rippling the
12
+ carry lo->hi (INC/DEC rr, ADD HL,rr, ADD SP,e, LD HL,SP+e), exactly like the
13
+ hardware. No unit ever sees a domain it wasn't exhaustively verified on.
14
+
15
+ M-cycle commit: the core never touches memory directly -- every access goes through
16
+ bus.read/bus.write, and the bus ticks the rest of the machine (PPU dots, timer) one
17
+ m-cycle per access plus explicit internal cycles. A write therefore lands on the dot
18
+ stream at its true position INSIDE the instruction, closing the gap the per-dot brick
19
+ documented.
20
+ """
21
+ from gb_units import (NOP, LD_NN_SP, STOP, JR, JRcc, LDrrnn, ADDHLrr, LDmA, LDAm,
22
+ INCrr, DECrr, INCr, DECr, LDrn, ROTA, DAAc, CPLc, SCFc, CCFc,
23
+ LDrr_, HALTc, ALUr, ALUn, RETcc, LDHnA, ADDSPe, LDHAn, LDHLSPe,
24
+ POPrr, RETc, RETIc, JPHL, LDSPHL, JPcc, LDCA, LDnnA, LDAC,
25
+ LDAnn, JPnn, CBPREF, DIc, EIc, CALLcc, PUSHrr, CALLnn, RSTc, ILL)
26
+
27
+ IF_VBLANK, IF_STAT, IF_TIMER, IF_SERIAL, IF_JOYPAD = 1, 2, 4, 8, 16
28
+ ROTNAMES = ["RLC", "RRC", "RL", "RR", "SLA", "SRA", "SWAP", "SRL"]
29
+
30
+
31
+ class SM83:
32
+ def __init__(self, bus, units):
33
+ self.bus = bus; self.u = units
34
+ # post-bootrom DMG state
35
+ self.A, self.B, self.C, self.D, self.E, self.H, self.L = 0x01, 0x00, 0x13, 0x00, 0xD8, 0x01, 0x4D
36
+ self.fZ, self.fN, self.fH, self.fC = 1, 0, 1, 1 # F = 0xB0
37
+ self.SP, self.PC = 0xFFFE, 0x0100
38
+ self.IME = False; self.ei_pending = False; self.halted = False
39
+ self.instr_count = 0
40
+
41
+ # ---- register plumbing (wiring, not logic) ----
42
+ def get_hl(self): return (self.H << 8) | self.L
43
+ def set_hl(self, v): self.H, self.L = (v >> 8) & 0xFF, v & 0xFF
44
+ def F(self): return (self.fZ << 7) | (self.fN << 6) | (self.fH << 5) | (self.fC << 4)
45
+ def setF(self, v):
46
+ self.fZ, self.fN, self.fH, self.fC = (v >> 7) & 1, (v >> 6) & 1, (v >> 5) & 1, (v >> 4) & 1
47
+
48
+ def get_r(self, i):
49
+ if i == 6: return self.bus.read(self.get_hl())
50
+ return (self.B, self.C, self.D, self.E, self.H, self.L, None, self.A)[i]
51
+ def set_r(self, i, v):
52
+ v &= 0xFF
53
+ if i == 0: self.B = v
54
+ elif i == 1: self.C = v
55
+ elif i == 2: self.D = v
56
+ elif i == 3: self.E = v
57
+ elif i == 4: self.H = v
58
+ elif i == 5: self.L = v
59
+ elif i == 6: self.bus.write(self.get_hl(), v)
60
+ else: self.A = v
61
+
62
+ def get_rr(self, p): # BC DE HL SP
63
+ return [(self.B << 8) | self.C, (self.D << 8) | self.E, self.get_hl(), self.SP][p]
64
+ def set_rr(self, p, v):
65
+ v &= 0xFFFF
66
+ if p == 0: self.B, self.C = v >> 8, v & 0xFF
67
+ elif p == 1: self.D, self.E = v >> 8, v & 0xFF
68
+ elif p == 2: self.set_hl(v)
69
+ else: self.SP = v
70
+
71
+ def cond(self, cc):
72
+ return [self.fZ == 0, self.fZ == 1, self.fC == 0, self.fC == 1][cc]
73
+
74
+ # ---- bus helpers ----
75
+ def fetch(self):
76
+ v = self.bus.read(self.PC); self.PC = (self.PC + 1) & 0xFFFF; return v
77
+ def fetch16(self):
78
+ lo = self.fetch(); return lo | (self.fetch() << 8)
79
+ def push16(self, v):
80
+ self.bus.tick(1)
81
+ self.SP = (self.SP - 1) & 0xFFFF; self.bus.write(self.SP, (v >> 8) & 0xFF)
82
+ self.SP = (self.SP - 1) & 0xFFFF; self.bus.write(self.SP, v & 0xFF)
83
+ def pop16(self):
84
+ lo = self.bus.read(self.SP); self.SP = (self.SP + 1) & 0xFFFF
85
+ hi = self.bus.read(self.SP); self.SP = (self.SP + 1) & 0xFFFF
86
+ return lo | (hi << 8)
87
+
88
+ # ---- composed 16-bit arithmetic: verified 8-bit units, rippled carry ----
89
+ def add16_hl(self, rr):
90
+ lo, _, _, c = self.u.adc(self.L, rr & 0xFF, 0)
91
+ hi, _, h, c2 = self.u.adc(self.H, rr >> 8, c)
92
+ self.H, self.L = hi, lo
93
+ self.fN, self.fH, self.fC = 0, h, c2
94
+ self.bus.tick(1)
95
+ def inc16(self, v):
96
+ lo, _, _, c = self.u.adc(v & 0xFF, 1, 0)
97
+ hi, _, _, _ = self.u.adc(v >> 8, 0, c)
98
+ return (hi << 8) | lo
99
+ def dec16(self, v):
100
+ lo, _, _, c = self.u.sbc(v & 0xFF, 1, 0)
101
+ hi, _, _, _ = self.u.sbc(v >> 8, 0, c)
102
+ return (hi << 8) | lo
103
+ def sp_plus_e(self, e):
104
+ """SP + signed e. H/C from the LOW byte add (documented SM83 behavior)."""
105
+ lo, _, h, c = self.u.adc(self.SP & 0xFF, e, 0)
106
+ hi, _, _, _ = self.u.adc(self.SP >> 8, 0xFF if e & 0x80 else 0x00, c)
107
+ self.fZ, self.fN, self.fH, self.fC = 0, 0, h, c
108
+ return (hi << 8) | lo
109
+
110
+ def alu(self, kind, v):
111
+ """8-bit ALU group dispatch through verified units."""
112
+ if kind == 0: self.A, self.fZ, self.fH, self.fC = self.u.adc(self.A, v, 0); self.fN = 0
113
+ elif kind == 1: self.A, self.fZ, self.fH, self.fC = self.u.adc(self.A, v, self.fC); self.fN = 0
114
+ elif kind == 2: self.A, self.fZ, self.fH, self.fC = self.u.sbc(self.A, v, 0); self.fN = 1
115
+ elif kind == 3: self.A, self.fZ, self.fH, self.fC = self.u.sbc(self.A, v, self.fC); self.fN = 1
116
+ elif kind == 4: self.A, self.fZ = self.u.logic("AND", self.A, v); self.fN, self.fH, self.fC = 0, 1, 0
117
+ elif kind == 5: self.A, self.fZ = self.u.logic("XOR", self.A, v); self.fN, self.fH, self.fC = 0, 0, 0
118
+ elif kind == 6: self.A, self.fZ = self.u.logic("OR", self.A, v); self.fN, self.fH, self.fC = 0, 0, 0
119
+ else: _, self.fZ, self.fH, self.fC = self.u.sbc(self.A, v, 0); self.fN = 1 # CP
120
+
121
+ # ---- interrupts ----
122
+ def service_interrupt(self):
123
+ pend = self.bus.io_IE() & self.bus.io_IF() & 0x1F
124
+ if not pend:
125
+ return False
126
+ bit = (pend & -pend).bit_length() - 1
127
+ self.bus.clear_IF(1 << bit)
128
+ self.IME = False
129
+ self.bus.tick(2)
130
+ self.push16(self.PC) # push16 includes 1 internal tick
131
+ self.PC = 0x40 + 8 * bit
132
+ return True
133
+
134
+ # ---- one instruction (or interrupt entry / halt cycle) ----
135
+ def step(self):
136
+ if self.ei_pending:
137
+ self.ei_pending = False; self.IME = True
138
+ if self.halted:
139
+ if self.bus.io_IE() & self.bus.io_IF() & 0x1F:
140
+ self.halted = False
141
+ else:
142
+ self.bus.tick(1); return
143
+ if self.IME and self.service_interrupt():
144
+ return
145
+ op = self.fetch()
146
+ cls, y, z = self.u.decode(op)
147
+ self.instr_count += 1
148
+ u = self.u; p = y >> 1
149
+
150
+ if cls == NOP: pass
151
+ elif cls == LD_NN_SP:
152
+ a = self.fetch16(); self.bus.write(a, self.SP & 0xFF)
153
+ self.bus.write((a + 1) & 0xFFFF, self.SP >> 8)
154
+ elif cls == STOP: self.bus.tick(2)
155
+ elif cls == JR:
156
+ e = self.fetch(); e = e - 256 if e > 127 else e
157
+ self.PC = (self.PC + e) & 0xFFFF; self.bus.tick(1)
158
+ elif cls == JRcc:
159
+ e = self.fetch(); e = e - 256 if e > 127 else e
160
+ if self.cond(y - 4): self.PC = (self.PC + e) & 0xFFFF; self.bus.tick(1)
161
+ elif cls == LDrrnn: self.set_rr(p, self.fetch16())
162
+ elif cls == ADDHLrr: self.add16_hl(self.get_rr(p))
163
+ elif cls == LDmA or cls == LDAm:
164
+ addr = [(self.B << 8) | self.C, (self.D << 8) | self.E,
165
+ self.get_hl(), self.get_hl()][p]
166
+ if cls == LDmA: self.bus.write(addr, self.A)
167
+ else: self.A = self.bus.read(addr)
168
+ if p == 2: self.set_hl(self.inc16(addr))
169
+ elif p == 3: self.set_hl(self.dec16(addr))
170
+ elif cls == INCrr: self.set_rr(p, self.inc16(self.get_rr(p))); self.bus.tick(1)
171
+ elif cls == DECrr: self.set_rr(p, self.dec16(self.get_rr(p))); self.bus.tick(1)
172
+ elif cls == INCr:
173
+ r, self.fZ, self.fH = u.inc(self.get_r(y)); self.set_r(y, r); self.fN = 0
174
+ elif cls == DECr:
175
+ r, self.fZ, self.fH = u.dec(self.get_r(y)); self.set_r(y, r); self.fN = 1
176
+ elif cls == LDrn: self.set_r(y, self.fetch())
177
+ elif cls == ROTA:
178
+ self.A, _, self.fC = u.rot(ROTNAMES[y], self.A, self.fC)
179
+ self.fZ = self.fN = self.fH = 0
180
+ elif cls == DAAc:
181
+ self.A, self.fZ, self.fC = u.daa(self.A, self.fN, self.fH, self.fC); self.fH = 0
182
+ elif cls == CPLc: self.A = u.cpl(self.A); self.fN = self.fH = 1
183
+ elif cls == SCFc: self.fN = self.fH = 0; self.fC = 1
184
+ elif cls == CCFc: self.fN = self.fH = 0; self.fC ^= 1
185
+ elif cls == HALTc: self.halted = True
186
+ elif cls == LDrr_: self.set_r(y, self.get_r(z))
187
+ elif cls == ALUr: self.alu(y, self.get_r(z))
188
+ elif cls == ALUn: self.alu(y, self.fetch())
189
+ elif cls == RETcc:
190
+ self.bus.tick(1)
191
+ if self.cond(y): self.PC = self.pop16(); self.bus.tick(1)
192
+ elif cls == LDHnA: self.bus.write(0xFF00 | self.fetch(), self.A)
193
+ elif cls == LDHAn: self.A = self.bus.read(0xFF00 | self.fetch())
194
+ elif cls == ADDSPe: self.SP = self.sp_plus_e(self.fetch()); self.bus.tick(2)
195
+ elif cls == LDHLSPe: self.set_hl(self.sp_plus_e(self.fetch())); self.bus.tick(1)
196
+ elif cls == POPrr:
197
+ v = self.pop16()
198
+ if p == 3: self.A = v >> 8; self.setF(v & 0xFF) # AF
199
+ else: self.set_rr(p, v)
200
+ elif cls == RETc: self.PC = self.pop16(); self.bus.tick(1)
201
+ elif cls == RETIc: self.PC = self.pop16(); self.bus.tick(1); self.IME = True
202
+ elif cls == JPHL: self.PC = self.get_hl()
203
+ elif cls == LDSPHL: self.SP = self.get_hl(); self.bus.tick(1)
204
+ elif cls == JPcc:
205
+ a = self.fetch16()
206
+ if self.cond(y): self.PC = a; self.bus.tick(1)
207
+ elif cls == LDCA: self.bus.write(0xFF00 | self.C, self.A)
208
+ elif cls == LDAC: self.A = self.bus.read(0xFF00 | self.C)
209
+ elif cls == LDnnA: self.bus.write(self.fetch16(), self.A)
210
+ elif cls == LDAnn: self.A = self.bus.read(self.fetch16())
211
+ elif cls == JPnn: self.PC = self.fetch16(); self.bus.tick(1)
212
+ elif cls == CBPREF: self.step_cb()
213
+ elif cls == DIc: self.IME = False; self.ei_pending = False
214
+ elif cls == EIc: self.ei_pending = True
215
+ elif cls == CALLcc:
216
+ a = self.fetch16()
217
+ if self.cond(y): self.push16(self.PC); self.PC = a
218
+ elif cls == PUSHrr:
219
+ v = ((self.A << 8) | self.F()) if p == 3 else self.get_rr(p)
220
+ self.push16(v)
221
+ elif cls == CALLnn:
222
+ a = self.fetch16(); self.push16(self.PC); self.PC = a
223
+ elif cls == RSTc: self.push16(self.PC); self.PC = y * 8
224
+ else:
225
+ raise NotImplementedError(f"illegal opcode {op:#04x} at {self.PC - 1:#06x}")
226
+
227
+ def step_cb(self):
228
+ kind, sub, tgt = self.u.cbdecode(self.fetch())
229
+ u = self.u
230
+ if kind == 0: # rotates/shifts
231
+ res, z, c = u.rot(ROTNAMES[sub], self.get_r(tgt), self.fC)
232
+ self.set_r(tgt, res)
233
+ self.fZ, self.fN, self.fH, self.fC = z, 0, 0, c
234
+ elif kind == 1: # BIT
235
+ self.fZ = u.bit(self.get_r(tgt), sub); self.fN, self.fH = 0, 1
236
+ else: # RES / SET
237
+ self.set_r(tgt, u.setres(kind == 3, self.get_r(tgt), sub))