Z tego artykułu dowiesz się, jak efektywnie obsługiwać obrót urządzenia w aplikacji Vulkan przez wdrożenie wstępnego obracania.
Vulkan pozwala określić znacznie więcej informacji o stanie renderowania niż w trybie 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ć obracanie powierzchni, dodając pass kompozytora. 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, która odpowiada 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 powiązana z GPU, Android Compositor może też zastępować jej działanie GPU, powodując dodatkową utratę wydajności.
Podczas uruchamiania tytułów dostępnych 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/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, zastosuj metodę 3. Jest to tzw. wstępna rotacja. Informuje ona 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 łańcucha 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 obsługują zwykle wiele orientacji lub tylko jedną orientację, w której powierzchnia renderowania ma inną orientację niż ta, którą urządzenie uznaje za orientację tożsamości. Na przykład aplikacja w orientacji poziomej na telefonie z orientacją pionową lub aplikacja w orientacji pionowej na tablecie z orientacją poziomą.
Modyfikuj plik 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ę, wystarczy skonfigurować zamianę tylko raz podczas uruchamiania/wznawiania aplikacji.
Pobierz rozdzielczość ekranu tożsamości i parametry 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. Najpewniejszym sposobem uzyskania tego efektu jest wywołanie funkcji vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
podczas uruchamiania aplikacji i zapisanie zwróconego zakresu. Zamień szerokość i wysokość na podstawie zwróconego parametru currentTransform
, aby zapisywać 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 of the app's window surface in the display's natural orientation.
Wykrywanie zmian orientacji urządzenia (Android 10 i nowsze)
Najbardziej niezawodnym sposobem wykrywania zmiany orientacji w aplikacji jest sprawdzenie, czy funkcja vkQueuePresentKHR()
zwraca wartość 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 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 odpytywania
Na urządzeniach z wersją Android 10 lub 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 metodę vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
, a następnie porównaj zwrócone pole currentTransform
z przekształconą aktualnie zapisaną powierzchnią (w tym przykładzie kodu zapisanym 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 sondowanie
vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
trwało od 0,120 do 0,250 ms, a na Pixelu 1 XL z Androidem 8. Ankiety trwały 110–0350 ms.
Korzystanie z wywołań zwrotnych
Drugą opcją na urządzeniach z Androidem w wersji starszej niż 10 jest zarejestrowanie wywołania zwrotnego onNativeWindowResized()
w celu wywołania funkcji ustawiającej flagę orientationChanged
, która sygnalizuje 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 stopni, np. z poziomej na pionową i odwrotnie. Inne zmiany orientacji nie spowodują odtworzenia łańcucha zamiany.
Na przykład zmiana z orientacji poziomej na odwrotną nie spowoduje jej aktywowania, więc kompozytor Androida będzie musiał wykonać odwrócenie 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. Na 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ówimy to w następnym punkcie),
Utwórz ponownie Framebuffers za pomocą DisplayImages nowego swapchaina. Uwaga: obrazy załączników (np. obrazy 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.
Rekreacja w systemie wymiany
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;
Gdy struktura VkSurfaceCapabilities
zostanie wypełniona nowymi informacjami, możesz sprawdzić, czy nastąpiła zmiana orientacji, korzystając z pola 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 zakresem displaySizeIdentity
zapisanym 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 matrycy MVP
Ostatnią rzeczą, którą musisz zrobić, jest zastosowanie transformacji wstępnej przez zastosowanie macierzy obrotu do macierzy MVP. Polega to na zastosowaniu obrotu w obszarze klipsa, dzięki czemu wynikowy obraz jest obrócony do bieżącej orientacji 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 widocznego obszaru lub obszaru nożyczkowego niepełnoekranowego, trzeba je zaktualizować odpowiednio do orientacji 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ść informacji, 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 z wyprzedzeniem 90 stopni
- dFdx musi być zmapowany na dFdy.
- Parametr dFdy musi być zmapowany na parametr -dFdx.
- W przypadku obrotu o 270 stopni:
- dFdx musi być zmapowany na -dFdy.
- Identyfikator dFdy musi być zmapowany na parametr dFdx.
- W przypadku obrot o 180 stopni:
- Tag dFdx musi być zmapowany na parametr -dFdx.
- Identyfikator 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 prerotacji. 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 identyfikatora 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ż aktualizowany zgodnie z orientacją wyświetlacza.
- Odpowiednio aktualizuj widoczny obszar i prostokąty nożyczek w zależności od aplikacji.
Przykładowa aplikacja: minimalna wersja na Androida przed rotacją