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 @@
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 @@
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