In diesem Artikel wird beschrieben, wie Sie die Gerätedrehung in Ihrer Vulkan-Anwendung durch Implementierung der Vorrotation effizient handhaben können.
Mit Vulkan können Sie viel mehr Informationen zum Renderingstatus angeben als mit OpenGL. Bei Vulkan müssen Sie Dinge, die in OpenGL vom Treiber verarbeitet werden, explizit implementieren, z. B. die Geräteausrichtung und ihre Beziehung zur Ausrichtung der Rendering-Oberfläche. Es gibt drei Möglichkeiten, wie Android die Renderfläche des Geräts mit der Geräteausrichtung abgleichen kann:
- Das Android-Betriebssystem kann die Display Processing Unit (DPU) des Geräts verwenden, die die Oberflächenrotation in der Hardware effizient verarbeiten kann. Nur auf unterstützten Geräten verfügbar.
- Das Android-Betriebssystem kann die Oberflächenrotation durch Hinzufügen eines Compositor-Durchlaufs verarbeiten. Dies hat je nach Umgang des Compositors mit der Drehung des Ausgabebilds Auswirkungen auf die Leistung.
- Die Anwendung selbst kann die Oberflächenrotation verarbeiten, indem sie ein gedrehtes Bild auf einer Rendering-Oberfläche rendert, die der aktuellen Ausrichtung des Displays entspricht.
Welche dieser Methoden sollten Sie verwenden?
Derzeit gibt es keine Möglichkeit für eine Anwendung, zu erkennen, ob die außerhalb der Anwendung verarbeitete Oberflächenrotation kostenlos ist. Auch wenn eine DPU dies für Sie übernimmt, ist wahrscheinlich mit einer messbaren Leistungseinbuße zu rechnen. Wenn Ihre Anwendung CPU-gebunden ist, wird dies aufgrund der erhöhten GPU-Nutzung durch den Android Compositor, der normalerweise mit einer erhöhten Frequenz ausgeführt wird, zu einem Energieproblem. Wenn Ihre Anwendung GPU-gebunden ist, kann der Android Compositor auch die GPU-Arbeit Ihrer Anwendung vorzeitig beenden, was zu einem zusätzlichen Leistungsverlust führt.
Bei der Ausführung von Versandtiteln auf dem Pixel 4XL haben wir festgestellt, dass SurfaceFlinger (die Aufgabe mit höherer Priorität, die den Android Compositor steuert):
Unterbricht regelmäßig die Arbeit der Anwendung, was zu 1–3 ms langen Frame-Zeit-Einbußen führt, und
Erhöht den Druck auf den Vertex-/Texturspeicher der GPU, da der Compositor den gesamten Framebuffer lesen muss, um seine Kompositionsarbeit zu erledigen.
Durch die richtige Ausrichtung wird die GPU-Preemption durch SurfaceFlinger fast vollständig verhindert. Die GPU-Frequenz sinkt um 40 %, da die vom Android Compositor verwendete erhöhte Frequenz nicht mehr benötigt wird.
Damit Oberflächenrotationen mit möglichst geringem Aufwand verarbeitet werden, wie im vorherigen Fall, solltest du Methode 3 implementieren. Dies wird als Vorrotation bezeichnet. Dadurch wird dem Android-Betriebssystem mitgeteilt, dass Ihre App die Drehung der Oberfläche übernimmt. Dazu können Sie Flags für die Oberflächentransformation übergeben, die die Ausrichtung beim Erstellen der Swapchain angeben. Dadurch wird verhindert, dass der Android Compositor die Drehung selbst vornimmt.
Zu wissen, wie das Flag für die Oberflächentransformation festgelegt wird, ist für jede Vulkan-Anwendung wichtig. Anwendungen unterstützen in der Regel entweder mehrere Ausrichtungen oder eine einzelne Ausrichtung, bei der die Rendering-Oberfläche eine andere Ausrichtung hat als die, die das Gerät als seine Identitätsausrichtung betrachtet. Beispiele: Eine Anwendung, die nur im Querformat ausgeführt werden kann, auf einem Smartphone, das nur im Hochformat ausgeführt werden kann, oder eine Anwendung, die nur im Hochformat ausgeführt werden kann, auf einem Tablet, das nur im Querformat ausgeführt werden kann.
AndroidManifest.xml ändern
Wenn Sie die Gerätedrehung in Ihrer App verarbeiten möchten, ändern Sie zuerst die Datei AndroidManifest.xml der Anwendung, um Android mitzuteilen, dass Ihre App Änderungen der Ausrichtung und Bildschirmgröße verarbeitet. Dadurch wird verhindert, dass Android die Android-Activity zerstört und neu erstellt und die Funktion onDestroy() auf der vorhandenen Fensteroberfläche aufruft, wenn sich die Ausrichtung ändert. Fügen Sie dazu die Attribute orientation (zur Unterstützung von API-Level <13) und screenSize im Abschnitt configChanges der Aktivität hinzu:
<activity android:name="android.app.NativeActivity"
android:configChanges="orientation|screenSize">
Wenn die Bildschirmausrichtung Ihrer App mit dem Attribut screenOrientation festgelegt ist, müssen Sie dies nicht tun. Wenn deine Anwendung eine feste Ausrichtung verwendet, muss die Swapchain nur einmal beim Starten/Fortsetzen der Anwendung eingerichtet werden.
Bildschirmauflösung und Kameraparameter abrufen
Ermitteln Sie als Nächstes die Bildschirmauflösung des Geräts, die mit dem Wert VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR verknüpft ist. Diese Auflösung ist mit der Identitätsausrichtung des Geräts verknüpft und daher die Auflösung, auf die die Swapchain immer eingestellt werden muss. Am zuverlässigsten erhalten Sie diese Informationen, wenn Sie beim Start der Anwendung vkGetPhysicalDeviceSurfaceCapabilitiesKHR() aufrufen und den zurückgegebenen Bereich speichern. Tauschen Sie die Breite und Höhe basierend auf dem ebenfalls zurückgegebenen currentTransform aus, um sicherzustellen, dass Sie die Bildschirmauflösung der Identität speichern:
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“ ist eine VkExtent2D-Struktur, mit der wir die Identitätsauflösung der Fensterfläche der App in der natürlichen Ausrichtung des Displays speichern.
Änderungen der Geräteausrichtung erkennen (Android 10 und höher)
Die zuverlässigste Methode, eine Ausrichtungsänderung in Ihrer Anwendung zu erkennen, besteht darin, zu prüfen, ob die Funktion vkQueuePresentKHR() den Wert VK_SUBOPTIMAL_KHR zurückgibt. Beispiel:
auto res = vkQueuePresentKHR(queue_, &present_info);
if (res == VK_SUBOPTIMAL_KHR){
orientationChanged = true;
}
Hinweis:Diese Lösung funktioniert nur auf Geräten mit Android 10 und höher. Diese Android-Versionen geben VK_SUBOPTIMAL_KHR von vkQueuePresentKHR() zurück. Das Ergebnis dieser Prüfung wird in orientationChanged gespeichert, einem boolean, auf das über die Haupt-Rendering-Schleife der Anwendung zugegriffen werden kann.
Änderungen der Geräteausrichtung erkennen (vor Android 10)
Für Geräte mit Android 10 oder älter ist eine andere Implementierung erforderlich, da VK_SUBOPTIMAL_KHR nicht unterstützt wird.
Umfragen verwenden
Auf Geräten mit einer Android-Version vor Android 10 können Sie die aktuelle Gerätetransformation alle pollingInterval Frames abfragen, wobei pollingInterval eine vom Programmierer festgelegte Granularität ist. Dazu rufen Sie vkGetPhysicalDeviceSurfaceCapabilitiesKHR() auf und vergleichen dann das zurückgegebene currentTransform-Feld mit dem der aktuell gespeicherten Oberflächentransformation (in diesem Codebeispiel in pretransformFlag gespeichert).
currFrameCount++;
if (currFrameCount >= pollInterval){
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
if (pretransformFlag != capabilities.currentTransform) {
window_resized = true;
}
currFrameCount = 0;
}
Auf einem Pixel 4 mit Android 10 dauerte die Abfrage von vkGetPhysicalDeviceSurfaceCapabilitiesKHR() zwischen 0,120 und 0,250 ms und auf einem Pixel 1XL mit Android 8 zwischen 0,110 und 0,350 ms.
Callbacks verwenden
Eine zweite Option für Geräte mit Android 10 oder niedriger ist die Registrierung eines onNativeWindowResized()-Callbacks, um eine Funktion aufzurufen, die das Flag orientationChanged setzt und der Anwendung so signalisiert, dass eine Ausrichtungsänderung stattgefunden hat:
void android_main(struct android_app *app) {
...
app->activity->callbacks->onNativeWindowResized = ResizeCallback;
}
Dabei ist ResizeCallback so definiert:
void ResizeCallback(ANativeActivity *activity, ANativeWindow *window){
orientationChanged = true;
}
Das Problem bei dieser Lösung ist, dass onNativeWindowResized() nur bei Ausrichtungsänderungen um 90 Grad aufgerufen wird, z. B. beim Wechsel vom Quer- ins Hochformat oder umgekehrt. Andere Ausrichtungsänderungen lösen keine Neuerstellung der Swapchain aus.
Beispielsweise wird sie nicht durch einen Wechsel vom Querformat zum umgekehrten Querformat ausgelöst. In diesem Fall muss der Android-Compositor die Drehung für Ihre Anwendung ausführen.
Umgang mit der Änderung der Ausrichtung
Um die Ausrichtungsänderung zu verarbeiten, rufen Sie die Routine für die Ausrichtungsänderung am Anfang der Haupt-Rendering-Schleife auf, wenn die Variable orientationChanged auf „true“ gesetzt ist. Beispiel:
bool VulkanDrawFrame() {
if (orientationChanged) {
OnOrientationChange();
}
In der Funktion OnOrientationChange() führen Sie alle erforderlichen Schritte aus, um die Swapchain neu zu erstellen. Das bedeutet Folgendes:
Löschen Sie alle vorhandenen Instanzen von
FramebufferundImageView.Erstellen Sie die Swapchain neu und löschen Sie die alte Swapchain (wird als Nächstes erläutert).
Erstelle die Framebuffer mit den DisplayImages der neuen Swapchain neu. Hinweis:Anhangsbilder (z. B. Tiefen-/Schablonenbilder) müssen in der Regel nicht neu erstellt werden, da sie auf der Identitätsauflösung der vorrotierten Swapchain-Bilder basieren.
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;
}
Am Ende der Funktion setzen Sie das Flag orientationChanged auf „false“, um anzugeben, dass die Ausrichtungsänderung verarbeitet wurde.
Swapchain-Neuerstellung
Im vorherigen Abschnitt haben wir erwähnt, dass die Swapchain neu erstellt werden muss. Dazu müssen Sie zuerst die neuen Eigenschaften der Rendering-Oberfläche abrufen:
void createSwapChain(VkSwapchainKHR oldSwapchain) {
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
pretransformFlag = capabilities.currentTransform;
Nachdem die Struktur VkSurfaceCapabilities mit den neuen Informationen gefüllt wurde, kannst du jetzt im Feld currentTransform prüfen, ob sich die Ausrichtung geändert hat. Sie speichern sie für später im Feld pretransformFlag, da Sie sie später benötigen, wenn Sie Anpassungen an der MVP-Matrix vornehmen.
Geben Sie dazu die folgenden Attribute in der VkSwapchainCreateInfo-Struktur an:
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);
}
Das Feld imageExtent wird mit dem displaySizeIdentity-Umfang gefüllt, den Sie beim Start der Anwendung gespeichert haben. Das Feld preTransform wird mit der Variablen pretransformFlag gefüllt, die auf das Feld „currentTransform“ von surfaceCapabilities festgelegt ist. Außerdem legen Sie das Feld oldSwapchain auf die Swapchain fest, die gelöscht wird.
Anpassung der MVP-Matrix
Als Letztes müssen Sie die Vortransformation anwenden, indem Sie eine Rotationsmatrix auf Ihre MVP-Matrix anwenden. Dadurch wird die Drehung im Clip-Bereich angewendet, sodass das resultierende Bild in die aktuelle Geräteausrichtung gedreht wird. Anschließend können Sie diese aktualisierte MVP-Matrix einfach an Ihren Vertex-Shader übergeben und wie gewohnt verwenden, ohne die Shader ändern zu müssen.
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;
Überlegung – Nicht-Vollbild-Viewport und Schere
Wenn Ihre Anwendung einen nicht im Vollbildmodus angezeigten Darstellungsbereich/Scherenbereich verwendet, muss dieser entsprechend der Ausrichtung des Geräts aktualisiert werden. Dazu müssen Sie die dynamischen Viewport- und Scissor-Optionen während der Vulkan-Pipeline-Erstellung aktivieren:
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);
Die tatsächliche Berechnung der Darstellungsbereich-Ausdehnung während der Aufzeichnung des Befehlspuffers sieht so aus:
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);
Die Variablen x und y definieren die Koordinaten der oberen linken Ecke des Darstellungsbereichs, während w und h die Breite und Höhe des Darstellungsbereichs definieren.
Dieselbe Berechnung kann auch verwendet werden, um den Scheren-Test festzulegen. Sie ist hier der Vollständigkeit halber aufgeführt:
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);
Überlegung – Fragment-Shader-Derivate
Wenn Ihre Anwendung Ableitungsberechnungen wie dFdx und dFdy verwendet, sind möglicherweise zusätzliche Transformationen erforderlich, um das gedrehte Koordinatensystem zu berücksichtigen, da diese Berechnungen im Pixelbereich ausgeführt werden. Dazu muss die App einen Hinweis auf die „preTransform“ an den Fragment-Shader übergeben (z. B. eine Ganzzahl, die die aktuelle Geräteausrichtung darstellt) und diesen verwenden, um die Ableitungsberechnungen richtig zuzuordnen:
- Für einen um 90 Grad vorrotierten Frame
- dFdx muss dFdy zugeordnet werden.
- dFdy muss -dFdx zugeordnet werden.
- Für einen um 270 Grad vorrotierten Frame:
- dFdx muss -dFdy zugeordnet werden.
- dFdy muss dFdx zugeordnet werden.
- Für einen um 180 Grad vorrotierten Frame:
- dFdx muss -dFdx zugeordnet werden.
- dFdy muss -dFdy zugeordnet werden.
Fazit
Damit deine Anwendung Vulkan unter Android optimal nutzen kann, ist die Implementierung der Vorrotation ein Muss. Die wichtigsten Erkenntnisse aus diesem Artikel sind:
- Achte darauf, dass das Flag „pretransform“ beim Erstellen oder Neuerstellen der Swapchain mit dem Flag übereinstimmt, das vom Android-Betriebssystem zurückgegeben wird. Dadurch wird der Compositor-Overhead vermieden.
- Die Größe der Swapchain sollte auf die Identitätsauflösung der Fensteroberfläche der App in der natürlichen Ausrichtung des Displays festgelegt bleiben.
- Drehe die MVP-Matrix im Clip-Space, um die Ausrichtung des Geräts zu berücksichtigen, da die Auflösung/Größe der Swapchain nicht mehr mit der Ausrichtung des Displays aktualisiert wird.
- Aktualisieren Sie die Viewport- und Scherenrechtecke nach Bedarf für Ihre Anwendung.
Beispiel-App: Minimale Android-Vorrotation