От кода до пикселей: как работает рендеринг. Часть 4. Стадии композитного потока и работа с GPU.

От кода до пикселей: как работает рендеринг. Часть 4. Стадии композитного потока и работа с GPU.

Anastasia Kotova

Предыдущие части

От кода до пикселей: как работает рендеринг. Часть 1. Браузерные движки.

От кода до пикселей: как работает рендеринг. Часть 2. Конвейер рендеринга.

От кода до пикселей: как работает рендеринг. Часть 3. Стадии основного потока.


В предыдущей части мы посмотрели на конвейер рендеринга в Chromium и разобрали первые восемь фаз, которые преимущественно выполняются в основном потоке. В целом весь пайплайн выглядит так:

1. Парсинг
   → парсинг HTML и CSS
   → построение DOM и таблиц стилей
2. Animate
   → обновление анимаций и временных эффектов
3. Style
   → применение CSS и вычисление итоговых стилей
4. Layout
   → расчёт размеров и позиций элементов
5. Pre-paint
   → подготовка к отрисовке, обновление визуальных состояний
6. Scroll
   → обработка прокрутки и смещения контента
7. Paint
   → формирование команд отрисовки
8. Commit
   → передача данных с main thread на compositor
9. Layerize
   → разбиение сцены на слои
10. Raster / Decode / Paint Worklets
   → растрирование, декодирование изображений, генерация текстур
11. Activate
   → сборка кадра для отображения
12. Aggregate
   → объединение кадров от разных источников
13. Draw
  → отрисовка через GPU

В этой статье мы разберём оставшиеся этапы — то, что происходит после передачи данных на композитный поток.

Layerize

Как мы уже видели раньше, композитный поток работает со своей копией данных из property trees (четырёх деревьев: transform, clip, effect и scroll) и display list — списка команд для рисования. При этом display list уже разбит на paint chunks — группы соседних команд с одинаковым состоянием property trees. Границы между чанками проходят в тех местах, где состояние меняется. Например, если одно поддерево прокручивается, а соседнее — нет, они окажутся в разных чанках.

Фаза layerize превращает плоский список paint chunks в иерархию композитных слоёв (composited layers). Эти слои будут растеризовываться независимо друг от друга, а затем собираться GPU в финальную картинку. На вход layerize получает упорядоченный список paint chunks с их состояниями property trees, а на выходе формирует набор объектов-слоёв. Каждый такой слой содержит часть display items и соответствующий фрагмент property trees.

На этом этапе браузер решает важную компромиссную задачу. С одной стороны, можно было бы объединить все paint chunks в один огромный слой и растеризовать всё целиком, но тогда при любом изменении пришлось бы полностью перерисовывать сцену. С другой стороны, можно было бы создать отдельный слой для каждого чанка, но это быстро привело бы к перерасходу GPU-памяти. Layerize балансирует между этими крайностями и создаёт слои только там, где это действительно даёт выигрыш в производительности.

Алгоритм layerize работает в два шага. Сначала для каждого paint chunk создаётся объект PendingLayer — кандидат на отдельный слой. Затем алгоритм пытается объединять соседние PendingLayer в один слой, если это возможно. Объединение может быть запрещено, например, в следующих случаях:

  • чанк имеет явные причины находиться в отдельном слое: элемент анимируется на композитном потоке или использует will-change
  • объединение приведёт к большому количеству пустого пространства: чанки находятся далеко друг от друга, и между ними появится «пустой» слой, который зря тратит память

После того как финальный список PendingLayer сформирован, для каждого создаётся настоящий композитный слой. Этот слой уже живёт в пространстве композитного потока и содержит записанную последовательность команд отрисовки, которая позже будет растеризована в текстуры.

Raster, decode & paint worklets

Raster — это этап, на котором display lists превращаются в реальные пиксели, записанные в GPU-текстуры. Каждый композитный слой разбивается на квадратные тайлы, и растеризация происходит по тайлам. Тайлы сортируются по приоритету: сначала обрабатываются те, что находятся в текущем viewport, затем те, которые скоро понадобятся при прокрутке, и только потом остальные. Сама растеризация выполняется на GPU в viz process с помощью Skia, чтобы не блокировать ни основной поток, ни копозитный.

Decode — это отдельная задача, которая часто работает параллельно с растеризацией. Изображения (JPEG, PNG и другие) хранятся в display list в сжатом виде, чтобы не увеличивать объём данных при передаче с основного потока. Декодирование — достаточно тяжёлая операция, и выполнять её на композитном потоке означало бы просадки FPS. Поэтому в Chromium декодирование вынесено в отдельные worker-потоки.

Paint worklets — это часть CSS Houdini, которая позволяет разработчикам писать собственный код для генерации графики. Когда compositor растеризует тайл с элементом, использующим paint worklet, он вызывает код worklet, получает результат и встраивает его в текстуру. Важно, что paint worklet может вызываться много раз — при изменении размеров элемента или CSS-переменных — поэтому его код должен быть максимально лёгким.

Activate

Во время фазы commit данные с основного потока попадают не сразу в «живое» дерево слоёв, а во временное pending tree. Фаза activate — это момент, когда pending tree становится active tree.

Chromium всегда поддерживает как минимум два дерева слоёв на композитном потоке:

  • active tree — используется для генерации кадров прямо сейчас
  • pending tree — содержит обновления с основного потока и растеризуется в фоне

