В этой статье описывается, как эффективно обрабатывать поворот устройства в вашем приложении Vulkan путем реализации предварительного поворота.
В Vulkan можно указать гораздо больше информации о состоянии рендеринга, чем в OpenGL. В Vulkan необходимо явно реализовывать вещи, которые обрабатываются драйвером в OpenGL, такие как ориентация устройства и её связь с ориентацией поверхности рендеринга . Android может обрабатывать согласование поверхности рендеринга устройства с его ориентацией тремя способами:
- Операционная система Android может использовать встроенный в устройство процессор обработки изображений (DPU), который эффективно обрабатывает вращение поверхности устройства на аппаратном уровне. Доступно только на поддерживаемых устройствах.
- Операционная система Android может обрабатывать вращение поверхности, добавив проход композитора. Это повлияет на производительность в зависимости от того, как композитору придётся обрабатывать вращение выходного изображения.
- Само приложение может обрабатывать вращение поверхности, отображая повернутое изображение на поверхности рендеринга, соответствующей текущей ориентации дисплея.
Which of these methods should you use?
В настоящее время приложение не может определить, будет ли вращение поверхности, обрабатываемое вне приложения, бесплатным. Даже если есть DPU, который позаботится об этом за вас, всё равно, вероятно, будет ощутимое снижение производительности. Если ваше приложение сильно зависит от ЦП, это становится проблемой энергопотребления из-за повышенной загрузки графического процессора Android Compositor, который обычно работает на повышенной частоте. Если же ваше приложение сильно зависит от графического процессора, то Android Compositor может также прерывать работу графического процессора вашего приложения, что приводит к дополнительной потере производительности.
При запуске игр, уже поступивших в продажу, на Pixel 4XL мы заметили, что SurfaceFlinger (задача с более высоким приоритетом, управляющая композитором Android) выполняет следующие действия:
Регулярно прерывает работу приложения, вызывая задержки во времени кадра на 1-3 мс, и
Это создает дополнительную нагрузку на вершинную/текстурную память графического процессора, поскольку композитору приходится считывать весь кадровый буфер для выполнения своей работы по композиции.
Правильная обработка ориентации практически полностью предотвращает вытеснение графического процессора программой SurfaceFlinger, при этом частота графического процессора падает на 40%, поскольку повышенная частота, используемая Android Compositor, больше не требуется.
Чтобы обеспечить корректную обработку поворотов поверхности с минимальными накладными расходами, как показано в предыдущем примере, следует реализовать метод 3. Он называется предварительным поворотом . Это сообщает ОС Android, что ваше приложение обрабатывает поворот поверхности. Это можно сделать, передав флаги преобразования поверхности, которые определяют ориентацию во время создания цепочки обмена. Это предотвращает самостоятельный поворот со стороны композитора Android.
Умение устанавливать флаг преобразования поверхности важно для каждого приложения Vulkan. Приложения, как правило, либо поддерживают несколько ориентаций, либо поддерживают одну ориентацию, при которой поверхность рендеринга находится в ориентации, отличной от той, которую устройство считает своей истинной ориентацией. Например, приложение только для альбомной ориентации на телефоне с портретной ориентацией или приложение только для портретной ориентации на планшете с альбомной ориентацией.
Modify 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 , которую мы используем для хранения указанного разрешения размера окна приложения в естественной ориентации дисплея.
Detect Device Orientation Changes (Android 10+)
Наиболее надежный способ обнаружения изменения ориентации экрана в вашем приложении — это проверка того, возвращает ли функция vkQueuePresentKHR() VK_SUBOPTIMAL_KHR . Например:
auto res = vkQueuePresentKHR(queue_, &present_info);
if (res == VK_SUBOPTIMAL_KHR){
orientationChanged = true;
}
Примечание: это решение работает только на устройствах под управлением Android 10 и более поздних версий. В этих версиях Android функция vkQueuePresentKHR() возвращает значение VK_SUBOPTIMAL_KHR . Результат этой проверки сохраняется в 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;
}
Where ResizeCallback is defined as:
void ResizeCallback(ANativeActivity *activity, ANativeWindow *window){
orientationChanged = true;
}
Проблема этого решения заключается в том, что onNativeWindowResized() вызывается только при изменении ориентации на 90 градусов, например, при переходе из альбомной ориентации в портретную или наоборот. Другие изменения ориентации не вызовут повторного создания цепочки обмена. Например, изменение ориентации с альбомной на обратную альбомную не вызовет этого, и для вашего приложения потребуется, чтобы композитор Android выполнил переворот.
Handling the Orientation Change
Для обработки изменения ориентации экрана вызовите процедуру изменения ориентации в начале основного цикла отрисовки, когда переменная 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 Recreation
В предыдущем разделе мы упомянули о необходимости пересоздания цепочки обменов. Первые шаги для этого включают получение новых характеристик поверхности рендеринга:
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 Matrix Adjustment
Последнее, что вам нужно сделать, это применить предварительное преобразование, применив матрицу вращения к вашей матрице 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);
Consideration - Fragment Shader Derivatives
Если ваше приложение использует вычисления производных, такие как dFdx и dFdy , могут потребоваться дополнительные преобразования для учета повернутой системы координат, поскольку эти вычисления выполняются в пиксельном пространстве. Это требует от приложения передачи некоторого указания на предварительное преобразование (например, целого числа, представляющего текущую ориентацию устройства) во фрагментный шейдер и использования этого значения для правильного сопоставления вычислений производных:
- For a 90 degree pre-rotated frame
- dFdx must be mapped to dFdy
- dFdy must be mapped to -dFdx
- Для рамы, предварительно повернутой на 270 градусов
- dFdx must be mapped to -dFdy
- dFdy must be mapped to dFdx
- Для рамы, предварительно повернутой на 180 градусов ,
- dFdx must be mapped to -dFdx
- Параметр dFdy должен быть сопоставлен с параметром -dFdy.
Заключение
Для того чтобы ваше приложение максимально эффективно использовало Vulkan на Android, необходимо реализовать предварительный поворот экрана. Наиболее важные выводы из этой статьи:
- Убедитесь, что при создании или пересоздании цепочки обмена флаг предварительного преобразования установлен в соответствии с флагом, возвращаемым операционной системой Android. Это позволит избежать накладных расходов композитора.
- Размер цепочки обменов должен оставаться неизменным и соответствовать разрешению окна приложения в его естественной ориентации на экране.
- Поверните матрицу MVP в пространстве клипа, чтобы учесть ориентацию устройств, поскольку разрешение/область цепочки обмена больше не обновляется в зависимости от ориентации дисплея.
- При необходимости обновите прямоугольники области просмотра и области обрезки в соответствии с требованиями вашего приложения.
Sample App: Minimal Android pre-rotation