330. Композиция горизонтальной области просмотра фреймбуфера

Составить область просмотра 256x240 из двух соседних фреймбуферов nametable по горизонтали.

Урок 330 из 356 · tests/chapter_13_scrolling/test_330_compose_horizontal_framebuffer_viewport.py

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

emulator/rendering/background_viewport.py

Справочная документация

https://www.nesdev.org/wiki/PPU_scrolling
https://www.nesdev.org/wiki/PPU_nametables

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

Каждый отрисованный nametable имеет ширину 256 пикселей, но горизонтальная прокрутка может привести к тому, что один экран будет содержать пиксели из двух соседних nametable. Этот шаг рассматривает их как один логический фон 512x240 и выбирает из него область просмотра 256x240.

Модель координат

left framebuffer:  logical X   0-255
right framebuffer: logical X 256-511

Для каждого целевого X экрана

logical X = (viewport X + screen X) modulo 512

Операция по модулю оборачивает правый край логической пары обратно к её левому краю.

Пример стыка

viewport X = 200

screen X   0-55  <- left X 200-255
screen X  56-255 <- right X   0-199

Инварианты

  • оба исходных фреймбуфера имеют размер ровно 256x240
  • возвращаемый фреймбуфер имеет размер ровно 256x240
  • ни один из исходных фреймбуферов не изменяется
  • каждый выходной пиксель берётся из одной и той же строки Y одного исходного фреймбуфера

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

Эта функция не реализует зеркалирование nametable картриджа. PpuBus владеет зеркалированием адресов; эта функция лишь составляет уже отрисованных логических соседей.

Вне рамок

  • вертикальная прокрутка
  • композиция маски непрозрачности фона
  • чтения PPU или PpuBus
  • рендеринг тайлов nametable
  • интеграция с Console
  • pygame
  • точные переносы v/t по тактам

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

# emulator/rendering/background_viewport.py

from emulator.rendering.framebuffer import Framebuffer


def compose_horizontal_framebuffer_viewport(
    left: Framebuffer,
    right: Framebuffer,
    viewport_x: int,
) -> Framebuffer:
    expected_size = (
        NAMETABLE_PIXEL_WIDTH,
        NAMETABLE_PIXEL_HEIGHT,
    )

    if (left.width, left.height) != expected_size:
        raise ValueError("Left nametable framebuffer must be 256x240")

    if (right.width, right.height) != expected_size:
        raise ValueError("Right nametable framebuffer must be 256x240")

    logical_width = NAMETABLE_PIXEL_WIDTH * 2
    result = Framebuffer(
        width=NAMETABLE_PIXEL_WIDTH,
        height=NAMETABLE_PIXEL_HEIGHT,
    )

    for screen_y in range(NAMETABLE_PIXEL_HEIGHT):
        for screen_x in range(NAMETABLE_PIXEL_WIDTH):
            logical_x = (viewport_x + screen_x) % logical_width

            if logical_x < NAMETABLE_PIXEL_WIDTH:
                source = left
                source_x = logical_x
            else:
                source = right
                source_x = logical_x - NAMETABLE_PIXEL_WIDTH

            color = source.get_pixel(source_x, screen_y)
            result.set_pixel(screen_x, screen_y, color)

    return result

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

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