Преодоление разрыва в абстракции между OpenGL и WebGL в Emscripten

от Shachar Langbeheim

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

Когда мы создавали рендерер видео для Boosted, мы определились с несколькими простыми требованиями:

1. Рендерер должен предоставлять API, позволяющий редактировать и воспроизводить видео с дополнительными эффектами во внешнем интерфейсе.

2. Он должен предоставлять API, который позволяет экспортировать отредактированные видео через внешний или внутренний интерфейс.

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

Один код, чтобы управлять ими всеми

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

1. Напишите два отдельных рендерера — один для внешнего интерфейса, один для внутреннего или

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

Одна память для управления ими всеми

Просмотрев варианты, мы решили работать с C++, скомпилированным в обычный бинарник Linux в бэкенде и через Emscripten в WebAssembly во фронтенде. Декодирование кадров производилось с помощью FFmpeg в бэкенде и с помощью HTMLVideoElement во фронтенде. При рендеринге использовался OpenGL в серверной части и WebGL во внешнем интерфейсе, поскольку Emscripten может имитировать команды OpenGL с использованием объектов WebGL.

Мы использовали WebGL и OpenGL для преобразования видеокадров (а также любых эффектов, текста и т. д.) в выходные кадры. Чтобы передать видеокадры в графический API, нам нужно было загрузить их в формат изображения, используемый API, который называется текстуры.

Это представило нам неожиданное несоответствие абстракции. Содержимое каждого элемента HTMLVideoElement нужно было скопировать в текстуру WebGL, но наши текстуры управлялись как текстуры OpenGL слоем моделирования Emscripten. Тем временем элемент видео обрабатывался кодом JavaScript. Это дало нам два барьера для преодоления:

1. WebAssembly использует отдельный буфер памяти. Это означает, что любой объект или массив, которые легко доступны в JavaScript, необходимо скопировать, чтобы они были доступны в скомпилированном WebAssembly. А лишние копии целых кадров стоят дорого.

2. WebGL является объектно-ориентированным, а OpenGL работает как конечный автомат. Это означает, что когда мы обрабатываем текстуры в WebGL, у нас есть ссылки на текстурные объекты. Когда мы используем текстуры в OpenGL (или интерфейс Emscripten, который имитирует его), мы обращаемся к ним, используя имена текстур, которые являются непрозрачными идентификаторами.

Простое решение состояло в том, чтобы извлечь данные кадра из видеоэлемента в виде массива байтов, скопировать его в линейную память, а затем загрузить массив в текстуру с помощью команды OpenGL glTexImage2D. Это работало со всеми существующими абстракциями, но было мучительно медленно: мы достигли однозначного числа кадров в секунду даже на iMac с оптимальными характеристиками. И хотя браузеры поддерживают быструю загрузку текстур WebGL из HTMLVideoElement с помощью команды texImage2D, мы получили информацию, быстро загруженную в неправильную абстракцию, которая недоступна коду C++ рендерера.

И в браузере их привязать

Время для нового решения: мы представили функцию getNewId, используемую в Emscripten для создания непрозрачных имен OpenGL, и добавили код, который вручную модифицировал таблицы, которые Emscripten использует для имитации поведения OpenGL. Этот код использовался для вставки и удаления текстур WebGL из таблиц и добавления имен к каждой текстуре. Теперь у нас был интерфейс, позволяющий использовать текстуру WebGL в JavaScript, чтобы наши кадры загружались быстро. Точно так же мы могли бы получить имя OpenGL для каждой текстуры, чтобы средство визуализации могло использовать его. Недостатком было то, что нам приходилось вручную регистрировать и отменять регистрацию наших текстур, а также обходить уровень абстракции Emscripten. Но теперь, когда 60 кадров в секунду легко достижимы (даже при обработке нескольких клипов), усилия того стоили.

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

Если хотите, можете посмотреть выступление, в котором это объясняется более подробно. Вот, пожалуйста.

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

Похоже на вас? Подать заявку здесь.