Z tego artykułu dowiesz się, jak efektywnie obsługiwać obrót urządzenia w aplikacji Vulkan, wdrażając wstępną rotację.
Dzięki Vulkanowi możesz podać znacznie więcej informacji o stanie renderowania niż w przypadku OpenGL. W Vulkan musisz jawnie zaimplementować elementy obsługiwane przez sterownik w OpenGL, takie jak orientacja urządzenia i jej związek z orientacją powierzchni renderowania. Android może dostosować powierzchnię renderowania do orientacji urządzenia na 3 sposoby:
- System operacyjny Android może korzystać z jednostki przetwarzania wyświetlacza (DPU), która może efektywnie obsługiwać obracanie powierzchni na poziomie sprzętowym. Dostępne tylko na obsługiwanych urządzeniach.
- System operacyjny Android może obsługiwać obrót powierzchni, dodając przetwarzanie kompozycji. Może to mieć wpływ na wydajność w zależności od tego, jak kompozytor musi sobie poradzić z obracaniem obrazu wyjściowego.
- Aplikacja może obsługiwać obracanie powierzchni, renderując obracany obraz na powierzchnię renderowania odpowiadającą bieżącej orientacji wyświetlacza.
Której z tych metod należy użyć?
Obecnie aplikacja nie może wiedzieć, czy obracanie powierzchni poza aplikacją będzie bezpłatne. Nawet jeśli masz dostęp do zespołu pomocy technicznej, który może Ci pomóc, prawdopodobnie będziesz musiał zapłacić karę za spadek skuteczności. Jeśli aplikacja jest związana z procesorem, problemem staje się zużycie energii przez procesor graficzny, który zwykle działa z podwyższoną częstotliwością. Jeśli aplikacja jest związana z procesorem graficznym, kompozytor Androida może też wyprzedzić pracę procesora graficznego aplikacji, powodując dodatkową utratę wydajności.
Podczas uruchamiania tytułów w wersji produkcyjnej na urządzeniu Pixel 4XL zauważyliśmy, że SurfaceFlinger (czynność o wyższym priorytecie, która uruchamia kompozytor Androida):
Regularnie wyprzedza działanie aplikacji, co powoduje opóźnienia w czasie wyświetlania klatek o 1–3 ms.
Zwiększa obciążenie pamięci wierzchołków i tekstur GPU, ponieważ kompozytor musi odczytać cały bufor ramki, aby wykonać kompozycję.
Prawidłowe przetwarzanie orientacji powoduje, że SurfaceFlinger prawie całkowicie zatrzymuje wywłaszczanie GPU, a częstotliwość GPU spada o 40%, ponieważ częstotliwość podwyższona używana przez kompozytor Androida nie jest już potrzebna.
Aby zapewnić prawidłowe obracanie powierzchni z jak najmniejszym obciążeniem, jak w poprzednim przypadku, należy zastosować metodę 3. Nazywamy to przed rotacją. Informuje system operacyjny Androida, że Twoja aplikacja obsługuje obracanie powierzchni. Aby to zrobić, możesz przekazać flagi przekształcania powierzchni, które określają orientację podczas tworzenia sekwencji wymiany. Zatrzymuje to kompozytor Androida przed samodzielnym przeprowadzeniem rotacji.
Umiejętność ustawiania flagi transformacji powierzchni jest ważna w przypadku każdej aplikacji Vulkan. Aplikacje zwykle obsługują wiele orientacji lub jedną orientację, w której powierzchnia renderowania jest w innej orientacji niż orientacja identyfikacyjna urządzenia. Na przykład aplikacja w orientacji poziomej na telefonie z orientacją pionową lub aplikacja w orientacji pionowej na tablecie z orientacją poziomą.
Modyfikowanie pliku AndroidManifest.xml
Aby obsłużyć obrót urządzenia w aplikacji, zacznij od zmiany pliku AndroidManifest.xml
aplikacji, aby poinformować Androida, że aplikacja będzie obsługiwać zmiany orientacji i rozmiaru ekranu. Zapobiega to zniszczeniu i ponowemu utworzeniu obiektu Activity
w Androidzie oraz wywołaniu funkcji onDestroy()
na istniejącej powierzchni okna po zmianie orientacji. Aby to zrobić, dodaj atrybuty orientation
(aby obsługiwać poziom interfejsu API <13) i screenSize
do sekcji configChanges
aktywności:
<activity android:name="android.app.NativeActivity"
android:configChanges="orientation|screenSize">
Jeśli aplikacja ustawia orientację ekranu za pomocą atrybutu screenOrientation
, nie musisz tego robić. Jeśli aplikacja ma stałą orientację, swapchain trzeba skonfigurować tylko raz podczas uruchamiania lub wznawiania aplikacji.
Pobieranie rozdzielczości ekranu identyfikacyjnego i parametrów kamery
Następnie wykryj rozdzielczość ekranu urządzenia powiązaną z wartością VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR
. Rozdzielczość jest powiązana z orientacją tożsamości urządzenia i dlatego zawsze musi być ustawiona w swapchain. Najbardziej niezawodnym sposobem jest wywołanie funkcji vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
podczas uruchamiania aplikacji i zapisanie zwracanego zakresu. Zamień szerokość i wysokość na podstawie wartości currentTransform
, która jest również zwracana, aby mieć pewność, że przechowujesz 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óra służy do przechowywania wspomnianej tożsamości.
resolution to powierzchnia okna aplikacji w naturalnej orientacji wyświetlacza.
Wykrywanie zmian orientacji urządzenia (Android 10 lub nowszy)
Najbardziej niezawodnym sposobem wykrywania zmiany orientacji w aplikacji jest sprawdzenie, czy funkcja vkQueuePresentKHR()
zwraca wartość VK_SUBOPTIMAL_KHR
. 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 Androidem 10 lub nowszym. Te wersje Androida zwracająVK_SUBOPTIMAL_KHR
z vkQueuePresentKHR()
. Wynik tego sprawdzenia jest przechowywany w orientationChanged
, czyli boolean
, który jest dostępny z głównej pętli renderowania aplikacji.
Wykrywanie zmian orientacji urządzenia (przed Androidem 10)
W przypadku urządzeń z Androidem 10 lub starszym potrzebna jest inna implementacja, ponieważ VK_SUBOPTIMAL_KHR
nie jest obsługiwana.
Korzystanie z ankiet
Na urządzeniach z wersją Android 10 i starszą możesz sprawdzać bieżącą transformację urządzenia co pollingInterval
ramek, gdzie pollingInterval
to poziom szczegółowości określony przez programistę. Aby to zrobić, wywołaj funkcję vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
, a potem porównaj zwrócone pole currentTransform
z polem bieżącej przekształcenia powierzchni (w tym przykładzie kodu przechowywanego w pliku pretransformFlag
).
currFrameCount++;
if (currFrameCount >= pollInterval){
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
if (pretransformFlag != capabilities.currentTransform) {
window_resized = true;
}
currFrameCount = 0;
}
Na urządzeniu Pixel 4 z Androidem 10 odpytywanievkGetPhysicalDeviceSurfaceCapabilitiesKHR()
trwało 0,120–0,250 s, a na urządzeniu Pixel 1XL z Androidem 8 – 0,110–0,350 s.
Używanie wywołań zwrotnych
Drugą opcją na urządzeniach z Androidem w wersji starszej niż 10 jest zarejestrowanie wywołania zwrotnego onNativeWindowResized()
, aby wywołać funkcję, która ustawia flagę orientationChanged
, sygnalizując aplikacji, że nastąpiła zmiana orientacji:
void android_main(struct android_app *app) {
...
app->activity->callbacks->onNativeWindowResized = ResizeCallback;
}
Gdzie ResizeCallback jest zdefiniowany jako:
void ResizeCallback(ANativeActivity *activity, ANativeWindow *window){
orientationChanged = true;
}
Problem z tym rozwiązaniem polega na tym, że funkcja onNativeWindowResized()
jest wywoływana tylko w przypadku zmian orientacji o 90°, np. z poziomej na pionową i odwrotnie. Inne zmiany orientacji nie spowodują odtworzenia łańcucha wymiany.
Na przykład zmiana orientacji z poziomej na odwróconą poziomą nie spowoduje jej uruchomienia, ponieważ kompozytor Androida musi przeprowadzić odwrócenie w aplikacji.
Obsługa zmiany orientacji
Aby obsłużyć zmianę orientacji, wywołaj rutynę zmiany orientacji na początku głównej pętli renderowania, gdy zmienna orientationChanged
ma wartość true. Przykład:
bool VulkanDrawFrame() {
if (orientationChanged) {
OnOrientationChange();
}
W ramach funkcji OnOrientationChange()
musisz wykonać wszystkie czynności niezbędne do odtworzenia łańcucha wymiany. Oznacza to, że:
zniszczenie wszystkich istniejących instancji
Framebuffer
iImageView
,Utwórz ponownie łańcuch wymiany, usuwając stary łańcuch wymiany (omówiony w następnym punkcie),
Utwórz ponownie Framebuffers za pomocą DisplayImages nowego swapchaina. Uwaga: obrazów załączników (np. obrazów głębi lub szablonów) zwykle nie trzeba ponownie tworzyć, ponieważ są one oparte na identycznej rozdzielczości obrazów z łańcucha zamiany.
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 zresetuj flagę orientationChanged
na false, aby pokazać, że zmiana orientacji została obsłużona.
Odtworzenie w ramach swapchain
W poprzedniej sekcji wspominaliśmy, że trzeba ponownie utworzyć łańcuch wymiany. Pierwszym krokiem jest uzyskanie nowych właściwości powierzchni renderowania:
void createSwapChain(VkSwapchainKHR oldSwapchain) {
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
pretransformFlag = capabilities.currentTransform;
Po wypełnieniu struktury VkSurfaceCapabilities
nowymi informacjami możesz sprawdzić, czy nastąpiła zmiana orientacji, sprawdzając pole currentTransform
. Zapisz go w polu pretransformFlag
, ponieważ będzie Ci potrzebny, gdy wprowadzisz zmiany w macierz MVP.
Aby to zrobić, w strukturze VkSwapchainCreateInfo
podaj te atrybuty:
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 zasięgiem displaySizeIdentity
, który został zapisany podczas uruchamiania aplikacji. Pole preTransform
zostanie wypełnione zmienną pretransformFlag
(która jest ustawiona na pole currentTransform obiektu surfaceCapabilities
). Pole oldSwapchain
jest też ustawione na swapchain, który zostanie zniszczony.
Korekta macierzy MVP
Ostatnią rzeczą, którą musisz zrobić, jest zastosowanie transformacji wstępnej przez zastosowanie macierzy obrotu do macierzy MVP. Polecenie to powoduje obrócenie obrazu w obszarze klipu, tak aby wynikowy obraz był obrócony zgodnie z bieżącą orientacją urządzenia. Następnie możesz po prostu przekazać zaktualizowaną macierz MVP do shadera wierzchołka i używać jej jak zwykle bez konieczności modyfikowania shaderów.
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;
Uwaga – obszar okna pełnoekranowego i nożyczki
Jeśli Twoja aplikacja używa widoku/regionu nożyczek, który nie zajmuje całego ekranu, musisz go zaktualizować zgodnie z orientacją urządzenia. Wymaga to włączenia opcji dynamicznego Viewport i Scissor podczas tworzenia potoku Vulkana:
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);
Rzeczywiste obliczenie zakresu widoku podczas nagrywania za pomocą bufora 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
definiują współrzędne lewego górnego rogu widocznego obszaru, a z kolei w
i h
definiują odpowiednio szerokość i wysokość widocznego obszaru.
Tego samego obliczenia można użyć do przeprowadzenia testu nożyczek. Aby zachować pełność, podajemy je 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);
Uwaga – pochodne fragmentu shadera
Jeśli Twoja aplikacja korzysta z obliczeń pochodnych, takich jak dFdx
i dFdy
, mogą być potrzebne dodatkowe transformacje, aby uwzględnić obrócony układ współrzędnych, ponieważ te obliczenia są wykonywane w przestrzeni pikseli. Wymaga to, aby aplikacja przekazała do fragment shadera pewną wskazówkę dotyczącą preTransform (np. liczbę całkowitą reprezentującą bieżącą orientację urządzenia), która posłuży do prawidłowego mapowania obliczeń pochodnej:
- W przypadku klatki obróconej o 90 stopni
- dFdx musi być mapowany na dFdy.
- dFdy musi być zmapowane na -dFdx.
- W przypadku obrotowanego o 270 stopni kadru:
- dFdx musi być zmapowany na -dFdy.
- dFdy musi być zmapowany na dFdx.
- W przypadku obrot o 180 stopni:
- dFdx musi być zmapowany na -dFdx.
- dFdy musi być zmapowany na -dFdy
Podsumowanie
Aby aplikacja mogła w pełni wykorzystać możliwości Vulkana na Androidzie, konieczne jest wdrożenie wstępnej rotacji. Najważniejsze wnioski z tego artykułu:
- Upewnij się, że podczas tworzenia lub ponownego tworzenia łańcucha wymiany flaga pretransformacji jest ustawiona tak, aby pasowała do flagi zwracanej przez system operacyjny Androida. Pozwoli to uniknąć obciążenia kompozytora.
- Rozmiar swapchaina powinien być równy rozdzielczości identycznej z powierzchnią okna aplikacji w naturalnej orientacji wyświetlacza.
- Obróć macierz MVP w przestrzeni klipu, aby uwzględnić orientację urządzenia, ponieważ rozdzielczość/zakres swapchain nie jest już aktualizowana wraz z orientacją wyświetlacza.
- W razie potrzeby zaktualizuj obszar widoku i prostokąty nożyczek.
Przykładowa aplikacja: minimalna wersja na Androida przed rotacją