Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3bde5bc
Update issue template
Baekalfen Aug 26, 2025
0c01f45
bootrom .sym changed ordering
Baekalfen Oct 25, 2025
fa8f3fa
Refactor tile and sprite caches
Baekalfen Aug 26, 2025
76dbd90
Move getitem/setitem for LDH/0xFF00 to separate functions
Baekalfen Oct 6, 2024
46cd4b6
Fix unnecessary "+=" to "|=" in opcodes
Baekalfen May 29, 2025
16fc177
Change flag opcode operations to "|=" instead of "+="
Baekalfen Sep 5, 2025
3ad692a
ALU opcodes reuse operands to avoid repeated lookups
Baekalfen Oct 23, 2025
f942bb7
Improve carry flag in shifts and rotate
Baekalfen Oct 24, 2025
13c5c58
Avoid repeat .getitem for RRC_10E, RLC_106, RR_11E, SRA_12E, SWAP_136…
Baekalfen Oct 24, 2025
72f2492
Fix 0x to 0b for flag reset in E8 F8
Baekalfen Oct 25, 2025
caf5ba2
Shortcut getitem to bootrom
Baekalfen Oct 27, 2025
66e8b1d
Add correct CPU cycles to interrupt handler. Fix several mooneye tests.
Baekalfen Oct 28, 2025
01be899
Shortcut memory access in fetch and execute
Baekalfen Oct 28, 2025
ce893c3
Raise exception when using PyBoyMemoryView as iterator
Baekalfen Oct 28, 2025
1fc11b1
Remove redundant sound.stop()
Baekalfen Nov 14, 2025
5aca66d
Save/load from buffer
Baekalfen Nov 10, 2025
f6424f5
Improve error handling in OpenGL window
Baekalfen Nov 13, 2025
84a05ca
Fix deprecation warning in Image.fromarray(..., mode=...)
Baekalfen Nov 14, 2025
19c3628
Python 3.9 EOL
Baekalfen Nov 14, 2025
5e6fe69
Python 3.14 added to GH
Baekalfen Nov 14, 2025
af2d961
Fix Python 3.14 on Windows changing zlib
Baekalfen Nov 16, 2025
b179f9a
Fix setup.py multithreading
Baekalfen Nov 16, 2025
9a3fc0a
Fix sprite_cache_no to int
Baekalfen Nov 16, 2025
71894c7
Bump version to v2.6.1
Baekalfen Nov 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/ISSUE_TEMPLATE/issue-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ assignees: ''

---

Describe in detail how to reproduce the bug you're reporting. Preferably with a short code example to reproduce the bug.
Describe in detail how to reproduce the bug you're reporting. Preferably with a short code example to reproduce the bug like this:
```python
>>> from pyboy import PyBoy
>>> p = PyBoy(..., window="SDL2")
>>> p.memory[0,0x1234] # Example: I expected an int, but it throws exception
```

Please make sure you've already checked the documentation: https://docs.pyboy.dk

If you're seeking help for a project, then Discord is a better place to go https://discord.gg/wUbag3KNqQ
If this is not a bug report, and you're seeking help for a project, then Discord is a better place to go https://discord.gg/wUbag3KNqQ

