310. Background opaque mask

Build a background opacity mask from nametable and pattern table data.

Lesson 310 of 356 · tests/chapter_09_sprite_rendering/test_310_background_opaque_mask.py

File to update

emulator/rendering/nametable_renderer.py

Why this step exists

Sprites have a priority bit that can place them behind non-transparent background pixels. To implement that correctly, the compositor needs to know whether each background pixel is opaque.

RGB framebuffer data alone is not enough because once a background pixel is converted to RGB, we lose the original CHR color index.

This step adds a pure mask

BackgroundOpaqueMask = list[bool]

Where

False -> background CHR color index was 0
True  -> background CHR color index was 1, 2, or 3

Suggested implementation example

BackgroundOpaqueMask = list[bool]


def build_background_opaque_mask(
    pattern_table: bytes,
    nametable: bytes,
) -> BackgroundOpaqueMask:
    if len(nametable) != NAMETABLE_SIZE:
        raise ValueError("Nametable must be 960 bytes")

    decoded_tiles = decode_pattern_table(pattern_table)

    opaque_mask: BackgroundOpaqueMask = [False] * (
        BACKGROUND_WIDTH * BACKGROUND_HEIGHT
    )

    for tile_y in range(NAMETABLE_ROWS):
        for tile_x in range(NAMETABLE_TILES_PER_ROW):
            tile_index = nametable[tile_y * NAMETABLE_TILES_PER_ROW + tile_x]
            tile = decoded_tiles[tile_index]

            for pixel_y in range(CHR_TILE_HEIGHT):
                for pixel_x in range(CHR_TILE_WIDTH):
                    color_index = tile[pixel_y][pixel_x]

                    screen_x = tile_x * CHR_TILE_WIDTH + pixel_x
                    screen_y = tile_y * CHR_TILE_HEIGHT + pixel_y

                    mask_index = screen_y * BACKGROUND_WIDTH + screen_x
                    opaque_mask[mask_index] = color_index != 0

    return opaque_mask

Why no attribute table or palette RAM? Opacity depends only on the CHR color index. Attribute table and palette RAM decide which colors are displayed, but they do not change whether a background pixel's CHR color index is zero or nonzero.

Out of scope

  • applying sprite priority
  • changing the framebuffer compositor
  • sprite rendering
  • sprite 0 hit
  • sprite overflow
  • pygame

Run this lesson

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