340. Таймирование vram по горизонтали

Применить горизонтальные инкременты v и горизонтальное копирование из t в v в точках таймирования PPU.

Урок 340 из 356 · tests/chapter_13_scrolling/test_340_horizontal_vram_timing.py

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

emulator/ppu/ppu.py

Справочные материалы

https://www.nesdev.org/wiki/PPU_scrolling#During_rendering
https://www.nesdev.org/wiki/PPU_rendering#Line-by-line_timing

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

Шаги 338 и 339 реализовали чистые механизмы горизонтальных адресов. Этот шаг привязывает их ко времени PPU.

На видимых и предварительных строках развёртки при включённой отрисовке

dots 8, 16, 24, ... 256:
    increment horizontal v after each fetched tile

dot 257:
    copy coarse X and horizontal nametable from t into v

dots 328 and 336:
    increment horizontal v while prefetching the first tiles for the next scanline

Отрисовка считается включённой, когда в PPUMASK включена отрисовка либо фона, либо спрайтов. Адресное таймирование неактивно в строках развёртки пост-рендера и VBlank.

Упрощённая временная шкала

1---------256 257 --------320 321------336 ----340
tile fetches  reload X          prefetch tiles
  ^ every 8                     ^ 328 and 336

Важный порядок операций

PPU.step() сначала продвигает self.cycle. Полученное значение рассматривается как текущая точка, и лишь затем применяется горизонтальное адресное таймирование. Поэтому PPU, начинающий с cycle 256, выполняет горизонтальную перезагрузку после одного шага в точке 257.

Распространённое заблуждение

Горизонтальные инкременты не ограничены видимыми пикселями. Точки 328 и 336 подготавливают первые два тайла фона для следующей строки развёртки и также инкрементируют v.

Вне объёма работ

  • вертикальный инкремент в точке 256
  • вертикальное копирование из t в v во время предварительной строки развёртки
  • запись области просмотра по строкам развёртки
  • изменения фреймбуфера и маски непрозрачности

Полный пример реализации

# emulator/ppu/ppu.py

class PPU:
    ...

    # --- NEW BLOCK: APPLY HORIZONTAL ADDRESS TIMING ---
    def _step_horizontal_rendering_address(self) -> None:
        rendering_enabled = self.mask & (
            MASK_SHOW_BACKGROUND | MASK_SHOW_SPRITES
        )
        if not rendering_enabled:
            return

        rendering_scanline = (
            0 <= self.scanline < 240
            or self.scanline == PPU_PRE_RENDER_SCANLINE
        )
        if not rendering_scanline:
            return

        visible_fetch_increment = (
            1 <= self.cycle <= 256
            and self.cycle % 8 == 0
        )
        prefetch_increment = (
            321 <= self.cycle <= 336
            and self.cycle % 8 == 0
        )

        if visible_fetch_increment or prefetch_increment:
            self.vram_addr = increment_horizontal_vram_addr(
                self.vram_addr
            )

        if self.cycle == 257:
            self.vram_addr = copy_horizontal_scroll_bits(
                self.vram_addr,
                self.temp_vram_addr,
            )

    def step(self, cycles: int = 1) -> None:
        ...

        for _ in range(cycles):
            self.cycle += 1

            # --- NEW LINE: APPLY TIMING AT THE CURRENT DOT ---
            self._step_horizontal_rendering_address()

            ...

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

uv run pytest tests/chapter_13_scrolling/test_340_horizontal_vram_timing.py -v