355. Кэширование пикселей кадрового буфера nametable

Кэшировать повторные пиксели кадрового буфера nametable по точным графическим входным данным.

Урок 355 из 356 · tests/chapter_14_optimizations/test_355_cache_nametable_framebuffer_pixels.py

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

emulator/rendering/nametable_renderer.py

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

Фронтенд запрашивает у эмулятора полное фоновое изображение для каждого отображаемого кадра. Формирование одного логического изображения nametable требует декодирования данных паттернов, выбора палитры тайлов из таблицы атрибутов и записи 256×240 RGB-пикселей. Прокрутка может изменять видимую область соседних nametable без изменения исходных графических данных внутри каждого из них, поэтому последовательные кадры часто запрашивают одно и то же ресурсоёмкое исходное изображение.

При 60 FPS один полный кадр располагает примерно 16,67 мс на шаги CPU/PPU, отрисовку фона и спрайтов, загрузку кадрового буфера и вывод на экран. Перестроение 61 440 пикселей из неизменённых байтов расходует этот ограниченный бюджет, не создавая новой информации. Ограниченный кэш с адресацией по содержимому превращает повторные исходные отрисовки в поиск по неизменяемым пикселям плюс копирование списка, высвобождая время кадра для работы, которая действительно изменилась.

Имена и обязанности

- No existing function is renamed.
- nametable_with_attributes_to_framebuffer() remains unchanged. It is still the
  lower-level operation that performs the expensive render on a cache miss.
- nametable_with_palette_ram_to_framebuffer() keeps its public name and signature,
  but its body changes into an ownership-preserving wrapper around cached pixels.
- _cached_nametable_with_palette_ram_pixels() is a new private helper containing
  the old public function's palette-building and rendering work.

Полная реализация

# functools.lru_cache is already imported for Test 354. Add this import if the
# previous step is being reproduced independently:
from functools import lru_cache


@lru_cache(maxsize=8)
def _cached_nametable_with_palette_ram_pixels(
    nametable_bytes: bytes,
    attribute_table: bytes,
    pattern_table_bytes: bytes,
    palette_ram: bytes,
) -> tuple[RGBColor, ...]:
    background_palettes = build_background_palettes_from_palette_ram(
        palette_ram
    )

    framebuffer = nametable_with_attributes_to_framebuffer(
        nametable_bytes,
        attribute_table,
        pattern_table_bytes,
        background_palettes,
    )

    # The cache must own immutable data so callers cannot poison later hits.
    return tuple(framebuffer.pixels)


def nametable_with_palette_ram_to_framebuffer(
    nametable_bytes: bytes,
    attribute_table: bytes,
    pattern_table_bytes: bytes,
    palette_ram: bytes,
) -> Framebuffer:
    cached_pixels = _cached_nametable_with_palette_ram_pixels(
        nametable_bytes,
        attribute_table,
        pattern_table_bytes,
        palette_ram,
    )

    # Preserve the historical mutable ownership contract.
    return Framebuffer(
        width=BACKGROUND_WIDTH,
        height=BACKGROUND_HEIGHT,
        pixels=list(cached_pixels),
    )

Где редактировать

Замените существующее тело nametable_with_palette_ram_to_framebuffer() обёрткой, показанной выше, и добавьте рядом новый приватный кэшированный хелпер. Не оставляйте старые операторы построения палитры и рендеринга в публичной обёртке, поскольку это выполнило бы ресурсоёмкую работу до обращения к кэшу.

Граница проверки типов

nametable_with_attributes_to_framebuffer() возвращает Framebuffer, поэтому прямой возврат этого вызова из _cached_nametable_with_palette_ram_pixels() нарушает объявленный тип возврата tuple[RGBColor, ...]. Сначала привяжите Framebuffer к показанной выше локальной переменной, затем верните tuple(framebuffer.pixels). Не меняйте аннотацию кэшированного хелпера на Framebuffer, поскольку это привело бы к кэшированию изменяемого публичного состояния.

Важные инварианты

  • все четыре точных неизменяемых байтовых входа участвуют в ключе кэша
  • равное содержимое байтов переиспользует работу, даже если объекты имеют разную идентичность
  • изменение любого графического входа вызывает промах кэша
  • кэшированные пиксели неизменяемы
  • публичные объекты Framebuffer и списки пикселей имеют независимое владение
  • кэш ограничен восемью записями

Влияние на производительность

Попадания в кэш исключают декодирование паттернов, реконструкцию палитры, обход тайлов и генерацию RGB-пикселей. Промахи кэша сохраняют исходный путь отрисовки, так что изменение любых визуальных входных данных по-прежнему даёт свежие и корректные пиксели. Ограничение размера контролирует использование памяти, сохраняя недавно переиспользованные изображения nametable.

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

Не кэшируйте и не возвращайте один изменяемый экземпляр Framebuffer. Механизм ускорения заключается в переиспользовании неизменяемого содержимого пикселей, а не в разделении изменяемого владения кадром.

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

uv run pytest tests/chapter_14_optimizations/test_355_cache_nametable_framebuffer_pixels.py -v