Это разделение нужно для того, чтобы пока pending tree обрабатывается, active tree мог продолжать генерировать кадры для скролла и анимаций. Активация происходит только тогда, когда все видимые тайлы pending tree полностью готовы. До этого момента браузер продолжает рисовать старую версию сцены, даже если она уже устарела.

Процесс activate похож на commit, но работает целиком внутри композитного потока — данные переносятся с pending tree на active tree. Это атомарная операция: пользователь либо видит полностью обновлённый кадр, либо всё ещё старый, без промежуточных состояний. После активации pending tree освобождается и используется для следующего обновления.

Результатом фазы activate становится compositor frame — набор инструкций для отрисовки всей сцены, разбитой на render pass. Каждый render pass содержит упорядоченный список quads — примитивов рисования. Каждый quad ссылается на GPU-текстуру, задаёт координаты, трансформации и эффекты. Чаще всего это texture quads с растеризованными тайлами с предыдущих этапов, но также бывают solid color quads (сплошная заливка цветом) и video quads. Готовый compositor frame отправляется в viz process для дальнейшей обработки.

Aggregate

Aggregate — это фаза, на которой compositor frames из разных источников объединяются в один итоговый кадр, готовый к отображению.

В современном Chrome существует несколько независимых источников compositor frames:

  • render processes создают кадры для вкладок и iframe
  • browser process создаёт кадры для UI браузера

Все они отправляют свои compositor frames в единый viz process. Каждый frame получает уникальный id, и другие compositor frames могут ссылаться на него через SurfaceDrawQuad.

Агрегация работает рекурсивно. Алгоритм начинает с корневого frame (обычно UI браузера или основной страницы вкладки) и проходит по всем quads в порядке отрисовки. Если quad обычный — texture или solid color — он просто копируется в итоговый compositor frame. Если quad — SurfaceDrawQuad, агрегатор находит соответствующий compositor frame и рекурсивно встраивает его содержимое. Во время этого процесса также выполняются оптимизации: например, если iframe полностью за пределами экрана, его quads можно вообще не добавлять.

В результате получается единый compositor frame со всеми quads, выровненными в общей системе координат экрана.

Draw

Draw — это финальная фаза, на которой aggregated compositor frame превращается в реальные GPU-команды.

Viz process использует несколько backend-реализаций для работы с GPU. Основной современный backend — SkiaRenderer. Он проходит по всем render pass внутри aggregated frame и по каждому quad генерирует команды отрисовки.

Для SkiaRenderer процесс состоит из двух этапов. Сначала формируются Deferred Display Lists (DDL) — структуры Skia, в которых записываются команды отрисовки, но они ещё не выполняются на GPU. Для каждого quad вызывается соответствующая Skia-команда, и она добавляется в DDL. Когда все DDL готовы, они передаются на GPU main thread в viz process. Этот поток — единственный, который напрямую общается с GPU-драйвером. Skia проигрывает DDL и превращает их в реальные GPU-инструкции.

Что гарантированно создаёт композитный слой

Исходя из архитектуры пайплайна и работы layerize, можно выделить элементы DOM, которые почти гарантированно получат собственный composited layer и будут растеризовываться отдельно. Это важно учитывать при оптимизации: независимые слои ускоряют анимации и скролл, но их избыток увеличивает расход памяти и накладные расходы.

Причины создания слоя

  1. 3D transforms и perspective: Элементы с transform: translateZ(), rotateX(), rotateY() или perspective получают отдельный слой, чтобы трансформации можно было применять без перерисовки содержимого.
  2. Compositor-анимируемые свойства: Анимации transform, opacity, filter, backdrop-filter, включая will-change, создают слой автоматически.
  3. Video и Canvas: <video> и <canvas> получают слой, так как их содержимое обновляется независимо от layout и может использоваться как GPU-текстура.
  4. Overflow scrolling: Элементы с overflow: scroll или auto и реальным скроллом получают слой для обработки прокрутки на compositor thread.
  5. position: fixed и position: sticky: Такие элементы меняют позицию при скролле, и compositor может обновлять их transform без пересчёта layout.
  6. Iframe: Каждый <iframe>, особенно из другого origin, получает слой из-за изоляции и безопасности.
  7. Элементы с composited descendants: Если потомок требует слоя, родитель часто тоже становится composited.
  8. CSS filters и backdrop-filter: Элементы с фильтрами часто выносятся в отдельный слой.

Что не создаёт слой автоматически

  • Обычные CSS свойства, например color, background-color, border, width, height — они требуют полного цикла
  • Анимации не-compositor свойств (например, width, left, top) — они анимируются на основном потоке и пересчитывают layout каждый кадр
  • Элементы с z-index сами по себе — z-index влияет только на порядок отрисовки, но не создаёт слой без других причин
  • Абсолютное позиционирование (position: absolute) без других триггеров — оно не меняется при скролле viewport, так что не требует слоя

Практические рекомендации:

  • Используйте will-change: transform или transform: translateZ(0) только для элементов, которые реально будут анимироваться — избыточные слои тратят memory
  • Проверяйте количество слоёв через DevTools → три точки → More Tools → Layers
  • Там же можно посмотреть, по какой причине тот или иной элемент был вынесен в отдельный слой (Конкретный слой → Details → Compositing Reasons)

Report Page