337. План прокрутки vram с привязкой ко времени
Разобраться в плане прокрутки v/t/x с привязкой ко времени, прежде чем менять пошаговое выполнение PPU.
Урок 337 из 356 · tests/chapter_13_scrolling/test_337_plan_timed_vram_scrolling.py
Файл для подтверждения после чтения
emulator/ppu/ppu.pyСправочные материалы
https://www.nesdev.org/wiki/PPU_scrolling
https://www.nesdev.org/wiki/PPU_scrolling#During_rendering
https://www.nesdev.org/wiki/PPU_rendering#Line-by-line_timingЗачем нужен этот шаг чтения
PPU.step() — подсистема, критичная к таймированию. Небольшая ошибка может дать изображение, которое выглядит почти правильным, хотя при этом используется неверная строка развёртки, таблица имён или позиция прокрутки. Прежде чем менять её, нам нужна устойчивая ментальная модель и чёткий план миграции.
Текущая проблема
Существующая область просмотра на уровне кадра декодирует temp_vram_addr (t) плюс fine X после завершения кадра. Это работает для простой синтетической прокрутки, но не подходит для игр, которые меняют прокрутку во время кадра.
Super Mario Bros. использует примерно такую схему:
visible rows 0-30:
fixed status-bar scroll
sprite-zero split:
CPU prepares another horizontal position
visible rows 31-239:
moving gameplay scrollОдного значения, полученного после кадра, недостаточно, чтобы описать обе области.
Есть и вторая проблема: t — это не исключительно значение прокрутки. $2006 (PPUADDR) также записывает t, пока игры загружают таблицы имён и палитры. Например:
CPU writes PPUADDR $3F00
-> t becomes $3F00Декодирование этого итогового значения как значения прокрутки может на короткое время выбрать неверную область просмотра. Именно поэтому при запуске или переходах между уровнями может казаться, что происходит быстрая прокрутка, хотя экран чёрный.
Четыре внутренних значения прокрутки
v = current VRAM address used by rendering
t = temporary VRAM address prepared by CPU register writes
x = fine horizontal pixel offset
w = first/second-write toggle for $2005 and $2006Интуитивная модель
t is the next address configuration being prepared.
v is the address currently moving through rendering.
x is the 0-7 pixel offset inside the first tile.
w remembers which half of a two-write register comes next.Распространённое заблуждение
t is the current scroll position for the entire frame.Правильная модель
CPU writes assemble t and x.
PPU timing copies selected fields from t into v.
Rendering advances v while tiles and scanlines are processed.Важные операции, привязанные ко времени
background-fetch dots, every 8 dots:
increment horizontal v
dot 256:
increment vertical v
dot 257:
copy horizontal fields from t into v
pre-render dots 280-304:
copy vertical fields from t into vГоризонтальные поля
coarse X
horizontal nametable bitВертикальные поля
coarse Y
fine Y
vertical nametable bitПочему копирование избирательно
В точке 257 должно обновляться только горизонтальное положение следующей строки развёртки. Копирование всего t заменило бы и вертикальное состояние в неправильный момент.
Почему мы записываем по строкам развёртки
Существующий отрисовщик таблиц имён уже формирует правильные исходные фреймбуферы RGB. Заменять его полноценным конвейером выборки пикселей по каждой точке пока не нужно. Вместо этого таймирование PPU будет записывать эффективную позицию v + x один раз для каждой видимой строки развёртки:
scanline 0 -> viewport X 0
scanline 1 -> viewport X 0
...
scanline 30 -> viewport X 0
scanline 31 -> viewport X 40
...После кадра высокоуровневый отрисовщик использует эти записанные позиции, копируя каждую выходную строку ровно один раз.
Деталь упреждающей выборки
В точке 1 значение v уже сдвинуто вперёд на два тайла, потому что точки 321-336 поместили первые два тайла фона в аппаратные сдвиговые регистры. Снимок строки развёртки должен компенсировать эти два инкремента coarse X при вычислении видимой области просмотра. Иначе весь фон окажется смещённым на 16 пикселей.
План совместимости
- сохранить существующее поведение регистров v, t, x, w и публичные вспомогательные функции отрисовки
- добавить чистые операции над адресами до их применения внутри PPU.step()
- сохранить старый путь области просмотра на уровне кадра как запасной вариант на время миграции
- добавить привязанную ко времени отрисовку по строкам развёртки только после того, как появится полный набор состояний кадра
- сохранить идентичный выбор строк для фреймбуфера и маски непрозрачности
- запускать
uv run pytestпосле каждого нумерованного шага
Предстоящие сфокусированные шаги
- чистый горизонтальный инкремент v и переключение таблицы имён при переполнении
- чистое копирование горизонтальных полей из t в v
- горизонтальные инкременты в точках выборки и копирование в точке 257
- чистый вертикальный инкремент с участием fine Y и строк 29-31
- чистое копирование вертикальных полей из t в v
- вертикальный инкремент в точке 256 и вертикальное копирование в предварительной строке развёртки
- эффективное состояние v + fine X для каждой видимой строки развёртки
- составление фреймбуфера с ограничением по строкам
- идентичное составление маски непрозрачности с ограничением по строкам
- учитывающая строки развёртки маска для планирования попадания нулевого спрайта
- ручная проверка в Super Mario Bros. строки состояния, движения, переходов и FPS
Граница точности
Этот подход моделирует таймирование адресов прокрутки и выбор области просмотра по строкам развёртки. Это ещё не полноценный отрисовщик фона с выборкой и сдвиговыми регистрами на уровне отдельных точек. Записи в регистры CPU также таймируются с текущей гранулярностью пошагового выполнения инструкций.
Обязательное действие после чтения
Добавьте ровно этот комментарий рядом с полями vram_addr, temp_vram_addr, fine_x и second_write_toggle в emulator/ppu/ppu.py:
# pass_test_337Не реализуйте изменения прокрутки на этом шаге. Комментарий — единственное изменение продакшн-файла, необходимое для прохождения Test 337.
Запустить этот урок
uv run pytest tests/chapter_13_scrolling/test_337_plan_timed_vram_scrolling.py -v