321. PPU устанавливает запланированное срабатывание sprite zero hit

Пусть тайминг PPU устанавливает sprite 0 hit в ранее обнаруженной позиции экрана.

Урок 321 из 356 · tests/chapter_11_sprite_zero_hit/test_321_ppu_sets_scheduled_sprite_zero_hit.py

Файл для обновления

emulator/ppu/ppu.py

Зачем нужен этот шаг

Шаг 320 умеет находить первый перекрывающийся пиксель спрайта 0/фона и возвращать его позицию на экране:

(screen_x, screen_y)

PPU не должен устанавливать бит 6 PPUSTATUS сразу же при обнаружении этой позиции. Он должен сохранить позицию и установить флаг только тогда, когда эмулируемый тайминг PPU достигнет соответствующего видимого пикселя.

Упрощённое отображение координат

screen y -> PPU scanline y
screen x -> PPU cycle x + 1

Координаты видимого фреймбуфера начинаются с x=0, тогда как упрощённый тайминг PPU в этом проекте считает, что видимый вывод начинается с цикла 1:

screen x=0 -> PPU cycle 1
screen x=1 -> PPU cycle 2
screen x=40 -> PPU cycle 41

Предлагаемые изменения реализации

# --- NEW LINE ---
SpriteZeroHitPosition = tuple[int, int]
# --- END NEW LINE ---


@dataclass
class PPU:
    ...
    scanline: int = 0
    frame: int = 0
    nmi_requested: bool = False
    # --- NEW BLOCK ---
    sprite_zero_hit_position: SpriteZeroHitPosition | None = None

    def set_sprite_zero_hit_position(
        self,
        position: SpriteZeroHitPosition | None,
    ) -> None:
        self.sprite_zero_hit_position = position
    # --- END NEW BLOCK ---

    def step(self, cycles: int = 1) -> None:
        ...
        for _ in range(cycles):
            self.cycle += 1

            # --- NEW BLOCK ---
            if self.sprite_zero_hit_position is not None:
                hit_x, hit_y = self.sprite_zero_hit_position

                if self.scanline == hit_y and self.cycle == hit_x + 1:
                    self.status |= SPRITE_ZERO_HIT
                    self.sprite_zero_hit_position = None
            # --- END NEW BLOCK ---

            ...

Зачем потреблять позицию? Позиция описывает одно будущее событие тайминга. После срабатывания события установка её в None предотвращает повторное срабатывание того же запланированного события в более позднем кадре. Сам флаг PPUSTATUS остаётся установленным до очистки на пре-рендере из шага 319.

Важное различие

set_sprite_zero_hit_position((x, y))
    stores a future position

PPU.step()
    sets SPRITE_ZERO_HIT when timing reaches that position

Важная граница

PPU получает только нейтральный tuple[int, int] | None. Он не должен импортировать модули рендеринга или знать, как было обнаружено перекрытие CHR/фона.

Вне рамок

  • подключение в Console
  • вызов find_sprite_zero_hit_position()
  • выбор таблиц шаблонов спрайта/фона
  • правила включения рендеринга PPUMASK
  • аппаратное исключение x=255
  • коррекция OAM Y+1
  • проверка на Super Mario Bros.

Запустить этот урок

uv run pytest tests/chapter_11_sprite_zero_hit/test_321_ppu_sets_scheduled_sprite_zero_hit.py -v