Disclaimer: пост написан на основе изрядно отредактированных логов чата closedcircles.com, отсюда и стиль изложения, и наличие уточняющих вопросов.  Главный термин который надо знать относительно VR — motion-to-photon latency.
Иначе говоря, задержка между поворотом головы и последним фотоном изображения (отрисованного с ракурса нового положения головы) покинувшим экран.
Эмпирически выведено что motion-to-photon latency 20 msec и ниже позволяет достигать presence — т.е. ощущения что двигаешь головой в виртуальном мире.
Важны ли значения меньше 20 ms или нет — непонятно, но в общем цель — достигнуть 20.
GearVR правдами и неправдами достигает, и я расскажу как. Заранее прошу извинить за свободное использование английских терминов вперемешку с транслитерированными и переведенными. GearVR это если кто не знает headset в который втыкается телефон. В самом хедсете экрана нет — там есть линзы и IMU.
IMU это стандартное gyroscope + accelerometer. Есть ли в GearVR магнетометр я точно не знаю, вроде нет — но в любом случае на практике магнетометр не очень важен.
Есть стандартные способы получать ориентацию устройства с помощью gyro и accelerometer, я останавливаться на этом не буду. Важный момент: В GearVR свой IMU, который дает новые данные об ориентации на частоте 1000 Hz с минимальным latency (скорее всего порядка 1 ms).
Это в разы лучше встроенных сенсоров в телефонах (в Samsung Note 5 gyro частоты 200 Hz и latency 10-15 ms). Значит, базовая расстановка — у нас есть IMU данные, мы их прочитали в начале кадра и надо отрендерить два кадра (стерео картинку). В GearVR нет positional tracking — мы знаем только ориентацию головы. С помощью ориентации головы получаются приблизительные данные для камер двух глаз — условно, если мы повернули голову направо, то позиция глаз тоже двигается направо т.к. голова вращается вокруг сустава в шее, и известно насколько далеко глаза от этой точки вращения смещены.
Ну и мы знаем примерное (среднее) расстояние между глаз пользователя, поэтому несложной геометрией получаем две камеры, рисуем две картинки. Как уже сказано, в GearVR есть две линзы — по одной на глаз. Линзы нужны по двум причинам: Линза "выпрямляет" лучи идущие из глаза, получается ощущение оптического фокуса на бесконечности:
 Собственно получается так что линза с такими параметрами имеет достаточно значительный distortion — если просто отрендерить картинку в оба глаза то объекты будут заметно искажаться; чем дальше от центра линзы (глаза), тем больше.
Поэтому мы возьмем картинку полученную стандартным 3D рендером и ее при выводе на экран исказим, чтобы после искажения линзы получилось примерно то же самое. Для этого обычно рисуется distortion mesh который выглядит примерно так:
 Плюс еще если у вас достаточно много лишнего времени на GPU, то можно в этом же проходе попытаться скорректировать chromatic aberration
