Управление ориентацией устройства с помощью предварительного вращения Vulkan

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

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

  1. ОС Android может использовать процессор дисплея (DPU) устройства, который может эффективно обрабатывать вращение поверхности аппаратно. Доступно только на поддерживаемых устройствах.
  2. ОС Android может обрабатывать вращение поверхности, добавляя проход композитора. Это повлечет за собой снижение производительности в зависимости от того, как наборщику приходится обращаться с поворотом выходного изображения.
  3. Само приложение может обрабатывать вращение поверхности, отображая повернутое изображение на поверхности рендеринга, которая соответствует текущей ориентации дисплея.

Какой из этих методов вам следует использовать?

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

При запуске игр на Pixel 4XL мы увидели, что SurfaceFlinger (задача с более высоким приоритетом, которая управляет Android Compositor):

  • Регулярно вытесняет работу приложения, вызывая нарушения времени кадра на 1–3 мс и

  • Увеличивает нагрузку на память вершин/текстур графического процессора, поскольку композитору приходится читать весь буфер кадра для выполнения работы по композиции.

Правильная обработка ориентации практически полностью останавливает вытеснение графического процессора с помощью SurfaceFlinger, а частота графического процессора падает на 40 %, поскольку повышенная частота, используемая Android Compositor, больше не нужна.

Чтобы обеспечить правильную обработку поворотов поверхности с минимальными издержками, как показано в предыдущем случае, следует реализовать метод 3. Он известен как предварительное вращение . Это сообщает ОС Android, что ваше приложение обрабатывает вращение поверхности. Вы можете сделать это, передав флаги преобразования поверхности, которые определяют ориентацию во время создания цепочки обмена. Это не позволяет Android Compositor выполнять вращение самостоятельно .

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

Измените AndroidManifest.xml.

Чтобы управлять поворотом устройства в вашем приложении, начните с изменения файла AndroidManifest.xml приложения, чтобы сообщить Android, что ваше приложение будет обрабатывать изменения ориентации и размера экрана. Это не позволяет Android уничтожить и воссоздать Activity Android, а также вызвать функцию onDestroy() на существующей поверхности окна при изменении ориентации. Это делается путем добавления атрибутов orientation (для поддержки уровня API <13) и screenSize в раздел configChanges действия:

<activity android:name="android.app.NativeActivity"
          android:configChanges="orientation|screenSize">

Если ваше приложение фиксирует ориентацию экрана с помощью атрибута screenOrientation , вам не нужно этого делать. Кроме того, если ваше приложение использует фиксированную ориентацию, ему нужно будет настроить цепочку обмена только один раз при запуске/возобновлении приложения.

Получите разрешение экрана идентификации и параметры камеры.

Затем определите разрешение экрана устройства, связанное со значением 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() . Это означает, что вы:

  1. Уничтожьте все существующие экземпляры Framebuffer и ImageView .

  2. Воссоздать цепочку обмена, уничтожив старую цепочку обмена (о которой речь пойдет далее), и

  3. Воссоздайте фреймбуферы с помощью 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 , могут потребоваться дополнительные преобразования для учета повернутой системы координат, поскольку эти вычисления выполняются в пиксельном пространстве. Для этого приложение должно передать некоторую информацию о предварительном преобразовании во фрагментный шейдер (например, целое число, представляющее текущую ориентацию устройства) и использовать его для правильного сопоставления производных вычислений:

  • Для рамы, предварительно повернутой на 90 градусов.
    • dFdx должен быть сопоставлен с dFdy
    • dFdy должен быть сопоставлен с -dFdx
  • Для рамы, предварительно повернутой на 270 градусов.
    • dFdx должен быть сопоставлен с -dFdy
    • dFdy должен быть сопоставлен с dFdx
  • Для рамы, предварительно повернутой на 180 градусов ,
    • dFdx должен быть сопоставлен с -dFdx
    • dFdy должен быть сопоставлен с -dFdy

Заключение

