DEV Community

собачья будка
собачья будка

Posted on • Updated on

весь веб на максимум fps: как webrender избавляется от рывков [перевод]

Вольный перевод статьи Lin Clark "The whole web at maximum FPS: How WebRender gets rid of jank"

Весь веб на максимум FPS: Как WebRender избавляется от рывков

The Firefox Quantum приближается, принося с собой много улучшений производительности, включая супер быстрый CSS движок, который мы перенесли из Servo. В Servo есть еще один кусок, который мы еще не перенесли, но сделаем это очень скоро. Это WebRender, который внедряется в Firefox, как часть проекта Quantum Render.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/01-500x299.png

WebRender известен своей чрезвычайной скоростью, но сам по себе он не для того, чтобы ускорить рендеринг, а для того, чтобы сделать его плавнее.

С помощью WebRender мы хотим, чтобы наши приложения начали запускаться в шелково-гладкие 60 кадров в сенкуду или еще лучше, вне зависимости от того, насколько большой дисплей или насколько страница меняется от фрейма к фрейму. И это работает. Страницы, которые пыхтят в 15 FPS в Chrome или теперешнем Firefox запускаются в 60 FPS с помощью WebRender.

И как же WebRender это делает? Это фундаментальные изменения способа работы движка рендеринга чтобы сделать его более похожим на игровой 3D движок. Давайте взглянем, что же это значит. Но сначала...

Что делает рендерер?

В статье про Stylo я говорила про то, как браузер проходит путь от HTML и CSS файлов до пикселей на экране, и как большинство браузеров делают это за пять шагов.

Мы омжем разделить эти пять шагов на две части. Первая часть, обычно, строит план. Чтобы этот план построить, HTML и CSS комбинируются с информацией, вроде размера viewport, чтобы точно вычислить как должен выглядеть каждый элемент - его ширину, высоту, цвет и т.д. . Конечным результатом будет что-то, называющееся каркасным деревом (frame tree) или деревом рендера.

Вторая часть - отрисовка и композитинг - это то, что делает рендерер. Он берет план и превращает его в пиксели на экране.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/02-500x438.png

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

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/03-500x245.png

Даже в случаях, когда ничего толком и не изменяется - например, когда вы скроллите или выделяете текст на странице - браузеру все равно нужно пройтись по второй части шагов, чтобы отрисовать новые пиксели.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/04-500x244.png

Если вы хотите, чтобы штуки вроде скроллинга или анимаций выглядели плавно, им необходимо отрабатываться в 60 кадров в секунду.

Вы наверняка слышали эту фразу - кадры в секунду (FPS) - раньше, без особого понимания, что же она значит. Я представляю себе это, как книгу с рисунками, которые статичны, но если вы начнете используете большой палец, чтобы быстро пролистать ее - изображения начнут двигаться.

В случае с нашей книгой, чтобы анимация ее была плавной, нам нужно перелистывать по 60 страниц в секунду.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/a-05.gif

Страницы этой книги сделаны из миллиметровки. В ней существует много-много маленьких квадратов, и каждый из них может содержать только один цвет.

Работа рендерера заключается в том, чтобы заполнять квадраты нашей книги. Как только все они будут заполнены, процесс рендеринга кадра завершится.

Конечно, никакой миллиметровки в нашем компьютере нет. Вместо того там находится участок памяти, который называется кадровым буфером. Каждый адрес памяти в кадровом буфере можно сравнить с квадратиком в миллиметровке... Т. е., он соответствует пикселю на экране. Браузер заполнит каждый участок числами, которые представляют значения цвета в системе RGBA (red, green, blue, alpha).

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/06-500x166.png

И когда дисплею понадобится себя обновить - он посмотрит на участки памяти.

Дисплеи большинства компьютеров будут обновляться 60 раз в секунду, потому браузеры пытаются рендерить страницы за 60 кадров в секунду. Это значит, что у браузера есть 16.67 миллисекунд на все установки - стилизация с помощью CSS, макет, отрисовка - и заполнить все участки кадрового буфера цветами пикселей. А период между двумя конечными кадрами (16.67 ms) называется рамочным бюджетом (frame budget).

