From b8a81dd7a3f74b1c66c053ca2e24ee6480bd2750 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Thu, 15 Sep 2022 13:31:30 +0100 Subject: [PATCH 1/5] Implementing serial support over IP socket Co-authored-by: thejomas --- pyboy/__main__.py | 8 ++++ pyboy/core/mb.pxd | 1 + pyboy/core/mb.py | 7 +++ pyboy/core/serial.py | 102 ++++++++++++++++++++++++++++++++++++++++--- pyboy/pyboy.py | 2 + 5 files changed, 113 insertions(+), 7 deletions(-) diff --git a/pyboy/__main__.py b/pyboy/__main__.py index 1cd68f571..ba7c768a2 100644 --- a/pyboy/__main__.py +++ b/pyboy/__main__.py @@ -107,6 +107,11 @@ def valid_sample_rate(freq): help="Add GameShark cheats on start-up. Add multiple by comma separation (i.e. '010138CD, 01033CD1')", ) +parser.add_argument("--serial-bind", action="store_true", help="Bind to this TCP addres for using Link Cable") +parser.add_argument( + "--serial-address", default=None, type=str, help="Connect (or bind) to this TCP addres for using Link Cable" +) + gameboy_type_parser = parser.add_mutually_exclusive_group() gameboy_type_parser.add_argument( "--dmg", action="store_const", const=False, dest="cgb", help="Force emulator to run as original Game Boy (DMG)" @@ -160,6 +165,9 @@ def valid_sample_rate(freq): def main(): argv = parser.parse_args() + if argv.serial_bind and not argv.serial_address: + parser.error("--serial-bind requires --serial-address") + print( """ The Game Boy controls are as follows: diff --git a/pyboy/core/mb.pxd b/pyboy/core/mb.pxd index 01f610b2d..6b280b07e 100644 --- a/pyboy/core/mb.pxd +++ b/pyboy/core/mb.pxd @@ -39,6 +39,7 @@ cdef class Motherboard: cdef pyboy.core.serial.Serial serial cdef pyboy.core.sound.Sound sound cdef pyboy.core.cartridge.base_mbc.BaseMBC cartridge + cdef object serial cdef bint bootrom_enabled cdef char[1024] serialbuffer cdef uint16_t serialbuffer_count diff --git a/pyboy/core/mb.py b/pyboy/core/mb.py index 32f81953c..d111cbb92 100644 --- a/pyboy/core/mb.py +++ b/pyboy/core/mb.py @@ -11,6 +11,7 @@ INTR_TIMER, INTR_SERIAL, INTR_HIGHTOLOW, + INTR_SERIAL, OPCODE_BRK, MAX_CYCLES, ) @@ -32,6 +33,8 @@ def __init__( sound_sample_rate, cgb, randomize=False, + serial_address=None, + serial_bind=None, ): if bootrom_file is not None: logger.info("Boot-ROM file provided") @@ -53,6 +56,7 @@ def __init__( self.interaction = interaction.Interaction() self.ram = ram.RAM(cgb, randomize=randomize) self.cpu = cpu.CPU(self) + self.serial = serial.Serial(serial_address, serial_bind) if cgb: self.lcd = lcd.CGBLCD( @@ -228,6 +232,7 @@ def buttonevent(self, key): def stop(self, save): self.sound.stop() + self.serial.stop() if save: self.cartridge.stop() @@ -346,6 +351,8 @@ def tick(self): if self.timer.tick(self.cpu.cycles): self.cpu.set_interruptflag(INTR_TIMER) + if self.serial.tick(cycles): + self.cpu.set_interruptflag(INTR_SERIAL) if lcd_interrupt := self.lcd.tick(self.cpu.cycles): self.cpu.set_interruptflag(lcd_interrupt) diff --git a/pyboy/core/serial.py b/pyboy/core/serial.py index 02b157ee4..7bd7b0d3c 100644 --- a/pyboy/core/serial.py +++ b/pyboy/core/serial.py @@ -3,13 +3,17 @@ # GitHub: https://github.com/Baekalfen/PyBoy # +import socket +import logging from pyboy.utils import MAX_CYCLES +logger = logging.getLogger(__name__) + CYCLES_8192HZ = 128 class Serial: - def __init__(self): + def __init__(self, serial_address, serial_bind, serial_interrupt_based=True): self.SB = 0xFF # Always 0xFF for a disconnected link cable self.SC = 0 self.transfer_enabled = 0 @@ -19,6 +23,37 @@ def __init__(self): self.clock = 0 self.clock_target = MAX_CYCLES + self.connection = None + + self.trans_bits = 0 + self.serial_interrupt_based = serial_interrupt_based + + if not serial_address: + logger.info("No serial address supplied. Link Cable emulated as disconnected.") + return + + if not serial_address.count(".") == 3 and serial_address.count(":") == 1: + logger.info("Only IP-addresses of the format x.y.z.w:abcd is supported") + return + + address_ip, address_port = serial_address.split(":") + address_tuple = (address_ip, int(address_port)) + + if serial_bind: + self.binding_connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.binding_connection.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + logger.info(f"Binding to {serial_address}") + self.binding_connection.bind(address_tuple) + self.binding_connection.listen(1) + self.connection, _ = self.binding_connection.accept() + logger.info(f"Client has connected!") + else: + self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + logger.info(f"Connecting to {serial_address}") + self.connection.connect(address_tuple) + logger.info(f"Connection successful!") + # self.connection.setblocking(False) + def set_SB(self, value): # Always 0xFF when cable is disconnected. Connecting is not implemented yet. self.SB = 0xFF @@ -47,16 +82,64 @@ def tick(self, _cycles): self.clock += cycles interrupt = False - if self.transfer_enabled and self.clock >= self.clock_target: - self.SC &= 0x80 - self.transfer_enabled = 0 - # self._cycles_to_interrupt = MAX_CYCLES - self.clock_target = MAX_CYCLES - interrupt = True + if self.transfer_enabled: + if self.connection is None: + # Disconnected emulation + if self.clock >= self.clock_target: + self.SC &= 0x80 + self.transfer_enabled = 0 + # self._cycles_to_interrupt = MAX_CYCLES + self.clock_target = MAX_CYCLES + interrupt = True + else: + # Connected emulation + if self.clock >= self.clock_target: + # if self.SC & 1: # Master + send_bit = bytes([(self.SB >> 7) & 1]) + self.connection.send(send_bit) + + data = self.connection.recv(1) + self.SB = ((self.SB << 1) & 0xFF) | data[0] & 1 + + logger.info(f"recv sb: {self.SB:08b}") + self.trans_bits += 1 + + if self.trans_bits == 8: + self.trans_bits = 0 + self.SC &= 0b0111_1111 + return True + return False self._cycles_to_interrupt = self.clock_target - self.clock return interrupt + # if self.serial_interrupt_based: + # if self.SC & 1: # Master + # if self.SC & 0x80: + # logger.info(f'Master sending!') + # self.connection.send(bytes([self.SB])) + # # self.connection.setblocking(True) + # data = self.connection.recv(1) + # self.SB = data[0] + # self.SC &= 0b0111_1111 + # return True + # else: + # try: + # if self.SC & 0x80: + # # self.connection.setblocking(False) + # logger.info(f'Slave recv!') + # self.connection.send(bytes([self.SB])) + # data = self.connection.recv(1) + # self.SB = data[0] + # self.SC &= 0b0111_1111 + # return True + # except BlockingIOError: + # pass + # return False + # return False + # else: + # Check if serial is in progress + def save_state(self, f): f.write(self.SB) f.write(self.SC) @@ -76,3 +159,8 @@ def load_state(self, f, state_version): self._cycles_to_interrupt = f.read_64bit() self.clock = f.read_64bit() self.clock_target = f.read_64bit() + + + def stop(self): + if self.connection: + self.connection.close() diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 2451dd965..77978ea4b 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -209,6 +209,8 @@ def __init__( sound_sample_rate, cgb, randomize=randomize, + serial_address=kwargs["serial_address"], + serial_bind=kwargs["serial_bind"], ) # Validate all kwargs From c29ffe9b6f5bfa2f265b4aabdab5d2ed93628dce Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Thu, 15 Sep 2022 22:40:19 +0200 Subject: [PATCH 2/5] interrupt-based working. wip --- pyboy/__main__.py | 9 ++-- pyboy/core/mb.pxd | 1 - pyboy/core/mb.py | 7 +-- pyboy/core/serial.pxd | 7 +-- pyboy/core/serial.py | 102 +++++------------------------------------- pyboy/pyboy.py | 8 +++- 6 files changed, 28 insertions(+), 106 deletions(-) diff --git a/pyboy/__main__.py b/pyboy/__main__.py index ba7c768a2..ae3a7007d 100644 --- a/pyboy/__main__.py +++ b/pyboy/__main__.py @@ -109,7 +109,10 @@ def valid_sample_rate(freq): parser.add_argument("--serial-bind", action="store_true", help="Bind to this TCP addres for using Link Cable") parser.add_argument( - "--serial-address", default=None, type=str, help="Connect (or bind) to this TCP addres for using Link Cable" + "--serial-address", default=None, type=str, help="Connect (or bind) to this TCP address for using Link Cable" +) +parser.add_argument( + "--serial-interrupt-based", action="store_true", help="Use only interrupt-based transfers for using Link Cable" ) gameboy_type_parser = parser.add_mutually_exclusive_group() @@ -165,8 +168,8 @@ def valid_sample_rate(freq): def main(): argv = parser.parse_args() - if argv.serial_bind and not argv.serial_address: - parser.error("--serial-bind requires --serial-address") + if (argv.serial_bind or argv.serial_interrupt_based) and not argv.serial_address: + parser.error("--serial-bind and --serial-interrupt-based requires --serial-address") print( """ diff --git a/pyboy/core/mb.pxd b/pyboy/core/mb.pxd index 6b280b07e..01f610b2d 100644 --- a/pyboy/core/mb.pxd +++ b/pyboy/core/mb.pxd @@ -39,7 +39,6 @@ cdef class Motherboard: cdef pyboy.core.serial.Serial serial cdef pyboy.core.sound.Sound sound cdef pyboy.core.cartridge.base_mbc.BaseMBC cartridge - cdef object serial cdef bint bootrom_enabled cdef char[1024] serialbuffer cdef uint16_t serialbuffer_count diff --git a/pyboy/core/mb.py b/pyboy/core/mb.py index d111cbb92..4f586adee 100644 --- a/pyboy/core/mb.py +++ b/pyboy/core/mb.py @@ -9,7 +9,6 @@ PyBoyException, PyBoyOutOfBoundsException, INTR_TIMER, - INTR_SERIAL, INTR_HIGHTOLOW, INTR_SERIAL, OPCODE_BRK, @@ -35,6 +34,7 @@ def __init__( randomize=False, serial_address=None, serial_bind=None, + serial_interrupt_based=False, ): if bootrom_file is not None: logger.info("Boot-ROM file provided") @@ -52,11 +52,10 @@ def __init__( logger.debug("Cartridge type auto-detected to %s", ("CGB" if self.cartridge.cgb else "DMG")) self.timer = timer.Timer() - self.serial = serial.Serial() + self.serial = serial.Serial(serial_address, serial_bind, serial_interrupt_based) self.interaction = interaction.Interaction() self.ram = ram.RAM(cgb, randomize=randomize) self.cpu = cpu.CPU(self) - self.serial = serial.Serial(serial_address, serial_bind) if cgb: self.lcd = lcd.CGBLCD( @@ -351,8 +350,6 @@ def tick(self): if self.timer.tick(self.cpu.cycles): self.cpu.set_interruptflag(INTR_TIMER) - if self.serial.tick(cycles): - self.cpu.set_interruptflag(INTR_SERIAL) if lcd_interrupt := self.lcd.tick(self.cpu.cycles): self.cpu.set_interruptflag(lcd_interrupt) diff --git a/pyboy/core/serial.pxd b/pyboy/core/serial.pxd index 0caa01615..89a46da1d 100644 --- a/pyboy/core/serial.pxd +++ b/pyboy/core/serial.pxd @@ -3,13 +3,12 @@ # GitHub: https://github.com/Baekalfen/PyBoy # +cimport cython from libc.stdint cimport int64_t, uint8_t, uint16_t, uint32_t, uint64_t +from pyboy.logging.logging cimport Logger from pyboy.utils cimport IntIOInterface -import cython - -from pyboy.logging.logging cimport Logger cdef uint64_t MAX_CYCLES, CYCLES_8192HZ cdef Logger logger @@ -19,8 +18,10 @@ cdef class Serial: cdef int64_t _cycles_to_interrupt cdef uint64_t last_cycles, clock, clock_target cdef bint transfer_enabled, double_speed, internal_clock + cdef bint serial_connected cdef bint tick(self, uint64_t) noexcept nogil + cdef void stop(self) noexcept cdef void set_SB(self, uint8_t) noexcept nogil cdef void set_SC(self, uint8_t) noexcept nogil diff --git a/pyboy/core/serial.py b/pyboy/core/serial.py index 7bd7b0d3c..7c7b1c3b3 100644 --- a/pyboy/core/serial.py +++ b/pyboy/core/serial.py @@ -3,17 +3,16 @@ # GitHub: https://github.com/Baekalfen/PyBoy # -import socket -import logging +import pyboy from pyboy.utils import MAX_CYCLES -logger = logging.getLogger(__name__) +logger = pyboy.logging.get_logger(__name__) CYCLES_8192HZ = 128 class Serial: - def __init__(self, serial_address, serial_bind, serial_interrupt_based=True): + def __init__(self): self.SB = 0xFF # Always 0xFF for a disconnected link cable self.SC = 0 self.transfer_enabled = 0 @@ -23,37 +22,6 @@ def __init__(self, serial_address, serial_bind, serial_interrupt_based=True): self.clock = 0 self.clock_target = MAX_CYCLES - self.connection = None - - self.trans_bits = 0 - self.serial_interrupt_based = serial_interrupt_based - - if not serial_address: - logger.info("No serial address supplied. Link Cable emulated as disconnected.") - return - - if not serial_address.count(".") == 3 and serial_address.count(":") == 1: - logger.info("Only IP-addresses of the format x.y.z.w:abcd is supported") - return - - address_ip, address_port = serial_address.split(":") - address_tuple = (address_ip, int(address_port)) - - if serial_bind: - self.binding_connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.binding_connection.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - logger.info(f"Binding to {serial_address}") - self.binding_connection.bind(address_tuple) - self.binding_connection.listen(1) - self.connection, _ = self.binding_connection.accept() - logger.info(f"Client has connected!") - else: - self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - logger.info(f"Connecting to {serial_address}") - self.connection.connect(address_tuple) - logger.info(f"Connection successful!") - # self.connection.setblocking(False) - def set_SB(self, value): # Always 0xFF when cable is disconnected. Connecting is not implemented yet. self.SB = 0xFF @@ -82,64 +50,16 @@ def tick(self, _cycles): self.clock += cycles interrupt = False - if self.transfer_enabled: - if self.connection is None: - # Disconnected emulation - if self.clock >= self.clock_target: - self.SC &= 0x80 - self.transfer_enabled = 0 - # self._cycles_to_interrupt = MAX_CYCLES - self.clock_target = MAX_CYCLES - interrupt = True - else: - # Connected emulation - if self.clock >= self.clock_target: - # if self.SC & 1: # Master - send_bit = bytes([(self.SB >> 7) & 1]) - self.connection.send(send_bit) - - data = self.connection.recv(1) - self.SB = ((self.SB << 1) & 0xFF) | data[0] & 1 - - logger.info(f"recv sb: {self.SB:08b}") - self.trans_bits += 1 - - if self.trans_bits == 8: - self.trans_bits = 0 - self.SC &= 0b0111_1111 - return True - return False + if self.transfer_enabled and self.clock >= self.clock_target: + # Disconnected emulation + self.SC &= 0x80 + self.transfer_enabled = 0 + self.clock_target = MAX_CYCLES + interrupt = True self._cycles_to_interrupt = self.clock_target - self.clock return interrupt - # if self.serial_interrupt_based: - # if self.SC & 1: # Master - # if self.SC & 0x80: - # logger.info(f'Master sending!') - # self.connection.send(bytes([self.SB])) - # # self.connection.setblocking(True) - # data = self.connection.recv(1) - # self.SB = data[0] - # self.SC &= 0b0111_1111 - # return True - # else: - # try: - # if self.SC & 0x80: - # # self.connection.setblocking(False) - # logger.info(f'Slave recv!') - # self.connection.send(bytes([self.SB])) - # data = self.connection.recv(1) - # self.SB = data[0] - # self.SC &= 0b0111_1111 - # return True - # except BlockingIOError: - # pass - # return False - # return False - # else: - # Check if serial is in progress - def save_state(self, f): f.write(self.SB) f.write(self.SC) @@ -160,7 +80,5 @@ def load_state(self, f, state_version): self.clock = f.read_64bit() self.clock_target = f.read_64bit() - def stop(self): - if self.connection: - self.connection.close() + pass diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 77978ea4b..232fba165 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -92,6 +92,9 @@ def __init__( color_palette=defaults["color_palette"], cgb_color_palette=defaults["cgb_color_palette"], title_status=False, + serial_address=None, + serial_bind=None, + serial_interrupt_based=False, **kwargs, ): """ @@ -209,8 +212,9 @@ def __init__( sound_sample_rate, cgb, randomize=randomize, - serial_address=kwargs["serial_address"], - serial_bind=kwargs["serial_bind"], + serial_address=serial_address, + serial_bind=serial_bind, + serial_interrupt_based=serial_interrupt_based, ) # Validate all kwargs From 6a19e47f0b6d202da3152290de9eda367524c561 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Wed, 21 May 2025 09:06:43 +0200 Subject: [PATCH 3/5] Serial somewhat working, still corruption --- pyboy/core/mb.py | 3 + pyboy/core/serial.pxd | 11 +- pyboy/core/serial.py | 254 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 252 insertions(+), 16 deletions(-) diff --git a/pyboy/core/mb.py b/pyboy/core/mb.py index 4f586adee..d34ca04a9 100644 --- a/pyboy/core/mb.py +++ b/pyboy/core/mb.py @@ -398,6 +398,9 @@ def getitem(self, i): if self.serial.tick(self.cpu.cycles): self.cpu.set_interruptflag(INTR_SERIAL) if i == 0xFF01: + # logger.debug(("Master " if self.serial.is_master else "Slave ") + "Read SB: %02x", self.serial.SB) + assert self.serial.sending_state == 2 # PASSIVE + assert self.serial.SC & 0x80 == 0 # No transfer active return self.serial.SB elif i == 0xFF02: return self.serial.SC diff --git a/pyboy/core/serial.pxd b/pyboy/core/serial.pxd index 89a46da1d..f32c7eb5e 100644 --- a/pyboy/core/serial.pxd +++ b/pyboy/core/serial.pxd @@ -13,12 +13,15 @@ from pyboy.utils cimport IntIOInterface cdef uint64_t MAX_CYCLES, CYCLES_8192HZ cdef Logger logger +cdef uint64_t SENDING, RECEIVING, PASSIVE + cdef class Serial: cdef uint64_t SB, SC cdef int64_t _cycles_to_interrupt cdef uint64_t last_cycles, clock, clock_target cdef bint transfer_enabled, double_speed, internal_clock - cdef bint serial_connected + cdef bint serial_connected, is_master + cdef uint64_t sending_state cdef bint tick(self, uint64_t) noexcept nogil cdef void stop(self) noexcept @@ -28,3 +31,9 @@ cdef class Serial: cdef int save_state(self, IntIOInterface) except -1 cdef int load_state(self, IntIOInterface, int) except -1 + + cdef object connection, binding_connection + cdef uint8_t trans_bits + cdef bint serial_interrupt_based + + cdef void disconnect(self) noexcept nogil \ No newline at end of file diff --git a/pyboy/core/serial.py b/pyboy/core/serial.py index 7c7b1c3b3..cb81edaa4 100644 --- a/pyboy/core/serial.py +++ b/pyboy/core/serial.py @@ -3,42 +3,147 @@ # GitHub: https://github.com/Baekalfen/PyBoy # +import time +import socket import pyboy from pyboy.utils import MAX_CYCLES +import queue logger = pyboy.logging.get_logger(__name__) +try: + import cython +except ImportError: + + class _mock: + def __enter__(self): + pass + + def __exit__(self, *args): + pass + + exec( + """ +class cython: + gil = _mock() + nogil = _mock() +""", + globals(), + locals(), + ) + + CYCLES_8192HZ = 128 +async_recv = queue.Queue() + + +def async_comms(socket): + while True: + item = socket.recv(1) + async_recv.put(item) + + +SENDING, RECEIVING, PASSIVE = 0, 1, 2 + class Serial: - def __init__(self): + def __init__(self, serial_address, serial_bind, serial_interrupt_based): self.SB = 0xFF # Always 0xFF for a disconnected link cable self.SC = 0 self.transfer_enabled = 0 + self.is_master = False self.internal_clock = 0 self._cycles_to_interrupt = 0 self.last_cycles = 0 self.clock = 0 self.clock_target = MAX_CYCLES + self.serial_connected = False + self.connection = None + self.sending_state = PASSIVE + + self.trans_bits = 0 + self.serial_interrupt_based = serial_interrupt_based + + self.all_data = {"send": [], "recv": []} + + logger.debug("Serial starts: %s, %d, %d", serial_address, serial_bind, serial_interrupt_based) + + if not serial_address: + logger.debug("No serial address supplied. Link Cable emulated as disconnected.") + return + + if not serial_address.count(".") == 3 and serial_address.count(":") == 1: + logger.error("Only IP-addresses of the format x.y.z.w:abcd is supported") + return + + address_ip, address_port = serial_address.split(":") + address_tuple = (address_ip, int(address_port)) + + self.binding_connection = None + if serial_bind: + self.binding_connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.binding_connection.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + logger.debug(f"Binding to {serial_address}") + self.binding_connection.bind(address_tuple) + self.binding_connection.listen(1) + self.connection, _ = self.binding_connection.accept() + logger.debug("Client has connected!") + else: + self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + for _ in range(10): + logger.debug(f"Connecting to {serial_address}") + try: + self.connection.connect(address_tuple) + break + except ConnectionRefusedError: + time.sleep(1) + except OSError: + time.sleep(1) + + logger.debug("Connection successful!") + + self.serial_connected = self.connection is not None + # if self.serial_interrupt_based: + # logger.debug("Interrupt-based serial emulation active!") + # self.recv_thread = threading.Thread(target=async_comms, args=(self.connection,)) + # self.recv_thread.start() + def set_SB(self, value): - # Always 0xFF when cable is disconnected. Connecting is not implemented yet. - self.SB = 0xFF + # if not value == self.SB: + # logger.debug(("Master " if self.is_master else "Slave ") + "Write SB: %02x", value) + # logger.debug("SB set %02x", value) + if not self.serial_connected: + # Always 0xFF when cable is disconnected. Connecting is not implemented yet. + self.SB = 0xFF + else: + self.SB = value def set_SC(self, value): # cgb, double_speed + # logger.debug(("Master " if self.is_master else "Slave ") + "Write SC: %02x", value) self.SC = value self.transfer_enabled = self.SC & 0x80 + self.is_master = self.SC & 1 + if self.is_master: + self.sending_state = SENDING + else: + self.sending_state = RECEIVING + # logger.debug("SC set %02x, %s", value, self.transfer_enabled) # TODO: # if cgb and (self.SC & 0b10): # High speed transfer # self.double_speed = ... self.internal_clock = self.SC & 1 # 0: external, 1: internal - if self.internal_clock: - self.clock_target = self.clock + 8 * CYCLES_8192HZ + if not self.serial_connected: + if self.internal_clock: + self.clock_target = self.clock + 8 * CYCLES_8192HZ + else: + # Will never complete, as there is no connection + self.transfer_enabled = 0 # Technically it is enabled, but no reason to track it. + self.clock_target = MAX_CYCLES else: - # Will never complete, as there is no connection - self.transfer_enabled = 0 # Technically it is enabled, but no reason to track it. - self.clock_target = MAX_CYCLES + # If connected then immediate send data + self.clock_target = self.clock self._cycles_to_interrupt = self.clock_target - self.clock def tick(self, _cycles): @@ -50,20 +155,109 @@ def tick(self, _cycles): self.clock += cycles interrupt = False - if self.transfer_enabled and self.clock >= self.clock_target: - # Disconnected emulation - self.SC &= 0x80 - self.transfer_enabled = 0 - self.clock_target = MAX_CYCLES - interrupt = True + if self.transfer_enabled: + if not self.serial_connected: + if self.clock >= self.clock_target: + # Disconnected emulation + self.SC &= 0x80 + self.transfer_enabled = 0 + self.clock_target = MAX_CYCLES + interrupt = True + elif self.serial_interrupt_based: # Connected emulation + self.clock_target = MAX_CYCLES # interrupt-based serial has no timing, just asap + # TODO: SB-read-write based transfers? Schedule interrupt, but don't transfer before SB is read. Transfer whatever is needed and resync both. + with cython.gil: + try: + if self.is_master: + if self.SC & 0x80: + if self.sending_state == SENDING: + # logger.debug("Master sending!") + self.connection.send(bytes([self.SB])) + self.all_data["send"].append(self.SB) + self.sending_state = RECEIVING + logger.debug("Master byte sent: %02x", self.SB) + self.clock_target = 0 + elif self.sending_state == RECEIVING: + try: + data = self.connection.recv( + 1, socket.MSG_DONTWAIT + ) # TODO: Timeout if the other side disconnects + if len(data) > 0: + self.SB = data[0] + self.all_data["recv"].append(self.SB) + logger.debug("Master byte received: %02x", self.SB) + self.SC &= 0b0111_1111 + interrupt = True + self.sending_state = PASSIVE + self.clock_target = MAX_CYCLES + else: + logger.error("Master disconnect!") + self.disconnect() + except BlockingIOError: + pass + # else: + # self.clock_target = self.clock + 8 * CYCLES_8192HZ + else: + if self.sending_state == SENDING: + self.connection.send(bytes([self.SB])) + self.all_data["send"].append(self.SB) + logger.debug("Slave byte sent: %02x", self.SB) + # data = self.connection.recv(1) + self.SB = self.SB_latched + self.SC &= 0b0111_1111 + interrupt = True + self.sending_state = PASSIVE + self.clock_target = MAX_CYCLES + elif self.sending_state == RECEIVING: + try: + data = self.connection.recv(1, socket.MSG_DONTWAIT) + if len(data) > 0: + self.SB_latched = data[0] + logger.debug("Slave recv! %02x", self.SB_latched) + self.all_data["recv"].append(self.SB_latched) + self.sending_state = SENDING + self.clock_target = 0 + else: + logger.error("Slave disconnect!") + self.disconnect() + except BlockingIOError: + # logger.debug("Slave no data!") + # self.clock_target = self.clock + 8 * CYCLES_8192HZ + pass + except (ConnectionResetError, BrokenPipeError) as ex: + logger.error(("Master " if self.is_master else "Slave ") + "Exception in serial tick: %s", ex) + self.disconnect() + else: + raise Exception(("Master " if self.is_master else "Slave ") + "Invalid mode") self._cycles_to_interrupt = self.clock_target - self.clock return interrupt + # Clock based serial: + # else: + # exit(2) + # # if self.SC & 1: # Master + # send_bit = bytes([(self.SB >> 7) & 1]) + # self.connection.send(send_bit) + + # data = self.connection.recv(1) + # self.SB = ((self.SB << 1) & 0xFF) | data[0] & 1 + + # logger.debug(f"recv sb: {self.SB:08b}") + # self.trans_bits += 1 + + # if self.trans_bits == 8: + # self.trans_bits = 0 + # self.SC &= 0b0111_1111 + # interrupt = True + + # self.clock_target = self.clock + CYCLES_8192HZ + def save_state(self, f): f.write(self.SB) f.write(self.SC) f.write(self.transfer_enabled) + f.write(self.is_master) f.write(self.internal_clock) f.write_64bit(self.last_cycles) f.write_64bit(self._cycles_to_interrupt) @@ -74,11 +268,41 @@ def load_state(self, f, state_version): self.SB = f.read() self.SC = f.read() self.transfer_enabled = f.read() + self.is_master = f.read() self.internal_clock = f.read() self.last_cycles = f.read_64bit() self._cycles_to_interrupt = f.read_64bit() self.clock = f.read_64bit() self.clock_target = f.read_64bit() + def disconnect(self): + logger.debug("DISCONNECTING") + with cython.gil: + if self.serial_connected: + if self.connection: + self.connection.close() + if self.binding_connection: + self.binding_connection.close() + # if self.serial_interrupt_based and self.recv_thread: + # self.recv_thread.kill() + + self.sending_state = PASSIVE + self.serial_connected = False + self.SB = 0xFF + self.clock_target = MAX_CYCLES + def stop(self): - pass + from pprint import pprint as pp + + pp(self.all_data) + if hasattr(self, "binding_connection") and self.binding_connection is not None: + with open("master_recv.bin", "wb") as f: + f.write(bytes(self.all_data["recv"])) + with open("master_send.bin", "wb") as f: + f.write(bytes(self.all_data["send"])) + else: + with open("slave_recv.bin", "wb") as f: + f.write(bytes(self.all_data["recv"])) + with open("slave_send.bin", "wb") as f: + f.write(bytes(self.all_data["send"])) + self.disconnect() From d3f7b6537aded7ed2933b5c236d8d020f962e639 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Tue, 20 May 2025 17:53:24 +0200 Subject: [PATCH 4/5] Redefine logging levels to minimize noise and improve usability --- pyboy/core/cartridge/cartridge.py | 2 +- pyboy/core/cartridge/mbc1.py | 2 +- pyboy/core/cartridge/mbc2.py | 2 +- pyboy/core/cartridge/rtc.py | 2 +- pyboy/core/mb.py | 2 +- pyboy/core/sound.py | 2 +- pyboy/logging/__init__.py | 12 ++++++------ pyboy/pyboy.py | 14 +++++++------- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/pyboy/core/cartridge/cartridge.py b/pyboy/core/cartridge/cartridge.py index 39e823dfc..9e6cd36e7 100644 --- a/pyboy/core/cartridge/cartridge.py +++ b/pyboy/core/cartridge/cartridge.py @@ -53,7 +53,7 @@ def load_romfile(filename): logger.debug("Loading ROM file: %d bytes", len(romdata)) if len(romdata) == 0: - logger.error("ROM file is empty!") + logger.critical("ROM file is empty!") raise PyBoyException("Empty ROM file") banksize = 16 * 1024 diff --git a/pyboy/core/cartridge/mbc1.py b/pyboy/core/cartridge/mbc1.py index 30e7591eb..42cff0c9b 100644 --- a/pyboy/core/cartridge/mbc1.py +++ b/pyboy/core/cartridge/mbc1.py @@ -47,7 +47,7 @@ def setitem(self, address, value): def getitem(self, address): if 0xA000 <= address < 0xC000: if not self.rambank_initialized: - logger.error("RAM banks not initialized: %0.4x", address) + logger.warning("RAM banks not initialized: %0.4x", address) if not self.rambank_enabled: return 0xFF diff --git a/pyboy/core/cartridge/mbc2.py b/pyboy/core/cartridge/mbc2.py index 9acd11ca2..988f5dd53 100644 --- a/pyboy/core/cartridge/mbc2.py +++ b/pyboy/core/cartridge/mbc2.py @@ -34,7 +34,7 @@ def getitem(self, address): return self.rombanks[self.rombank_selected, address - 0x4000] elif 0xA000 <= address < 0xC000: if not self.rambank_initialized: - logger.error("RAM banks not initialized: %0.4x", address) + logger.warning("RAM banks not initialized: %0.4x", address) if not self.rambank_enabled: return 0xFF diff --git a/pyboy/core/cartridge/rtc.py b/pyboy/core/cartridge/rtc.py index eb613128a..036415091 100644 --- a/pyboy/core/cartridge/rtc.py +++ b/pyboy/core/cartridge/rtc.py @@ -22,7 +22,7 @@ def __init__(self, filename): self.halt = 0 if not os.path.exists(self.filename): - logger.info("No RTC file found. Skipping.") + logger.debug("No RTC file found. Skipping.") else: with open(self.filename, "rb") as f: self.load_state(IntIOWrapper(f), STATE_VERSION) diff --git a/pyboy/core/mb.py b/pyboy/core/mb.py index d34ca04a9..872fb3819 100644 --- a/pyboy/core/mb.py +++ b/pyboy/core/mb.py @@ -37,7 +37,7 @@ def __init__( serial_interrupt_based=False, ): if bootrom_file is not None: - logger.info("Boot-ROM file provided") + logger.debug("Boot-ROM file provided") self.cartridge = cartridge.load_cartridge(gamerom) logger.debug("Cartridge started:\n%s", str(self.cartridge)) diff --git a/pyboy/core/sound.py b/pyboy/core/sound.py index 9ddd6fba1..b0ebb5a90 100644 --- a/pyboy/core/sound.py +++ b/pyboy/core/sound.py @@ -299,7 +299,7 @@ def sample(self): right_sample = 0 if self.audiobuffer_head >= self.audiobuffer_length: - logger.critical("Buffer overrun! %d of %d", self.audiobuffer_head, self.audiobuffer_length) + logger.warning("Buffer overrun! %d of %d", self.audiobuffer_head, self.audiobuffer_length) return self.audiobuffer[self.audiobuffer_head] = left_sample self.audiobuffer[self.audiobuffer_head + 1] = right_sample diff --git a/pyboy/logging/__init__.py b/pyboy/logging/__init__.py index 38637e819..eec805942 100644 --- a/pyboy/logging/__init__.py +++ b/pyboy/logging/__init__.py @@ -1,12 +1,12 @@ ( - DEBUG, - INFO, - WARNING, - ERROR, - CRITICAL, + DEBUG, # Only for developers + WARNING, # Unexpected failure that can be worked around + ERROR, # Unexpected failure that impacts usability + CRITICAL, # Unexpected failure where we cannot continue + INFO, # Normal operation, that the user *needs* or *wants* to be aware of ) = range(5) -_log_level = WARNING +_log_level = INFO def get_log_level(): diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 232fba165..50ef940dd 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -226,7 +226,7 @@ def __init__( for k, v in kwargs.items(): if k not in defaults and k not in plugin_manager_keywords: - logger.error("Unknown keyword argument: %s", k) + logger.critical("Unknown keyword argument: %s", k) raise KeyError(f"Unknown keyword argument: {k}") # Performance measures @@ -616,7 +616,7 @@ def _pause(self): self.paused = True self.save_target_emulationspeed = self.target_emulationspeed self.target_emulationspeed = 1 - logger.info("Emulation paused!") + logger.debug("Emulation paused!") self._update_window_title() self._plugin_manager.paused(True) @@ -625,7 +625,7 @@ def _unpause(self): return self.paused = False self.target_emulationspeed = self.save_target_emulationspeed - logger.info("Emulation unpaused!") + logger.debug("Emulation unpaused!") self._update_window_title() self._plugin_manager.paused(False) @@ -683,9 +683,9 @@ def stop(self, save=True): provided game-ROM. """ if self.initialized and not self.stopped: - logger.info("###########################") - logger.info("# Emulator is turning off #") - logger.info("###########################") + logger.debug("###########################") + logger.debug("# Emulator is turning off #") + logger.debug("###########################") self._plugin_manager.stop() self.mb.stop(save) self.stopped = True @@ -1125,7 +1125,7 @@ def _load_symbols(self): gamerom_file_no_ext, rom_ext = os.path.splitext(self.gamerom) for sym_path in [self.symbols_file, gamerom_file_no_ext + ".sym", gamerom_file_no_ext + rom_ext + ".sym"]: if sym_path and os.path.isfile(sym_path): - logger.info("Loading symbol file: %s", sym_path) + logger.debug("Loading symbol file: %s", sym_path) with open(sym_path) as f: for _line in f.readlines(): line = _line.strip() From fb364e5d54b776462b026601ce3924c1a2b42990 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Tue, 20 May 2025 17:54:09 +0200 Subject: [PATCH 5/5] Fix crash in SDL2 when sound is disabled --- pyboy/plugins/window_sdl2.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyboy/plugins/window_sdl2.py b/pyboy/plugins/window_sdl2.py index 9d7f1b788..5a8d794c8 100644 --- a/pyboy/plugins/window_sdl2.py +++ b/pyboy/plugins/window_sdl2.py @@ -282,10 +282,11 @@ def post_tick(self): def paused(self, pause): self.sound_paused = pause - if self.sound_paused: - sdl2.SDL_PauseAudioDevice(self.sound_device, 1) - else: - sdl2.SDL_PauseAudioDevice(self.sound_device, 0) + if self.sound_support: + if self.sound_paused: + sdl2.SDL_PauseAudioDevice(self.sound_device, 1) + else: + sdl2.SDL_PauseAudioDevice(self.sound_device, 0) def enabled(self): if self.pyboy_argv.get("window") in ("SDL2", None):