Чтобы ваше приложение максимально эффективно использовало возможности Vulkan на Android, необходимо реализовать предварительную ротацию. Наиболее важные выводы из этой статьи:

  • Убедитесь, что во время создания или восстановления цепочки обмена флаг предварительного преобразования установлен в соответствии с флагом, возвращаемым операционной системой Android. Это позволит избежать накладных расходов на компоновщик.
  • Сохраняйте размер цепочки обмена фиксированным в соответствии с разрешением поверхности окна приложения в естественной ориентации дисплея.
  • Поверните матрицу MVP в пространстве отсечения, чтобы учесть ориентацию устройств, поскольку разрешение/экстент цепочки обмена больше не обновляется вместе с ориентацией дисплея.
  • Обновите область просмотра и прямоугольники-ножницы по мере необходимости вашего приложения.

Пример приложения: минимальная предварительная ротация Android

,

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

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

  1. ОС Android может использовать процессор дисплея (DPU) устройства, который может эффективно обрабатывать вращение поверхности аппаратно. Доступно только на поддерживаемых устройствах.
  2. ОС Android может обрабатывать вращение поверхности, добавляя проход композитора. Это повлечет за собой снижение производительности в зависимости от того, как наборщику приходится обращаться с поворотом выходного изображения.
  3. Само приложение может обрабатывать вращение поверхности, отображая повернутое изображение на поверхности рендеринга, которая соответствует текущей ориентации дисплея.

Какой из этих методов вам следует использовать?

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

При запуске игр на Pixel 4XL мы увидели, что SurfaceFlinger (задача с более высоким приоритетом, которая управляет Android Compositor):

  • Регулярно вытесняет работу приложения, вызывая нарушения времени кадра на 1–3 мс и

  • Увеличивает нагрузку на память вершин/текстур графического процессора, поскольку композитору приходится читать весь буфер кадра для выполнения работы по композиции.

Правильная обработка ориентации почти полностью останавливает вытеснение графического процессора с помощью SurfaceFlinger, а частота графического процессора падает на 40 %, поскольку повышенная частота, используемая Android Compositor, больше не нужна.

Чтобы обеспечить правильную обработку поворотов поверхности с минимальными издержками, как показано в предыдущем случае, следует реализовать метод 3. Он известен как предварительное вращение . Это сообщает ОС Android, что ваше приложение обрабатывает вращение поверхности. Вы можете сделать это, передав флаги преобразования поверхности, которые определяют ориентацию во время создания цепочки обмена. Это не позволяет Android Compositor выполнять вращение самостоятельно .

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

Измените AndroidManifest.xml.

Чтобы управлять поворотом устройства в вашем приложении, начните с изменения файла AndroidManifest.xml приложения, чтобы сообщить Android, что ваше приложение будет обрабатывать изменения ориентации и размера экрана. Это не позволяет Android уничтожить и воссоздать Activity Android, а также вызвать функцию onDestroy() на существующей поверхности окна при изменении ориентации. Это делается путем добавления атрибутов orientation (для поддержки уровня API <13) и screenSize в раздел configChanges действия:

<activity android:name="android.app.NativeActivity"
          android:configChanges="orientation|screenSize">

Если ваше приложение фиксирует ориентацию экрана с помощью атрибута screenOrientation , вам не нужно этого делать. Кроме того, если ваше приложение использует фиксированную ориентацию, ему нужно будет настроить цепочку обмена только один раз при запуске/возобновлении приложения.

Получите разрешение экрана идентификации и параметры камеры.

Затем определите разрешение экрана устройства, связанное со значением 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() . Это означает, что вы:

  1. Уничтожьте все существующие экземпляры Framebuffer и ImageView .

  2. Воссоздать цепочку обмена, уничтожив старую цепочку обмена (о которой речь пойдет далее), и

  3. Воссоздайте фреймбуферы с помощью 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 , могут потребоваться дополнительные преобразования для учета повернутой системы координат, поскольку эти вычисления выполняются в пиксельном пространстве. Для этого приложение должно передать некоторую информацию о предварительном преобразовании во фрагментный шейдер (например, целое число, представляющее текущую ориентацию устройства) и использовать его для правильного сопоставления производных вычислений:

  • Для рамы, предварительно повернутой на 90 градусов.
    • dFdx должен быть сопоставлен с dFdy
    • dFdy должен быть сопоставлен с -dFdx
  • Для рамы, предварительно повернутой на 270 градусов.
    • dFdx должен быть сопоставлен с -dFdy
    • dFdy должен быть сопоставлен с dFdx
  • Для рамы, предварительно повернутой на 180 градусов ,
    • dFdx должен быть сопоставлен с -dFdx
    • dFdy должен быть сопоставлен с -dFdy