Возможно, вы когда-ниюудь слышали, как люди говорили о потере кадров. Потерянный кадр - это когда система не успевает закончить всю работу во время рамочного бюджета. Дисплей пытается получить новый кадр из кадрового буфера перед тем, как браузер закончит его заливку. В этом случае, дисплей отобразит старую версию кадра еще раз.

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

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/a-07.gif

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

Краткий экскурс в отрисовку и композитинг

Примечание: Разные браузерные движки на этапах отрисовки и композитинга очень отличаются. Одноплатформенные браузеры (Edge и Safari) работают немного другим образом, чем кроссплатформенные (Firefox и Chrome).

Даже в более ранних браузерах были кое-какие оптимизации, чтобы страницы отрисовывались быстрее. Например, если вы прокручиваете контент, браузер сохранит часть, которая была видна, и сдвинет ее. После того отрисует новые пиксели в теперь уже пустом месте, а не перерисует всё и сразу.

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

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

Это по-настоящему уменьшало количество необходимой работы, когда на странице особо ничего и не менялось... Например, когда у вас есть один лишь моргающий курсор.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/a-08.gif

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

Представляем слои и композитинг

Использование слоёв может сильно помочь, если на странице меняются большие участки... Ну, по крайней мере в некоторых из таких случаев.

Слои в браузере очень похожи на слови в Photoshop или слои кожицы лука. В целом, нужно просто отрисовывать различные элементы страницы на различных уровнях, а потом помещать эти слови поверх друг друга.

Слои долгое время были часть браузера, но не всегда использовались для конкретно ускорения чего-то. Во-первых, их использовали для того, чтобы просто убедиться, что страница отрисовывается правильно. Они соответствовали чему-то, что называлось контекстами наложения (stacking contexts).

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

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/09-500x304.png

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

Это и делают браузеры. Они сохраняют слои, а потом перерисовывают только те из них, которые изменяются. И, в некоторых случаях, слои даже не меняются, а просто переставляются - например, если анимация передвигается по экрану, или что-то прокручивается.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/a-10-1.gif

Этот процесс организации слоев называется композитингом. Композитор запускается:

  • от исходных битмапов: фон (включая пустые ячейки, где должен находиться прокручиваемый контент) и сам прокручиваемых контент
  • до целевого битмапа, который отображается на экране

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

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/11-500x243.png

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

Я уже говорила это раньше, но основной поток похож на full-stack разработчика, потому что, кроме того, что отвечал за DOM, макет и JavaScript, так еще и отрисовка и композитинг добавились.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/12-500x173.png

Каждая миллисекунда, которую основной поток тратит на отрисовку и композитинг, - это миллисекунда, которую уже не потратить на JavaScript или макет.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/13-500x202.png

И была одна часть железа нашего компьютера, которая валялась почти без работы, да еще и была создана специально для графики. Это была GPU, которую еще с 90-х для отрисовки кадров использовали игры. И GPU все разрастался и мощнел с тех пор.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/14-500x249.png

Ускоренный GPU композитинг

Потому разработчики стали выполнять задачи на GPU.

Есть две задачи, которые потенциально могли быть отданы на обработку GPU:

  1. Отрисовка слоев
  2. Их композитинг

Перенести отрисовку на GPU может быть сложным делом. Потому, по большей части, кроссплатформенные браузеры продолжают выполнять ее на CPU.

Но композитинг был тем, с чем GPU справился бы очень быстро, да еще и перенести его было достаточно просто.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/15-500x257.png

Некоторые браузеры пошли еще дальше и добавили композитинг поток в CPU, который стал контролировал работу по композитингу, выполняемую на GPU. Это значит, что если основной поток был чем-то занят (например, запускал JavaScript), поток композитинга все еще мог обрабатывать вещи, вроде прокрутки контента, для пользователя.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/16-500x418.png

И так вся работа с композитингом была убрана из основного потока. Но, тем не менее, на нем все еще оставалось много дел. Всякий раз, когда нам нужно перерисовать слой, основной поток должен был заниматься этим, а потом переносить слой на GPU.