If you used ChatGPT to produce 6 pages of word soup, your issue will be closed without an answer.
4 changes: 2 additions & 2 deletions .github/workflows/pr-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest, ubuntu-24.04-arm]
python-version: [3.9, "3.10", 3.11, 3.12, 3.13]
python-version: ["3.10", 3.11, 3.12, 3.13, 3.14]

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -151,7 +151,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm]
python-version: ['cp39-cp39', 'cp310-cp310', 'cp311-cp311', 'cp312-cp312', 'cp313-cp313']
python-version: ['cp310-cp310', 'cp311-cp311', 'cp312-cp312', 'cp313-cp313', 'cp314-cp314']
manylinux-version: ['manylinux_2_28_x86_64', 'musllinux_1_2_x86_64', 'manylinux_2_28_aarch64', 'musllinux_1_2_aarch64']
exclude:
- os: ubuntu-24.04-arm
Expand Down
2 changes: 1 addition & 1 deletion extras/bootrom/bootrom_cgb.sym
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
00:0087 main.wave_table
00:0097 main.effect
00:00a5 main.exit_vblank
00:00bb main.logo
00:00bb main.P1
00:00bb main.logo
00:00c3 main.P2
00:00cb main.Y1
00:00d3 main.B2
Expand Down
2 changes: 1 addition & 1 deletion extras/bootrom/bootrom_dmg.sym
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
00:0087 main.wave_table
00:0097 main.effect
00:00a5 main.exit_vblank
00:00bb main.logo
00:00bb main.P1
00:00bb main.logo
00:00c3 main.P2
00:00cb main.Y1
00:00d3 main.B2
Expand Down
3 changes: 2 additions & 1 deletion pyboy/api/tile.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ def image(self):
utils.PillowImportError()._raise_import_error()

if utils.cython_compiled:
return Image.fromarray(self._image_data().base, mode=self.raw_buffer_format)
# Image.fromarray(..., mode=...) is now deprecated. No good alternative.
return Image.fromarray(self._image_data().base).convert(self.raw_buffer_format)
else:
return Image.frombytes(self.raw_buffer_format, (8, 8), self._image_data())

Expand Down
2 changes: 1 addition & 1 deletion pyboy/core/bootrom.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ cdef Logger logger
cdef class BootROM:
cdef uint8_t[:] bootrom
cdef bint cgb
cdef uint8_t getitem(self, uint16_t) noexcept nogil
# cdef uint8_t getitem(self, uint16_t) noexcept nogil
4 changes: 2 additions & 2 deletions pyboy/core/bootrom.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ def __init__(self, bootrom_file, cartridge_cgb):
self.bootrom = array.array("B", struct.unpack("%iB" % len(rom), rom))
self.cgb = len(rom) > 0x100

def getitem(self, addr):
return self.bootrom[addr]
# def getitem(self, addr):
# return
2 changes: 1 addition & 1 deletion pyboy/core/cartridge/base_mbc.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ from pyboy.utils cimport IntIOInterface
cdef Logger logger

cdef class BaseMBC:
cdef str filename
cdef str gamename
cdef uint8_t[:, :] rombanks
cdef uint8_t[:,:] rambanks
Expand All @@ -31,6 +30,7 @@ cdef class BaseMBC:
cdef uint16_t rombank_selected_low
cdef bint cgb

cdef void stop(self, object, object) noexcept
cdef int save_state(self, IntIOInterface) except -1
cdef int load_state(self, IntIOInterface, int) except -1
cdef int save_ram(self, IntIOInterface) except -1
Expand Down
22 changes: 9 additions & 13 deletions pyboy/core/cartridge/base_mbc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#

import array
import os

import pyboy
from pyboy.utils import IntIOWrapper, PyBoyException, PyBoyInvalidInputException
Expand All @@ -15,16 +14,15 @@


class BaseMBC:
def __init__(self, filename, rombanks, external_ram_count, carttype, sram, battery, rtc_enabled):
self.filename = filename + ".ram"
def __init__(self, rombanks, ram_file, rtc_file, external_ram_count, carttype, sram, battery, rtc_enabled):
self.rombanks = rombanks
self.carttype = carttype

self.battery = battery
self.rtc_enabled = rtc_enabled

if self.rtc_enabled:
self.rtc = RTC(filename)
self.rtc = RTC(rtc_file)
else:
self.rtc = None

