Skip to content

Commit

Permalink
Cythonized sprite sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
Baekalfen committed Nov 1, 2023
1 parent c2ee61b commit 9f986ca
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 24 deletions.
11 changes: 7 additions & 4 deletions pyboy/core/lcd.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
#

import cython

from cpython.array cimport array
from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t, int16_t
from libc.stdint cimport int16_t, uint8_t, uint16_t, uint32_t, uint64_t

cimport pyboy.utils
from pyboy cimport utils
from pyboy.utils cimport IntIOInterface


cdef uint8_t INTR_VBLANK, INTR_LCDC, INTR_TIMER, INTR_SERIAL, INTR_HIGHTOLOW
cdef uint16_t LCDC, STAT, SCY, SCX, LY, LYC, DMA, BGP, OBP0, OBP1, WY, WX
cdef int ROWS, COLS, TILES, FRAME_CYCLES, VIDEO_RAM, OBJECT_ATTRIBUTE_MEMORY
Expand Down Expand Up @@ -116,9 +120,7 @@ cdef class Renderer:
cdef uint32_t[:,:] _screenbuffer
cdef uint32_t[:,:] _tilecache0, _spritecache0, _spritecache1

cdef int[:] sprites_to_render_n
cdef int[:] sprites_to_render_x
cpdef (int, int) key_priority(self, int) noexcept
cdef int[10] sprites_to_render
cdef int ly_window
cdef void invalidate_tile(self, int, int) noexcept

Expand Down Expand Up @@ -169,6 +171,7 @@ cdef class Renderer:
_n=int,
)
cdef void scanline_sprites(self, LCD, int, uint32_t[:,:], bint) noexcept
cdef void sort_sprites(self, int) noexcept

cdef void clear_cache(self) noexcept
cdef void clear_tilecache0(self) noexcept
Expand Down
51 changes: 31 additions & 20 deletions pyboy/core/lcd.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ def __init__(self, cgb):
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.sprites_to_render = array("i", [0] * 10)

self.clear_cache()

Expand Down Expand Up @@ -533,31 +534,41 @@ def scanline(self, lcd, y):
# 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 key_priority(self, x):
# NOTE: Cython is being insufferable, and demands a non-lambda function
return (self.sprites_to_render_x[x], self.sprites_to_render_n[x])
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.
# Sort descending because of the sprite priority.

for i in range(1, sprite_count):
key = self.sprites_to_render[i] # The current element to be inserted into the sorted portion
j = i - 1 # Index of the last element in the sorted portion of the array

# Move elements of the sorted portion greater than the key to the right
while j >= 0 and key > self.sprites_to_render[j]:
self.sprites_to_render[j + 1] = self.sprites_to_render[j]
j -= 1

# Insert the key into its correct position in the sorted portion
self.sprites_to_render[j + 1] = key

def scanline_sprites(self, lcd, ly, buffer, ignore_priority):
if not lcd._LCDC.sprite_enable or lcd.disable_renderer:
return

spriteheight = 16 if lcd._LCDC.sprite_height else 8

sprite_count = 0
self.sprites_to_render_n = array("i", [0] * 10)
self.sprites_to_render_x = array("i", [0] * 10)

# Find the first 10 sprites in OAM that appears on this scanline.
# The lowest X-coordinate has priority, when overlapping

# Loop through OAM, find 10 first sprites for scanline. Order based on X-coordinate high-to-low. Render them.
spriteheight = 16 if lcd._LCDC.sprite_height else 8
sprite_count = 0
for n in range(0x00, 0xA0, 4):
y = lcd.OAM[n] - 16 # Documentation states the y coordinate needs to be subtracted by 16
x = lcd.OAM[n + 1] - 8 # Documentation states the x coordinate needs to be subtracted by 8

if y <= ly < y + spriteheight:
self.sprites_to_render_n[sprite_count] = n
self.sprites_to_render_x[sprite_count] = x # Used for sorting for priority
# x is used for sorting for priority
if self.cgb:
self.sprites_to_render[sprite_count] = n << 16 | x
else:
self.sprites_to_render[sprite_count] = x << 16 | n
sprite_count += 1

if sprite_count == 10:
Expand All @@ -568,10 +579,14 @@ def scanline_sprites(self, lcd, ly, buffer, ignore_priority):
# Z-fighting.) In CGB mode, the first sprite in OAM ($FE00-$FE03) has the highest priority, and so on. In
# Non-CGB mode, the smaller the X coordinate, the higher the priority. The tie breaker (same X coordinates) is
# the same priority as in CGB mode.
sprites_priority = sorted(range(sprite_count), key=self.key_priority)
self.sort_sprites(sprite_count)

for _n in sprites_priority[::-1]:
n = self.sprites_to_render_n[_n]
for _n in self.sprites_to_render[:sprite_count]:
if self.cgb:
n = _n >> 16
else:
n = _n & 0xFF
# n = self.sprites_to_render_n[_n]
y = lcd.OAM[n] - 16 # Documentation states the y coordinate needs to be subtracted by 16
x = lcd.OAM[n + 1] - 8 # Documentation states the x coordinate needs to be subtracted by 8
tileindex = lcd.OAM[n + 2]
Expand Down Expand Up @@ -797,10 +812,6 @@ def __init__(self):

self.clear_cache()

def key_priority(self, x):
# Define sprite sorting for CGB
return (self.sprites_to_render_n[x], self.sprites_to_render_x[x])

def clear_cache(self):
self.clear_tilecache0()
self.clear_tilecache1()
Expand Down

0 comments on commit 9f986ca

Please sign in to comment.