Некоторые браузеры перекинули рисование в другой поток (и мы в Firefox работаем над этим). Но все еще было бы гораздо быстрее отдать эту маленькую часть работки - отрисовку - на GPU.

Ускоренная GPU отрисовка

Так браузеры начали отдавать и отрисовку на GPU.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/17-500x425.png

Браузеры все еще в процессе внедрения этого. Некоторые отрисовывают на GPU все время, пока остальные могут так поступать только на нескольких конкретных платформах (например, только на Windows, или только на мобильных устройства).

Отрисовка на GPU делает несколько вещей. Освобождает CPU, чтобы тот мог заняться JavaScript'ом и макетом. К тому же, GPU гораздо быстрее в отрисовке пикселей, чем CPU, потому он ускоряет отрисовку. Это также означает меньшее количество данных, необходимых для копирования с CPU на GPU.

Но поддержка этого разделения между отрисовкой и композинтигом все еще несколько затратна, даже если оба процесса выполняются на GPU. Это разделение также ограничивает некоторые оптимизации для ускорения работы GPU.

Здесь и появляется WebRender. Он в корне меняет способ рендеринга, убирая разделение на отрисовку и композитинг, что дает нам возможность адаптировать производительность нашего рендерера, предоставляя более лучший ux для современного веба и более лучшую поддержку частых юзкейсов, которые мы увидим в вебе будущего.

Это значит, что нам не нужно просто ускорить отрисовку кадров... Мы хотим заставить их отрисовываться более последовательно и без рывков. И даже тогда, когда пикселей на отрисовку огромное количество, как на 4k дисплеях или WebVR устройствах, мы все еще хотим получать такой же "гладкий" опыт.

В каких случаях текущие браузеры начинают дергаться?

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

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/a-08.gif

Разбивка страниц на слои расширила число лучших сценариев. Если вам нужно отрисовать несколько слоев и потом просто двигать их между собой - архитектура отрисовки и композитинга отлично сработает.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/a-18.gif

Но есть и компромиссы с использованием слоев. Они могут занимать много памяти и, порой, даже тормозить все. Браузерам нужно комбинировать слои только когда это имеет смысл.. но тяжело определить, когда тот самый смысл есть.

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

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/19-500x309.png

А может оказаться так, что вы останетесь с одним слоем, когда должны были с несколькими. Этот один слой будет перекрашиваться и передаваться в композитор, который будет использовать его, ничего не меняя.

Это значит, что количество работ по отрисовке удваивается, касаясь каждого пикселя дважды без какой-либо выгоды. Было бы гораздо быстрее просто отрендерить страницу, пропустив шаг композитинга.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/20-500x311.png

Также бывают случаи, когда и слои особо не помогают. Например, если вы анимируете цвет фона - весь слой перерисовывается. Потому они полезны только для малого количества CSS свойств.

И даже если вы попадаете в лучшие сценарии, когда кадры занимают только малую часть бюджета, рывки все еще могут появиться. Для заметного рывка достаточно, чтобы хотя бы пара кадров попала в плохие сценарии.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/21-500x69.png

Такие сценарии называются обрывами производительности. Ваше приложение, вроде как, хорошо справляется, но только пока не наткнется на один из этих плохих сценарией (вроде анимации цвета фона) и внезапно частота кадров в вашем приложении обрывается вниз...

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/22-500x368.png

Но от этих обрывов мы можем избавиться

Как? Последуем примеру игровых 3D движков.

Используем GPU, как игровой движок

Что, если мы перестанем гадать, какие слои нам нужны? Что, если мы избавимся от этой границы между отрисовкой и комопзитингом и просто вренемся к отрисовке каждого пикселя в каждом кадре?

Такая идея может показаться нелепой, но, на самом деле, она имеет прецедент. Современные видео игры перерисовывают каждый пиксель, и поддерживают 60 кадров в секунду более надежно, чем браузеры. И делают это они неожиданным способом... Вместо создания тех самых прямоугольников и слоев для минимизации отрисовки, они просто перерисовывают весь экран.

Не будет ли медленнее рендеринг веб страниц таким путем?

Если рисовать на CPU - будет, но GPU спроектирован для такой работы.