Заключение

Чтобы ваше приложение максимально эффективно использовало возможности Vulkan на Android, необходимо реализовать предварительную ротацию. Наиболее важные выводы из этой статьи:

  • Убедитесь, что во время создания или восстановления цепочки обмена флаг предварительного преобразования установлен в соответствии с флагом, возвращаемым операционной системой Android. Это позволит избежать накладных расходов на компоновщик.
  • Сохраняйте размер цепочки обмена фиксированным в соответствии с разрешением поверхности окна приложения в естественной ориентации дисплея.
  • Поверните матрицу MVP в пространстве отсечения, чтобы учесть ориентацию устройств, поскольку разрешение/экстент цепочки обмена больше не обновляется вместе с ориентацией дисплея.
  • Обновите область просмотра и прямоугольники-ножницы по мере необходимости вашего приложения.

Пример приложения: минимальная предварительная ротация Android

,

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

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

  1. ОС Android может использовать процессор дисплея (DPU) устройства, который может эффективно обрабатывать вращение поверхности аппаратно. Доступно только на поддерживаемых устройствах.
  2. ОС Android может обрабатывать вращение поверхности, добавляя проход композитора. Это повлечет за собой снижение производительности в зависимости от того, как наборщику приходится обращаться с поворотом выходного изображения.
  3. Само приложение может обрабатывать вращение поверхности, отображая повернутое изображение на поверхности рендеринга, которая соответствует текущей ориентации дисплея.

Какой из этих методов вам следует использовать?

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

При запуске игр на Pixel 4XL мы увидели, что SurfaceFlinger (задача с более высоким приоритетом, которая управляет Android Compositor):

  • Регулярно вытесняет работу приложения, вызывая нарушения времени кадра на 1–3 мс и

  • Увеличивает нагрузку на память вершин/текстур графического процессора, поскольку композитору приходится читать весь буфер кадра для выполнения работы по композиции.

Правильная обработка ориентации почти полностью останавливает вытеснение графического процессора с помощью SurfaceFlinger, а частота графического процессора падает на 40 %, поскольку повышенная частота, используемая Android Compositor, больше не нужна.

Чтобы обеспечить правильную обработку поворотов поверхности с минимальными издержками, как показано в предыдущем случае, следует реализовать метод 3. Он известен как предварительное вращение . Это сообщает ОС Android, что ваше приложение обрабатывает вращение поверхности. Вы можете сделать это, передав флаги преобразования поверхности, которые определяют ориентацию во время создания цепочки обмена. Это не позволяет Android Compositor выполнять вращение самостоятельно .

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

Измените AndroidManifest.xml.

Чтобы управлять поворотом устройства в вашем приложении, начните с изменения файла AndroidManifest.xml приложения, чтобы сообщить Android, что ваше приложение будет обрабатывать изменения ориентации и размера экрана. Это не позволяет Android уничтожить и воссоздать Activity Android, а также вызвать функцию onDestroy() на существующей поверхности окна при изменении ориентации. Это делается путем добавления атрибутов orientation (для поддержки уровня API <13) и screenSize в раздел configChanges действия:

<activity android:name="android.app.NativeActivity"
          android:configChanges="orientation|screenSize">

Если ваше приложение фиксирует ориентацию экрана с помощью атрибута screenOrientation , вам не нужно этого делать. Кроме того, если ваше приложение использует фиксированную ориентацию, ему нужно будет настроить цепочку обмена только один раз при запуске/возобновлении приложения.

Получите разрешение экрана идентификации и параметры камеры.

Затем определите разрешение экрана устройства, связанное со значением 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() . Это означает, что вы:

  1. Уничтожьте все существующие экземпляры Framebuffer и ImageView .

  2. Воссоздать цепочку обмена, уничтожив старую цепочку обмена (о которой речь пойдет далее), и

  3. Воссоздайте фреймбуферы с помощью 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 в Swapchain, который будет уничтожен.