(который тоже получается из-за свойств линзы).
На практике на GearVR по умолчанию этот фильтр отключен — чтобы его скомпенсировать надо вместо 1 текстурной выборки в шейдере делать 3, а это мобильное железо. Итак, вводная закончена, давайте разбираться с latency :) В предложенной схеме никаких 20 ms не будет и вообще все будет очень плохо.
Потому что... Дисплей понятно совсем не CRT, но при этом понятие "луча" считывающего данные все еще осталось — контроллер дисплея считывает дисплей сканлайн за сканлайном и зажигает пиксели.
И чтобы считать весь дисплей — уходит 16 мс.
Сканлайны на телефоне обычно "короткие" — когда вы используете VR то телефон в ландшафтном режиме, и сканлайны это вертикальные полосочки, идущие слева направо (ну или справа налево если телефон перевернут...)
Соответственно самый левый сканлайн (в левом глазу) обновляется на 16 мс раньше чем самый правый (в правом глазу). В итоге короче получилось что-то типа 65 ms latency
Что невообразимо больше чем 20. Плюс есть еще одна проблема…
Обычно дисплей работает в full persistence mode — а именно, сканлайн зажигается новыми цветами, и потом остается в таком режиме фактически весь кадр — т.е. 16 мс.
Оказывается, что за 16 мс можно достаточно далеко повернуть голову, и в результате этот сканлайн глазом регистрируется в разных точках retina с одним и тем же цветом.
И получается smearing при повороте головы. Значит если все это не побеждать — GearVR игры выглядели бы как Google Cardboard.
Надо побеждать. Ключевой технологией является технология которую Oculus называет Time Warp.
Еще она называется reprojection, и наверное как-нибудь еще, но мне нравится слово timewarp.
Идея в следующем — если взять картинку отрендеренную камерой С, потом взять камеру C' которая является камерой C повернутой (но не смещенной) на не очень большое расстояние (единицы-десятки градусов), и отрендерить ей картинку…
То оказывается что вторую картинку из первой можно получить простым 2D преобразованием. Почему это так? На пальцах, поскольку в картинке отрендеренной из камеры каждый пиксель соответствует лучу из камеры в точку где этот пиксель расположен, то ясно что при повороте камеры пересечение двух фрустумов будет содержать лучи которые будут соответствовать пикселям одного и того же цвета. Если зафиксировать матрицы и поворачивать голову влево-вправо то на небольших углах поворота тот факт что ты матрицы зафиксировал неочевиден.
Это собственно главный тест того что time warp работает корректно
(зафиксировал матрицы своего рендера и смотришь можно ли плюс-минус 5 градусов вертеть головой).
Можно и больше чем 5 градусов но там уже много информации брать неоткуда поэтому большая часть экрана становится черной.
Собственно "трясти девайс" это стандартный stress test для хедсетов в которых есть positional tracking — фиксируешь голову а дальше руками высокочастотно сдвигаешь хедсет на голове влево-вправо, и значит картинка должна быть стабильной — для этого собственно и надо sub-20 ms (ну и качественный трекинг). Timewarp делается просто — мы уже рисуем тот самый distortion mesh.
Поэтому можно просто в вершинном шейдере "повернуть" текстурные координаты (в 3D пространстве).
Пиксельный шейдер не меняется, timewarp получается фактически бесплатным. На сколько поворачивать?
На разницу между поворотом головы сейчас и поворотом головы с которым была отрисована исходная картинка.
Чем больше поворот — тем больше будет пустого пространства, его можно заполнять черным цветом
(или можно делать clamp адресацию, на небольших углах поворота получается чуть лучше чем черное, но на больших получается странное). Теперь, когда у нас есть механизм "доворота" картинки, давайте разберемся как бы его можно использовать чтобы уменьшить latency. Сначала разберемся с full persistence — это технически не влияет на motion-to-photon, но таки надо убирать.
Для телефонов которые поддерживает GearVR написан специальный драйвер дисплея который включает low persistence mode:
Каждый сканлайн зажигается как раньше нужными цветами, а дальше почти сразу — я точно не знаю через сколько, но скажем 3-4 ms — тухнет (становится черным).
Оказывается что достаточно большое количество людей в силу особенностей восприятия не замечают "моргания".
Говорят что граница восприятия варьируется от где-то 50 Hz моргания до 85 Hz — что является одной из главных причин почему десктопные VR хедсеты (Rift/Vive) работают с частотой 90 Hz.
Плюс low persistence в хедсете воспринимается лучше чем просто моргающая с частотой 60 Hz панель в реальном мире, потому что вроде бы моргает все что ты видишь, и мозг адаптируется... Reference links и видео:
https://www.youtube.com/watch?v=_FlV6pgwlrk
https://en.wikipedia.org/wiki/Persistence_of_vision
https://en.wikipedia.org/wiki/Flicker_fusion_threshold Теперь при движении головой у нас больше нет smearing, но все еще есть 65 ms latency…
Давайте теперь разберемся как на самом деле в GearVR работает рендер.
То что я написал выше про то что в начале рендер кадра мы читаем данные с IMU, потом генерируем OpenGL команды и потом отдаем их GPU который рисует кадр — остается валидным.
Это игровой тред и игровой GL контекст. Есть еще один тред который создает GearVR SDK и у него есть свой GL context, работающий в high priority режиме
(на новых версиях Android такой контекст можно теперь создать самому с помощью EGL расширения).
High priority режим означает что команды, отданные GPU на этом контексте, начнут исполняться GPU практически сразу. Я не знаю деталей GPU scheduling на конкретно GPU которые поддерживают GearVR — предположу, что там preemption granularity это draw call — т.е. если у вас нет огромных 10 msec draw calls то можно за одну-две миллисекунды закончить текущий и начать следующий.
На десктопном GPU у NVidia эта гранулярность обычно в draw call а на AMD если я не ошибаюсь 1 wave (для compute), ну и десктопные вендоры работают над улучшением т.к. эта фича тоже важна на десктопном VR... Вторая особенность этого VR контекста в том…
Что он рисует сразу в front buffer — в тот самый буфер из которого происходит scan out — таким образом пропуская все композиторы и прочую ерунду.
Как мы уже разобрались, scan out занимает 16 мс и идет по сканлайнам один за другим в строгом порядке.
Сканлайны это вертикальные полоски которые идут слева направо (в ландшафтном режиме), поэтому половина полосок содержат картинку из левого глаза (сканируются за 8 мс), а вторая половина полосок содержит картинку из правого глаза (сканируется за 8 мс).
А значит можно попробовать рисовать в правый глаз пока дисплей сканирует левый и наоборот. Отмечу на всякий случай — это не означает что мы будем рисовать прямо треугольники из кадра игры!
Тут нужен очень аккуратный тайминг и четкие гарантии, поэтому игра там как получится рисует кадр, а VR контекст будет его корректировать с помощью time warp. Итак, этот самый VR тред просыпается дважды — когда vsync интервал начался, и через 8 мс после этого (в середине scan out).
Читает свежие IMU данные (с latency 1 ms), а дальше генерирует две матрицы корректирующего поворота. Почему две?
Из-за scan out левый сканлайн левого глаза и правый сканлайн левого глаза разнесены во времени на 8 мс (с точки зрения photon emission time), поэтому в идеале надо их корректировать по-разному.
Поэтому мы читаем данные IMU (latency 1 ms), и генерируем две ориентации головы используя gyroscope данные для prediction вперед.
Первую мы предиктим вперед на 8 мс, вторую на 16 мс (сейчас будет понятно почему). Потом сабмитим draw call в high priority context в котором половина того distortion mesh сверху для нужного глаза и надеемся что GPU его достаточно быстро подберет и отрисует.
Через 8 мс после того как мы сгенерировали ориентации scan out дойдет до той части экрана в которую мы только что нарисовали данные и начнет ее показывать.
В итоге полная latency до photon emission крайнего слева сканлайна каждого глаза получается примерно 9 ms, а крайнего справа — примерно 17 ms. (те кто 30 лет назад на Amiga писали race-the-beam rasterizers — прослезитесь) Собственно если порезать distortion mesh на вертикальные полосочки и иметь более четкие timing гарантии — как я понимаю, с текущим GPU это невозможно — то можно было бы уменьшать motion-to-photon до <10 ms.
Как видно, половина (8 ms) текущей latency это типа safety буфер чтобы уложиться и засабмитить draw call на GPU и чтобы он его отрисовал, и половина — это худший scanout latency для сканлайна.
Если резать на полосочки и иметь более четкий тайминг то можно это попробовать решать.
Или прямо интегрировать time warp в scanout контроллер, операция warp — это просто билинейный семплинг текстуры, не очень сложно.
Но по большому счету текущий latency — good enough. Если увеличить частоту дисплея до 90 Hz чтобы убрать проблемы с морганием для более чувствительных людей, то получится на той же технологии сразу примерно 10 ms. Итого, проснулись сразу после vsync — в это время scan out начал процессить левый глаз — и нарисовали правый глаз в front buffer.
Потом заснули до середины vsync interval, проснулись — в это время scan out начал процессить правый глаз — и нарисовали левый глаз в front buffer.
Ad infinitum. Отмечу, time warp имеет забавную проблему…
Он умеет только довернуть картинку.
Не умеет ее смещать — при смещении начинаются проблемы disocclusion, нужен depth buffer, больше ресурсов итп.
Но на самом деле глаза два, и когда вы поворачиваете голову — глаза смещаются и поворачиваются одновременно. Поэтому чем больше timewarp коррекция тем более неправильной получается stereo картинка и тем сложнее фокусироваться.
На практике получается что time warp на 20-30 msec к видимым проблемам не приводит, кроме как при огромных скоростях поворота где convergence перестает работать, наверное. Еще пара моментов которые GearVR умеет делать, и которые в обычном Андроиде невозможны... Во-первых, можно включить realtime приоритет своему рендер потоку (ну и он включается VR потоку)
Чтобы всякие там потоки garbage collection не очень мешали. Во-вторых, можно включить (точнее, нужно) fixed clock frequency.
По-умолчанию частота CPU и GPU варьируется в зависимости от нагрузки и прочих факторов.
На практике в Андроиде эти механизмы работают очень странно, и могут привести к тому что нагрузка которая должна укладываться в 16 ms на кадр несколько секунд подряд не укладывается.
Поэтому SDK отключает динамический adjustment и позволяет выбрать приложению фиксированные frequency scales для CPU/GPU отдельно. А, еще забыл!
Про Cardboard.
Вот как видно из вышеперечисленного — есть ровно ОДИН момент в hardware у GearVR.
Хорошие сенсоры с маленьким latency и большим refresh rate.
Все остальное — software.
(есть еще линзы но в GearVR линзы насколько я понимаю — ничем не выделяются, в cardboard девайсах такие же считай) Судя по моим экспериментам с сенсорами на разных телефонах с т.з. latency…
У Samsung и Apple трекинг заметно хуже, но у Nexus 6 трекинг хуже но уже в пределах разумного.
(ЕМНИП 4ms refresh interval, avg 3 ms latency)
Т.е. на Nexus 6 — можно делать все что я написал выше и иметь experience натурально сравнимый с GearVR, но в картонной коробке.
Проблема в том что практически все что я написал выше не сделать из user mode — нужно патчить ядро и драйвера.
Так что авторы приложений этого делать не могут, но вот Гугл — вполне.