GPU созданы для экстремального параллелизма. Я говорила о нем в моей прошлой статье о Stylo. С параллелизмом, машина может выполнять несколько вещей одновременно. Количество их ограничено количеством имеющихся ядер.

CPU обычно имеет от 2-х до 8-и ядер. GPU же - как минимум, несколько сотен ядер, и, чаще всего, более тысячи.

Однако эти ядра работают несколько иначе. Они не могут действовать полностью независимо, как ядра CPU. Вместо того, они обычно работают над чем-то вместе, запуская одни инструкции в разных участках данных.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/23-500x227.png

Для заливки пикселей это именно то, что нужно. Каждый пиксель будет заполнен другим ядром. Поскольку работа ведется над сотнями пикселей за раз, GPU гораздо быстрее справится с этим делом, чем CPU… но только если мы убедимся, что у все эти ядра заняты работой.

Ядрам необходимо выполнять работу над одной задачей в один промежуток времени. Дело в том, что GPU имеет довольно жесткий набор необходимых шагов, и их API довольно ограничены. Давайте посмотрим, как это работает.

Сначала нужно указать GPU что рисовать. Т. е., придать им формы и пояснить, как заполнять их.

Для того, наша отрисовка разбивается на маленькие формы (чаще всего треугольники). Это формы находятся в 3D пространстве, потому некоторые из них могут располагаться позади других. После чего все x, y, z координаты углов треугольников помещаются в массив.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/24.png

Затем мы выполняем вызов отрисовки, указывая GPU рисовать эти формы.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/25-500x337.png

Здесь к работе подключится GPU. Все ядра начнут работать над одной задачей одновременно. Они:

  • Выяснят, где находится все углы форм. Процесс называется затенением вершин (vertex shading).

    https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/26-500x318.png

  • Найдут линии, которые соединяют углы. Из этого уже можно выяснить, какие пиксели покрывают форму. Процесс называется растеризацией (rasterization).

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/27-500x323.png

  • Зная все покрывающие форму пиксели, пройдут по каждому из них и выяснят, какого цвета они должны быть. Процесс называется затенением пикселей (pixel shading).

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/28-500x318.png

Завершающие шаги могут быть выполнены разными способами. Чтобы указать GPU конкретный, нужно передать ей программу, которая называется пиксельным шейдером (pixel shader). Затенение пикселей (pixel shading) - одна из частей GPU, которую мы можем запрограммировать.

Некоторые шейдеры довольно просты. Например, если наша форма одного цвета, то шейдерной программе нужно просто вернуть этот цвет для каждого пикселя в форме.

А некоторые шейдеры более сложные. Например, когда у нас есть фоновое изображение. Нужно выяснить, какая часть изображения соответствует каждому пикселю, например, поместив сетку, как на миллиметровке, поверх изображения. Таким образом, мы поймем, какие цвета соответствуют каждому участку изображения. Процесс называется отображением текстуры (texture mapping), поскольку преобразует наше изображение (называемое текстурой) в пиксели.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/29-500x369.png

GPU будет выполнять нашу шейдерную программу для каждого пикселя. Разные ядра будут работать над разными пикселями одновременно, параллельно, но все они должны использовать одну пиксель-шейдерную программу. Когда мы говорим GPU отрисовать нашу фигуру - мы говорим ей, какой пиксельный шейдер использовать.

Для чуть ли не любой веб страницы различные ее части будут использовать различные пиксельные шейдеры.

Поскольку шейдер применяется ко всем фигурам в вызове отрисовки, обычно приходится разбивать вызовы на несколько подгрупп, которые зовутся партиями. Чтобы держать все ядра как можно более занятыми, понадобится создать небольшое количество партий с кучей фигур в каждой.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/30-500x350.png

Таким образом GPU разбивает работу на сотни или тысячи ядер. Только из-за этого параллелизма мы можем думать о перерисовке вообще всего на каждом кадре. Но даже с ним работы все еще много. Но даже с ним нужно с умом подходить к тому, что мы делаем. И как раз тут вступает в дело WebRender...

Как WebRender работает с GPU

