Geräteausrichtung mit Vulkan-Vorrotation behandeln

In diesem Artikel wird beschrieben, wie Sie die Geräterotation in Ihrer Vulkan-Anwendung durch Implementierung der Vorrotation optimal handhaben.

Mit Vulkan können Sie wesentlich mehr Informationen zum Renderingstatus angeben als mit OpenGL. Bei Vulkan müssen Sie Elemente, die vom Treiber in OpenGL gehandhabt werden, explizit implementieren, z. B. die Geräteausrichtung und ihre Beziehung zur Rendering-Oberflächenausrichtung. Es gibt drei Möglichkeiten, wie Android die Rendering-Oberfläche des Geräts mit der Geräteausrichtung abgleichen kann:

  1. Das Android-Betriebssystem kann die Display Processing Unit (DPU) des Geräts verwenden, die die Oberflächendrehung in Hardware effizient handhaben kann. Nur auf unterstützten Geräten verfügbar.
  2. Das Android-Betriebssystem kann die Oberflächenrotation verarbeiten, indem eine Compositor-Karte hinzugefügt wird. Dies verursacht Leistungskosten, die davon abhängen, wie der Compositor mit dem Drehen des Ausgabebildes umgehen muss.
  3. Die Anwendung selbst kann die Oberflächendrehung verarbeiten, indem sie ein gedrehtes Bild auf einer Rendering-Oberfläche rendert, die der aktuellen Ausrichtung des Bildschirms entspricht.

Welche dieser Methoden sollten Sie verwenden?

Derzeit kann eine Anwendung nicht feststellen, ob die außerhalb der Anwendung ausgeführte Oberflächenrotation kostenlos ist. Selbst wenn es ein DPU gibt, das sich darum kümmert, wird es dennoch mit einer messbaren Leistungseinbußen rechnen. Wenn Ihre Anwendung CPU-gebunden ist, wird dies aufgrund der erhöhten GPU-Nutzung durch den Android Compositor, der normalerweise mit erhöhter Frequenz ausgeführt wird, zu einem Energieproblem. Wenn Ihre Anwendung GPU-gebunden ist, kann der Android Compositor die GPU-Arbeit Ihrer Anwendung auch vorzeitig beenden, was zu einem zusätzlichen Leistungsverlust führt.

Beim Versand von Titeln für Pixel 4 XL haben wir festgestellt, dass SurfaceFlinger (die Aufgabe mit höherer Priorität, die den Android Compositor antreibt):

  • Hebt die Arbeit der Anwendung regelmäßig auf, was zu 1-3-ms-Treffern bis zur Frametime führt und

  • Erhöht die Belastung des Vertex-/Texturarbeitsspeichers der GPU, da der Compositor für seine Zusammensetzung den gesamten Framepuffer lesen muss.

Bei einer ordnungsgemäßen Ausrichtung wird das vorzeitige Beenden von GPUs durch SurfaceFlinger fast vollständig unterbunden. Die GPU-Frequenz sinkt um 40 %, da die vom Android Compositor verwendete erhöhte Frequenz nicht mehr benötigt wird.

Damit Oberflächenrotationen mit so wenig Aufwand wie möglich korrekt verarbeitet werden (wie im vorherigen Fall), sollten Sie Methode 3 implementieren. Dies wird als Vorrotation bezeichnet. Dadurch wird dem Android-Betriebssystem mitgeteilt, dass Ihre App die Oberflächenrotation verarbeitet. Dazu können Sie Surface-Transformations-Flags übergeben, die die Ausrichtung beim Erstellen der Auslagerungskette angeben. Dadurch wird der Android Compositor daran gehindert, die Rotation selbst auszuführen.

Für jede Vulkan-Anwendung ist es wichtig, dass Sie das Flag für die Oberflächentransformation festlegen. Anwendungen unterstützen entweder mehrere Ausrichtungen oder eine einzelne Ausrichtung, wobei die Renderingoberfläche von der vom Gerät als Identitätsausrichtung des Geräts betrachteten Ausrichtung ausgerichtet ist. Dies kann beispielsweise eine App im Querformat auf einem Smartphone im Hochformat oder auf einem Tablet im Querformat sein.

„AndroidManifest.xml“ modifizieren

Wenn du die Geräterotation in deiner App verwalten möchtest, musst du zuerst die Datei AndroidManifest.xml der App ändern, um Android mitzuteilen, dass deine App auf Änderungen der Ausrichtung und der Bildschirmgröße vorbereitet ist. Dadurch wird verhindert, dass Android das Android-Activity zerstört und neu erstellt und die Funktion onDestroy() auf der vorhandenen Fensteroberfläche aufruft, wenn eine Ausrichtungsänderung auftritt. Dazu werden die Attribute orientation (zur Unterstützung des API-Levels <13) und screenSize im Abschnitt configChanges der Aktivität hinzugefügt:

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