Регулировка матрицы 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;

Рассмотрение - неполный просмотр экрана и ножницы

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

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 , могут потребоваться дополнительные преобразования для учета вращающейся системы координат, поскольку эти вычисления выполняются в пиксельном пространстве. Это требует, чтобы приложение передало некоторую индикацию претрансформы в фрагментный шейдер (например, целое число, представляющее текущую ориентацию устройства), и используйте его для правильного отображения производных вычислений:

  • Для предварительной рамки на 90 градусов
    • DFDX должен быть нанесен на карту DFDY
    • DFDY должен быть нанесен на карту с -DFDX
  • Для рамки до 270 градусов предварительно
    • DFDX должен быть нанесен на карту на -dfdy
    • DFDY должен быть нанесен на карту DFDX
  • Для 180-градусного предварительного рамки,
    • DFDX должен быть сопоставлен с -dfdx
    • DFDY должен быть нанесен на карту на -dfdy

Заключение

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

  • Убедитесь, что во время создания или отдыха Swapchain флаг предварительной формы установлен в соответствии с флагом, возвращенным операционной системой Android. Это будет избежать накладных расходов композитора.
  • Держите размер Swapchain, прикрепленным к разрешению идентификации поверхности окна приложения в естественной ориентации дисплея.
  • Поверните матрицу MVP в пространстве клипа, чтобы учесть ориентацию устройств, потому что разрешение/экстент Swapchain больше не обновляется с ориентацией дисплея.
  • Обновление просмотра и ножничных прямоугольников по мере необходимости для вашего приложения.

Пример приложения: минимальная предварительная дотация Android

,

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

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

  1. ОС Android может использовать блок обработки дисплея устройства (DPU), который может эффективно обрабатывать вращение поверхности в оборудовании. Доступно только на поддерживаемых устройствах.
  2. ОС Android может обрабатывать вращение поверхности, добавив проход композитора. Это будет иметь стоимость производительности в зависимости от того, как композитор должен иметь дело с вращанием выходного изображения.
  3. Само применение может обрабатывать вращение поверхности, отображая повернутое изображение на поверхность рендеринга, которая соответствует ориентации тока дисплея.

Какой из этих методов вы должны использовать?

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

При запуске названий доставки на Pixel 4XL мы видели этот Surfaceflinger (задача с более высоким приоритетом, которая управляет композитором Android):

  • Регулярно преодолевает работу приложения, вызывая 1-3 мс удара в FrameTimes, и

  • Оказывает повышенное давление на память вершины/текстуры GPU, потому что композитор должен прочитать весь кадр -буфер, чтобы выполнить свою композицию.

Ориентация по обращению должным образом практически полностью прекращает преодоление GPU Surfaceflinger, в то время как частота графического процессора падает на 40%, поскольку повышенная частота, используемая композитором Android, больше не требуется.

Чтобы убедиться, что вращения поверхности обрабатываются должным образом с максимально возможным количеством накладных расходов, как видно в предыдущем случае, вы должны реализовать метод 3. Это известно как предварительная дотация . Это говорит о ОС Android, что ваше приложение обрабатывает вращение поверхности. Вы можете сделать это, проходя мимо поверхностного преобразования флагов, которые указывают ориентацию во время создания Swapchain. Это мешает композитору Android делать само вращение.

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

Изменить AndroidManifest.xml

Чтобы обработать вращение устройства в вашем приложении, начните с изменения файла AndroidManifest.xml приложения, чтобы сообщить Android, что ваше приложение будет обрабатывать ориентацию и изменения размера экрана. Это мешает Android уничтожить и воссоздать Activity Android и вызывая функцию onDestroy() на существующей поверхности окна, когда происходит изменение ориентации. Это делается путем добавления orientation (для поддержки уровня API <13) и screenSize атрибутов в раздел configChanges деятельности:

<activity android:name="android.app.NativeActivity"
          android:configChanges="orientation|screenSize">

