diff --git a/.github/workflows/pxd.yml b/.github/workflows/pxd.yml deleted file mode 100644 index f627efa5..00000000 --- a/.github/workflows/pxd.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Cython - -on: - push: - branches: - - master - - main - paths: - - 'pxd/**' - - '.github/workflows/pxd.yml' - - '.github/workflows/common.yml' - - 'flake.*' - pull_request: - paths: - - 'pxd/**' - - '.github/workflows/pxd.yml' - - '.github/workflows/common.yml' - - 'flake.*' - workflow_dispatch: - -jobs: - common: - name: Common - uses: ./.github/workflows/common.yml - with: - workdir: pxd - format-pkgs: python3.11-venv black - build-pkgs: python3.11-venv libsdl2-dev - cache-paths: | - pxd/venv - cache-key: venv diff --git a/pxd/.gitignore b/pxd/.gitignore deleted file mode 100644 index 1c27166c..00000000 --- a/pxd/.gitignore +++ /dev/null @@ -1,22 +0,0 @@ -.idea -.venv -__pycache__ -.mypy_cache -build - -Makefile -*.gb -*.asm -*.o -*.so - -*.swp -*.txt -*.sav - -Cython -*.c -*.h -*.html -build_temp -*.egg-info/ diff --git a/pxd/README.md b/pxd/README.md deleted file mode 100644 index aef17b46..00000000 --- a/pxd/README.md +++ /dev/null @@ -1,17 +0,0 @@ -RosettaBoy Cython -================= -Cythonized version of the Python implementation. - -Usage ------ -``` -./build.sh -./rosettaboy-release game.gb -``` - -Requirements ------------- -- Python 3.10 -- Cython 3.0.0a11 -- PySDL2 -- SDL2 diff --git a/pxd/format.sh b/pxd/format.sh deleted file mode 100755 index 77f8fee7..00000000 --- a/pxd/format.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -eu - -cd $(dirname $0) - -VENVDIR=${BUILD_ROOT:-$(realpath $(dirname $0))/build}/$(basename $(pwd))-$(uname)-$(uname -m)-black -if [ ! -d $VENVDIR ]; then - python3 -m venv $VENVDIR - $VENVDIR/bin/pip install pysdl2 pysdl2-dll mypy black Cython==3.0.0a11 -fi - -$VENVDIR/bin/black src/*.py diff --git a/pxd/src/__init__.py b/pxd/src/__init__.py deleted file mode 100644 index e6866a51..00000000 --- a/pxd/src/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -__all__ = [ - "args", - "buttons", - "cart", - "clock", - "consts", - "cpu", - "errors", - "gameboy", - "gpu", - "main", - "ram", -] diff --git a/pxd/src/args.py b/pxd/src/args.py deleted file mode 100644 index db062f5b..00000000 --- a/pxd/src/args.py +++ /dev/null @@ -1,35 +0,0 @@ -import typing as t -import argparse -import sys - - -def parse_args(args: t.List[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser() - parser.add_argument("rom") - parser.add_argument( - "--info", action="store_true", default=False, help="Show ROM metadata" - ) - parser.add_argument("-c", "--debug-cpu", action="store_true", default=False) - parser.add_argument("-g", "--debug-gpu", action="store_true", default=False) - parser.add_argument("-r", "--debug-ram", action="store_true", default=False) - parser.add_argument("-H", "--headless", action="store_true", default=False) - parser.add_argument("-S", "--silent", action="store_true", default=False) - parser.add_argument("-t", "--turbo", action="store_true", default=False) - parser.add_argument("-v", "--version", action="version", version=sys.version) - parser.add_argument( - "-f", - "--frames", - type=int, - help="Exit after N frames", - default=0, - metavar="N", - ) - parser.add_argument( - "-p", - "--profile", - type=int, - help="Exit after N seconds", - default=0, - metavar="N", - ) - return parser.parse_args(args) diff --git a/pxd/src/buttons.py b/pxd/src/buttons.py deleted file mode 100644 index 7a26d959..00000000 --- a/pxd/src/buttons.py +++ /dev/null @@ -1,153 +0,0 @@ -import cython - -if not cython.compiled: - import sdl2 - -import ctypes -import typing as t -from .errors import Quit - -if cython.compiled: - from cython.cimports.cpython.mem import PyMem_Malloc, PyMem_Free -else: - from .cpu import CPU - from .consts import Interrupt, Mem, u8, booli - - -class _Joypad: - def __init__(self): - self.MODE_BUTTONS: cython.int = 1 << 5 - self.MODE_DPAD: cython.int = 1 << 4 - self.DOWN: cython.int = 1 << 3 - self.START: cython.int = 1 << 3 - self.UP: cython.int = 1 << 2 - self.SELECT: cython.int = 1 << 2 - self.LEFT: cython.int = 1 << 1 - self.B: cython.int = 1 << 1 - self.RIGHT: cython.int = 1 << 0 - self.A: cython.int = 1 << 0 - - -Joypad = _Joypad() - - -class Buttons: - def __init__(self, cpu: CPU, headless: bool) -> None: - if not headless: - sdl2.SDL_InitSubSystem(sdl2.SDL_INIT_GAMECONTROLLER) - - self.cpu = cpu - - self.cycle = 0 - self.turbo = False - - self.up = False - self.down = False - self.left = False - self.right = False - self.a = False - self.b = False - self.start = False - self.select = False - - def tick(self) -> None: - self.cycle += 1 - self.update_buttons() - if self.cycle % 17556 == 20: - if self.handle_inputs(): - self.cpu.stop = False - self.cpu.interrupt(Interrupt.JOYPAD) - - def update_buttons(self) -> None: - JOYP: u8 - JOYP = ~self.cpu.ram.get(Mem.JOYP) - JOYP &= 0x30 - if JOYP & Joypad.MODE_DPAD: - if self.up: - JOYP |= Joypad.UP - if self.down: - JOYP |= Joypad.DOWN - if self.left: - JOYP |= Joypad.LEFT - if self.right: - JOYP |= Joypad.RIGHT - if JOYP & Joypad.MODE_BUTTONS: - if self.b: - JOYP |= Joypad.B - if self.a: - JOYP |= Joypad.A - if self.start: - JOYP |= Joypad.START - if self.select: - JOYP |= Joypad.SELECT - - self.cpu.ram.set(Mem.JOYP, ~JOYP & 0x3F) - - def handle_inputs(self) -> bool: - need_interrupt = False - key: cython.int - - if cython.compiled: - event: cython.pointer(sdl2.SDL_Event) - event = cython.cast( - cython.pointer(sdl2.SDL_Event), - PyMem_Malloc(cython.sizeof(sdl2.SDL_Event)), - ) - else: - event = sdl2.SDL_Event() - while ( - sdl2.SDL_PollEvent(event if cython.compiled else ctypes.byref(event)) != 0 - ): - if event.type == sdl2.SDL_QUIT: - raise Quit() - elif event.type == sdl2.SDL_KEYDOWN: - key = event.key.keysym.sym - need_interrupt = True - if key == sdl2.SDLK_ESCAPE: - raise Quit() - elif key == sdl2.SDLK_LSHIFT: - self.turbo = True - need_interrupt = False - elif key == sdl2.SDLK_z: - self.b = True - elif key == sdl2.SDLK_x: - self.a = True - elif key == sdl2.SDLK_RETURN: - self.start = True - elif key == sdl2.SDLK_SPACE: - self.select = True - elif key == sdl2.SDLK_UP: - self.up = True - elif key == sdl2.SDLK_DOWN: - self.down = True - elif key == sdl2.SDLK_LEFT: - self.left = True - elif key == sdl2.SDLK_RIGHT: - self.right = True - else: - need_interrupt = False - elif event.type == sdl2.SDL_KEYUP: - key = event.key.keysym.sym - if key == sdl2.SDLK_LSHIFT: - self.turbo = False - elif key == sdl2.SDLK_z: - self.b = False - elif key == sdl2.SDLK_x: - self.a = False - elif key == sdl2.SDLK_RETURN: - self.start = False - elif key == sdl2.SDLK_SPACE: - self.select = False - elif key == sdl2.SDLK_UP: - self.up = False - elif key == sdl2.SDLK_DOWN: - self.down = False - elif key == sdl2.SDLK_LEFT: - self.left = False - elif key == sdl2.SDLK_RIGHT: - self.right = False - - if cython.compiled: - PyMem_Free(event) - - return need_interrupt diff --git a/pxd/src/cart.py b/pxd/src/cart.py deleted file mode 100644 index a87d040e..00000000 --- a/pxd/src/cart.py +++ /dev/null @@ -1,155 +0,0 @@ -import typing as t -import struct -from enum import Enum - -import cython - -if cython.compiled: - from cython.cimports.cpython.mem import PyMem_Malloc, PyMem_Free -else: - from .consts import u8 -from .errors import LogoChecksumFailed, HeaderChecksumFailed - - -class CartType(Enum): - ROM_ONLY: u8 = 0x00 - ROM_MBC1: u8 = 0x01 - ROM_MBC1_RAM: u8 = 0x02 - ROM_MBC1_RAM_BATT: u8 = 0x03 - ROM_MBC2: u8 = 0x05 - ROM_MBC2_BATT: u8 = 0x06 - ROM_RAM: u8 = 0x08 - ROM_RAM_BATT: u8 = 0x09 - ROM_MMM01: u8 = 0x0B - ROM_MMM01_SRAM: u8 = 0x0C - ROM_MMM01_SRAM_BATT: u8 = 0x0D - ROM_MBC3_TIMER_BATT: u8 = 0x0F - ROM_MBC3_TIMER_RAM_BATT: u8 = 0x10 - ROM_MBC3: u8 = 0x11 - ROM_MBC3_RAM: u8 = 0x12 - ROM_MBC3_RAM_BATT: u8 = 0x13 - ROM_MBC5: u8 = 0x19 - ROM_MBC5_RAM: u8 = 0x1A - ROM_MBC5_RAM_BATT: u8 = 0x1B - ROM_MBC5_RUMBLE: u8 = 0x1C - ROM_MBC5_RUMBLE_RAM: u8 = 0x1D - ROM_MBC5_RUMBLE_RAM_BATT: u8 = 0x1E - POCKET_CAMERA: u8 = 0x1F - BANDAI_TAMA5: u8 = 0xFD - HUDSON_HUC3: u8 = 0xFE - HUDSON_HUC1: u8 = 0xFF - - -KB: int = 1024 - - -def parse_rom_size(val: u8) -> int: - return (32 * KB) << val - - -def parse_ram_size(val: u8) -> int: - return { - 0: 0, - 2: 8 * KB, - 3: 32 * KB, - 4: 128 * KB, - 5: 64 * KB, - }.get(val, 0) - - -class Cart: - def __init__(self, rom: str) -> None: - with open(rom, "rb") as fp: - self.data_bytes = fp.read() - - if cython.compiled: - data_len: cython.int = len(self.data_bytes) - self.data = cython.cast( - cython.pointer(u8), PyMem_Malloc(cython.sizeof(u8) * data_len) - ) - i: cython.int - for i in range(data_len): - self.data[i] = self.data_bytes[i] - else: - self.data = self.data_bytes - - self.rsts: bytes - self.init: t.Tuple[int] - self.logo: t.Tuple[int] - self.name: str - self.is_gbc: bool - self.licensee: u16 - self.is_sgb: bool - self.cart_type: CartType - self.rom_size: cython.int - self.ram_size: cython.int - self.destination: u8 - self.old_licensee: u8 - self.rom_version: u8 - self.complement_check: u8 - self.checksum: u8 - - fmts: t.List[t.Tuple[str, str, t.Optional[t.Callable[[t.Any], t.Any]]]] = [ - ("256s", "rsts", None), - ("4B", "init", None), - ("48B", "logo", None), - ("15s", "name", lambda x: x.strip(b"\x00").decode()), - ("B", "is_gbc", lambda x: x == 0x80), - ("H", "licensee", None), - ("B", "is_sgb", lambda x: x == 0x03), - ("B", "cart_type", lambda x: CartType(x)), - ("B", "rom_size", lambda x: parse_rom_size(x)), - ("B", "ram_size", lambda x: parse_ram_size(x)), - ("B", "destination", None), - ("B", "old_licensee", None), - ("B", "rom_version", None), - ("B", "complement_check", None), - # Checksum (higher byte first) produced by - # adding all bytes of a cartridge except for - # two checksum bytes and taking two lower - # bytes of the result. (GameBoy ignores this - # value.) - (">H", "checksum", None), - ] - offset: int = 0 - for fmt, name, mod in fmts: - val = struct.unpack_from(fmt, self.data_bytes, offset) - offset += struct.calcsize(fmt) - if len(val) == 1: - val = val[0] - if mod: - val = mod(val) - setattr(self, name, val) - - if cython.compiled: - self.ram = cython.cast( - cython.pointer(u8), PyMem_Malloc(cython.sizeof(u8) * self.ram_size) - ) - i: cython.int - for i in range(self.ram_size): - self.ram[i] = 0 - else: - self.ram = [0] * self.ram_size - - logo_checksum = sum(list(self.logo)) - if logo_checksum != 5446: - raise LogoChecksumFailed(logo_checksum) - - header_checksum = ( - sum(struct.unpack("26B", self.data_bytes[0x0134:0x014E])) + 25 - ) & 0xFF - if header_checksum != 0: - raise HeaderChecksumFailed(header_checksum) - - def __dealloc__(self): - PyMem_Free(self.ram) - PyMem_Free(self.data) - - def __str__(self) -> str: - return "\n".join( - [ - f"{k}: {v}" - for k, v in self.__dict__.items() - if k not in {"data", "logo", "init", "rsts"} - ] - ) diff --git a/pxd/src/clock.py b/pxd/src/clock.py deleted file mode 100644 index 2f9a07f5..00000000 --- a/pxd/src/clock.py +++ /dev/null @@ -1,49 +0,0 @@ -import cython - -if not cython.compiled: - import sdl2 - -import time -from .errors import Timeout - -if not cython.compiled: - from .buttons import Buttons - - -class Clock: - def __init__( - self, - buttons: Buttons, - frames: cython.longlong, - profile: cython.longlong, - turbo: bool, - ): - self.buttons = buttons - self.cycle = 0 - self.frame = 0 - self.start = sdl2.SDL_GetTicks() - self.frames = frames - self.profile = profile - self.turbo = turbo - self.last_frame_start = 0 - - def tick(self) -> None: - self.cycle += 1 - - # Do a whole frame's worth of sleeping at the start of each frame - if self.cycle % 17556 == 20: - # Sleep if we have time left over - time_spent: cython.int = sdl2.SDL_GetTicks() - self.last_frame_start - sleep_for = (1000 / 60) - time_spent - if sleep_for > 0 and not self.turbo and not self.buttons.turbo: - sdl2.SDL_Delay(int(sleep_for)) - self.last_frame_start = sdl2.SDL_GetTicks() - - # Exit if we've hit the frame or time limit - duration = (self.last_frame_start - self.start) / 1_000 - if (self.frames != 0 and self.frame >= self.frames) or ( - self.profile != 0 and duration >= self.profile - ): - raise Timeout(self.frame, duration) - - self.frame += 1 diff --git a/pxd/src/consts.py b/pxd/src/consts.py deleted file mode 100644 index 6c3c5954..00000000 --- a/pxd/src/consts.py +++ /dev/null @@ -1,102 +0,0 @@ -import typing as t -import cython - -if not cython.compiled: - u8 = cython.uchar - u16 = cython.ushort - i8 = cython.char - booli = bool - - -def as_u8(val: cython.int) -> u8: - return val - - -def as_bool(val: cython.int) -> cython.bint: - return 1 if val else 0 - - -class _Mem: - def __init__(self): - self.VBLANK_HANDLER: u16 = 0x40 - self.LCD_HANDLER: u16 = 0x48 - self.TIMER_HANDLER: u16 = 0x50 - self.SERIAL_HANDLER: u16 = 0x58 - self.JOYPAD_HANDLER: u16 = 0x60 - - self.TILE_DATA: u16 = 0x8000 - self.MAP_0: u16 = 0x9800 - self.MAP_1: u16 = 0x9C00 - self.OAM_BASE: u16 = 0xFE00 - - self.JOYP: u16 = 0xFF00 - - self.SB: u16 = 0xFF01 # Serial Data - self.SC: u16 = 0xFF02 # Serial Control - - self.DIV: u16 = 0xFF04 - self.TIMA: u16 = 0xFF05 - self.TMA: u16 = 0xFF06 - self.TAC: u16 = 0xFF07 - - self.IF_: u16 = 0xFF0F - - self.NR10: u16 = 0xFF10 - self.NR11: u16 = 0xFF11 - self.NR12: u16 = 0xFF12 - self.NR13: u16 = 0xFF13 - self.NR14: u16 = 0xFF14 - - self.NR20: u16 = 0xFF15 - self.NR21: u16 = 0xFF16 - self.NR22: u16 = 0xFF17 - self.NR23: u16 = 0xFF18 - self.NR24: u16 = 0xFF19 - - self.NR30: u16 = 0xFF1A - self.NR31: u16 = 0xFF1B - self.NR32: u16 = 0xFF1C - self.NR33: u16 = 0xFF1D - self.NR34: u16 = 0xFF1E - - self.NR40: u16 = 0xFF1F - self.NR41: u16 = 0xFF20 - self.NR42: u16 = 0xFF21 - self.NR43: u16 = 0xFF22 - self.NR44: u16 = 0xFF23 - - self.NR50: u16 = 0xFF24 - self.NR51: u16 = 0xFF25 - self.NR52: u16 = 0xFF26 - - self.LCDC: u16 = 0xFF40 - self.STAT: u16 = 0xFF41 - self.SCY: u16 = 0xFF42 # SCROLL_Y - self.SCX: u16 = 0xFF43 # SCROLL_X - self.LY: u16 = 0xFF44 # LY aka currently drawn line 0-153 >144 = vblank - self.LYC: u16 = 0xFF45 - self.DMA: u16 = 0xFF46 - self.BGP: u16 = 0xFF47 - self.OBP0: u16 = 0xFF48 - self.OBP1: u16 = 0xFF49 - self.WY: u16 = 0xFF4A - self.WX: u16 = 0xFF4B - - self.BOOT: u16 = 0xFF50 - - self.IE: u16 = 0xFFFF - - -Mem = _Mem() - - -class _Interrupt: - def __init__(self): - self.VBLANK: u8 = 1 << 0 - self.STAT: u8 = 1 << 1 - self.TIMER: u8 = 1 << 2 - self.SERIAL: u8 = 1 << 3 - self.JOYPAD: u8 = 1 << 4 - - -Interrupt = _Interrupt() diff --git a/pxd/src/errors.py b/pxd/src/errors.py deleted file mode 100644 index d13faa09..00000000 --- a/pxd/src/errors.py +++ /dev/null @@ -1,123 +0,0 @@ -import typing as t - -import cython - -if not cython.compiled: - from .consts import u8 - - -class EmuError(Exception): - exit_code = 1 - - -class UnsupportedCart(EmuError): - def __init__(self, cart_type: t.Any) -> None: - self.cart_type = cart_type - - -# Controlled exit, ie we are deliberately stopping emulation -class ControlledExit(EmuError): - """Inheriting from EmuError""" - - -class Quit(ControlledExit): - exit_code = 0 - - def __str__(self) -> str: - return "User has exited the emulator" - - -class Timeout(ControlledExit): - exit_code = 0 - - def __init__(self, frames: int, duration: float) -> None: - self.frames = frames - self.duration = duration - - def __str__(self) -> str: - return "Emulated %5d frames in %5.2fs (%.0ffps)" % ( - self.frames, - self.duration, - self.frames / self.duration, - ) - - -class UnitTestPassed(ControlledExit): - exit_code = 0 - - def __str__(self) -> str: - return "Unit test passed" - - -class UnitTestFailed(ControlledExit): - exit_code = 2 - - def __str__(self) -> str: - return "Unit test failed" - - -# Game error, ie the game developer has a bug -class GameException(EmuError): - """Inheriting from EmuError""" - - exit_code = 3 - - -class InvalidOpcode(GameException): - def __init__(self, opcode: u8) -> None: - self.opcode = opcode - - def __str__(self) -> str: - return f"Invalid opcode {self.opcode}" - - -class InvalidRamRead(GameException): - def __init__(self, ram_bank: int, offset: int, ram_size: int) -> None: - self.ram_bank = ram_bank - self.offset = offset - self.ram_size = ram_size - - def __str__(self) -> str: - return f"Read from RAM bank {self.ram_bank} offset {self.offset} >= ram size {self.ram_size}" - - -class InvalidRamWrite(GameException): - def __init__(self, ram_bank: int, offset: int, ram_size: int) -> None: - self.ram_bank = ram_bank - self.offset = offset - self.ram_size = ram_size - - def __str__(self) -> str: - return f"Write to RAM bank {self.ram_bank} offset {self.offset} >= ram size {self.ram_size}" - - -# User error, ie the user gave us an ivalid or corrupt input file -class UserException(EmuError): - """Inheriting From EmuError""" - - exit_code = 4 - - -class RomMissing(UserException): - def __init__(self, filename: str, err: Exception) -> None: - self.filename = filename - self.err = err - - def __str__(self) -> str: - return f"Error opening {self.filename}: {self.err}" - - -class LogoChecksumFailed(UserException): - def __init__(self, logo_checksum: int) -> None: - self.logo_checksum = logo_checksum - - def __str__(self) -> str: - return f"Logo checksum failed: {self.logo_checksum} != 5446" - - -class HeaderChecksumFailed(UserException): - def __init__(self, header_checksum: int) -> None: - self.header_checksum = header_checksum - - def __str__(self) -> str: - return f"Header checksum failed: {self.header_checksum} != 0" diff --git a/pxd/src/gameboy.py b/pxd/src/gameboy.py deleted file mode 100644 index 6df83579..00000000 --- a/pxd/src/gameboy.py +++ /dev/null @@ -1,31 +0,0 @@ -import argparse - -import cython - -if not cython.compiled: - from .cart import Cart - from .cpu import CPU - from .gpu import GPU - from .clock import Clock - from .buttons import Buttons - from .ram import RAM - - -class GameBoy: - def __init__(self, args: argparse.Namespace) -> None: - self.cart = Cart(args.rom) - self.ram = RAM(self.cart, debug=args.debug_ram) - self.cpu = CPU(self.ram, debug=args.debug_cpu) - self.gpu = GPU(self.cpu, debug=args.debug_gpu, headless=args.headless) - self.buttons = Buttons(self.cpu, headless=args.headless) - self.clock = Clock(self.buttons, args.frames, args.profile, args.turbo) - - def run(self) -> None: - while True: - self.tick() - - def tick(self) -> None: - self.cpu.tick() - self.gpu.tick() - self.buttons.tick() - self.clock.tick() diff --git a/pxd/src/gpu.py b/pxd/src/gpu.py deleted file mode 100644 index 9da8b63d..00000000 --- a/pxd/src/gpu.py +++ /dev/null @@ -1,531 +0,0 @@ -import cython - -if not cython.compiled: - import sdl2 - -import typing as t - -if cython.compiled: - from cython.cimports.cpython.mem import PyMem_Malloc, PyMem_Free -else: - from .consts import * - from .cpu import CPU - -SCALE = 2 - - -def make_SDL_Rect(x, y, w, h): - if cython.compiled: - q: cython.pointer(sdl2.SDL_Rect) - q = cython.cast( - cython.pointer(sdl2.SDL_Rect), PyMem_Malloc(cython.sizeof(sdl2.SDL_Rect)) - ) - q.x, q.y, q.w, q.h = x, y, w, h - else: - q = sdl2.SDL_Rect(x=x, y=y, w=w, h=h) - return q - - -def make_SDL_Point(x, y): - if cython.compiled: - p: sdl2.SDL_Point - p.x, p.y = x, y - else: - p = sdl2.SDL_Point(x=x, y=y) - return p - - -class _LCDC: - def __init__(self): - self.ENABLED: u8 = 1 << 7 - self.WINDOW_MAP: u8 = 1 << 6 - self.WINDOW_ENABLED: u8 = 1 << 5 - self.DATA_SRC: u8 = 1 << 4 - self.BG_MAP: u8 = 1 << 3 - self.OBJ_SIZE: u8 = 1 << 2 - self.OBJ_ENABLED: u8 = 1 << 1 - self.BG_WIN_ENABLED: u8 = 1 << 0 - - -LCDC = _LCDC() - - -class _Stat: - def __init__(self): - self.LYC_INTERRUPT: u8 = 1 << 6 - self.OAM_INTERRUPT: u8 = 1 << 5 - self.VBLANK_INTERRUPT: u8 = 1 << 4 - self.HBLANK_INTERRUPT: u8 = 1 << 3 - self.LYC_EQUAL: u8 = 1 << 2 - self.MODE_BITS: u8 = 1 << 1 | 1 << 0 - - self.HBLANK: u8 = 0x00 - self.VBLANK: u8 = 0x01 - self.OAM: u8 = 0x02 - self.DRAWING: u8 = 0x03 - - -Stat = _Stat() - - -class Sprite: - y: u8 - x: u8 - tile_id: u8 - flags: u8 - - def __init__(self, *args, **kwargs): - if not cython.compiled: - self.__cinit__(*args, **kwargs) - - def __cinit__(self, x: u8, y: u8, tile_id: u8, flags: u8): - self.x = x - self.y = y - self.tile_id = tile_id - self.flags = flags - - @staticmethod - def create(x: u8, y: u8, tile_id: u8, flags: u8): - if cython.compiled: - return Sprite.__new__(Sprite, x, y, tile_id, flags) - else: - return Sprite(x, y, tile_id, flags) - - def is_live(self) -> bool: - return self.x > 0 and self.x < 168 and self.y > 0 and self.y < 160 - - def palette(self) -> bool: - return self.flags & (1 << 4) != 0 - - def x_flip(self) -> bool: - return self.flags & (1 << 5) != 0 - - def y_flip(self) -> bool: - return self.flags & (1 << 6) != 0 - - def behind(self) -> bool: - return self.flags & (1 << 7) != 0 - - -rmask: t.Final[int] = 0x000000FF -gmask: t.Final[int] = 0x0000FF00 -bmask: t.Final[int] = 0x00FF0000 -amask: t.Final[int] = 0xFF000000 - - -class GPU: - def __init__(self, cpu: CPU, debug: bool = False, headless: bool = False) -> None: - self.cpu = cpu - self.headless = headless - self.debug = debug - self.cycle = 0 - self.title = "RosettaBoy - " + (cpu.ram.cart.name or "") - - # Window - size = (160, 144) - if self.debug: - size = ( - 160 + 256, - 144, - ) - - if not headless: - sdl2.SDL_InitSubSystem(sdl2.SDL_INIT_VIDEO) - self.hw_window = sdl2.SDL_CreateWindow( - self.title.encode("utf8"), # window title - sdl2.SDL_WINDOWPOS_UNDEFINED, # initial x position - sdl2.SDL_WINDOWPOS_UNDEFINED, # initial y position - size[0] * SCALE, # width, in pixels - size[1] * SCALE, # height, in pixels - sdl2.SDL_WINDOW_ALLOW_HIGHDPI - | sdl2.SDL_WINDOW_RESIZABLE, # flags - see below - ) - self.hw_renderer = sdl2.SDL_CreateRenderer(self.hw_window, -1, 0) - sdl2.SDL_SetHint( - sdl2.SDL_HINT_RENDER_SCALE_QUALITY, b"nearest" - ) # vs "linear" - sdl2.SDL_RenderSetLogicalSize(self.hw_renderer, size[0], size[1]) - self.hw_buffer = sdl2.SDL_CreateTexture( - self.hw_renderer, - ( - sdl2.SDL_PixelFormatEnum.SDL_PIXELFORMAT_ABGR8888 - if cython.compiled - else sdl2.SDL_PIXELFORMAT_ABGR8888 - ), - sdl2.SDL_TEXTUREACCESS_STREAMING, - size[0], - size[1], - ) - elif not cython.compiled: - self.hw_window = None - self.hw_renderer = None - self.hw_buffer = None - - self.buffer: sdl2.SDL_Surface = sdl2.SDL_CreateRGBSurface( - 0, size[0], size[1], 32, rmask, gmask, bmask, amask - ) - self.renderer: sdl2.SDL_Renderer = sdl2.SDL_CreateSoftwareRenderer(self.buffer) - - # Colors - self.colors = [ - sdl2.SDL_Color(r=0x9B, g=0xBC, b=0x0F, a=0xFF), - sdl2.SDL_Color(r=0x8B, g=0xAC, b=0x0F, a=0xFF), - sdl2.SDL_Color(r=0x30, g=0x62, b=0x30, a=0xFF), - sdl2.SDL_Color(r=0x0F, g=0x38, b=0x0F, a=0xFF), - ] - # printf("SDL_Init failed: %s\n", sdl2.SDL_GetError()) - - # GPU.~GPU(): - # sdl2.SDL_FreeSurface(self.buffer) - # if(self.hw_window) sdl2.SDL_DestroyWindow(self.hw_window) - # sdl2.SDL_Quit() - - def tick(self) -> None: - self.cycle += 1 - - # CPU STOP stops all LCD activity until a button is pressed - if self.cpu.stop: - return 0 - - # Check if LCD enabled at all - lcdc = self.cpu.ram.get(Mem.LCDC) - if not (lcdc & LCDC.ENABLED): - # When LCD is re-enabled, LY is 0 - # Does it become 0 as soon as disabled?? - self.cpu.ram.set(Mem.LY, 0) - if not self.debug: - return 0 - - lx = self.cycle % 114 - ly = (self.cycle // 114) % 154 - self.cpu.ram.set(Mem.LY, ly) - - stat: u8 = self.cpu.ram.get(Mem.STAT) - stat &= ~Stat.MODE_BITS - stat &= ~Stat.LYC_EQUAL - - # LYC compare & interrupt - if ly == self.cpu.ram.get(Mem.LYC): - stat |= Stat.LYC_EQUAL - if stat & Stat.LYC_INTERRUPT: - self.cpu.interrupt(Interrupt.STAT) - - # Set mode - if lx == 0 and ly < 144: - stat |= Stat.OAM - if stat & Stat.OAM_INTERRUPT: - self.cpu.interrupt(Interrupt.STAT) - - elif lx == 20 and ly < 144: - stat |= Stat.DRAWING - - if ly == 0: - # TODO: how often should we update palettes? - # Should every pixel reference them directly? - self.update_palettes() - c = self.bgp[0] - sdl2.SDL_SetRenderDrawColor(self.renderer, c.r, c.g, c.b, c.a) - sdl2.SDL_RenderClear(self.renderer) - - self.draw_line(ly) - if ly == 143: - if self.debug: - self.draw_debug() - - if self.hw_renderer: - sdl2.SDL_UpdateTexture( - self.hw_buffer, - cython.NULL if cython.compiled else None, - ( - self.buffer.pixels - if cython.compiled - else self.buffer.contents.pixels - ), - ( - self.buffer.pitch - if cython.compiled - else self.buffer.contents.pitch - ), - ) - sdl2.SDL_RenderCopy( - self.hw_renderer, - self.hw_buffer, - cython.NULL if cython.compiled else None, - cython.NULL if cython.compiled else None, - ) - sdl2.SDL_RenderPresent(self.hw_renderer) - - elif lx == 63 and ly < 144: - stat |= Stat.HBLANK - if stat & Stat.HBLANK_INTERRUPT: - self.cpu.interrupt(Interrupt.STAT) - - elif lx == 0 and ly == 144: - stat |= Stat.VBLANK - if stat & Stat.VBLANK_INTERRUPT: - self.cpu.interrupt(Interrupt.STAT) - self.cpu.interrupt(Interrupt.VBLANK) - - self.cpu.ram.set(Mem.STAT, stat) - - def update_palettes(self) -> None: - raw_bgp: u8 = self.cpu.ram.get(Mem.BGP) - self.bgp = [ - self.colors[(raw_bgp >> 0) & 0x3], - self.colors[(raw_bgp >> 2) & 0x3], - self.colors[(raw_bgp >> 4) & 0x3], - self.colors[(raw_bgp >> 6) & 0x3], - ] - - raw_obp0: u8 = self.cpu.ram.get(Mem.OBP0) - self.obp0 = [ - self.colors[(raw_obp0 >> 0) & 0x3], - self.colors[(raw_obp0 >> 2) & 0x3], - self.colors[(raw_obp0 >> 4) & 0x3], - self.colors[(raw_obp0 >> 6) & 0x3], - ] - - raw_obp1: u8 = self.cpu.ram.get(Mem.OBP1) - self.obp1 = [ - self.colors[(raw_obp1 >> 0) & 0x3], - self.colors[(raw_obp1 >> 2) & 0x3], - self.colors[(raw_obp1 >> 4) & 0x3], - self.colors[(raw_obp1 >> 6) & 0x3], - ] - - def draw_debug(self) -> None: - lcdc = self.cpu.ram.get(Mem.LCDC) - - # Tile data - tile_display_width: cython.int = 32 - tile_id: u8 - for tile_id in range(0, 384): - xy = make_SDL_Point( - x=160 + (tile_id % tile_display_width) * 8, - y=(tile_id // tile_display_width) * 8, - ) - - self.paint_tile(tile_id, xy, self.bgp, False, False) - - # Background scroll border - if lcdc & LCDC.BG_WIN_ENABLED: - rect = make_SDL_Rect(x=0, y=0, w=160, h=144) - sdl2.SDL_SetRenderDrawColor(self.renderer, 255, 0, 0, 0xFF) - sdl2.SDL_RenderDrawRect(self.renderer, rect) - if cython.compiled: - PyMem_Free(rect) - - # Window tiles - if lcdc & LCDC.WINDOW_ENABLED: - wnd_y = self.cpu.ram.get(Mem.WY) - wnd_x: cython.int = self.cpu.ram.get(Mem.WX) - rect = make_SDL_Rect(x=wnd_x - 7, y=wnd_y, w=160, h=144) - sdl2.SDL_SetRenderDrawColor(self.renderer, 0, 0, 255, 0xFF) - sdl2.SDL_RenderDrawRect(self.renderer, rect) - if cython.compiled: - PyMem_Free(rect) - - def draw_line(self, ly: cython.int) -> None: - lcdc = self.cpu.ram.get(Mem.LCDC) - - # Background tiles - if lcdc & LCDC.BG_WIN_ENABLED: - scroll_y: u8 = self.cpu.ram.get(Mem.SCY) - scroll_x: u8 = self.cpu.ram.get(Mem.SCX) - tile_offset = not (lcdc & LCDC.DATA_SRC) - tile_map: u16 = Mem.MAP_1 if (lcdc & LCDC.BG_MAP) else Mem.MAP_0 - - if self.debug: - xy = make_SDL_Point(x=256 - scroll_x, y=ly) - sdl2.SDL_SetRenderDrawColor(self.renderer, 255, 0, 0, 0xFF) - sdl2.SDL_RenderDrawPoint(self.renderer, xy.x, xy.y) - - y_in_bgmap: cython.int = ( - ly + scroll_y - ) % 256 # Might be faster to just let it overflow as u8 I guess. - tile_y: cython.int = y_in_bgmap // 8 - tile_sub_y: cython.int = y_in_bgmap % 8 - - lx: cython.int - for lx in range(0, 160 + 1, 8): - x_in_bgmap: cython.int = (lx + scroll_x) % 256 - tile_x: cython.int = x_in_bgmap // 8 - tile_sub_x: cython.int = x_in_bgmap % 8 - - tile_id: u8 = self.cpu.ram.get(tile_map + tile_y * 32 + tile_x) - if tile_offset and tile_id < 0x80: - tile_id += 0x100 - - xy = make_SDL_Point( - x=lx - tile_sub_x, - y=ly - tile_sub_y, - ) - - self.paint_tile_line(tile_id, xy, self.bgp, False, False, tile_sub_y) - - # Window tiles - if lcdc & LCDC.WINDOW_ENABLED: - wnd_y: u8 = self.cpu.ram.get(Mem.WY) - wnd_x: u8 = self.cpu.ram.get(Mem.WX) - tile_offset = not (lcdc & LCDC.DATA_SRC) - tile_map = Mem.MAP_1 if (lcdc & LCDC.WINDOW_MAP) else Mem.MAP_0 - - # blank out the background - rect = make_SDL_Rect( - x=wnd_x - 7, - y=wnd_y, - w=160, - h=144, - ) - - c = self.bgp[0] - sdl2.SDL_SetRenderDrawColor(self.renderer, c.r, c.g, c.b, c.a) - sdl2.SDL_RenderFillRect(self.renderer, rect) - if cython.compiled: - PyMem_Free(rect) - - y_in_bgmap: cython.int = ly - wnd_y - tile_y = y_in_bgmap // 8 - tile_sub_y = y_in_bgmap % 8 - - for tile_x in range(0, 20): - tile_id = self.cpu.ram.get(tile_map + tile_y * 32 + tile_x) - if tile_offset and tile_id < 0x80: - tile_id += 0x100 - - xy = make_SDL_Point( - x=tile_x * 8 + wnd_x - 7, - y=tile_y * 8 + wnd_y, - ) - - self.paint_tile_line(tile_id, xy, self.bgp, False, False, tile_sub_y) - - # Sprites - if lcdc & LCDC.OBJ_ENABLED: - dbl = lcdc & LCDC.OBJ_SIZE - - # TODO: sorted by x - # auto sprites: [Sprite 40] = [] - # memcpy(sprites, &ram.data[OAM_BASE], 40 * sizeof(Sprite)) - # for sprite in sprites.iter(): - n: u16 - for n in range(0, 40): - sprite: Sprite = Sprite.create( - y=self.cpu.ram.get(Mem.OAM_BASE + 4 * n + 0), - x=self.cpu.ram.get(Mem.OAM_BASE + 4 * n + 1), - tile_id=self.cpu.ram.get(Mem.OAM_BASE + 4 * n + 2), - flags=self.cpu.ram.get(Mem.OAM_BASE + 4 * n + 3), - ) - - if sprite.is_live(): - if cython.compiled: - palette = ( - cython.cast( - cython.pointer(cython.pointer(sdl2.SDL_Color)), - self.obp1, - ) - if sprite.palette() - else cython.cast( - cython.pointer(cython.pointer(sdl2.SDL_Color)), - self.obp0, - ) - ) - else: - palette = self.obp1 if sprite.palette else self.obp0 - # printf("Drawing sprite %d (from %04X) at %d,%d\n", tile_id, OAM_BASE + (sprite_id * 4) + 0, x, y) - xy = make_SDL_Point( - x=sprite.x - 8, - y=sprite.y - 16, - ) - - self.paint_tile( - sprite.tile_id, - xy, - cython.cast(cython.pointer(sdl2.SDL_Color), palette), - sprite.x_flip(), - sprite.y_flip(), - ) - - if dbl: - xy.y = sprite.y - 8 - self.paint_tile( - sprite.tile_id + 1, - xy, - cython.cast(cython.pointer(sdl2.SDL_Color), palette), - sprite.x_flip(), - sprite.y_flip(), - ) - - def paint_tile( - self, - tile_id: u8, # Other implementations use i16, but class Sprite uses u8? — Wow, u8 instead of int actually improved performance by like 5% I think. - offset: sdl2.SDL_Point, - palette: cython.array(sdl2.SDL_Color, 4), - flip_x: bool, - flip_y: bool, - ) -> None: - for y in range(0, 8): - self.paint_tile_line(tile_id, offset, palette, flip_x, flip_y, y) - - if self.debug: - rect = make_SDL_Rect( - x=offset.x, - y=offset.y, - w=8, - h=8, - ) - - c = gen_hue(tile_id) - sdl2.SDL_SetRenderDrawColor(self.renderer, c.r, c.g, c.b, c.a) - sdl2.SDL_RenderDrawRect(self.renderer, rect) - - if cython.compiled: - PyMem_Free(rect) - - def paint_tile_line( - self, - tile_id: u8, - offset: sdl2.SDL_Point, - palette: cython.array(sdl2.SDL_Color, 4), - flip_x: bool, - flip_y: bool, - y: cython.int, - ) -> None: - addr: u16 = Mem.TILE_DATA + tile_id * 16 + y * 2 - low_byte: u8 = self.cpu.ram.get(addr) - high_byte: u8 = self.cpu.ram.get(addr + 1) - x: u8 - for x in range(0, 8): - low_bit: u8 = (low_byte >> (7 - x)) & 0x01 - high_bit: u8 = (high_byte >> (7 - x)) & 0x01 - px: u8 = (high_bit << 1) | low_bit - # pallette #0 = transparent, so don't draw anything - if px > 0: - xy = make_SDL_Point( - x=offset.x + (7 - x if flip_x else x), - y=offset.y + (7 - y if flip_y else y), - ) - - c = palette[px] - sdl2.SDL_SetRenderDrawColor(self.renderer, c.r, c.g, c.b, c.a) - sdl2.SDL_RenderDrawPoint(self.renderer, xy.x, xy.y) - - -def gen_hue(n: u8) -> sdl2.SDL_Color: - region: u8 = n // 43 - remainder: u8 = (n - (region * 43)) * 6 - - q: u8 = 255 - remainder - t: u8 = remainder - - if region == 0: - return sdl2.SDL_Color(r=255, g=t, b=0, a=0xFF) - if region == 1: - return sdl2.SDL_Color(r=q, g=255, b=0, a=0xFF) - if region == 2: - return sdl2.SDL_Color(r=0, g=255, b=t, a=0xFF) - if region == 3: - return sdl2.SDL_Color(r=0, g=q, b=255, a=0xFF) - if region == 4: - return sdl2.SDL_Color(r=t, g=0, b=255, a=0xFF) - return sdl2.SDL_Color(r=255, g=0, b=q, a=0xFF) diff --git a/pxd/src/main.py b/pxd/src/main.py deleted file mode 100755 index d7fa4bda..00000000 --- a/pxd/src/main.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 - -import cython - -if not cython.compiled: - import sdl2 - -import sys -from typing import List -from traceback import print_exception - -from .args import parse_args -from .gameboy import GameBoy -from .errors import GameException, UserException, ControlledExit, EmuError - - -def main(argv: List[str]) -> int: - args = parse_args(argv[1:]) - - try: - gameboy = GameBoy(args) - gameboy.run() - except EmuError as e: - if isinstance(e, ControlledExit): - print(e) - else: - print_exception(e) - return e.exit_code - except (KeyboardInterrupt, BrokenPipeError): - pass - finally: - sdl2.SDL_Quit() - - return 0 - - -def cli_main(): - sys.exit(main(sys.argv)) - - -if __name__ == "__main__": - cli_main() diff --git a/pxd/src/ram.py b/pxd/src/ram.py deleted file mode 100644 index 503681a3..00000000 --- a/pxd/src/ram.py +++ /dev/null @@ -1,247 +0,0 @@ -import typing as t - -import cython - -if not cython.compiled: - from .cart import Cart - from .consts import Mem, u16, u8 - -ROM_BANK_SIZE: u16 = 0x4000 -RAM_BANK_SIZE: u16 = 0x2000 - - -@cython.cclass -class RAM: - def __init__(self, cart: Cart, debug: bool = False) -> None: - self.cart = cart - self.boot = self.get_boot() - if cython.compiled: - for i in range(65536): - self.data[i] = 0 - else: - self.data = [0] * (0xFFFF + 1) - self.debug = debug - - self.ram_enable = True - self.ram_bank_mode = False - self.rom_bank_low = 1 - self.rom_bank_high = 0 - self.rom_bank = 1 - self.ram_bank = 0 - - def get_boot(self) -> t.List[int]: - try: - # boot with the logo scroll if we have a boot rom - with open("boot.gb", "rb") as fp: - BOOT = list(fp.read(0x100)) - # NOP the DRM - BOOT[0xE9] = 0x00 - BOOT[0xEA] = 0x00 - BOOT[0xFA] = 0x00 - BOOT[0xFB] = 0x00 - except IOError: - # fmt: off - # Directly set CPU registers as - # if the logo had been scrolled - BOOT = [ - # prod memory - 0x31, 0xFE, 0xFF, # LD SP,$FFFE - - # enable LCD - 0x3E, 0x91, # LD A,$91 - 0xE0, 0x40, # LDH [Mem.:LCDC], A - - # set flags - 0x3E, 0x01, # LD A,$00 - 0xCB, 0x7F, # BIT 7,A (sets Z,n,H) - 0x37, # SCF (sets C) - - # set registers - 0x3E, 0x01, # LD A,$01 - 0x06, 0x00, # LD B,$00 - 0x0E, 0x13, # LD C,$13 - 0x16, 0x00, # LD D,$00 - 0x1E, 0xD8, # LD E,$D8 - 0x26, 0x01, # LD H,$01 - 0x2E, 0x4D, # LD L,$4D - - # skip to the end of the bootloader - 0xC3, 0xFD, 0x00, # JP 0x00FD - ] - # fmt: on - - # these 5 instructions must be the final 2 -- - # after these finish executing, PC needs to be 0x100 - BOOT += [0x00] * (0xFE - len(BOOT)) - BOOT += [0xE0, 0x50] # LDH 50,A (disable boot rom) - - assert len(BOOT) == 0x100, f"Bootloader must be 256 bytes ({len(BOOT)})" - return BOOT - - def get(self, addr: u16) -> u8: - val: u8 - bank: u16 - offset: u16 - val = self.data[addr] - if addr < 0x4000: - # ROM bank 0 - if self.data[Mem.BOOT] == 0 and addr < 0x100: - val = self.boot[addr] - else: - val = self.cart.data[addr] - elif addr < 0x8000: - # Switchable ROM bank - # TODO: array bounds check - offset = addr - 0x4000 - bank = self.rom_bank * ROM_BANK_SIZE - val = self.cart.data[bank + offset] - elif addr < 0xA000: - # VRAM - pass - elif addr < 0xC000: - # 8KB Switchable RAM bank - if not self.ram_enable: - raise Exception( - "Reading from external ram while disabled: {:04X}", addr - ) - bank: u16 = self.ram_bank * RAM_BANK_SIZE - offset: u16 = addr - 0xA000 - if bank + offset >= self.cart.ram_size: - # this should never happen because we die on ram_bank being - # set to a too-large value - raise Exception( - "Reading from external ram beyond limit: {:04x} ({:02x}:{:04x})", - bank + offset, - self.ram_bank, - offset, - ) - val = self.cart.ram[bank + offset] - elif addr < 0xD000: - # work RAM, bank 0 - pass - elif addr < 0xE000: - # work RAM, bankable in CGB - pass - elif addr < 0xFE00: - # ram[E000-FE00] mirrors ram[C000-DE00] - val = self.data[addr - 0x2000] - elif addr < 0xFEA0: - # Sprite attribute table - pass - elif addr < 0xFF00: - # Unusable - val = 0xFF - elif addr < 0xFF80: - # IO Registers - pass - elif addr < 0xFFFF: - # High RAM - pass - else: - # IE Register - pass - - if self.debug: - print(f"ram.get({addr:04X}) -> {val:02X}") - return val - - def set(self, addr: u16, val: u8) -> None: - if self.debug: - print(f"ram.get({addr:04X}) <- {val:02X}") - if addr < 0x2000: - self.ram_enable = val != 0 - elif addr < 0x4000: - self.rom_bank_low = val - self.rom_bank = (self.rom_bank_high << 5) | self.rom_bank_low - if self.debug: - print( - "rom_bank set to {}/{}", - self.rom_bank, - self.cart.rom_size / ROM_BANK_SIZE, - ) - if self.rom_bank * ROM_BANK_SIZE > self.cart.rom_size: - raise Exception("Set rom_bank beyond the size of ROM") - elif addr < 0x6000: - if self.ram_bank_mode: - self.ram_bank = val - if self.debug: - print( - "ram_bank set to {}/{}", - self.ram_bank, - self.cart.ram_size / RAM_BANK_SIZE, - ) - if self.ram_bank * RAM_BANK_SIZE > self.cart.ram_size: - raise Exception("Set ram_bank beyond the size of RAM") - else: - self.rom_bank_high = val - self.rom_bank = (self.rom_bank_high << 5) | self.rom_bank_low - if self.debug: - print( - "rom_bank set to {}/{}", - self.rom_bank, - self.cart.rom_size / ROM_BANK_SIZE, - ) - if self.rom_bank * ROM_BANK_SIZE > self.cart.rom_size: - raise Exception("Set rom_bank beyond the size of ROM") - elif addr < 0x8000: - self.ram_bank_mode = val != 0 - if self.debug: - print("ram_bank_mode set to {}", self.ram_bank_mode) - elif addr < 0xA000: - # VRAM - # TODO: if writing to tile RAM, update tiles in Mem.class? - pass - elif addr < 0xC000: - # external RAM, bankable - if not self.ram_enable: - raise Exception( - "Writing to external ram while disabled: {:04x}={:02x}", addr, val - ) - bank: cython.int = self.ram_bank * RAM_BANK_SIZE - offset: u16 = addr - 0xA000 - if self.debug: - print( - "Writing external RAM: {:04x}={:02x} ({:02x}:{:04x})", - bank + offset, - val, - self.ram_bank, - offset, - ) - if bank + offset >= self.cart.ram_size: - raise Exception( - "Writing to external ram beyond limit: {:04x} ({:02x}:{:04x})", - bank + offset, - self.ram_bank, - offset, - ) - self.cart.ram[bank + offset] = val - elif addr < 0xD000: - # work RAM, bank 0 - pass - elif addr < 0xE000: - # work RAM, bankable in CGB - pass - elif addr < 0xFE00: - # ram[E000-FE00] mirrors ram[C000-DE00] - self.data[addr - 0x2000] = val - elif addr < 0xFEA0: - # Sprite attribute table - pass - elif addr < 0xFF00: - # Unusable - if self.debug: - print("Writing to invalid ram: {:04x} = {:02x}", addr, val) - elif addr < 0xFF80: - # IO Registers - # if addr == Mem.:SCX as u16 { - # println!("LY = {}, SCX = {}", self.get(Mem.:LY), val); - # } - pass - elif addr < 0xFFFF: - # High RAM - pass - else: - # IE Register - pass - - self.data[addr] = val diff --git a/py/.gitignore b/py/.gitignore index e6babee3..b70dfb5c 100644 --- a/py/.gitignore +++ b/py/.gitignore @@ -5,6 +5,7 @@ __pycache__ .pyre build rbmp +rbcy Makefile *.gb diff --git a/pxd/build.sh b/py/build_cython.sh similarity index 77% rename from pxd/build.sh rename to py/build_cython.sh index 7172b163..570fc8d4 100755 --- a/pxd/build.sh +++ b/py/build_cython.sh @@ -8,10 +8,17 @@ VENVDIR=${BUILD_ROOT:-$(realpath $(dirname $0))/build}/$(basename $(pwd))-$(echo if [ ! -d $VENVDIR ]; then python3 -m venv $VENVDIR - $VENVDIR/bin/pip install pysdl2 pysdl2-dll Cython setuptools + $VENVDIR/bin/pip install pysdl2 pysdl2-dll mypy Cython setuptools fi source $VENVDIR/bin/activate +rm -rf rbcy +cp -r src rbcy +cp pxd/*.pxd rbcy +mv rbcy/cpu-cython.py rbcy/cpu.py +sed -i.bak 's/from src./from rbcy./' rbcy/*.py +rm -f rbcy/*.bak + python3 setup.py build "$@" \ --build-base "$BUILDDIR/base" \ --build-purelib "$BUILDDIR/purelib" \ @@ -26,4 +33,4 @@ set -eu source $VENVDIR/bin/activate PYTHONPATH="$BUILDDIR/lib/" exec python3 "$BUILDDIR/scripts/main.py" \$* EOD -chmod +x rosettaboy-release +chmod +x rosettaboy-cython diff --git a/pxd/include/CySDL2/SDL2.pxd b/py/include/CySDL2/SDL2.pxd similarity index 100% rename from pxd/include/CySDL2/SDL2.pxd rename to py/include/CySDL2/SDL2.pxd diff --git a/pxd/include/CySDL2/__init__.pxd b/py/include/CySDL2/__init__.pxd similarity index 100% rename from pxd/include/CySDL2/__init__.pxd rename to py/include/CySDL2/__init__.pxd diff --git a/pxd/include/CySDL2/pixelformats.pxd b/py/include/CySDL2/pixelformats.pxd similarity index 100% rename from pxd/include/CySDL2/pixelformats.pxd rename to py/include/CySDL2/pixelformats.pxd diff --git a/pxd/main.py b/py/main.py similarity index 100% rename from pxd/main.py rename to py/main.py diff --git a/pxd/src/buttons.pxd b/py/pxd/buttons.pxd similarity index 100% rename from pxd/src/buttons.pxd rename to py/pxd/buttons.pxd diff --git a/pxd/src/cart.pxd b/py/pxd/cart.pxd similarity index 100% rename from pxd/src/cart.pxd rename to py/pxd/cart.pxd diff --git a/pxd/src/clock.pxd b/py/pxd/clock.pxd similarity index 100% rename from pxd/src/clock.pxd rename to py/pxd/clock.pxd diff --git a/pxd/src/consts.pxd b/py/pxd/consts.pxd similarity index 100% rename from pxd/src/consts.pxd rename to py/pxd/consts.pxd diff --git a/pxd/src/cpu.pxd b/py/pxd/cpu.pxd similarity index 100% rename from pxd/src/cpu.pxd rename to py/pxd/cpu.pxd diff --git a/pxd/src/gameboy.pxd b/py/pxd/gameboy.pxd similarity index 100% rename from pxd/src/gameboy.pxd rename to py/pxd/gameboy.pxd diff --git a/pxd/src/gpu.pxd b/py/pxd/gpu.pxd similarity index 100% rename from pxd/src/gpu.pxd rename to py/pxd/gpu.pxd diff --git a/pxd/src/main.pxd b/py/pxd/main.pxd similarity index 100% rename from pxd/src/main.pxd rename to py/pxd/main.pxd diff --git a/pxd/src/ram.pxd b/py/pxd/ram.pxd similarity index 100% rename from pxd/src/ram.pxd rename to py/pxd/ram.pxd diff --git a/pxd/setup.py b/py/setup.py similarity index 70% rename from pxd/setup.py rename to py/setup.py index 801bc422..623e967d 100644 --- a/pxd/setup.py +++ b/py/setup.py @@ -8,17 +8,17 @@ scripts=["main.py"], entry_points={ 'console_scripts': [ - 'rosettaboy-pxd=src.main:cli_main', + 'rosettaboy-pxd=rbcy.main:cli_main', ], }, ext_modules=cythonize( - module_list=[Extension(name="*", sources=["src/*.py"], libraries=["SDL2"])], + module_list=[Extension(name="*", sources=["rbcy/*.py"], libraries=["SDL2"])], annotate=True, compiler_directives={ "language_level": 3, "profile": True, "annotation_typing": True, }, - include_path=["src", "include"], + include_path=["rbcy", "include"], ) ) diff --git a/pxd/src/cpu.py b/py/src/cpu-cython.py similarity index 100% rename from pxd/src/cpu.py rename to py/src/cpu-cython.py diff --git a/py/src/cpu.py b/py/src/cpu.py index b6a2d286..510750df 100644 --- a/py/src/cpu.py +++ b/py/src/cpu.py @@ -315,17 +315,12 @@ def flag(i: int, c: str) -> str: else: base = OP_NAMES[op] arg = OpArg(self.ram, self.PC + 1, OP_TYPES[op]) - match OP_TYPES[op]: - case 0: - op_str = base - case 1: - op_str = base.replace("u8", f"{arg.u8:02X}") - case 2: - op_str = base.replace("u16", f"{arg.u16:04X}") - case 3: - op_str = base.replace("i8", f"{arg.i8:+d}") - case _: - raise Exception("Unreachable") + op_str = [ + base, + base.replace("u8", f"{arg.u8:02X}"), + base.replace("u16", f"{arg.u16:04X}"), + base.replace("i8", f"{arg.i8:+d}"), + ][OP_TYPES[op]] # print print( @@ -656,17 +651,12 @@ def tick_main(self, op: int) -> None: # ADD HL,rr case 0x09 | 0x19 | 0x29 | 0x39: - match op: - case 0x09: - val16 = self.BC - case 0x19: - val16 = self.DE - case 0x29: - val16 = self.HL - case 0x39: - val16 = self.SP - case _: - raise Exception("Unreachable") + val16 = { + 0x09: self.BC, + 0x19: self.DE, + 0x29: self.HL, + 0x39: self.SP, + }[op] self.FLAG_H = (self.HL & 0x0FFF) + (val16 & 0x0FFF) > 0x0FFF self.FLAG_C = (self.HL + val16) > 0xFFFF @@ -1085,40 +1075,32 @@ def pop(self) -> u16: return val def get_reg(self, n: u8) -> u8: - match n & 0x07: - case 0: - return self.B - case 1: - return self.C - case 2: - return self.D - case 3: - return self.E - case 4: - return self.H - case 5: - return self.L - case 6: - return self.ram[self.HL] - case 7: - return self.A - raise Exception("This should never happen") + return [ + self.B, + self.C, + self.D, + self.E, + self.H, + self.L, + self.ram[self.HL], + self.A, + ][n & 0x07] def set_reg(self, n: u8, val: u8) -> None: - match n & 0x07: - case 0: - self.B = val - case 1: - self.C = val - case 2: - self.D = val - case 3: - self.E = val - case 4: - self.H = val - case 5: - self.L = val - case 6: - self.ram[self.HL] = val - case 7: - self.A = val + r = n & 0x07 + if r == 0: + self.B = val + if r == 1: + self.C = val + if r == 2: + self.D = val + if r == 3: + self.E = val + if r == 4: + self.H = val + if r == 5: + self.L = val + if r == 6: + self.ram[self.HL] = val + if r == 7: + self.A = val