Wenn die Bildschirmausrichtung in Ihrer App mithilfe des Attributs screenOrientation korrigiert wird, müssen Sie dies nicht tun. Wenn Ihre Anwendung eine feste Ausrichtung verwendet, muss die Swapchain nur einmal beim Start/Fortsetzen der Anwendung eingerichtet werden.

Parameter für Bildschirmauflösung und Kamera abrufen

Ermittle als Nächstes die Bildschirmauflösung des Geräts, die dem Wert VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR zugeordnet ist. Diese Auflösung hängt von der Identitätsausrichtung des Geräts ab und ist daher diejenige, auf die die Swapchain immer eingestellt werden muss. Die zuverlässigste Methode hierfür ist ein Aufruf von vkGetPhysicalDeviceSurfaceCapabilitiesKHR() beim Start der Anwendung und Speichern der zurückgegebenen Erweiterung. Tauschen Sie die Breite und Höhe entsprechend dem ebenfalls zurückgegebenen currentTransform aus, damit die Bildschirmauflösung des Identitätsanbieters gespeichert wird:

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 die Identitätsauflösung der Fensteroberfläche der App in der natürlichen Ausrichtung der Anzeige gespeichert wird.

Änderungen der Geräteausrichtung erkennen (Android 10 und höher)

Die zuverlässigste Methode, um eine Ausrichtungsänderung in einer Anwendung zu erkennen, besteht darin, zu prüfen, ob die vkQueuePresentKHR()-Funktion VK_SUBOPTIMAL_KHR zurückgibt. Beispiele:

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 oder höher. Diese Android-Versionen geben VK_SUBOPTIMAL_KHR von vkQueuePresentKHR() zurück. Wir speichern das Ergebnis dieser Prüfung in orientationChanged, einem boolean, auf den über die Haupt-Renderingschleife der Anwendungen zugegriffen werden kann.

Änderungen der Geräteausrichtung erkennen (vor Android 10)

Für Geräte mit Android 10 oder niedriger ist eine andere Implementierung erforderlich, da VK_SUBOPTIMAL_KHR nicht unterstützt wird.

Umfragen verwenden

Auf Geräten mit einer früheren Version als Android 10 kannst du die aktuelle Gerätetransformation alle pollingInterval Frames abfragen. Dabei ist pollingInterval ein vom Programmierer festgelegten Detaillierungsgrad. Dazu rufen Sie vkGetPhysicalDeviceSurfaceCapabilitiesKHR() auf und vergleichen dann das zurückgegebene Feld currentTransform mit 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 vkGetPhysicalDeviceSurfaceCapabilitiesKHR() zwischen 0,120 und 0,250 ms und auf einem Pixel 1 XL mit Android 8 0,110 bis 0,350 ms.

Callbacks verwenden

Eine zweite Option für Geräte unter Android 10 besteht darin, einen onNativeWindowResized()-Callback zum Aufrufen einer Funktion zu registrieren, die das Flag orientationChanged festlegt und der App signalisiert, dass die Ausrichtung geändert wurde:

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

Dabei ist ResizeCallback definiert als:

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

Das Problem bei dieser Lösung besteht darin, dass onNativeWindowResized() nur für 90-Grad-Ausrichtungsänderungen aufgerufen wird, z. B. vom Querformat ins Hochformat oder umgekehrt. Andere Ausrichtungsänderungen lösen die Neuerstellung der Swap-Kette nicht aus. So wird beispielsweise eine Änderung von Querformat zu umgekehrter Querformatierung nicht ausgelöst. In diesem Fall muss der Android-Compositor die Drehung für Ihre Anwendung vornehmen.

Mit der Ausrichtungsänderung umgehen

Um die Ausrichtungsänderung zu verarbeiten, rufen Sie die Routine für die Ausrichtungsänderung oben in der Haupt-Renderingschleife auf, wenn die Variable orientationChanged auf „true“ gesetzt ist. Beispiele:

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

