diff --git a/docs/api/screen.html b/docs/api/screen.html index bd8701056..81078dfe2 100644 --- a/docs/api/screen.html +++ b/docs/api/screen.html @@ -230,12 +230,9 @@

Module pyboy.api.screen

list: Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off. """ - # self.tilemap_position_list = np.asarray(self.mb.lcd.renderer._scanlineparameters, dtype=np.uint8).reshape(144, 5)[:, :4] - # self.tilemap_position_list = self.mb.lcd.renderer._scanlineparameters - # # return self.mb.lcd.renderer._scanlineparameters if self.mb.lcd._LCDC.lcd_enable: - return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters] + return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd._scanlineparameters] else: return [[0, 0, 0, 0] for line in range(144)] @@ -467,12 +464,9 @@

Classes

list: Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off. """ - # self.tilemap_position_list = np.asarray(self.mb.lcd.renderer._scanlineparameters, dtype=np.uint8).reshape(144, 5)[:, :4] - # self.tilemap_position_list = self.mb.lcd.renderer._scanlineparameters - # # return self.mb.lcd.renderer._scanlineparameters if self.mb.lcd._LCDC.lcd_enable: - return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters] + return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd._scanlineparameters] else: return [[0, 0, 0, 0] for line in range(144)] @@ -569,12 +563,9 @@

Returns

list: Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off. """ - # self.tilemap_position_list = np.asarray(self.mb.lcd.renderer._scanlineparameters, dtype=np.uint8).reshape(144, 5)[:, :4] - # self.tilemap_position_list = self.mb.lcd.renderer._scanlineparameters - # # return self.mb.lcd.renderer._scanlineparameters if self.mb.lcd._LCDC.lcd_enable: - return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters] + return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd._scanlineparameters] else: return [[0, 0, 0, 0] for line in range(144)] diff --git a/docs/api/tile.html b/docs/api/tile.html index 195df2998..a1198e1a5 100644 --- a/docs/api/tile.html +++ b/docs/api/tile.html @@ -220,9 +220,9 @@

Module pyboy.api.tile

byte1 = self.mb.lcd.VRAM1[self.data_address + k - VRAM_OFFSET] byte2 = self.mb.lcd.VRAM1[self.data_address + k + 1 - VRAM_OFFSET] + colorcode = self.mb.lcd.renderer.colorcode(byte1, byte2) for x in range(8): - colorcode = utils.color_code(byte1, byte2, 7 - x) - self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode) + self.data[k // 2][x] = self.mb.lcd.BGP.getcolor((colorcode >> x * 8) & 0xFF) return self.data def __eq__(self, other): @@ -419,9 +419,9 @@

Classes

byte1 = self.mb.lcd.VRAM1[self.data_address + k - VRAM_OFFSET] byte2 = self.mb.lcd.VRAM1[self.data_address + k + 1 - VRAM_OFFSET] + colorcode = self.mb.lcd.renderer.colorcode(byte1, byte2) for x in range(8): - colorcode = utils.color_code(byte1, byte2, 7 - x) - self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode) + self.data[k // 2][x] = self.mb.lcd.BGP.getcolor((colorcode >> x * 8) & 0xFF) return self.data def __eq__(self, other): diff --git a/docs/index.html b/docs/index.html index 01acbc673..175e11905 100644 --- a/docs/index.html +++ b/docs/index.html @@ -186,7 +186,7 @@

Kwargs

for k, v in defaults.items(): if k not in kwargs: - kwargs[k] = kwargs.get(k, defaults[k]) + kwargs[k] = v _log_level(log_level) @@ -3201,14 +3201,12 @@

Args

start -= 0x4000 stop -= 0x4000 # Cartridge ROM Banks - assert stop < 0x4000, "Out of bounds for reading ROM bank" + assert stop <= 0x4000, "Out of bounds for reading ROM bank" assert bank <= self.mb.cartridge.external_rom_count, "ROM Bank out of range" - # TODO: If you change a RAM value outside of the ROM banks above, the memory value will stay the same no matter - # what the game writes to the address. This can be used so freeze the value for health, cash etc. if bank == -1: assert start <= 0xFF, "Start address out of range for bootrom" - assert stop <= 0xFF, "Start address out of range for bootrom" + assert stop <= 0x100, "Start address out of range for bootrom" if not is_single: # Writing slice of memory space if hasattr(v, "__iter__"): @@ -3240,7 +3238,7 @@

Args

stop -= 0x8000 # CGB VRAM Banks assert self.mb.cgb or (bank == 0), "Selecting bank of VRAM is only supported for CGB mode" - assert stop < 0x2000, "Out of bounds for reading VRAM bank" + assert stop <= 0x2000, "Out of bounds for reading VRAM bank" assert bank <= 1, "VRAM Bank out of range" if bank == 0: @@ -3273,7 +3271,7 @@

Args

start -= 0xA000 stop -= 0xA000 # Cartridge RAM banks - assert stop < 0x2000, "Out of bounds for reading cartridge RAM bank" + assert stop <= 0x2000, "Out of bounds for reading cartridge RAM bank" assert bank <= self.mb.cartridge.external_ram_count, "ROM Bank out of range" if not is_single: # Writing slice of memory space @@ -3295,7 +3293,7 @@

Args

stop -= 0x1000 # CGB VRAM banks assert self.mb.cgb or (bank == 0), "Selecting bank of WRAM is only supported for CGB mode" - assert stop < 0x1000, "Out of bounds for reading VRAM bank" + assert stop <= 0x1000, "Out of bounds for reading VRAM bank" assert bank <= 7, "WRAM Bank out of range" if not is_single: # Writing slice of memory space @@ -3377,13 +3375,13 @@

Args

True ``` """ - def __init__(self, cpu): self.cpu = cpu @property def A(self): return self.cpu.A + @A.setter def A(self, value): self.cpu.A = value & 0xFF @@ -3391,6 +3389,7 @@

Args

@property def F(self): return self.cpu.F + @F.setter def F(self, value): self.cpu.F = value & 0xF0 @@ -3398,6 +3397,7 @@

Args

@property def B(self): return self.cpu.B + @B.setter def B(self, value): self.cpu.B = value & 0xFF @@ -3405,6 +3405,7 @@

Args

@property def C(self): return self.cpu.C + @C.setter def C(self, value): self.cpu.C = value & 0xFF @@ -3412,6 +3413,7 @@

Args

@property def D(self): return self.cpu.D + @D.setter def D(self, value): self.cpu.D = value & 0xFF @@ -3419,6 +3421,7 @@

Args

@property def E(self): return self.cpu.E + @E.setter def E(self, value): self.cpu.E = value & 0xFF @@ -3426,6 +3429,7 @@

Args

@property def HL(self): return self.cpu.HL + @HL.setter def HL(self, value): self.cpu.HL = value & 0xFFFF @@ -3433,6 +3437,7 @@

Args

@property def SP(self): return self.cpu.SP + @SP.setter def SP(self, value): self.cpu.SP = value & 0xFFFF @@ -3440,6 +3445,7 @@

Args

@property def PC(self): return self.cpu.PC + @PC.setter def PC(self, value): self.cpu.PC = value & 0xFFFF diff --git a/docs/utils.html b/docs/utils.html index 34ddbd65d..d9aadd879 100644 --- a/docs/utils.html +++ b/docs/utils.html @@ -32,7 +32,7 @@

Module pyboy.utils

__all__ = ["WindowEvent", "dec_to_bcd", "bcd_to_dec"] -STATE_VERSION = 10 +STATE_VERSION = 11 ############################################################## # Buffer classes @@ -143,10 +143,7 @@

Module pyboy.utils

