В этой статье описывается, как эффективно обрабатывать поворот устройства в приложении Vulkan, реализуя предварительный поворот.
С помощью Vulkan можно указать гораздо больше информации о состоянии рендеринга, чем с помощью OpenGL. При использовании Vulkan необходимо явно реализовать параметры, обрабатываемые драйвером в OpenGL, такие как ориентация устройства и её связь с ориентацией поверхности рендеринга . Android может согласовать поверхность рендеринга устройства с его ориентацией тремя способами:
- Операционная система Android может использовать блок обработки отображения (DPU) устройства, который эффективно обрабатывает вращение поверхности на аппаратном уровне. Доступно только на поддерживаемых устройствах.
- Операционная система Android может обрабатывать поворот поверхности, добавляя проход компоновщика. Это может привести к снижению производительности, зависящему от того, как компоновщик справляется с поворотом выходного изображения.
- Приложение может самостоятельно управлять поворотом поверхности, отображая повернутое изображение на поверхности рендеринга, которая соответствует текущей ориентации дисплея.
Какой из этих методов следует использовать?
В настоящее время приложение не может определить, будет ли вращение поверхности, обрабатываемое вне приложения, бесплатным. Даже если есть DPU, который позаботится об этом, всё равно придётся заплатить ощутимую потерю производительности. Если ваше приложение ограничено ресурсами центрального процессора, это становится проблемой из-за повышенной нагрузки на графический процессор (GPU) со стороны Android Compositor, который обычно работает на повышенной частоте. Если ваше приложение ограничено ресурсами графического процессора (GPU), то Android Compositor также может вытеснять его работу, что приводит к дополнительному снижению производительности.
При запуске доставляемых игр на Pixel 4XL мы увидели, что SurfaceFlinger (задача с более высоким приоритетом, которая управляет Android Compositor):
Регулярно прерывает работу приложения, вызывая задержки в кадре на 1–3 мс и
Оказывает повышенную нагрузку на память вершин/текстур графического процессора, поскольку для выполнения работы по композиции компоновщику приходится считывать весь буфер кадра.
Правильная обработка ориентации практически полностью устраняет вытеснение графического процессора SurfaceFlinger, в то время как частота графического процессора падает на 40%, поскольку повышенная частота, используемая Android Compositor, больше не нужна.
Чтобы обеспечить корректную обработку поворотов поверхности с минимальными накладными расходами, как показано в предыдущем случае, следует реализовать метод 3. Это называется предварительным поворотом (pre-rotation) . Он сообщает ОС Android, что ваше приложение обрабатывает поворот поверхности. Это можно сделать, передав флаги преобразования поверхности, которые определяют ориентацию, во время создания цепочки обмена. Это предотвращает выполнение поворота компонентом Android Compositor.
Знание того, как установить флаг преобразования поверхности, важно для каждого приложения Vulkan. Приложения, как правило, поддерживают либо несколько ориентаций, либо одну, когда поверхность рендеринга находится в ориентации, отличной от той, которую устройство считает своей ориентацией идентификации. Например, приложение с альбомной ориентацией на телефоне с портретной ориентацией или приложение с портретной ориентацией на планшете с альбомной ориентацией.
Изменить AndroidManifest.xml
Чтобы управлять поворотом устройства в вашем приложении, начните с изменения файла AndroidManifest.xml
, чтобы сообщить Android, что ваше приложение будет обрабатывать изменения ориентации и размера экрана. Это предотвратит уничтожение и повторное создание Android Activity
и вызов функции onDestroy()
для существующей поверхности окна при изменении ориентации. Это достигается добавлением атрибутов orientation
(для поддержки API уровня <13) и screenSize
в раздел configChanges
активности:
<activity android:name="android.app.NativeActivity"
android:configChanges="orientation|screenSize">
Если ваше приложение фиксирует ориентацию экрана с помощью атрибута screenOrientation
, вам не нужно этого делать. Кроме того, если ваше приложение использует фиксированную ориентацию, то настроить цепочку обмена потребуется только один раз при запуске/возобновлении работы приложения.
Получите разрешение экрана Identity и параметры камеры
Затем определите разрешение экрана устройства, связанное со значением VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR
. Это разрешение связано с ориентацией устройства, и поэтому цепочке обмена всегда необходимо задать именно это значение. Самый надёжный способ получить это разрешение — вызвать vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
при запуске приложения и сохранить возвращённый размер. Поменяйте местами ширину и высоту на основе возвращаемого значения currentTransform
, чтобы гарантировать сохранение разрешения экрана, соответствующего идентификатору:
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
uint32_t width = capabilities.currentExtent.width;
uint32_t height = capabilities.currentExtent.height;
if (capabilities.currentTransform & VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR ||
capabilities.currentTransform & VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR) {
// Swap to get identity width and height
capabilities.currentExtent.height = width;
capabilities.currentExtent.width = height;
}
displaySizeIdentity = capabilities.currentExtent;
displaySizeIdentity — это структура VkExtent2D
, которую мы используем для хранения указанного разрешения идентификации поверхности окна приложения в естественной ориентации дисплея.
Обнаружение изменений ориентации устройства (Android 10+)
Самый надёжный способ обнаружить изменение ориентации в вашем приложении — проверить, возвращает ли функция vkQueuePresentKHR()
VK_SUBOPTIMAL_KHR
. Например:
auto res = vkQueuePresentKHR(queue_, &present_info);
if (res == VK_SUBOPTIMAL_KHR){
orientationChanged = true;
}
Примечание: это решение работает только на устройствах под управлением Android 10 и более поздних версий. Эти версии Android возвращают VK_SUBOPTIMAL_KHR
из vkQueuePresentKHR()
. Результат этой проверки сохраняется в orientationChanged
— boolean
, доступном из основного цикла рендеринга приложения.
Обнаружение изменений ориентации устройства (до Android 10)
Для устройств под управлением Android 10 и старше требуется другая реализация, поскольку VK_SUBOPTIMAL_KHR
не поддерживается.
Использование опроса
На устройствах до Android 10 можно опрашивать текущее преобразование устройства каждые кадры pollingInterval
, где pollingInterval
— это уровень детализации, выбранный программистом. Это делается путем вызова vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
и последующего сравнения возвращаемого поля currentTransform
с полем текущего сохранённого преобразования поверхности (в этом примере кода оно хранится в pretransformFlag
).
currFrameCount++;
if (currFrameCount >= pollInterval){
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
if (pretransformFlag != capabilities.currentTransform) {
window_resized = true;
}
currFrameCount = 0;
}
На Pixel 4 под управлением Android 10 опрос vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
занял от 0,120 до 0,250 мс, а на Pixel 1XL под управлением Android 8 опрос занял от 0,110 до 0,350 мс.
Использование обратных вызовов
Второй вариант для устройств под управлением Android ниже 10 — зарегистрировать обратный вызов onNativeWindowResized()
для вызова функции, которая устанавливает флаг orientationChanged
, сигнализируя приложению об изменении ориентации:
void android_main(struct android_app *app) {
...
app->activity->callbacks->onNativeWindowResized = ResizeCallback;
}
Где ResizeCallback определяется как:
void ResizeCallback(ANativeActivity *activity, ANativeWindow *window){
orientationChanged = true;
}
Проблема этого решения заключается в том, что onNativeWindowResized()
вызывается только при изменении ориентации на 90 градусов, например, при переходе с альбомной на портретную и наоборот. Другие изменения ориентации не приведут к воссозданию цепочки переключений. Например, изменение ориентации с альбомной на обратную альбомную не приведёт к её запуску, и компоновщик Android должен будет выполнить переворот для вашего приложения.
Как справиться с изменением ориентации
Чтобы обработать изменение ориентации, вызовите процедуру изменения ориентации в начале основного цикла рендеринга, когда переменная orientationChanged
установлена в значение true. Например:
bool VulkanDrawFrame() {
if (orientationChanged) {
OnOrientationChange();
}
Всю работу, необходимую для воссоздания цепочки обмена, вы выполняете в функции OnOrientationChange()
. Это означает, что вы:
Уничтожьте все существующие экземпляры
Framebuffer
иImageView
,Пересоздайте цепочку обмена, одновременно уничтожая старую цепочку обмена (что будет обсуждаться далее), и
Пересоздайте буферы кадров с помощью DisplayImages новой цепочки обмена. Примечание: Прикреплённые изображения (например, изображения глубины/трафарета) обычно не требуют пересоздания, поскольку они основаны на разрешении идентичности предварительно повёрнутых изображений цепочки обмена.
void OnOrientationChange() {
vkDeviceWaitIdle(getDevice());
for (int i = 0; i < getSwapchainLength(); ++i) {
vkDestroyImageView(getDevice(), displayViews_[i], nullptr);
vkDestroyFramebuffer(getDevice(), framebuffers_[i], nullptr);
}
createSwapChain(getSwapchain());
createFrameBuffers(render_pass, depthBuffer.image_view);
orientationChanged = false;
}
А в конце функции вы сбрасываете флаг orientationChanged
на false, чтобы показать, что вы обработали изменение ориентации.
Swapchain Отдых
В предыдущем разделе мы упоминали о необходимости пересоздать цепочку обмена. Первым шагом к этому является получение новых характеристик поверхности рендеринга:
void createSwapChain(VkSwapchainKHR oldSwapchain) {
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
pretransformFlag = capabilities.currentTransform;
После заполнения структуры VkSurfaceCapabilities
новой информацией вы можете проверить, произошло ли изменение ориентации, проверив поле currentTransform
. Вы сохраните его в поле pretransformFlag
, так как оно понадобится вам позже при внесении изменений в матрицу MVP.
Для этого укажите следующие атрибуты в структуре VkSwapchainCreateInfo
:
VkSwapchainCreateInfoKHR swapchainCreateInfo{
...
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
.imageExtent = displaySizeIdentity,
.preTransform = pretransformFlag,
.oldSwapchain = oldSwapchain,
};
vkCreateSwapchainKHR(device_, &swapchainCreateInfo, nullptr, &swapchain_));
if (oldSwapchain != VK_NULL_HANDLE) {
vkDestroySwapchainKHR(device_, oldSwapchain, nullptr);
}
Поле imageExtent
будет заполнено значением экстента displaySizeIdentity
, сохранённым при запуске приложения. Поле preTransform
будет заполнено значением переменной pretransformFlag
(которой присвоено значение поля currentTransform объекта surfaceCapabilities
). Также необходимо указать в поле oldSwapchain
номер цепочки обмена, которая будет удалена.
Корректировка матрицы MVP
Последнее, что вам нужно сделать, — это применить предварительное преобразование, применив матрицу поворота к вашей матрице MVP. По сути, это применяет поворот в пространстве отсечения, чтобы результирующее изображение было повернуто в соответствии с текущей ориентацией устройства. Затем вы можете просто передать обновлённую матрицу MVP в вершинный шейдер и использовать её как обычно, без необходимости вносить изменения в шейдеры.
glm::mat4 pre_rotate_mat = glm::mat4(1.0f);
glm::vec3 rotation_axis = glm::vec3(0.0f, 0.0f, 1.0f);
if (pretransformFlag & VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR) {
pre_rotate_mat = glm::rotate(pre_rotate_mat, glm::radians(90.0f), rotation_axis);
}
else if (pretransformFlag & VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR) {
pre_rotate_mat = glm::rotate(pre_rotate_mat, glm::radians(270.0f), rotation_axis);
}
else if (pretransformFlag & VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR) {
pre_rotate_mat = glm::rotate(pre_rotate_mat, glm::radians(180.0f), rotation_axis);
}
MVP = pre_rotate_mat * MVP;
Рассмотрение — область просмотра, не занимающая весь экран, и обрезка
Если ваше приложение использует область просмотра/резки, не занимающую весь экран, их необходимо обновить в соответствии с ориентацией устройства. Для этого необходимо включить динамические параметры области просмотра и резки при создании конвейера Vulkan:
VkDynamicState dynamicStates[2] = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR,
};
VkPipelineDynamicStateCreateInfo dynamicInfo = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.dynamicStateCount = 2,
.pDynamicStates = dynamicStates,
};
VkGraphicsPipelineCreateInfo pipelineCreateInfo = {
.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
...
.pDynamicState = &dynamicInfo,
...
};
VkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineCreateInfo, nullptr, &mPipeline);
Фактическое вычисление размера области просмотра во время записи буфера команд выглядит следующим образом:
int x = 0, y = 0, w = 500, h = 400;
glm::vec4 viewportData;
switch (device->GetPretransformFlag()) {
case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR:
viewportData = {bufferWidth - h - y, x, h, w};
break;
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
viewportData = {bufferWidth - w - x, bufferHeight - h - y, w, h};
break;
case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR:
viewportData = {y, bufferHeight - w - x, h, w};
break;
default:
viewportData = {x, y, w, h};
break;
}
const VkViewport viewport = {
.x = viewportData.x,
.y = viewportData.y,
.width = viewportData.z,
.height = viewportData.w,
.minDepth = 0.0F,
.maxDepth = 1.0F,
};
vkCmdSetViewport(renderer->GetCurrentCommandBuffer(), 0, 1, &viewport);
Переменные x
и y
определяют координаты верхнего левого угла области просмотра, а w
и h
— её ширину и высоту соответственно. Аналогичное вычисление можно использовать для настройки теста «ножницы», и оно приведено здесь для полноты картины:
int x = 0, y = 0, w = 500, h = 400;
glm::vec4 scissorData;
switch (device->GetPretransformFlag()) {
case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR:
scissorData = {bufferWidth - h - y, x, h, w};
break;
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
scissorData = {bufferWidth - w - x, bufferHeight - h - y, w, h};
break;
case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR:
scissorData = {y, bufferHeight - w - x, h, w};
break;
default:
scissorData = {x, y, w, h};
break;
}
const VkRect2D scissor = {
.offset =
{
.x = (int32_t)viewportData.x,
.y = (int32_t)viewportData.y,
},
.extent =
{
.width = (uint32_t)viewportData.z,
.height = (uint32_t)viewportData.w,
},
};
vkCmdSetScissor(renderer->GetCurrentCommandBuffer(), 0, 1, &scissor);
Рассмотрение — производные фрагментного шейдера
Если ваше приложение использует производные вычисления, такие как dFdx
и dFdy
, могут потребоваться дополнительные преобразования для учёта повёрнутой системы координат, поскольку эти вычисления выполняются в пиксельном пространстве. Для этого приложение должно передать фрагментному шейдеру некое указание на preTransform (например, целое число, представляющее текущую ориентацию устройства) и использовать его для корректного отображения производных вычислений:
- Для предварительно повернутой на 90 градусов рамы
- dFdx должен быть сопоставлен с dFdy
- dFdy должен быть сопоставлен с -dFdx
- Для предварительно повернутой на 270 градусов рамы
- dFdx должен быть сопоставлен с -dFdy
- dFdy должен быть сопоставлен с dFdx
- Для предварительно повернутого на 180 градусов кадра,
- dFdx должен быть сопоставлен с -dFdx
- dFdy должен быть сопоставлен с -dFdy
Заключение
Чтобы ваше приложение максимально эффективно использовало Vulkan на Android, необходимо реализовать предварительную ротацию. Вот наиболее важные выводы из этой статьи:
- Убедитесь, что при создании или воссоздании цепочки обмена флаг pretransform установлен в соответствии с флагом, возвращаемым операционной системой Android. Это позволит избежать накладных расходов на компоновщик.
- Сохраняйте размер цепочки обмена фиксированным в соответствии с разрешением поверхности окна приложения в естественной ориентации дисплея.
- Поверните матрицу MVP в пространстве клипов, чтобы учесть ориентацию устройства, поскольку разрешение/масштаб цепочки обмена больше не обновляется вместе с ориентацией дисплея.
- Обновляйте область просмотра и прямоугольники обрезки в соответствии с требованиями вашего приложения.
Пример приложения: Минимальная предварительная ротация Android