Пиксели и фреймбуфер

Любая картинка на экране — это сетка цветных точек, которую программа заполняет числами.

Фреймбуфер — область памяти, где для каждого пикселя экрана хранится его цвет; видеосистема читает её и выводит на дисплей.

Зачем это знать

Когда вы пишете шейдер, вы в конечном счёте управляете тем, какое число попадёт в каждую ячейку фреймбуфера. Не понимая, что экран — это просто прямоугольный массив чисел, легко запутаться в том, «откуда берётся цвет». Всё, что делает видеокарта при рисовании 3D-сцены, заканчивается одним: записью цвета в пиксели буфера кадра.

Что такое пиксель

Пиксель (от picture element) — наименьшая адресуемая точка растрового изображения. Экран 1920×1080 содержит 1920 столбцов и 1080 строк, итого 2 073 600 пикселей. Цвет одного пикселя чаще всего хранят четырьмя байтами: красный, зелёный, синий и альфа (прозрачность) — формат RGBA8. Каждый канал — число от 0 до 255.

Сетка пикселей 4x3 (каждая клетка — один пиксель):

  x=0   x=1   x=2   x=3
 +-----+-----+-----+-----+
 | RGBA| RGBA| RGBA| RGBA|  y=0
 +-----+-----+-----+-----+
 | RGBA| RGBA| RGBA| RGBA|  y=1
 +-----+-----+-----+-----+
 | RGBA| RGBA| RGBA| RGBA|  y=2
 +-----+-----+-----+-----+

Цвет белого пикселя в RGBA8 — это (255, 255, 255, 255); чёрного непрозрачного — (0, 0, 0, 255). Чистый красный — (255, 0, 0, 255).

Сколько памяти занимает кадр

Память под фреймбуфер легко посчитать: ширина × высота × байт_на_пиксель. Проверим на Python для разрешения Full HD при 4 байтах на пиксель.

width, height = 1920, 1080
bytes_per_pixel = 4  # RGBA8
total_bytes = width * height * bytes_per_pixel
print("Пикселей:", width * height)
print("Байт:", total_bytes)
print("Мегабайт:", round(total_bytes / (1024 * 1024), 2))

Вывод:

Пикселей: 2073600
Байт: 8294400
Мегабайт: 7.91

Почти 8 МБ на один кадр. Видеокарта хранит и переписывает такие буферы десятки раз в секунду, и это ещё без буфера глубины и промежуточных целей рендеринга.

Адресация: от (x, y) к индексу

В памяти буфер — это одномерный массив байтов. Чтобы добраться до пикселя (x, y), его координаты переводят в линейный индекс. Это та же арифметика, что и при работе с двумерными массивами.

def pixel_offset(x, y, width, bpp=4):
    return (y * width + x) * bpp

w = 1920
print("Пиксель (0,0)   -> байт", pixel_offset(0, 0, w))
print("Пиксель (1,0)   -> байт", pixel_offset(1, 0, w))
print("Пиксель (0,1)   -> байт", pixel_offset(0, 1, w))
print("Пиксель (100,50)-> байт", pixel_offset(100, 50, w))

Вывод:

Пиксель (0,0)   -> байт 0
Пиксель (1,0)   -> байт 4
Пиксель (0,1)   -> байт 7680
Пиксель (100,50)-> байт 384400

Соседний по горизонтали пиксель отстоит на 4 байта, а спуск на строку вниз прибавляет целую ширину строки (1920×4 = 7680 байт).

Как работает под капотом

Обычно используют двойную буферизацию: пока монитор показывает один буфер (front buffer), видеокарта рисует следующий кадр в другой (back buffer). Когда кадр готов, буферы меняют местами — «swap». Если менять их строго в момент, когда монитор закончил отрисовку строки (вертикальная синхронизация, VSync), картинка не «рвётся». Без синхронизации вы увидите tearing — горизонтальный разрыв, где верх кадра принадлежит старому изображению, а низ уже новому.

Частые ошибки

  • Путать пиксель и «точку» цвета в файле PNG: в сжатых форматах цвет хранится иначе, фреймбуфер же всегда несжатый массив.
  • Забывать про альфа-канал и считать пиксель тремя байтами — выравнивание поедет, индексы сместятся.
  • Считать, что (0,0) всегда в левом верхнем углу: в графических API ось Y может идти вверх (OpenGL) или вниз (многие 2D-системы).

Итоги

  • Изображение — прямоугольный массив пикселей; цвет пикселя обычно 4 байта RGBA.
  • Фреймбуфер — память, куда пишется цвет каждого пикселя перед выводом на экран.
  • Линейный индекс пикселя: (y*width + x) * bpp.
  • Двойная буферизация и VSync избавляют от мерцания и разрывов кадра.
Проверьте себя
1. Сколько байт занимает один пиксель в формате RGBA8?
A1 байт
B3 байта
C4 байта
D8 байт
2. Что хранит фреймбуфер?
AСписок вершин сцены
BЦвет каждого пикселя кадра
CТекстуры в сжатом виде
DПрограмму шейдера
3. Зачем нужна двойная буферизация?
AЧтобы хранить два разных изображения навсегда
BЧтобы рисовать следующий кадр, пока показывается текущий, и избежать мерцания
CЧтобы удвоить разрешение
DЧтобы экономить память