304. Render one sprite 8x8

Render one 8x8 sprite into framebuffer data.

Lesson 304 of 356 · tests/chapter_09_sprite_rendering/test_304_render_one_sprite_8x8.py

File to update

emulator/rendering/sprite_renderer.py

Why this step exists

The previous sprite steps decoded OAM entries, decoded sprite attributes, and built sprite palettes. Now we draw one sprite into a pure Framebuffer.

This is the first sprite step that produces visible pixel data, but it is still small and controlled:

one SpriteEntry
one CHR tile
one selected sprite palette
one Framebuffer target

Suggested implementation example

def render_sprite_8x8_to_framebuffer(
    framebuffer: Framebuffer,
    sprite: SpriteEntry,
    pattern_table: bytes,
    sprite_palettes: SpritePalettes,
) -> None:
    attributes = decode_sprite_attributes(sprite.attributes)

    tile_start = sprite.tile_index * 16
    tile_end = tile_start + 16

    if tile_end > len(pattern_table):
        raise ValueError("Pattern table does not contain sprite tile bytes")

    tile_bytes = pattern_table[tile_start:tile_end]
    color_indexes = decode_chr_tile(tile_bytes)
    palette = sprite_palettes[attributes.palette_id]

    for tile_y in range(8):
        for tile_x in range(8):
            color_index = color_indexes[tile_y][tile_x]

            if color_index == 0:
                continue

            screen_x = sprite.x + tile_x
            screen_y = sprite.y + tile_y

            if not (0 <= screen_x < framebuffer.width):
                continue
            if not (0 <= screen_y < framebuffer.height):
                continue

            framebuffer.set_pixel(screen_x, screen_y, palette[color_index])

Important transparency rule

For sprites, CHR color index 0 is transparent. It must not overwrite the existing framebuffer pixel.

Important Y-position simplification

Real NES sprite Y positioning has a hardware quirk where the stored OAM Y value is not exactly the visible top scanline. This tutorial step uses raw sprite.y as the screen Y position. More accurate timing/position behavior can come later.

Out of scope

  • horizontal/vertical flip support
  • sprite priority behind background
  • rendering all 64 sprites
  • background/sprite compositing policy
  • sprite 0 hit
  • sprite overflow
  • pygame

Run this lesson

uv run pytest tests/chapter_09_sprite_rendering/test_304_render_one_sprite_8x8.py -v