Obsługa orientacji urządzenia przy użyciu wstępnego obracania interfejsu Vulkan

Z tego artykułu dowiesz się, jak wydajnie obsługiwać obrót urządzenia w Twojej aplikacji Vulkan przez wdrożenie rotacji wstępnej.

Dzięki Vulkan możesz: określać znacznie więcej informacji o stanie renderowania niż w trybie OpenGL. W interfejsie Vulkan należy w sposób jawny wdrożyć elementy obsługiwane przez sterownik OpenGL, np. orientacji urządzenia i jego związku orientacji platformy renderowania. Android umożliwia uchwyt uzgadnia renderowaną powierzchnię urządzenia z orientacją urządzenia:

  1. System operacyjny Android może używać procesora wyświetlacza (DPU) urządzenia. który wydajnie radzi sobie z obrotem powierzchni. Dostępny w tych terminach: tylko na obsługiwanych urządzeniach.
  2. System operacyjny Android może obsłużyć obrót powierzchni, dodając do niego kartę kompozytora. Ten będzie kosztował w zależności od tego, jak kompozytor radzi sobie z obracając obraz wyjściowy.
  3. Sama aplikacja może obsłużyć obrót powierzchni, renderując obrócono obraz na powierzchnię renderowaną zgodnie z bieżącą orientacją wyświetlacz.

Którą z tych metod wykorzystasz?

Obecnie nie ma możliwości sprawdzenia przez aplikację, czy obrót powierzchni będzie obsługiwana poza aplikacją, będzie bezpłatna. Nawet jeśli jest dostępny DPU, ale nadal będzie najprawdopodobniej wymierny spadek skuteczności. aby zapłacić. Jeśli Twoja aplikacja jest powiązana z procesorem, staje się to problem z zasilaniem z powodu: zwiększone wykorzystanie GPU przez komponent Android Compositor, który zwykle wzmocnionej częstotliwości. Jeśli aplikacja jest powiązana z GPU, Android Compositor może również zaprzestać pracy GPU aplikacji, powodując wzrost wydajności straty.

W przypadku nazw dostaw Pixela 4 XL zauważyliśmy SurfaceFlinger (zadanie o wyższym priorytecie, które opiera się na Kompozytor):

  • Regularnie zakłóca działanie aplikacji, powodując 1–3 ms liczbę klatek na sekundę,

  • Wywiera zwiększoną presję na GPU pamięć wierzchołków/tekstur, ponieważ kompozytor musi odczytać całą Framebuffer, by utworzyć kompozycję.

Orientacja obsługi prawidłowo zatrzymuje zapobieganie wywłaszczaniu GPU przez usługę SurfaceFlinger całkowicie, podczas gdy częstotliwość GPU spada o 40% ze względu na wzmocnioną częstotliwość używaną przez Android Compositor nie jest już potrzebny.

Aby zapewnić prawidłowe obsługę obrotów powierzchni, tak jak w poprzednim przypadku, musisz wdrożyć metodę 3. Jest to tzw. wstępna rotacja. Informuje to system operacyjny Android, że aplikacja obsługuje obrót powierzchni. Możesz to zrobić, przekazując flagi przekształcenia powierzchni które określają orientację podczas tworzenia zamiany. Spowoduje to zatrzymanie samodzielnym obróceniu komponentu Android Compositor.

Umiejętność ustawienia flagi przekształcenia powierzchni jest ważna dla każdego interfejsu Vulkan aplikacji. Aplikacje obsługują zwykle wiele orientacji lub obsługują pojedynczą orientację, w której powierzchnia renderowania znajduje się w innym orientację, jak urządzenie określa swoją orientację tożsamości. Przykład: aplikacja tylko w orientacji poziomej na telefonie o orientacji pionowej lub tylko w orientacji pionowej. na tablecie o orientacji poziomej.

Modyfikuj plik AndroidManifest.xml

Aby obsługiwać obrót urządzenia w aplikacji, zacznij od zmiany jej ustawień AndroidManifest.xml plik, aby poinformować Androida, że aplikacja będzie obsługiwać orientację i rozmiar ekranu. Zapobiega to zniszczeniu i odtwarzaniu danych przez Androida Androida Activity i wywoływanie funkcji funkcji onDestroy() na istniejącej powierzchni okna, gdy nastąpi zmiana orientacji. Robi to: dodanie atrybutów orientation (w celu obsługi poziomu interfejsu API <13) oraz screenSize. w polu aktywności Sekcja configChanges:

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

Jeśli Twoja aplikacja poprawi orientację ekranu za pomocą funkcji screenOrientation , nie musisz tego robić. Ponadto, jeśli aplikacja używa ustalonego orientacja seksualna wymaga skonfigurowania swapchain tylko raz uruchamiania/wznawiania aplikacji.

