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