2026-02-24 22:12:20 -07:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from typing import Callable
|
|
|
|
|
|
2026-05-02 19:05:43 -07:00
|
|
|
from .debugger import Debugger, BreakContext
|
2026-02-25 17:26:59 -07:00
|
|
|
from .ppu import PPUState, RenderedFrame
|
2026-02-25 23:30:36 -07:00
|
|
|
from .cpu import ProcessorState
|
2026-02-24 22:12:20 -07:00
|
|
|
from .cart import Cartridge
|
|
|
|
|
from . import gbc_py as core
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GameBoyColor:
|
2026-03-11 16:06:04 -07:00
|
|
|
def __init__(self, cartridge: Cartridge = None, dbg_callback: Callable = None):
|
2026-02-24 22:12:20 -07:00
|
|
|
try:
|
|
|
|
|
self._gbc = (
|
2026-05-02 19:05:43 -07:00
|
|
|
core.GameBoyColor(lambda ctx: dbg_callback(self, BreakContext(ctx)))
|
2026-02-25 23:30:36 -07:00
|
|
|
if dbg_callback is not None
|
2026-02-24 22:12:20 -07:00
|
|
|
else core.GameBoyColor()
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
2026-03-11 16:06:04 -07:00
|
|
|
raise RuntimeError("Failed to initialize GameBoyColor core instance") from e
|
2026-02-24 22:12:20 -07:00
|
|
|
|
|
|
|
|
# Entirely optional, only check if provided
|
|
|
|
|
if cartridge is not None:
|
|
|
|
|
self.insert_cartridge(cartridge)
|
|
|
|
|
|
2026-03-11 16:06:04 -07:00
|
|
|
"""
|
2026-02-25 16:39:04 -07:00
|
|
|
|
|
|
|
|
Internal helpers for argument correctness checks
|
|
|
|
|
|
2026-03-11 16:06:04 -07:00
|
|
|
"""
|
2026-02-25 16:39:04 -07:00
|
|
|
|
|
|
|
|
def _check_bitwidth_byte(self, value: int):
|
|
|
|
|
if value > 0xFF or value < 0x00:
|
|
|
|
|
raise ValueError("`value` must be 8-bits")
|
|
|
|
|
|
|
|
|
|
def _check_bitwidth_addr(self, addr: int):
|
|
|
|
|
if addr > 0xFFFF or addr < 0x0000:
|
|
|
|
|
raise ValueError("`addr` must be 16-bits")
|
|
|
|
|
|
2026-03-11 16:06:04 -07:00
|
|
|
"""
|
2026-02-25 16:39:04 -07:00
|
|
|
|
|
|
|
|
Publicly exposed interface implementation
|
|
|
|
|
|
2026-03-11 16:06:04 -07:00
|
|
|
"""
|
2026-02-25 16:39:04 -07:00
|
|
|
|
2026-02-25 18:19:06 -07:00
|
|
|
@property
|
|
|
|
|
def cpu_state(self):
|
|
|
|
|
try:
|
|
|
|
|
cpu = self._gbc.get_cpu()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"Failed to initialize ProcessorState core instance"
|
|
|
|
|
) from e
|
|
|
|
|
return ProcessorState(cpu.get_state())
|
|
|
|
|
|
2026-02-25 17:26:59 -07:00
|
|
|
@property
|
|
|
|
|
def ppu_state(self):
|
|
|
|
|
try:
|
|
|
|
|
ppu = self._gbc.get_ppu()
|
|
|
|
|
except Exception as e:
|
2026-03-11 16:06:04 -07:00
|
|
|
raise RuntimeError("Failed to initialize PPUState core instance") from e
|
2026-02-25 17:26:59 -07:00
|
|
|
return PPUState(ppu.get_state())
|
|
|
|
|
|
2026-02-25 23:30:36 -07:00
|
|
|
@property
|
|
|
|
|
def debugger(self):
|
|
|
|
|
try:
|
|
|
|
|
debugger = self._gbc.get_debugger()
|
|
|
|
|
except Exception as e:
|
2026-03-11 16:06:04 -07:00
|
|
|
raise RuntimeError("Failed to initialize Debugger core instance") from e
|
2026-02-25 23:30:36 -07:00
|
|
|
return Debugger(debugger)
|
|
|
|
|
|
2026-02-25 17:26:59 -07:00
|
|
|
@property
|
|
|
|
|
def frame(self):
|
|
|
|
|
try:
|
|
|
|
|
frame = self._gbc.get_frame()
|
|
|
|
|
except Exception as e:
|
2026-03-11 16:06:04 -07:00
|
|
|
raise RuntimeError("Failed to acquire frame from core") from e
|
2026-02-25 17:26:59 -07:00
|
|
|
return RenderedFrame(frame)
|
|
|
|
|
|
2026-02-24 22:12:20 -07:00
|
|
|
def insert_cartridge(self, cartridge: Cartridge):
|
|
|
|
|
self._gbc.insert_cartridge(cartridge._raw)
|
|
|
|
|
|
2026-04-23 23:54:21 -07:00
|
|
|
def read_byte(self, addr: int, debug=False):
|
2026-02-25 16:39:04 -07:00
|
|
|
self._check_bitwidth_addr(addr)
|
|
|
|
|
addr_bus = self._gbc.get_bus()
|
2026-04-23 23:54:21 -07:00
|
|
|
return addr_bus.read_byte(addr, debug=debug)
|
2026-02-25 16:39:04 -07:00
|
|
|
|
2026-04-23 23:54:21 -07:00
|
|
|
def write_byte(self, addr: int, value: int, debug=False):
|
2026-02-25 16:39:04 -07:00
|
|
|
self._check_bitwidth_addr(addr)
|
|
|
|
|
self._check_bitwidth_byte(value)
|
|
|
|
|
addr_bus = self._gbc.get_bus()
|
|
|
|
|
return addr_bus.write_byte(addr, value)
|
|
|
|
|
|
2026-04-22 17:24:00 -07:00
|
|
|
def step_frame(self, big_step: bool = True):
|
2026-02-25 16:39:04 -07:00
|
|
|
cycles = 70224 # One frame worth of t-cycles
|
2026-04-22 17:24:00 -07:00
|
|
|
if big_step:
|
|
|
|
|
self.big_step(cycles=cycles)
|
|
|
|
|
else:
|
|
|
|
|
self.step(cycles=cycles)
|
2026-02-25 16:39:04 -07:00
|
|
|
|
2026-04-22 17:24:00 -07:00
|
|
|
def step_scanline(self, big_step: bool = True):
|
2026-02-25 16:39:04 -07:00
|
|
|
cycles = 456 # One scanline worth of t-cycles
|
2026-04-22 17:24:00 -07:00
|
|
|
if big_step:
|
|
|
|
|
self.big_step(cycles=cycles)
|
|
|
|
|
else:
|
|
|
|
|
self.step(cycles=cycles)
|
|
|
|
|
|
|
|
|
|
def big_step(self, cycles: int = None) -> int:
|
|
|
|
|
if cycles is not None:
|
|
|
|
|
return self._gbc.big_step_cycles(cycles)
|
|
|
|
|
else:
|
|
|
|
|
return self._gbc.big_step()
|
2026-02-25 16:39:04 -07:00
|
|
|
|
2026-02-24 22:12:20 -07:00
|
|
|
def step(self, cycles: int = None):
|
|
|
|
|
if cycles is not None:
|
|
|
|
|
self._gbc.step_cycles(cycles)
|
|
|
|
|
else: # Latent, not recommended
|
|
|
|
|
self._gbc.step()
|