Expand All @@ -43,18 +41,17 @@ def __init__(self, filename, rombanks, external_ram_count, carttype, sram, batte
self.rombank_selected = 1
self.rombank_selected_low = 0

if not os.path.exists(self.filename):
logger.debug("No RAM file found. Skipping.")
if ram_file is not None and self.battery:
self.load_ram(IntIOWrapper(ram_file))
else:
with open(self.filename, "rb") as f:
self.load_ram(IntIOWrapper(f))
logger.debug("No RAM file found. Skipping.")

def stop(self):
with open(self.filename, "wb") as f:
self.save_ram(IntIOWrapper(f))
def stop(self, ram_file, rtc_file):
if ram_file is not None and self.battery:
self.save_ram(IntIOWrapper(ram_file))

if self.rtc_enabled:
self.rtc.stop()
self.rtc.stop(rtc_file)

def save_state(self, f):
f.write(self.rombank_selected)
Expand Down Expand Up @@ -147,7 +144,6 @@ def __repr__(self):
return "\n".join(
[
"MBC class: %s" % self.__class__.__name__,
"Filename: %s" % self.filename,
"Game name: %s" % self.gamename,
"GB Color: %s" % str(self.rombanks[0, 0x143] == 0x80),
"Cartridge type: %s" % hex(self.carttype),
Expand Down
4 changes: 2 additions & 2 deletions pyboy/core/cartridge/cartridge.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ from .base_mbc cimport BaseMBC
cdef Logger logger

@cython.locals(carttype=uint8_t, cart_name=basestring, cart_line=basestring)
cpdef BaseMBC load_cartridge(str)
cpdef BaseMBC load_cartridge(object, object, object)
cdef bint validate_checksum(uint8_t[:,:]) noexcept

@cython.locals(romdata=array, banksize=int)
cdef uint8_t[:, :] load_romfile(str) noexcept
cdef uint8_t[:, :] load_romfile(object) noexcept

cdef dict CARTRIDGE_TABLE
cdef dict EXTERNAL_RAM_TABLE
11 changes: 5 additions & 6 deletions pyboy/core/cartridge/cartridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
logger = pyboy.logging.get_logger(__name__)


def load_cartridge(filename):
rombanks = load_romfile(filename)
def load_cartridge(gamerom_file, ram_file, rtc_file):
rombanks = load_romfile(gamerom_file)
if not validate_checksum(rombanks):
raise PyBoyException("Cartridge header checksum mismatch!")

Expand All @@ -36,7 +36,7 @@ def load_cartridge(filename):
logger.debug("Cartridge size: %d ROM banks of 16KB, %s RAM banks of 8KB", len(rombanks), external_ram_count)
cartmeta = CARTRIDGE_TABLE[carttype]

return cartmeta[0](filename, rombanks, external_ram_count, carttype, *cartmeta[1:])
return cartmeta[0](rombanks, ram_file, rtc_file, external_ram_count, carttype, *cartmeta[1:])


def validate_checksum(rombanks):
Expand All @@ -47,9 +47,8 @@ def validate_checksum(rombanks):
return rombanks[0, 0x14D] == x


def load_romfile(filename):
with open(filename, "rb") as romfile:
romdata = array("B", romfile.read())
def load_romfile(gamerom_file):
romdata = array("B", gamerom_file.read())

logger.debug("Loading ROM file: %d bytes", len(romdata))
if len(romdata) == 0:
Expand Down
3 changes: 1 addition & 2 deletions pyboy/core/cartridge/rtc.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ from pyboy.utils cimport IntIOInterface
cdef Logger logger

cdef class RTC:
cdef str filename
cdef bint latch_enabled
cdef cython.double timezero
cdef bint timelock
Expand All @@ -28,7 +27,7 @@ cdef class RTC:
cdef uint64_t day_carry
cdef uint64_t halt

cdef void stop(self) noexcept
cdef void stop(self, object) noexcept
cdef int save_state(self, IntIOInterface) except -1
cdef int load_state(self, IntIOInterface, int) except -1
@cython.locals(days=uint64_t)
Expand Down
22 changes: 9 additions & 13 deletions pyboy/core/cartridge/rtc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# License: See LICENSE.md file
# GitHub: https://github.com/Baekalfen/PyBoy

import os
import struct
import time

Expand All @@ -13,30 +12,27 @@


class RTC:
def __init__(self, filename):
self.filename = filename + ".rtc"

def __init__(self, rtc_file):
self.timezero = time.time()
self.timelock = False
self.day_carry = 0
self.halt = 0

if not os.path.exists(self.filename):
logger.info("No RTC file found. Skipping.")
else:
with open(self.filename, "rb") as f:
self.load_state(IntIOWrapper(f), STATE_VERSION)

self.latch_enabled = False
self.sec_latch = 0
self.min_latch = 0
self.hour_latch = 0
self.day_latch_low = 0
self.day_latch_high = 0

def stop(self):
with open(self.filename, "wb") as f:
self.save_state(IntIOWrapper(f))
if rtc_file is not None:
self.load_state(IntIOWrapper(rtc_file), STATE_VERSION)
else:
logger.info("No RTC file found. Skipping.")

def stop(self, rtc_file):
if rtc_file is not None:
self.save_state(IntIOWrapper(rtc_file))

def save_state(self, f):
for b in struct.pack("d", self.timezero):
Expand Down
2 changes: 1 addition & 1 deletion pyboy/core/cpu.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ cdef class CPU:
cdef void set_interruptflag(self, int) noexcept nogil
cdef bint handle_interrupt(self, uint8_t, uint16_t) noexcept nogil

@cython.locals(opcode=uint16_t)
@cython.locals(pc1=uint16_t,pc2=uint16_t,pc3=uint16_t, opcode=uint16_t, v=cython.int, a=cython.int, b=cython.int)
cdef inline uint8_t fetch_and_execute(self) noexcept nogil
@cython.locals(_cycles0=int64_t)
cdef int tick(self, int64_t) noexcept nogil
Expand Down
40 changes: 36 additions & 4 deletions pyboy/core/cpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,44 @@ def handle_interrupt(self, flag, addr):
self.SP &= 0xFFFF

self.PC = addr

# https://gbdev.io/pandocs/Interrupts.html#interrupt-handling
# "The entire process lasts 5 M-cycles."
self.cycles += 20

self.interrupt_master_enable = False

def fetch_and_execute(self):
opcode = self.mb.getitem(self.PC)
# HACK: Shortcut the mb.getitem() calls
if (not self.mb.bootrom_enabled) and self.PC + 2 < 0x4000:
pc1 = self.mb.cartridge.rombanks[self.mb.cartridge.rombank_selected_low, self.PC]
pc2 = self.mb.cartridge.rombanks[self.mb.cartridge.rombank_selected_low, self.PC + 1]
pc3 = self.mb.cartridge.rombanks[self.mb.cartridge.rombank_selected_low, self.PC + 2]
elif (not self.mb.bootrom_enabled) and self.PC + 2 < 0x8000: # 16kB switchable ROM bank
pc1 = self.mb.cartridge.rombanks[self.mb.cartridge.rombank_selected, self.PC - 0x4000]
pc2 = self.mb.cartridge.rombanks[self.mb.cartridge.rombank_selected, self.PC + 1 - 0x4000]
pc3 = self.mb.cartridge.rombanks[self.mb.cartridge.rombank_selected, self.PC + 2 - 0x4000]
else:
pc1 = self.mb.getitem(self.PC)
pc2 = self.mb.getitem((self.PC + 1) & 0xFFFF)
pc3 = self.mb.getitem((self.PC + 2) & 0xFFFF)

v = 0
opcode = pc1
if opcode == 0xCB: # Extension code
opcode = self.mb.getitem(self.PC + 1)
opcode = pc2
opcode += 0x100 # Internally shifting look-up table

return opcodes.execute_opcode(self, opcode)
else:
# CB opcodes do not have immediates
oplen = opcodes.OPCODE_LENGTHS[opcode]
if oplen == 2:
# 8-bit immediate
v = pc2
elif oplen == 3:
# 16-bit immediate
# Flips order of values due to big-endian
a = pc3
b = pc2
v = (a << 8) + b

return opcodes.execute_opcode(self, opcode, v)
Loading
Loading