Pobierz rozdzielczość ekranu tożsamości i parametry kamery

Następnie określ rozdzielczość ekranu urządzenia powiązane z wartością VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR. Ten jest powiązana z orientacją tożsamości urządzenia i jest w związku z tym wartość, która musi zawsze być ustawiana w ramach wymiany. Najbardziej niezawodnym sposobem jest nawiązanie połączenia vkGetPhysicalDeviceSurfaceCapabilitiesKHR() podczas uruchamiania aplikacji oraz przechowywania zwróconego zakresu. Zamień szerokość i wysokość na podstawie currentTransform, która jest zwracana, aby zapewnić, że przechowujesz pliki. rozdzielczość ekranu tożsamości:

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 to struktura VkExtent2D, której używamy do przechowywania tej tożsamości. rozdzielczości okna aplikacji w naturalnej orientacji wyświetlacza.

Wykrywanie zmian orientacji urządzenia (Android 10 i nowsze)

Najbardziej niezawodnym sposobem wykrywania zmiany orientacji w aplikacji jest aby sprawdzić, czy funkcja vkQueuePresentKHR() zwraca VK_SUBOPTIMAL_KHR Na przykład:

auto res = vkQueuePresentKHR(queue_, &present_info);
if (res == VK_SUBOPTIMAL_KHR){
  orientationChanged = true;
}

Uwaga: to rozwiązanie działa tylko na urządzeniach z Android 10 i nowsze. Te wersje Androida zwracają VK_SUBOPTIMAL_KHR od: vkQueuePresentKHR(). Wynik tego działania przechowujemy zamelduj się w orientationChanged, boolean, do którego mamy dostęp z aplikacji głównej pętli renderowania.

Wykrywanie zmian w orientacji urządzenia (starsze urządzenia z Androidem 10)

Na urządzeniach z Androidem 10 lub starszym możesz ustawić inne wymagana jest implementacja, ponieważ VK_SUBOPTIMAL_KHR nie jest obsługiwany.

Korzystanie z ankiet

Na urządzeniach z systemem starszym niż Android 10 możesz sondować bieżące przekształcenie urządzenia co Klatki: pollingInterval, gdzie pollingInterval to szczegółowość określona przez programistę. Aby to zrobić, zadzwoń do vkGetPhysicalDeviceSurfaceCapabilitiesKHR(), a następnie porównując zwrócone dane currentTransform z polem obecnie zapisanej powierzchni przekształcenia (w tym przykładowym kodzie przechowywanym w pretransformFlag).

currFrameCount++;
if (currFrameCount >= pollInterval){
  VkSurfaceCapabilitiesKHR capabilities;
  vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);

  if (pretransformFlag != capabilities.currentTransform) {
    window_resized = true;
  }
  currFrameCount = 0;
}

Na Pixelu 4 z Androidem 10 ankiety Działanie vkGetPhysicalDeviceSurfaceCapabilitiesKHR() trwało od 0,120 do 250 ms Pixel 1 XL z Androidem 8, odpytywanie trwało 0,110–0,350 ms.

Korzystanie z wywołań zwrotnych

Drugą opcją na urządzeniach z Androidem w wersji starszej niż 10 jest zarejestrowanie onNativeWindowResized(), aby wywołać funkcję, która ustawia flaga orientationChanged, sygnalizująca aplikacji zmianę orientacji. miało miejsce:

void android_main(struct android_app *app) {
  ...
  app->activity->callbacks->onNativeWindowResized = ResizeCallback;
}

Gdzie ResizeCallback jest zdefiniowana jako:

void ResizeCallback(ANativeActivity *activity, ANativeWindow *window){
  orientationChanged = true;
}

Problem z tym rozwiązaniem polega na tym, że onNativeWindowResized() otrzymuje tylko dla orientacji pionowej lub pionowej, na odwrót. Inne zmiany orientacji nie spowodują odtworzenia procesu wymiany. Na przykład zmiana orientacji z poziomego na odwrotny go nie uruchamiać, wymagając od kompozytora Androida wykonania odwrócenia aplikacji.

Radzenie sobie ze zmianą orientacji

Aby obsłużyć zmianę orientacji, wywołaj rutynę zmiany orientacji w na początku głównej pętli renderowania, gdy orientationChanged jest ustawiona na wartość prawda. Na przykład:

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

Wykonasz wszystkie czynności niezbędne, aby odtworzyć wymianę funkcję OnOrientationChange(). Oznacza to, że:

  1. Zniszcz wszystkie istniejące instancje Framebuffer i ImageView.

  2. Odtwórz zamianę i niszcząc starego mechanizmu wymiany (co zostanie omówione dalej), a także

  3. Odtwórz Framebuffers, używając obrazów DisplayImage w nowym narzędziu wymiany. Uwaga: obrazy załączników (np. obrazy z głębią/szablonami) zwykle nie które trzeba odtworzyć, są oparte na rozdzielczości tożsamości wstępnie obróconych obrazów zamienianych.

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;
}