Sie führen alle erforderlichen Schritte aus, um die Swapchain innerhalb der Funktion OnOrientationChange() neu zu erstellen. Das bedeutet:

  1. Löschen Sie alle vorhandenen Instanzen von Framebuffer und ImageView.

  2. Erstellen Sie die Swapchain und löschen Sie dabei die alte Swapchain (wie im Folgenden beschrieben).

  3. Erstellen Sie die Framebuffer mit den DisplayImages der neuen swapchain neu. Hinweis:Anhang-Images (z. B. Tiefen-/Schablonenbilder) müssen in der Regel nicht neu erstellt werden, da sie auf der Identitätsauflösung der vorrotierten Swapchain-Images 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 anzuzeigen, dass Sie die Ausrichtungsänderung vorgenommen haben.

Swapchain-Freizeit

Im vorherigen Abschnitt wurde erwähnt, dass die Swapchain neu erstellt werden muss. Zuerst müssen die neuen Eigenschaften der Rendering-Oberfläche abgerufen werden:

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

Wenn die Struktur VkSurfaceCapabilities mit den neuen Informationen gefüllt ist, können Sie jetzt im Feld currentTransform prüfen, ob eine Ausrichtungsänderung vorliegt. 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 Struktur VkSwapchainCreateInfo 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);
}

In das Feld imageExtent wird die Erweiterung displaySizeIdentity gefüllt, die Sie beim Start der Anwendung gespeichert haben. In das Feld preTransform wird die Variable pretransformFlag eingefügt, die auf das Feld „currentTransform“ von surfaceCapabilities gesetzt ist. Außerdem legen Sie im Feld oldSwapchain die Auslagerungskette fest, die gelöscht wird.

MVP-Matrixanpassung

Als Letztes müssen Sie die Vortransformation anwenden, indem Sie eine Rotationsmatrix auf Ihre MVP-Matrix anwenden. Im Wesentlichen wird dabei die Drehung im Clipbereich angewendet, sodass das resultierende Bild in die aktuelle Geräteausrichtung gedreht wird. Sie können diese aktualisierte MVP-Matrix dann einfach an Ihren Vertex-Shader übergeben und wie gewohnt verwenden, ohne Ihre 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;

Kaufbereitschaft – Darstellungsbereich und Schere (kein Vollbild)

Wenn Ihre Anwendung einen Darstellungsbereich/Scherenbereich (nicht Vollbild) verwendet, müssen diese entsprechend der Geräteausrichtung aktualisiert werden. Dazu müssen Sie beim Erstellen der Pipeline von Vulkan die Optionen für den dynamischen Darstellungsbereich und die Scheren 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 Größe des Darstellungsbereichs während der Aufzeichnung der Befehlspufferung 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 bzw. Höhe des Darstellungsbereichs definieren. Dieselbe Berechnung kann auch zum Einrichten des Scherentests verwendet werden und ist hier der Vollständigkeit halber enthalten:

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

Kaufbereitschaft – Fragment-Shader-Ableitungen

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 Pixelraum ausgeführt werden. Dazu muss die App eine Angabe der PreTransform-Methode an den Fragment-Shader übergeben (z. B. eine Ganzzahl für die aktuelle Geräteausrichtung) und diese verwenden, um die abgeleiteten Berechnungen korrekt zuzuordnen:

  • Für einen um 90 Grad vorgedrehten Rahmen
    • dFdx muss dFdy zugeordnet sein.
    • dFdy muss -dFdx zugeordnet sein.
  • Für einen um 270 Grad vorgedrehten Rahmen
    • dFdx muss -dFdy zugeordnet sein.
    • dFdy muss dFdx zugeordnet sein.
  • Für einen um 180 Grad vorgedrehten Rahmen
    • dFdx muss -dFdx zugeordnet sein.
    • dFdy muss -dFdy zugeordnet sein.

Fazit

Damit deine App Vulkan auf Android optimal nutzen kann, ist die Implementierung der Vorrotation erforderlich. Die wichtigsten Erkenntnisse aus diesem Artikel sind:

  • Achten Sie darauf, dass das Pretransform-Flag beim Erstellen oder Neuerstellen einer Swapchain so festgelegt ist, dass es mit dem vom Android-Betriebssystem zurückgegebenen Flag übereinstimmt. Dadurch wird der Compositor-Overhead vermieden.
  • Die Größe der Swapchain sollte an die Identitätsauflösung der Fensteroberfläche der App in der natürlichen Ausrichtung der Anzeige angepasst werden.
  • Rotieren Sie die MVP-Matrix im Clipbereich, um die Ausrichtung des Geräts zu berücksichtigen, da die Auflösung/Erweiterung der Auslagerungskette nicht mehr mit der Ausrichtung des Bildschirms aktualisiert wird.
  • Aktualisieren Sie die Darstellungsbereich- und Scherenrechtecke nach Bedarf für Ihre Anwendung.

Beispiel-App:Minimale Vorab-Rotation für Android