Если ваше приложение исправляет его ориентацию на экране, используя атрибут screenOrientation , вам не нужно это делать. Кроме того, если в вашем приложении используется фиксированная ориентация, то необходимо будет настроить Swapchain только один раз при запуске/резюме приложения.

Получите разрешение экрана идентификации и параметры камеры

Затем обнаружите разрешение экрана устройства, связанное с значением VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR . Это разрешение связано с ориентацией идентификации устройства и, следовательно, является той, на которую всегда нужно будет настроен на Swapchain. Самый надежный способ получить это - позвонить в 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() . Мы сохраняем результат этой проверки boolean orientationChanged .

Обнаружение изменений ориентации устройства (предварительно-ан-ан-Android 10)

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

Использование опроса

На устройствах Pre-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-250 мс и на Pixel 1XL Android 8, опрос потребовал 0,110-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 градусов, таких как переход от ландшафта к портрету или наоборот. Другие изменения ориентации не будут вызывать отдых Swapchain. Например, переход от ландшафта к обратному ландшафту не запустит его, требуя, чтобы композитор Android выполнял перелив для вашего приложения.

Обработка смены ориентации

Чтобы обработать изменение ориентации, вызовите подпрограмму изменения ориентации в верхней части основного цикла рендеринга, когда переменная orientationChanged установлена ​​на TRUE. Например:

bool VulkanDrawFrame() {
 if (orientationChanged) {
   OnOrientationChange();
}

Вы выполняете всю работу, необходимую для воссоздания Swapchain в рамках функции OnOrientationChange() . Это означает, что вы:

  1. Уничтожить любые существующие экземпляры Framebuffer и ImageView ,

  2. Воссоздать Swapchain, уничтожая старый Swapchain (который будет обсуждаться дальше), и

  3. Воссоздайте каркасы с помощью новой Swapchain's DisplayMages. ПРИМЕЧАНИЕ. Изображения привязанности (например, изображения глубины/трафарета) обычно не нужно воссоздать, поскольку они основаны на разрешении идентификации предварительно прикрепленных изображений Swapchain.

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 Recreation

В предыдущем разделе мы упоминаем необходимость воссоздать Swapchain. Первые шаги для этого связаны с получением новых характеристик поверхности рендеринга:

void createSwapChain(VkSwapchainKHR oldSwapchain) {
   VkSurfaceCapabilitiesKHR capabilities;
   vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
   pretransformFlag = capabilities.currentTransform;

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

Для этого укажите следующие атрибуты в VkSwapchainCreateInfo struct:

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 в Swapchain, который будет уничтожен.

Регулировка матрицы 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;

Рассмотрение - неполный просмотр экрана и ножницы

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

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 , могут потребоваться дополнительные преобразования для учета вращающейся системы координат, поскольку эти вычисления выполняются в пиксельном пространстве. Это требует, чтобы приложение передало некоторую индикацию претрансформы в фрагментный шейдер (например, целое число, представляющее текущую ориентацию устройства), и используйте его для правильного отображения производных вычислений:

  • Для предварительной рамки на 90 градусов
    • DFDX должен быть нанесен на карту DFDY
    • DFDY должен быть нанесен на карту с -DFDX
  • Для рамки до 270 градусов предварительно
    • DFDX должен быть нанесен на карту на -dfdy
    • DFDY должен быть нанесен на карту DFDX
  • Для 180-градусного предварительного рамки,
    • DFDX должен быть сопоставлен с -dfdx
    • DFDY должен быть нанесен на карту на -dfdy

Заключение

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

  • Убедитесь, что во время создания или отдыха Swapchain флаг предварительной формы установлен в соответствии с флагом, возвращенным операционной системой Android. Это будет избежать накладных расходов композитора.
  • Держите размер Swapchain, прикрепленным к разрешению идентификации поверхности окна приложения в естественной ориентации дисплея.
  • Поверните матрицу MVP в пространстве клипа, чтобы учесть ориентацию устройств, потому что разрешение/экстент Swapchain больше не обновляется с ориентацией дисплея.
  • Обновление просмотра и ножничных прямоугольников по мере необходимости для вашего приложения.

Пример приложения: минимальная предварительная дотация Android