Давайте снова глянем на шаги, которые браузер выполняет при рендеринге страницы. Два шага здесь изменятся.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/31-500x354.png

  1. Больше нет разделения на отрисовку и композитинг. Они оба теперь являются частью одного шага. GPU выполняет их одновременно на основе переданных команд графического API.
  2. Теперь для рендеринга макет дает нам другую структуру данных. Раньше она называлась деревом фрейма (frame tree) или деревом рендера в Chrome. Теперь же просто выдается список отображения.

Список отображения - это набор высокоуровневых инструкций для рисования. Он указывает, что нужно отрисовать, не привязываясь к какому-то конкретному графическому API.

Когда появляется что-то, что нужно отрисовать, основной поток передает список в RenderBackend, который является методом WebRender и запускается на CPU.

Работа RenderBackend заключается в том, чтобы получить список высокоуровневых инструкций и преобразовать его в вызовы отрисовки, необходимые GPU, после чего GPU раскидает их по партиям для более быстрого запуска.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/32-500x560.png

После чего RenderBackend передаст эти партии в поток композитора, который, в свою очередь, отправит их на GPU.

В свою очередь, RenderBackend пытается как можно больше ускорить вызовы отрисовок, которые передает на GPU, и для того использует несколько техник.

Удаление необязательных фигур из списка (Early culling)

Лучший способ ускорить время работы - не делать работу вовсе.

Во-первых, RenderBackend сокращает список отображаемых элементов, выясняя, какие конкретно элементы будут отображаться. Чтобы это сделать, он смотрит на вещи, вроде того, как далеко находится текущий скролл от каждого прокручиваемого участка.

Если какие-то части формы внутри этого участка - они будут включены в список. Если нет - удалены. Этот процесс называется ранней выбраковкой (early culling).

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/33-500x270.png

Минимизация количества промежуточных текстур (The render task tree)

Сейчас у нас есть дерево, которое содержит только используемые формы. Это дерево оформлено в виде контекста наложения, о котором мы говорили выше.

Эффекты, вроде CSS фильтров и контекста наложения, немного усложняют задачу. Например, скажем, наш элемент имеет свойство opacity в значении 0.5 и парочку потомков. Можно подумать, что каждый потомок будет полупрозрачен, но, по сути, полупрозрачной будет вся группа элементов.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/34-500x265.png

Из-за этого необходимо отрендерить группу в текстуру с полной непрозрачностью, и только потом, помещая их в блок родителя, изменить прозрачность всех текстуры.

Такие контексты наложения могут быть еще и вложенными. Сам родитель, например, может находиться в еще одном контексте наложения, что означает, что он сам должен быть отрендерен другой промежуточной текстурой, и так далее.

Создавать пространство для всех этих текстур может быть дорогой операцией. Нам нужно группировать штуки в одной промежуточной текстуре насколько это возможно.

Чтобы помочь GPU это сделать, мы создаем дерево задач рендеринга (render task tree). С его помощью мы узнаем, какие текстуры должны создаваться раньше остальных. Все независимые текстуры могут создаваться в первую очередь, благодаря чему могут группироваться в одной промежуточной.

Таким образом, для примера выше мы сначала выполним проход, чтобы вывести один угол box shadow (на деле все немного сложнее, но суть одна).

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/35-500x202.png

Вторым проходом мы отразим угол на все оставшиется углы блока, чтобы применить свойство box shadow. После чего сможем отрендерить группу с полной непрозрачностью.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/36-500x179.png

Далее нам осталось только поменять прозрачность текстуры и передать ее в конечную текстуру, которая уже выведется на экран.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/37-500x171.png

Выстраивая такое дерево задач, мы выясняем минимальное количество оставшихся за кадром результатов рендеринга, которые можем использовать. Это хорошо, поскольку, как я уже упоминала, создавать новое пространство для этих текстур дороговато.

А еще такое дерево помогает нам дозировать задачи.

Группировка вызовов отрисовки (Batching)

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

Уделяя внимание конкретно способу создания партий можно также значительно ускорить штуки. Нам нужно помещать в одну партию как можно больше фигур. На то есть несколько причин.