Na końcu funkcji zresetujesz flagę orientationChanged na wartość fałsz. aby pokazać, że zmieniono orientację.

Rekreacja w systemie wymiany

W poprzedniej sekcji wspominaliśmy o konieczności odtworzenia zamiany. W tym celu trzeba najpierw poznać nowe cechy platforma renderowania:

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

Dzięki strukturze VkSurfaceCapabilities zawierającej nowe informacje można teraz sprawdzić, czy nastąpiła zmiana orientacji, sprawdzając currentTransform. Zapiszesz je na później w pretransformFlag ponieważ będzie ono potrzebne później podczas wprowadzania zmian Macierz MVP.

W tym celu podaj następujące atrybuty w strukturze 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);
}

Pole imageExtent zostanie wypełnione wartością displaySizeIdentity, która zapisanych podczas uruchamiania aplikacji. Pole preTransform zostanie wypełnione ze zmienną pretransformFlag (ustawioną na pole currentTransform surfaceCapabilities). Ustawiasz również pole oldSwapchain na który zostanie zniszczony.

Korekta matrycy MVP

Ostatnią rzeczą, jaką musisz zrobić, jest zastosowanie przekształcenia wstępnego. przez zastosowanie macierzy rotacji do macierzy MVP. Zasadniczo chodzi o to, zastosuj obrót w miejscu klipu, tak aby powstały obraz został obrócony bieżącej orientacji urządzenia. Następnie można po prostu przekazać tę zaktualizowaną macierz MVP, w cieniowaniu wierzchołków i używać go w zwykły sposób bez konieczności programy do cieniowania.

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;

Rozważanie zakupu – niepełnoekranowy widoczny obszar i nożyce

Jeśli Twoja aplikacja używa widocznego obszaru lub obszaru nożyczkowego niepełnoekranowego, wymaga aktualizacji odpowiednio do orientacji urządzenia. Ten wymaga włączenia dynamicznych opcji widocznego obszaru i nożyczek w interfejsie Vulkan tworzenie potoku:

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);

Rzeczywista wielkość widocznego obszaru podczas rejestrowania w buforze poleceń wygląda tak:

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);

Zmienne x i y określają współrzędne lewego górnego rogu a w i h określają odpowiednio szerokość i wysokość. Te same obliczenia można też wykorzystać do wyznaczenia testu nożyczek. Są one uwzględniane tutaj:

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);

Rozważanie zakupu – pochodne generatora fragmentów kodu

Jeśli aplikacja używa obliczeń pochodnych, takich jak dFdx i dFdy, aby uwzględnić obróconą współrzędną, mogą być wymagane dodatkowe przekształcenia ponieważ obliczenia są wykonywane w przestrzeni pikselowej. Wymagana jest aplikacja do przekazania wskaźnika preTransform do modułu cieniowania fragmentów (np. reprezentująca aktualną orientację urządzenia) i użyj jej do zmapowania poprawnie obliczyć pochodne:

  • Ramka obrócona z wyprzedzeniem 90 stopni
    • Identyfikator dFdx musi być zmapowany na obiekt dFdy.
    • Identyfikator dFdy musi być zmapowany na parametr -dFdx.
  • Ramka obrócona z wyprzedzeniem 270 stopni
    • Tag dFdx musi być zmapowany na parametr -dFdy.
    • Identyfikator dFdy musi być zmapowany na obiekt dFdx.
  • W przypadku ramki obróconej z wyprzedzeniem 180 stopni
    • Tag dFdx musi być zmapowany na parametr -dFdx.
    • Identyfikator dFdy musi być zmapowany na -dFdy.

Podsumowanie

Aby Twoja aplikacja w pełni wykorzystywała Vulkan na Androidzie, Wprowadzenie wcześniejszej rotacji jest koniecznością. Oto najważniejsze wnioski, jakie należy wyciągnąć artykuły są:

  • Upewnij się, że podczas tworzenia lub odtwarzania swapchain flaga wstępnego przekształcania jest ustawiona tak, by była zgodna z flagą zwracaną przez system operacyjny Android. Pozwoli to uniknąć narzut kompozytora.
  • Nie zamykaj rozmiaru zamiany zgodnie z rozdzielczością tożsamości w oknie aplikacji w naturalnej orientacji ekranu.
  • Obróć macierz MVP w obszarze klipu, aby uwzględnić orientację urządzeń. ponieważ rozdzielczość/zakres wymiany nie jest już aktualizowany ekranu.
  • Odpowiednio aktualizuj widoczny obszar i prostokąty nożyczek w zależności od aplikacji.

Przykładowa aplikacja: minimalna rotacja przed rotacją na Androidzie