# Misc -# TODO: Would a lookup-table increase performance? For example a lookup table of each 4-bit nibble? -# That's 16**2 = 256 values. Index calculated as: (byte1 & 0xF0) | ((byte2 & 0xF0) >> 4) -# and then: (byte1 & 0x0F) | ((byte2 & 0x0F) >> 4) -# Then could even be preloaded for each color palette +# NOTE: Legacy function. Use look-up table in Renderer def color_code(byte1, byte2, offset): """Convert 2 bytes into color code at a given offset. diff --git a/pyboy/api/screen.py b/pyboy/api/screen.py index ce956c086..0dbeff732 100644 --- a/pyboy/api/screen.py +++ b/pyboy/api/screen.py @@ -202,12 +202,9 @@ def tilemap_position_list(self): list: Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off. """ - # self.tilemap_position_list = np.asarray(self.mb.lcd.renderer._scanlineparameters, dtype=np.uint8).reshape(144, 5)[:, :4] - # self.tilemap_position_list = self.mb.lcd.renderer._scanlineparameters - # # return self.mb.lcd.renderer._scanlineparameters if self.mb.lcd._LCDC.lcd_enable: - return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters] + return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd._scanlineparameters] else: return [[0, 0, 0, 0] for line in range(144)] diff --git a/pyboy/api/tile.pxd b/pyboy/api/tile.pxd index 4394963da..9da325e75 100644 --- a/pyboy/api/tile.pxd +++ b/pyboy/api/tile.pxd @@ -5,7 +5,7 @@ import cython -from libc.stdint cimport uint8_t, uint16_t, uint32_t +from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t from pyboy cimport utils from pyboy.core.mb cimport Motherboard @@ -25,5 +25,5 @@ cdef class Tile: cpdef object ndarray(self) noexcept cdef uint32_t[:,:] data # TODO: Add to locals instead - @cython.locals(byte1=uint8_t, byte2=uint8_t, colorcode=uint32_t) + @cython.locals(byte1=uint8_t, byte2=uint8_t, colorcode=uint64_t) cdef uint32_t[:,:] _image_data(self) noexcept diff --git a/pyboy/api/tile.py b/pyboy/api/tile.py index 779e63534..ee7bddc67 100644 --- a/pyboy/api/tile.py +++ b/pyboy/api/tile.py @@ -190,9 +190,9 @@ def _image_data(self): byte1 = self.mb.lcd.VRAM1[self.data_address + k - VRAM_OFFSET] byte2 = self.mb.lcd.VRAM1[self.data_address + k + 1 - VRAM_OFFSET] + colorcode = self.mb.lcd.renderer.colorcode(byte1, byte2) for x in range(8): - colorcode = utils.color_code(byte1, byte2, 7 - x) - self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode) + self.data[k // 2][x] = self.mb.lcd.BGP.getcolor((colorcode >> x * 8) & 0xFF) return self.data def __eq__(self, other): diff --git a/pyboy/core/cartridge/base_mbc.pxd b/pyboy/core/cartridge/base_mbc.pxd index c2967c622..5b8842795 100644 --- a/pyboy/core/cartridge/base_mbc.pxd +++ b/pyboy/core/cartridge/base_mbc.pxd @@ -28,6 +28,7 @@ cdef class BaseMBC: cdef bint rambank_initialized cdef uint16_t rambank_selected cdef uint16_t rombank_selected + cdef uint16_t rombank_selected_low cdef bint cgb cdef void save_state(self, IntIOInterface) noexcept diff --git a/pyboy/core/cartridge/base_mbc.py b/pyboy/core/cartridge/base_mbc.py index cc9766d52..f03bf0fdf 100644 --- a/pyboy/core/cartridge/base_mbc.py +++ b/pyboy/core/cartridge/base_mbc.py @@ -39,8 +39,9 @@ def __init__(self, filename, rombanks, external_ram_count, carttype, sram, batte self.rambank_enabled = False self.rambank_selected = 0 self.rombank_selected = 1 + self.rombank_selected_low = 0 - self.cgb = bool(self.getitem(0x0143) >> 7) + self.cgb = bool(self.rombanks[0, 0x0143] >> 7) if not os.path.exists(self.filename): logger.debug("No RAM file found. Skipping.") @@ -118,11 +119,7 @@ def overrideitem(self, rom_bank, address, value): logger.error("Invalid override address: %0.4x", address) def getitem(self, address): - if 0x0000 <= address < 0x4000: - return self.rombanks[0, address] - elif 0x4000 <= address < 0x8000: - return self.rombanks[self.rombank_selected, address - 0x4000] - elif 0xA000 <= address < 0xC000: + if 0xA000 <= address < 0xC000: # if not self.rambank_initialized: # logger.error("RAM banks not initialized: 0.4x", address) diff --git a/pyboy/core/cartridge/mbc1.py b/pyboy/core/cartridge/mbc1.py index cb705e76c..d04b30cff 100644 --- a/pyboy/core/cartridge/mbc1.py +++ b/pyboy/core/cartridge/mbc1.py @@ -37,19 +37,15 @@ def setitem(self, address, value): self.rambanks[self.rambank_selected % self.external_ram_count, address - 0xA000] = value # else: # logger.error("Invalid writing address: %0.4x", address) + if self.memorymodel == 1: + self.rombank_selected_low = (self.bank_select_register2 << 5) % self.external_rom_count + else: + self.rombank_selected_low = 0 + self.rombank_selected = ((self.bank_select_register2 << 5) | + self.bank_select_register1) % self.external_rom_count def getitem(self, address): - if 0x0000 <= address < 0x4000: - if self.memorymodel == 1: - self.rombank_selected = (self.bank_select_register2 << 5) % self.external_rom_count - else: - self.rombank_selected = 0 - return self.rombanks[self.rombank_selected, address] - elif 0x4000 <= address < 0x8000: - self.rombank_selected = \ - ((self.bank_select_register2 << 5) | self.bank_select_register1) % self.external_rom_count - return self.rombanks[self.rombank_selected, address - 0x4000] - elif 0xA000 <= address < 0xC000: + if 0xA000 <= address < 0xC000: if not self.rambank_initialized: logger.error("RAM banks not initialized: %0.4x", address) diff --git a/pyboy/core/lcd.pxd b/pyboy/core/lcd.pxd index 35b8b4155..4b4ed074a 100644 --- a/pyboy/core/lcd.pxd +++ b/pyboy/core/lcd.pxd @@ -47,8 +47,9 @@ cdef class LCD: cdef PaletteRegister OBP0 cdef PaletteRegister OBP1 cdef Renderer renderer + cdef uint8_t[144][5] _scanlineparameters - @cython.locals(interrupt_flag=uint8_t) + @cython.locals(interrupt_flag=uint8_t,bx=int,by=int,wx=int,wy=int) cdef uint8_t tick(self, int) noexcept nogil cdef int64_t cycles_to_interrupt(self) noexcept nogil @@ -62,8 +63,8 @@ cdef class LCD: cdef void save_state(self, IntIOInterface) noexcept cdef void load_state(self, IntIOInterface, int) noexcept - cdef (int, int) getwindowpos(self) noexcept nogil - cdef (int, int) getviewport(self) noexcept nogil + cdef inline (int, int) getwindowpos(self) noexcept nogil + cdef inline (int, int) getviewport(self) noexcept nogil # CGB cdef bint cgb @@ -108,6 +109,9 @@ cdef class LCDCRegister: cdef bint background_enable cdef bint cgb_master_priority + cdef uint16_t backgroundmap_offset + cdef uint16_t windowmap_offset + cpdef int _get_sprite_height(self) cdef class Renderer: @@ -124,19 +128,19 @@ cdef class Renderer: cdef array _tilecache0_raw, _spritecache0_raw, _spritecache1_raw cdef uint32_t[:,:] _screenbuffer cdef uint8_t[:,:] _screenbuffer_attributes - cdef uint32_t[:,:] _tilecache0, _spritecache0, _spritecache1 + cdef uint8_t[:,:] _tilecache0, _spritecache0, _spritecache1 + cdef uint64_t[:] _tilecache0_64, _tilecache1_64, _spritecache0_64, _spritecache1_64 + cdef uint32_t[:] colorcode_table cdef int[10] sprites_to_render cdef int ly_window cdef void invalidate_tile(self, int, int) noexcept nogil - cdef uint8_t[144][5] _scanlineparameters - cdef void blank_screen(self, LCD) noexcept nogil # CGB cdef array _tilecache1_raw - cdef uint32_t[:,:] _tilecache1 + cdef uint8_t[:,:] _tilecache1 @cython.locals( bx=int, @@ -150,12 +154,24 @@ cdef class Renderer: bg_priority=bint, xx=int, yy=int, - tilecache=uint32_t[:,:], + tilecache=uint8_t[:,:], bg_priority_apply=uint32_t, col0=uint8_t, + pixel=uint32_t, ) cdef void scanline(self, LCD, int) noexcept nogil + @cython.locals(tile_addr=uint64_t, tile=int) + cdef inline (int, int, uint16_t) _get_tile(self, uint8_t, uint8_t, uint16_t, LCD) noexcept nogil + cdef inline (int, int, uint8_t, bint, uint32_t, bint) _get_tile_cgb(self, uint8_t, uint8_t, uint16_t, LCD) noexcept nogil + @cython.locals(col0=uint8_t) + cdef inline void _pixel(self, uint8_t[:,:], uint32_t, int, int, int, int, uint32_t) noexcept nogil + cdef int scanline_background(self, int, int, int, int, int, LCD) noexcept nogil + cdef int scanline_window(self, int, int, int, int, int, LCD) noexcept nogil + cdef int scanline_background_cgb(self, int, int, int, int, int, LCD) noexcept nogil + cdef int scanline_window_cgb(self, int, int, int, int, int, LCD) noexcept nogil + cdef int scanline_blank(self, int, int, int, LCD) noexcept nogil + @cython.locals( spriteheight=int, spritecount=int, @@ -169,7 +185,7 @@ cdef class Renderer: yflip=bint, spritepriority=bint, palette=uint8_t, - spritecache=uint32_t[:,:], + spritecache=uint8_t[:,:], dy=int, dx=int, yy=int, @@ -180,7 +196,6 @@ cdef class Renderer: ) cdef void scanline_sprites(self, LCD, int, uint32_t[:,:], uint8_t[:,:], bint) noexcept nogil cdef void sort_sprites(self, int) noexcept nogil - cdef inline uint8_t color_code(self, uint8_t, uint8_t, uint8_t) noexcept nogil cdef void clear_cache(self) noexcept nogil cdef void clear_tilecache0(self) noexcept nogil @@ -194,7 +209,8 @@ cdef class Renderer: y=int, byte1=uint8_t, byte2=uint8_t, - colorcode=uint32_t, + colorcode_low=uint64_t, + colorcode_high=uint64_t, ) cdef void update_tilecache0(self, LCD, int, int) noexcept nogil @cython.locals( @@ -204,7 +220,8 @@ cdef class Renderer: y=int, byte1=uint8_t, byte2=uint8_t, - colorcode=uint32_t, + colorcode_low=uint64_t, + colorcode_high=uint64_t, ) cdef void update_tilecache1(self, LCD, int, int) noexcept nogil # CGB Only @cython.locals( @@ -214,7 +231,8 @@ cdef class Renderer: y=int, byte1=uint8_t, byte2=uint8_t, - colorcode=uint32_t, + colorcode_low=uint64_t, + colorcode_high=uint64_t, ) cdef void update_spritecache0(self, LCD, int, int) noexcept nogil @cython.locals( @@ -224,10 +242,13 @@ cdef class Renderer: y=int, byte1=uint8_t, byte2=uint8_t, - colorcode=uint32_t, + colorcode_low=uint64_t, + colorcode_high=uint64_t, ) cdef void update_spritecache1(self, LCD, int, int) noexcept nogil + @cython.locals(colorcode_low=uint64_t, colorcode_high=uint64_t) + cdef inline uint64_t colorcode(self, uint64_t, uint64_t) noexcept nogil cdef void save_state(self, IntIOInterface) noexcept cdef void load_state(self, IntIOInterface, int) noexcept @@ -240,7 +261,7 @@ cdef class Renderer: vertflip = uint8_t, bg_priority = uint8_t, ) - cdef (int, int, int, int, int) _cgb_get_background_map_attributes(self, LCD, int) noexcept nogil + cdef inline (int, int, int, int, int) _cgb_get_background_map_attributes(self, LCD, int) noexcept nogil cdef class CGBLCD(LCD): pass diff --git a/pyboy/core/lcd.py b/pyboy/core/lcd.py index 581f044bf..171d114c2 100644 --- a/pyboy/core/lcd.py +++ b/pyboy/core/lcd.py @@ -50,9 +50,6 @@ def __init__(self, cgb, cartridge_cgb, color_palette, cgb_color_palette, randomi self.LY = 0x00 self.LYC = 0x00 # self.DMA = 0x00 - self.BGP = PaletteRegister(0xFC) - self.OBP0 = PaletteRegister(0xFF) - self.OBP1 = PaletteRegister(0xFF) self.WY = 0x00 self.WX = 0x00 self.clock = 0 @@ -60,24 +57,26 @@ def __init__(self, cgb, cartridge_cgb, color_palette, cgb_color_palette, randomi self.frame_done = False self.double_speed = False self.cgb = cgb + self._scanlineparameters = [[0, 0, 0, 0, 0] for _ in range(ROWS)] if self.cgb: + # Setting for both modes, even though CGB is ignoring them. BGP[0] used in scanline_blank. + bg_pal, obj0_pal, obj1_pal = cgb_color_palette + self.BGP = PaletteRegister(0xFC, [(rgb_to_bgr(c)) for c in bg_pal]) + self.OBP0 = PaletteRegister(0xFF, [(rgb_to_bgr(c)) for c in obj0_pal]) + self.OBP1 = PaletteRegister(0xFF, [(rgb_to_bgr(c)) for c in obj1_pal]) if cartridge_cgb: logger.debug("Starting CGB renderer") self.renderer = CGBRenderer() else: logger.debug("Starting CGB renderer in DMG-mode") - # Running DMG ROM on CGB hardware use the default palettes - bg_pal, obj0_pal, obj1_pal = cgb_color_palette - self.BGP.palette_mem_rgb = [(rgb_to_bgr(c)) for c in bg_pal] - self.OBP0.palette_mem_rgb = [(rgb_to_bgr(c)) for c in obj0_pal] - self.OBP1.palette_mem_rgb = [(rgb_to_bgr(c)) for c in obj1_pal] + # Running DMG ROM on CGB hardware uses the palettes above self.renderer = Renderer(False) else: logger.debug("Starting DMG renderer") - self.BGP.palette_mem_rgb = [(rgb_to_bgr(c)) for c in color_palette] - self.OBP0.palette_mem_rgb = [(rgb_to_bgr(c)) for c in color_palette] - self.OBP1.palette_mem_rgb = [(rgb_to_bgr(c)) for c in color_palette] + self.BGP = PaletteRegister(0xFC, [(rgb_to_bgr(c)) for c in color_palette]) + self.OBP0 = PaletteRegister(0xFF, [(rgb_to_bgr(c)) for c in color_palette]) + self.OBP1 = PaletteRegister(0xFF, [(rgb_to_bgr(c)) for c in color_palette]) self.renderer = Renderer(False) def get_lcdc(self): @@ -166,6 +165,15 @@ def tick(self, cycles): elif self._STAT._mode == 0: # HBLANK self.clock_target += 206 * multiplier + # Recorded for API + bx, by = self.getviewport() + wx, wy = self.getwindowpos() + self._scanlineparameters[self.LY][0] = bx + self._scanlineparameters[self.LY][1] = by + self._scanlineparameters[self.LY][2] = wx + self._scanlineparameters[self.LY][3] = wy + self._scanlineparameters[self.LY][4] = self._LCDC.tiledata_select + self.renderer.scanline(self, self.LY) self.renderer.scanline_sprites( self, self.LY, self.renderer._screenbuffer, self.renderer._screenbuffer_attributes, False @@ -220,6 +228,14 @@ def save_state(self, f): f.write(self.WY) f.write(self.WX) + for y in range(ROWS): + f.write(self._scanlineparameters[y][0]) + f.write(self._scanlineparameters[y][1]) + # We store (WX - 7). We add 7 and mask 8 bits to make it easier to serialize + f.write((self._scanlineparameters[y][2] + 7) & 0xFF) + f.write(self._scanlineparameters[y][3]) + f.write(self._scanlineparameters[y][4]) + # CGB f.write(self.cgb) f.write(self.double_speed) @@ -258,6 +274,16 @@ def load_state(self, f, state_version): self.WY = f.read() self.WX = f.read() + if state_version >= 11: + for y in range(ROWS): + self._scanlineparameters[y][0] = f.read() + self._scanlineparameters[y][1] = f.read() + # Restore (WX - 7) as described above + self._scanlineparameters[y][2] = (f.read() - 7) & 0xFF + self._scanlineparameters[y][3] = f.read() + if state_version > 3: + self._scanlineparameters[y][4] = f.read() + # CGB if state_version >= 8: _cgb = f.read() @@ -288,11 +314,11 @@ def getviewport(self): class PaletteRegister: - def __init__(self, value): + def __init__(self, value, palette): self.value = 0 self.lookup = [0] * 4 + self.palette_mem_rgb = palette self.set(value) - self.palette_mem_rgb = [0] * 4 def set(self, value): # Pokemon Blue continuously sets this without changing the value @@ -301,14 +327,14 @@ def set(self, value): self.value = value for x in range(4): - self.lookup[x] = (value >> x * 2) & 0b11 + self.lookup[x] = self.palette_mem_rgb[(value >> x * 2) & 0b11] return True def get(self): return self.value def getcolor(self, i): - return self.palette_mem_rgb[self.lookup[i]] + return self.lookup[i] class STATRegister: @@ -367,6 +393,11 @@ def set(self, value): self.cgb_master_priority = self.background_enable # Different meaning on CGB # yapf: enable + # All VRAM addresses are offset by 0x8000 + # Following addresses are 0x9800 and 0x9C00 + self.backgroundmap_offset = 0x1800 if self.backgroundmap_select == 0 else 0x1C00 + self.windowmap_offset = 0x1800 if self.windowmap_select == 0 else 0x1C00 + def _get_sprite_height(self): return self.sprite_height @@ -388,9 +419,9 @@ def __init__(self, cgb): # Init buffers as white self._screenbuffer_raw = array("B", [0x00] * (ROWS*COLS*4)) self._screenbuffer_attributes_raw = array("B", [0x00] * (ROWS*COLS)) - self._tilecache0_raw = array("B", [0x00] * (TILES*8*8*4)) - self._spritecache0_raw = array("B", [0x00] * (TILES*8*8*4)) - self._spritecache1_raw = array("B", [0x00] * (TILES*8*8*4)) + self._tilecache0_raw = array("B", [0x00] * (TILES*8*8)) + self._spritecache0_raw = array("B", [0x00] * (TILES*8*8)) + self._spritecache1_raw = array("B", [0x00] * (TILES*8*8)) self.sprites_to_render = array("i", [0] * 10) self._tilecache0_state = array("B", [0] * TILES) @@ -400,14 +431,40 @@ def __init__(self, cgb): self._screenbuffer = memoryview(self._screenbuffer_raw).cast("I", shape=(ROWS, COLS)) self._screenbuffer_attributes = memoryview(self._screenbuffer_attributes_raw).cast("B", shape=(ROWS, COLS)) - self._tilecache0 = memoryview(self._tilecache0_raw).cast("I", shape=(TILES * 8, 8)) + self._tilecache0 = memoryview(self._tilecache0_raw).cast("B", shape=(TILES * 8, 8)) + self._tilecache0_64 = memoryview(self._tilecache0_raw).cast("Q", shape=(TILES * 8, )) + + # The look-up table only stored 4 bits from each byte, packed into a single byte + self.colorcode_table = array("I", [0x00000000] * (0x100)) # Should be "L"!? + """Convert 2 bytes into color code at a given offset. + + The colors are 2 bit and are found like this: + + Color of the first pixel is 0b10 + | Color of the second pixel is 0b01 + v v + 1 0 0 1 0 0 0 1 <- byte1 + 0 1 1 1 1 1 0 0 <- byte2 + """ + for byte in range(0x100): + byte1 = byte & 0xF + byte2 = (byte >> 4) & 0xF + v = 0 + for offset in range(4): + t = ((((byte2 >> (offset)) & 0b1) << 1) | ((byte1 >> (offset)) & 0b1)) + assert t < 4 + v |= t << (8 * (3-offset)) # Store them in little-endian + self.colorcode_table[byte] = v + # OBP0 palette - self._spritecache0 = memoryview(self._spritecache0_raw).cast("I", shape=(TILES * 8, 8)) + self._spritecache0 = memoryview(self._spritecache0_raw).cast("B", shape=(TILES * 8, 8)) + self._spritecache0_64 = memoryview(self._spritecache0_raw).cast("Q", shape=(TILES * 8, )) # OBP1 palette - self._spritecache1 = memoryview(self._spritecache1_raw).cast("I", shape=(TILES * 8, 8)) + self._spritecache1 = memoryview(self._spritecache1_raw).cast("B", shape=(TILES * 8, 8)) + self._spritecache1_64 = memoryview(self._spritecache1_raw).cast("Q", shape=(TILES * 8, )) + self._screenbuffer_ptr = c_void_p(self._screenbuffer_raw.buffer_info()[0]) - self._scanlineparameters = [[0, 0, 0, 0, 0] for _ in range(ROWS)] self.ly_window = 0 def _cgb_get_background_map_attributes(self, lcd, i): @@ -421,122 +478,161 @@ def _cgb_get_background_map_attributes(self, lcd, i): return palette, vbank, horiflip, vertflip, bg_priority def scanline(self, lcd, y): - bx, by = lcd.getviewport() - wx, wy = lcd.getwindowpos() - # TODO: Move to lcd class - self._scanlineparameters[y][0] = bx - self._scanlineparameters[y][1] = by - self._scanlineparameters[y][2] = wx - self._scanlineparameters[y][3] = wy - self._scanlineparameters[y][4] = lcd._LCDC.tiledata_select - if lcd.disable_renderer: return - # All VRAM addresses are offset by 0x8000 - # Following addresses are 0x9800 and 0x9C00 - background_offset = 0x1800 if lcd._LCDC.backgroundmap_select == 0 else 0x1C00 - wmap = 0x1800 if lcd._LCDC.windowmap_select == 0 else 0x1C00 - - # Used for the half tile at the left side when scrolling - offset = bx & 0b111 - - # Weird behavior, where the window has it's own internal line counter. It's only incremented whenever the - # window is drawing something on the screen. - if lcd._LCDC.window_enable and wy <= y and wx < COLS: - self.ly_window += 1 - - for x in range(COLS): - if lcd._LCDC.window_enable and wy <= y and wx <= x: - tile_addr = wmap + (self.ly_window) // 8 * 32 % 0x400 + (x-wx) // 8 % 32 - wt = lcd.VRAM0[tile_addr] - # If using signed tile indices, modify index - if not lcd._LCDC.tiledata_select: - # (x ^ 0x80 - 128) to convert to signed, then - # add 256 for offset (reduces to + 128) - wt = (wt ^ 0x80) + 128 - - bg_priority_apply = 0 - if self.cgb: - palette, vbank, horiflip, vertflip, bg_priority = self._cgb_get_background_map_attributes( - lcd, tile_addr - ) - if vbank: - self.update_tilecache1(lcd, wt, vbank) - tilecache = self._tilecache1 - else: - self.update_tilecache0(lcd, wt, vbank) - tilecache = self._tilecache0 + bx, by = lcd.getviewport() + wx, wy = lcd.getwindowpos() - xx = (7 - ((x-wx) % 8)) if horiflip else ((x-wx) % 8) - yy = (8*wt + (7 - (self.ly_window) % 8)) if vertflip else (8*wt + (self.ly_window) % 8) + x = 0 + if not self.cgb: + if lcd._LCDC.window_enable and wy <= y and wx < COLS: + # Window has it's own internal line counter. It's only incremented whenever the window is drawing something on the screen. + self.ly_window += 1 + + # Before window + if wx > x: + x += self.scanline_background(y, x, bx, by, wx, lcd) + + # Window hit + self.scanline_window(y, x, wx, wy, COLS - x, lcd) + elif lcd._LCDC.background_enable: + # No window + self.scanline_background(y, x, bx, by, COLS, lcd) + else: + self.scanline_blank(y, x, COLS, lcd) + else: + if lcd._LCDC.window_enable and wy <= y and wx < COLS: + # Window has it's own internal line counter. It's only incremented whenever the window is drawing something on the screen. + self.ly_window += 1 - pixel = lcd.bcpd.getcolor(palette, tilecache[yy, xx]) - col0 = (tilecache[yy, xx] == 0) & 1 - if bg_priority: - # We hide extra rendering information in the lower 8 bits (A) of the 32-bit RGBA format - bg_priority_apply = BG_PRIORITY_FLAG - else: - self.update_tilecache0(lcd, wt, 0) - xx = (x-wx) % 8 - yy = 8*wt + (self.ly_window) % 8 - pixel = lcd.BGP.getcolor(self._tilecache0[yy, xx]) - col0 = (self._tilecache0[yy, xx] == 0) & 1 - - self._screenbuffer[y, x] = pixel - # COL0_FLAG is 1 - self._screenbuffer_attributes[y, x] = bg_priority_apply | col0 - # self._screenbuffer_attributes[y, x] = bg_priority_apply - # if col0: - # self._screenbuffer_attributes[y, x] = self._screenbuffer_attributes[y, x] | col0 - # background_enable doesn't exist for CGB. It works as master priority instead - elif (not self.cgb and lcd._LCDC.background_enable) or self.cgb: - tile_addr = background_offset + (y+by) // 8 * 32 % 0x400 + (x+bx) // 8 % 32 - bt = lcd.VRAM0[tile_addr] - # If using signed tile indices, modify index - if not lcd._LCDC.tiledata_select: - # (x ^ 0x80 - 128) to convert to signed, then - # add 256 for offset (reduces to + 128) - bt = (bt ^ 0x80) + 128 - - bg_priority_apply = 0 - if self.cgb: - palette, vbank, horiflip, vertflip, bg_priority = self._cgb_get_background_map_attributes( - lcd, tile_addr - ) + # Before window + if wx > x: + x += self.scanline_background_cgb(y, x, bx, by, wx, lcd) - if vbank: - self.update_tilecache1(lcd, bt, vbank) - tilecache = self._tilecache1 - else: - self.update_tilecache0(lcd, bt, vbank) - tilecache = self._tilecache0 - xx = (7 - ((x+offset) % 8)) if horiflip else ((x+offset) % 8) - yy = (8*bt + (7 - (y+by) % 8)) if vertflip else (8*bt + (y+by) % 8) - - pixel = lcd.bcpd.getcolor(palette, tilecache[yy, xx]) - col0 = (tilecache[yy, xx] == 0) & 1 - if bg_priority: - # We hide extra rendering information in the lower 8 bits (A) of the 32-bit RGBA format - bg_priority_apply = BG_PRIORITY_FLAG - else: - self.update_tilecache0(lcd, bt, 0) - xx = (x+offset) % 8 - yy = 8*bt + (y+by) % 8 - pixel = lcd.BGP.getcolor(self._tilecache0[yy, xx]) - col0 = (self._tilecache0[yy, xx] == 0) & 1 - - self._screenbuffer[y, x] = pixel - self._screenbuffer_attributes[y, x] = bg_priority_apply | col0 - else: - # If background is disabled, it becomes white - self._screenbuffer[y, x] = lcd.BGP.getcolor(0) - self._screenbuffer_attributes[y, x] = 0 + # Window hit + self.scanline_window_cgb(y, x, wx, wy, COLS - x, lcd) + else: # background_enable doesn't exist for CGB. It works as master priority instead + # No window + self.scanline_background_cgb(y, x, bx, by, COLS, lcd) if y == 143: # Reset at the end of a frame. We set it to -1, so it will be 0 after the first increment self.ly_window = -1 + def _get_tile(self, y, x, offset, lcd): + tile_addr = offset + y//8*32%0x400 + x//8%32 + tile = lcd.VRAM0[tile_addr] + + # If using signed tile indices, modify index + if not lcd._LCDC.tiledata_select: + # (x ^ 0x80 - 128) to convert to signed, then + # add 256 for offset (reduces to + 128) + tile = (tile ^ 0x80) + 128 + + yy = 8*tile + y%8 + return tile, yy, tile_addr + + def _get_tile_cgb(self, y, x, offset, lcd): + tile, yy, tile_addr = self._get_tile(y, x, offset, lcd) + + palette, vbank, horiflip, vertflip, bg_priority = self._cgb_get_background_map_attributes(lcd, tile_addr) + + bg_priority_apply = 0 + if bg_priority: + # We hide extra rendering information in the lower 8 bits (A) of the 32-bit RGBA format + bg_priority_apply = BG_PRIORITY_FLAG + + if vertflip: + yy = (8*tile + (7 - (y) % 8)) + + return tile, yy, palette, horiflip, bg_priority_apply, vbank + + def _pixel(self, tilecache, pixel, x, y, xx, yy, bg_priority_apply): + col0 = (tilecache[yy, xx] == 0) & 1 + self._screenbuffer[y, x] = pixel + # COL0_FLAG is 1 + self._screenbuffer_attributes[y, x] = bg_priority_apply | col0 + + def scanline_window(self, y, _x, wx, wy, cols, lcd): + for x in range(_x, _x + cols): + xx = (x-wx) % 8 + if xx == 0 or x == _x: + wt, yy, _ = self._get_tile(self.ly_window, x - wx, lcd._LCDC.windowmap_offset, lcd) + self.update_tilecache0(lcd, wt, 0) + + pixel = lcd.BGP.getcolor(self._tilecache0[yy, xx]) + self._pixel(self._tilecache0, pixel, x, y, xx, yy, 0) + return cols + + def scanline_window_cgb(self, y, _x, wx, wy, cols, lcd): + bg_priority_apply = 0 + for x in range(_x, _x + cols): + xx = (x-wx) % 8 + if xx == 0 or x == _x: + wt, yy, w_palette, w_horiflip, bg_priority_apply, vbank = self._get_tile_cgb( + self.ly_window, x - wx, lcd._LCDC.windowmap_offset, lcd + ) + # NOTE: Not allowed to return memoryview in Cython tuple + if vbank: + self.update_tilecache1(lcd, wt, vbank) + tilecache = self._tilecache1 + else: + self.update_tilecache0(lcd, wt, vbank) + tilecache = self._tilecache0 + + if w_horiflip: + xx = 7 - xx + + pixel = lcd.bcpd.getcolor(w_palette, tilecache[yy, xx]) + self._pixel(tilecache, pixel, x, y, xx, yy, bg_priority_apply) + return cols + + def scanline_background(self, y, _x, bx, by, cols, lcd): + for x in range(_x, _x + cols): + # bx mask used for the half tile at the left side when scrolling + b_xx = (x + (bx & 0b111)) % 8 + if b_xx == 0 or x == 0: + bt, b_yy, _ = self._get_tile(y + by, x + bx, lcd._LCDC.backgroundmap_offset, lcd) + self.update_tilecache0(lcd, bt, 0) + + xx = b_xx + yy = b_yy + + pixel = lcd.BGP.getcolor(self._tilecache0[yy, xx]) + self._pixel(self._tilecache0, pixel, x, y, xx, yy, 0) + return cols + + def scanline_background_cgb(self, y, _x, bx, by, cols, lcd): + for x in range(_x, _x + cols): + # bx mask used for the half tile at the left side when scrolling + xx = (x + (bx & 0b111)) % 8 + if xx == 0 or x == 0: + bt, yy, b_palette, b_horiflip, bg_priority_apply, vbank = self._get_tile_cgb( + y + by, x + bx, lcd._LCDC.backgroundmap_offset, lcd + ) + # NOTE: Not allowed to return memoryview in Cython tuple + if vbank: + self.update_tilecache1(lcd, bt, vbank) + tilecache = self._tilecache1 + else: + self.update_tilecache0(lcd, bt, vbank) + tilecache = self._tilecache0 + + if b_horiflip: + xx = 7 - xx + + pixel = lcd.bcpd.getcolor(b_palette, tilecache[yy, xx]) + self._pixel(tilecache, pixel, x, y, xx, yy, bg_priority_apply) + return cols + + def scanline_blank(self, y, _x, cols, lcd): + # If background is disabled, it becomes white + for x in range(_x, _x + cols): + self._screenbuffer[y, x] = lcd.BGP.getcolor(0) + self._screenbuffer_attributes[y, x] = 0 + return cols + def sort_sprites(self, sprite_count): # Use insertion sort, as it has O(n) on already sorted arrays. This # functions is likely called multiple times with unchanged data. @@ -695,19 +791,6 @@ def clear_spritecache1(self): for i in range(TILES): self._spritecache1_state[i] = 0 - def color_code(self, byte1, byte2, offset): - """Convert 2 bytes into color code at a given offset. - - The colors are 2 bit and are found like this: - - Color of the first pixel is 0b10 - | Color of the second pixel is 0b01 - v v - 1 0 0 1 0 0 0 1 <- byte1 - 0 1 1 1 1 1 0 0 <- byte2 - """ - return (((byte2 >> (offset)) & 0b1) << 1) + ((byte1 >> (offset)) & 0b1) - def update_tilecache0(self, lcd, t, bank): if self._tilecache0_state[t]: return @@ -717,9 +800,7 @@ def update_tilecache0(self, lcd, t, bank): byte2 = lcd.VRAM0[t*16 + k + 1] y = (t*16 + k) // 2 - for x in range(8): - colorcode = self.color_code(byte1, byte2, 7 - x) - self._tilecache0[y, x] = colorcode + self._tilecache0_64[y] = self.colorcode(byte1, byte2) self._tilecache0_state[t] = 1 @@ -735,9 +816,7 @@ def update_spritecache0(self, lcd, t, bank): byte2 = lcd.VRAM0[t*16 + k + 1] y = (t*16 + k) // 2 - for x in range(8): - colorcode = self.color_code(byte1, byte2, 7 - x) - self._spritecache0[y, x] = colorcode + self._spritecache0_64[y] = self.colorcode(byte1, byte2) self._spritecache0_state[t] = 1 @@ -750,12 +829,15 @@ def update_spritecache1(self, lcd, t, bank): byte2 = lcd.VRAM0[t*16 + k + 1] y = (t*16 + k) // 2 - for x in range(8): - colorcode = self.color_code(byte1, byte2, 7 - x) - self._spritecache1[y, x] = colorcode + self._spritecache1_64[y] = self.colorcode(byte1, byte2) self._spritecache1_state[t] = 1 + def colorcode(self, byte1, byte2): + colorcode_low = self.colorcode_table[(byte1 & 0xF) | ((byte2 & 0xF) << 4)] + colorcode_high = self.colorcode_table[((byte1 >> 4) & 0xF) | (byte2 & 0xF0)] + return (colorcode_low << 32) | colorcode_high + def blank_screen(self, lcd): # If the screen is off, fill it with a color. for y in range(ROWS): @@ -764,29 +846,21 @@ def blank_screen(self, lcd): self._screenbuffer_attributes[y, x] = 0 def save_state(self, f): - for y in range(ROWS): - f.write(self._scanlineparameters[y][0]) - f.write(self._scanlineparameters[y][1]) - # We store (WX - 7). We add 7 and mask 8 bits to make it easier to serialize - f.write((self._scanlineparameters[y][2] + 7) & 0xFF) - f.write(self._scanlineparameters[y][3]) - f.write(self._scanlineparameters[y][4]) - for y in range(ROWS): for x in range(COLS): f.write_32bit(self._screenbuffer[y, x]) f.write(self._screenbuffer_attributes[y, x]) def load_state(self, f, state_version): - if state_version >= 2: + if 2 <= state_version < 11: + # Dummy reads to align scanline parameters. See LCD instead for y in range(ROWS): - self._scanlineparameters[y][0] = f.read() - self._scanlineparameters[y][1] = f.read() - # Restore (WX - 7) as described above - self._scanlineparameters[y][2] = (f.read() - 7) & 0xFF - self._scanlineparameters[y][3] = f.read() + f.read() + f.read() + f.read() + f.read() if state_version > 3: - self._scanlineparameters[y][4] = f.read() + f.read() if state_version >= 6: for y in range(ROWS): @@ -825,9 +899,10 @@ def __init__(self): self._tilecache1_state = array("B", [0] * TILES) Renderer.__init__(self, True) - self._tilecache1_raw = array("B", [0xFF] * (TILES*8*8*4)) + self._tilecache1_raw = array("B", [0xFF] * (TILES*8*8)) - self._tilecache1 = memoryview(self._tilecache1_raw).cast("I", shape=(TILES * 8, 8)) + self._tilecache1 = memoryview(self._tilecache1_raw).cast("B", shape=(TILES * 8, 8)) + self._tilecache1_64 = memoryview(self._tilecache1_raw).cast("Q", shape=(TILES * 8, )) self._tilecache1_state = array("B", [0] * TILES) self.clear_cache() @@ -856,8 +931,7 @@ def update_tilecache0(self, lcd, t, bank): byte2 = vram_bank[t*16 + k + 1] y = (t*16 + k) // 2 - for x in range(8): - self._tilecache0[y, x] = self.color_code(byte1, byte2, 7 - x) + self._tilecache0_64[y] = self.colorcode(byte1, byte2) self._tilecache0_state[t] = 1 @@ -874,8 +948,7 @@ def update_tilecache1(self, lcd, t, bank): byte2 = vram_bank[t*16 + k + 1] y = (t*16 + k) // 2 - for x in range(8): - self._tilecache1[y, x] = self.color_code(byte1, byte2, 7 - x) + self._tilecache1_64[y] = self.colorcode(byte1, byte2) self._tilecache1_state[t] = 1 @@ -892,8 +965,7 @@ def update_spritecache0(self, lcd, t, bank): byte2 = vram_bank[t*16 + k + 1] y = (t*16 + k) // 2 - for x in range(8): - self._spritecache0[y, x] = self.color_code(byte1, byte2, 7 - x) + self._spritecache0_64[y] = self.colorcode(byte1, byte2) self._spritecache0_state[t] = 1 @@ -910,8 +982,7 @@ def update_spritecache1(self, lcd, t, bank): byte2 = vram_bank[t*16 + k + 1] y = (t*16 + k) // 2 - for x in range(8): - self._spritecache1[y, x] = self.color_code(byte1, byte2, 7 - x) + self._spritecache1_64[y] = self.colorcode(byte1, byte2) self._spritecache1_state[t] = 1 @@ -1017,9 +1088,6 @@ def get(self): def getcolor(self, paletteindex, colorindex): # Each palette = 8 bytes or 4 colors of 2 bytes - # if not (paletteindex <= 7 and colorindex <= 3): - # logger.error("Palette Mem Index Error, tried: Palette %d color %d", paletteindex, colorindex) - return self.palette_mem_rgb[paletteindex*4 + colorindex] def save_state(self, f): diff --git a/pyboy/core/mb.py b/pyboy/core/mb.py index 1a07bcbf7..78264a527 100644 --- a/pyboy/core/mb.py +++ b/pyboy/core/mb.py @@ -336,9 +336,9 @@ def getitem(self, i): if self.bootrom_enabled and (i <= 0xFF or (self.cgb and 0x200 <= i < 0x900)): return self.bootrom.getitem(i) else: - return self.cartridge.getitem(i) + return self.cartridge.rombanks[self.cartridge.rombank_selected_low, i] elif 0x4000 <= i < 0x8000: # 16kB switchable ROM bank - return self.cartridge.getitem(i) + return self.cartridge.rombanks[self.cartridge.rombank_selected, i - 0x4000] elif 0x8000 <= i < 0xA000: # 8kB Video RAM if not self.cgb or self.lcd.vbk.active_bank == 0: return self.lcd.VRAM0[i - 0x8000] diff --git a/pyboy/plugins/debug.pxd b/pyboy/plugins/debug.pxd index 3c1d24b38..f876d0603 100644 --- a/pyboy/plugins/debug.pxd +++ b/pyboy/plugins/debug.pxd @@ -60,7 +60,7 @@ cdef class BaseDebugWindow(PyBoyWindowPlugin): cdef object buf_p @cython.locals(y=int, x=int, _y=int, _x=int) - cdef void copy_tile(self, uint32_t[:,:], int, int, int, uint32_t[:,:], bint, bint, uint32_t[:]) noexcept + cdef void copy_tile(self, uint8_t[:,:], int, int, int, uint32_t[:,:], bint, bint, uint32_t[:]) noexcept @cython.locals(i=int, tw=int, th=int, xx=int, yy=int) cdef void mark_tile(self, int, int, uint32_t, int, int, bint) noexcept @@ -75,7 +75,7 @@ cdef class TileViewWindow(BaseDebugWindow): cdef TileMap tilemap cdef uint32_t color - cdef uint32_t[:,:] tilecache # Fixing Cython locals + cdef uint8_t[:,:] tilecache # Fixing Cython locals cdef uint32_t[:] palette_rgb # Fixing Cython locals @cython.locals(mem_offset=uint16_t, tile_index=int, tile_column=int, tile_row=int) cdef void post_tick(self) noexcept @@ -91,7 +91,7 @@ cdef class TileViewWindow(BaseDebugWindow): cdef class TileDataWindow(BaseDebugWindow): cdef bint tilecache_select - cdef uint32_t[:,:] tilecache # Fixing Cython locals + cdef uint8_t[:,:] tilecache # Fixing Cython locals cdef uint32_t[:] palette_rgb # Fixing Cython locals @cython.locals(t=int, xx=int, yy=int) cdef void post_tick(self) noexcept @@ -113,7 +113,7 @@ cdef class SpriteWindow(BaseDebugWindow): @cython.locals(title=str) cdef void update_title(self) noexcept - cdef uint32_t[:,:] spritecache # Fixing Cython locals + cdef uint8_t[:,:] spritecache # Fixing Cython locals cdef uint32_t[:] palette_rgb # Fixing Cython locals cdef class SpriteViewWindow(BaseDebugWindow): diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index fec910499..f2c969134 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -1662,14 +1662,12 @@ def __setitem(self, start, stop, step, v, bank, is_single, is_bank): start -= 0x4000 stop -= 0x4000 # Cartridge ROM Banks - assert stop < 0x4000, "Out of bounds for reading ROM bank" + assert stop <= 0x4000, "Out of bounds for reading ROM bank" assert bank <= self.mb.cartridge.external_rom_count, "ROM Bank out of range" - # TODO: If you change a RAM value outside of the ROM banks above, the memory value will stay the same no matter - # what the game writes to the address. This can be used so freeze the value for health, cash etc. if bank == -1: assert start <= 0xFF, "Start address out of range for bootrom" - assert stop <= 0xFF, "Start address out of range for bootrom" + assert stop <= 0x100, "Start address out of range for bootrom" if not is_single: # Writing slice of memory space if hasattr(v, "__iter__"): @@ -1701,7 +1699,7 @@ def __setitem(self, start, stop, step, v, bank, is_single, is_bank): stop -= 0x8000 # CGB VRAM Banks assert self.mb.cgb or (bank == 0), "Selecting bank of VRAM is only supported for CGB mode" - assert stop < 0x2000, "Out of bounds for reading VRAM bank" + assert stop <= 0x2000, "Out of bounds for reading VRAM bank" assert bank <= 1, "VRAM Bank out of range" if bank == 0: @@ -1734,7 +1732,7 @@ def __setitem(self, start, stop, step, v, bank, is_single, is_bank): start -= 0xA000 stop -= 0xA000 # Cartridge RAM banks - assert stop < 0x2000, "Out of bounds for reading cartridge RAM bank" + assert stop <= 0x2000, "Out of bounds for reading cartridge RAM bank" assert bank <= self.mb.cartridge.external_ram_count, "ROM Bank out of range" if not is_single: # Writing slice of memory space @@ -1756,7 +1754,7 @@ def __setitem(self, start, stop, step, v, bank, is_single, is_bank): stop -= 0x1000 # CGB VRAM banks assert self.mb.cgb or (bank == 0), "Selecting bank of WRAM is only supported for CGB mode" - assert stop < 0x1000, "Out of bounds for reading VRAM bank" + assert stop <= 0x1000, "Out of bounds for reading VRAM bank" assert bank <= 7, "WRAM Bank out of range" if not is_single: # Writing slice of memory space diff --git a/pyboy/utils.pxd b/pyboy/utils.pxd index 2c7448581..0bf64ca44 100644 --- a/pyboy/utils.pxd +++ b/pyboy/utils.pxd @@ -31,7 +31,7 @@ cdef class IntIOWrapper(IntIOInterface): ############################################################## # Misc -cdef uint8_t color_code(uint8_t, uint8_t, uint8_t) noexcept nogil +cpdef uint8_t color_code(uint8_t, uint8_t, uint8_t) noexcept nogil ############################################################## # Window Events diff --git a/pyboy/utils.py b/pyboy/utils.py index ab8b2ff57..8dee8a1bb 100644 --- a/pyboy/utils.py +++ b/pyboy/utils.py @@ -5,7 +5,7 @@ __all__ = ["WindowEvent", "dec_to_bcd", "bcd_to_dec"] -STATE_VERSION = 10 +STATE_VERSION = 11 ############################################################## # Buffer classes @@ -116,10 +116,7 @@ def tell(self): # Misc -# TODO: Would a lookup-table increase performance? For example a lookup table of each 4-bit nibble? -# That's 16**2 = 256 values. Index calculated as: (byte1 & 0xF0) | ((byte2 & 0xF0) >> 4) -# and then: (byte1 & 0x0F) | ((byte2 & 0x0F) >> 4) -# Then could even be preloaded for each color palette +# NOTE: Legacy function. Use look-up table in Renderer def color_code(byte1, byte2, offset): """Convert 2 bytes into color code at a given offset. diff --git a/tests/test_external_api.py b/tests/test_external_api.py index 0d91420d7..d93883cb7 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -575,29 +575,3 @@ def test_button(default_rom): assert pyboy.events[0].event == WindowEvent.RELEASE_BUTTON_START pyboy.tick(1, False) assert len(pyboy.events) == 0 # No input - - -def test_get_set_override(default_rom): - pyboy = PyBoy(default_rom, window="null") - pyboy.set_emulation_speed(0) - pyboy.tick(1, False) - - assert pyboy.memory[0xFF40] == 0x00 - pyboy.memory[0xFF40] = 0x12 - assert pyboy.memory[0xFF40] == 0x12 - - assert pyboy.memory[0, 0x0002] == 0x42 # Taken from ROM bank 0 - assert pyboy.memory[0x0002] == 0xFF # Taken from bootrom - assert pyboy.memory[-1, 0x0002] == 0xFF # Taken from bootrom - pyboy.memory[-1, 0x0002] = 0x01 # Change bootrom - assert pyboy.memory[-1, 0x0002] == 0x01 # New value in bootrom - assert pyboy.memory[0, 0x0002] == 0x42 # Taken from ROM bank 0 - - pyboy.memory[0xFF50] = 1 # Disable bootrom - assert pyboy.memory[0x0002] == 0x42 # Taken from ROM bank 0 - - pyboy.memory[0, 0x0002] = 0x12 - assert pyboy.memory[0x0002] == 0x12 - assert pyboy.memory[0, 0x0002] == 0x12 - - pyboy.stop(save=False) diff --git a/tests/test_memoryview.py b/tests/test_memoryview.py index 88fc52c42..22054f11d 100644 --- a/tests/test_memoryview.py +++ b/tests/test_memoryview.py @@ -135,3 +135,63 @@ def test_cgb_banks(cgb_acid_file): # Any CGB file with pytest.raises(AssertionError): p.memory[8, 0xD000] = 1 # Only bank 0-7 + + +def test_get_set_override(default_rom): + pyboy = PyBoy(default_rom, window="null") + pyboy.set_emulation_speed(0) + pyboy.tick(1, False) + + assert pyboy.memory[0xFF40] == 0x00 + pyboy.memory[0xFF40] = 0x12 + assert pyboy.memory[0xFF40] == 0x12 + + assert pyboy.memory[0, 0x0002] == 0x42 # Taken from ROM bank 0 + assert pyboy.memory[0x0002] == 0xFF # Taken from bootrom + assert pyboy.memory[-1, 0x0002] == 0xFF # Taken from bootrom + pyboy.memory[-1, 0x0002] = 0x01 # Change bootrom + assert pyboy.memory[-1, 0x0002] == 0x01 # New value in bootrom + assert pyboy.memory[0, 0x0002] == 0x42 # Taken from ROM bank 0 + + pyboy.memory[0xFF50] = 1 # Disable bootrom + assert pyboy.memory[0x0002] == 0x42 # Taken from ROM bank 0 + + pyboy.memory[0, 0x0002] = 0x12 + assert pyboy.memory[0x0002] == 0x12 + assert pyboy.memory[0, 0x0002] == 0x12 + + pyboy.stop(save=False) + + +def test_boundaries(default_rom): + pyboy = PyBoy(default_rom, window="null") + pyboy.set_emulation_speed(0) + + # Boot ROM boundary - Expecting 0 to 0xFF both including to change + assert pyboy.memory[-1, 0x00] != 0 + assert pyboy.memory[-1, 0xFF] != 0 + pyboy.memory[-1, 0:0x100] = [0] * 0x100 # Clear boot ROM + with pytest.raises(AssertionError): + pyboy.memory[-1, 0:0x101] = [0] * 0x101 # Out of bounds + assert pyboy.memory[-1, 0x00] == 0 + assert pyboy.memory[-1, 0xFF] == 0 + + pyboy.memory[0xFF50] = 1 # Disable bootrom + + pyboy.memory[0, 0x0000] = 123 + pyboy.memory[0, 0x3FFF] = 123 + pyboy.memory[1, 0x4000] = 123 # Notice bank! [0,0x4000] would wrap around to 0 + + # ROM Bank 0 boundary - Expecting 0 to 0x3FFF both including to change + pyboy.memory[0, 0:0x4000] = [0] * 0x4000 + with pytest.raises(AssertionError): + pyboy.memory[0, 0:0x4001] = [0] * 0x4001 # Over boundary! + + # NOTE: Not specifying bank! Defaulting to 0 up to 0x3FFF and then 1 at 0x4000 + assert pyboy.memory[0x0000] == 0 + assert pyboy.memory[0x3FFF] == 0 + assert pyboy.memory[0x4000] == 123 + pyboy.memory[0, 0:0x4000] = [1] * 0x4000 + assert pyboy.memory[0x0000] == 1 + assert pyboy.memory[0x3FFF] == 1 + assert pyboy.memory[0x4000] == 123 diff --git a/tests/test_pyboy_lcd.py b/tests/test_pyboy_lcd.py index 283ef5d3b..d8a7e3b4f 100644 --- a/tests/test_pyboy_lcd.py +++ b/tests/test_pyboy_lcd.py @@ -8,7 +8,8 @@ import pytest -from pyboy.core.lcd import LCD +from pyboy.core.lcd import LCD, Renderer +from pyboy.utils import color_code is_pypy = platform.python_implementation() == "PyPy" @@ -81,6 +82,35 @@ def test_check_lyc(self): assert lcd._STAT.update_LYC(lcd.LYC, lcd.LY) == INTR_LCDC # Also trigger on second call assert lcd.get_stat() & 0b100 # LYC flag set + +@pytest.mark.skipif(not is_pypy, reason="This test requires access to internal registers not available in Cython") +class TestRenderer: + def test_colorcode_example(self): + renderer = Renderer(False) + + # Color of the first pixel is 0b10 + # | Color of the second pixel is 0b01 + # v v + # 1 0 0 1 0 0 0 1 <- byte1 + # 0 1 1 1 1 1 0 0 <- byte2 + b1_l = 0b0001 + b1_h = 0b1001 + b2_l = 0b1100 + b2_h = 0b0111 + assert renderer.colorcode_table[(b2_l << 4) | b1_l] == 0x01_00_02_02 + assert renderer.colorcode_table[(b2_h << 4) | b1_h] == 0x03_02_02_01 + + def test_colorcode_table(self): + renderer = Renderer(False) + + for byte1 in range(0x100): + for byte2 in range(0x100): + colorcode_low = renderer.colorcode_table[(byte1 & 0xF) | ((byte2 & 0xF) << 4)] + colorcode_high = renderer.colorcode_table[((byte1 >> 4) & 0xF) | (byte2 & 0xF0)] + for offset in range(4): + assert (colorcode_low >> (3-offset) * 8) & 0xFF == color_code(byte1, byte2, offset) + assert (colorcode_high >> (3-offset) * 8) & 0xFF == color_code(byte1, byte2, offset + 4) + # def test_tick(self): # lcd = LCD() # assert lcd.clock == 0