Первая: всякий раз, когда CPU говорит GPU вызвать отрисовку, он проделывает много работы. Ему нужно заняться вещами вроде настройки GPU, загрузки шейдерной программы и проверки всяких багов с железом. Вся эта работа наслаивается, и пока CPU ее выпоняет, GPU может простаивать впустую.

Вторая: у всякого изменения состояния есть цена. Скажем, нужно изменить шейдерную программу между партиями. На типичном GPU, нам придется дождаться, пока все ядра не закончат работу с текущим шейдером. Этот процесс называется очисткой конвейера (draining the pipeline). Пока конвейер не будет очищен, другие ядра будут простаивать.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/38-500x216.png

Из-за этого нам нужно дозировать задачи, как только возможно. Для типичного десктопного ПК на каждый кадр нужно около сотни или меньше вызовов отрисовок, и чтобы каждый из них хранил тысячи вершин. Таким образом параллелизм будет использоваться лучше всего.

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

На данный момент каждый из различных типов примитивов запрашивает собственный шейдер. Например, шейдер рамки (border shader), шейдер текста (text shader), шейдер изображения (image shader).

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/39-500x130.png

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

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

Уменьшение затенения пикселей (pixel shading) с помощью непрозрачных (opaque) и альфа (alpha) проходов (Z-culling)

Большинство веб страниц имеют множество накладывающихся друг на друга фигур. Например, текстовое поле поверх div с фоном, который сам находится поверх body с другим фоном.

Когда выяснен цвет пикселя, GPU может приступить к выяснению цвета пикселя каждой фигуры, но отображен будет только верхний слой. Процесс называется перерисовкой (overdraw) и тратит время GPU.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/40-500x353.png

Одна вещь, которую можно сделать, это отрендерить верхнюю фигуру в первую очередь. Для следующей фигуры, когда мы доберемся до того же пикселя, проверим, имеется или нет значение для него. Если значение есть - работу не делаем.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/41-500x332.png

Хотя, и такой подход чуток проблемен. Всякий раз, когда фигура становится полупрозрачной, необходимо смешивать (blend) цвета двух фигур. И, чтоб все выглядело правильно, работа должна выполняться от нижнего слоя к верхнему (back to front).

Что мы делаем, так это разбиваем работу на два этапа. Первый этап: мы делаем непрозрачный проход. Идем от верхнего к нижнему и рендерим все непрозрачные фигуры, пропуская все находящиеся позади других пиксели.

Затем создаем полупрозрачные фигуры, которые рендерятся от нижнего к верхнему слою. Если полупрозрачный пиксель находится поверх прозрачного, то они смешиваются, если находится позади - не высчитывается.

Этот процесс разбивки работы на непрозрачные и альфа проходы с пропуском ненужных вычислений называется Z-выбраковкой (Z-culling).

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

На этом этапе мы подготовили кадр, сделав все возможное для уменьшения количества работы.

… И мы готовы рисовать!

Пора настроить GPU и отрендерить наши партии.

https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/10/42-500x560.png

Предостережение: еще не вся работа на GPU

CPU все еще выполняет кучу работы по отрисовке. Например, мы все еще рендерим символы (глифы), которые используются в блоках текста на CPU. Такое легко сделать на GPU, но тяжело сопоставить пиксель-в-пиксель глифы, которые компьютер рендерит в других приложениях. Потому людей могут дезориентировать шрифты, отрендеренные на GPU. Мы эскпериментируем с этим в Pathfinder проекте.

На данный момент, такие штуки отрисовываются в битмапы на CPU, после чего загружаются в кэш текстуры (texture cache) на GPU. Этот кэш хранится от кадра к кадру, поскольку меняется не так часто.

И даже учитывая, что вся эта работа выполняется на CPU, мы все еще можем ускорить ее. Например, когда происходит отрисовка символов в шрифт, мы можем раскидать различные символы по ядрам. А делаем это, используя одну и ту же технику, что и Stylo для распараллеливания стилистических вычислений ... украли работу.

О Lin Clark

Lin работает над продвинутой разработкой в Mozilla, с фокусом на Rust и WebAssembly.

Больше статей от Lin Clark…

Top comments (0)