Geräteausrichtung mit Vulkan-Vorrotation behandeln

In diesem Artikel wird beschrieben, wie die Gerätedrehung effizient gehandhabt wird. in Ihrer Vulkan-Anwendung durch Implementieren der Vor-Rotation.

Vulkan bietet Ihnen folgende Möglichkeiten: mehr Informationen zum Rendering-Status angeben als mit OpenGL. Bei Vulkan müssen Sie Dinge explizit implementieren, die der Treiber OpenGL, z. B. die Geräteausrichtung und ihre Beziehung zu Ausrichtung der Rendering-Oberfläche. Es gibt drei Möglichkeiten, wie Android den Abgleich der Renderoberfläche des Geräts mit der Geräteausrichtung:

  1. Das Android-Betriebssystem kann die Display Processing Unit (DPU) des Geräts verwenden, das die Oberflächenrotation der Hardware effizient bewältigen kann. Verfügbar auf unterstützten Geräten.
  2. Das Android-Betriebssystem kann die Oberflächenrotation übernehmen, indem eine zusammengesetzte Karte bzw. ein zusammengesetztes Ticket hinzugefügt wird. Dieses haben Leistungskosten, die davon abhängen, wie der Compositor damit umgehen muss Drehen des Ausgabebilds.
  3. Die Anwendung selbst kann die Oberflächenrotation verarbeiten, indem ein gedrehtes Bild auf eine Rendering-Fläche, die der aktuellen Ausrichtung des Display.

Welche dieser Methoden sollten Sie verwenden?

Derzeit kann für eine Anwendung nicht festgestellt werden, ob die Oberflächenrotation die außerhalb der Anwendung abgewickelt werden, kostenlos. Selbst wenn eine Datenschutzaufsichtsbehörde Wenn Sie dies tun, wird die Leistung Ihrer Anzeigen zu bezahlen. Wenn Ihre Anwendung CPU-gebunden ist, wird dies zu einem Energieproblem. die erhöhte GPU-Nutzung durch den Android Compositor, der in der Regel erhöhte Frequenz. Wenn Ihre Anwendung GPU-gebunden ist, kann auch die GPU-Arbeit Ihrer Anwendung vorzeitig beenden, was zu zusätzlicher Leistung führt. Verlust.

Beim Versand von Titeln auf Pixel 4 XL konnten wir feststellen, dass SurfaceFlinger (die übergeordnete Aufgabe, die das Android-Betriebssystem Compositor):

  • Beendet die Arbeit der Anwendung regelmäßig, was 1–3 ms verursacht Treffer bis Frametimes

  • Erhöht die Druckstärke auf die GPU Vertex-/Texturspeicher, da der Compositor den gesamten Framebuffer für die Komposition.

Die korrekte Verarbeitung der Ausrichtung unterbricht das vorzeitige Beenden der GPU durch SurfaceFlinger fast während die GPU-Frequenz um 40% sinkt, da die vom Android Compositor wird nicht mehr benötigt.

Um sicherzustellen, dass Oberflächendrehungen mit so wenig Aufwand wie möglich Wie im vorherigen Fall gezeigt, sollten Sie Methode 3 implementieren. Dies wird als Vorrotation bezeichnet. Dadurch wird dem Android-Betriebssystem mitgeteilt, dass Ihre App die Rotation der Oberfläche übernimmt. Dazu übergeben Sie die Flags zur Oberflächentransformation. die die Ausrichtung während der Swapchain-Erstellung angeben. Dadurch wird der Vorgang angehalten Android Compositor daran, die Rotation selbst auszuführen.

Zu wissen, wie das Flag zur Oberflächentransformation festgelegt wird, ist für jeden Vulkan wichtig . Apps unterstützen entweder mehrere Ausrichtungen oder eine einzelne Ausrichtung unterstützen, bei der sich die Renderingfläche in einer anderen Ausrichtung auf das, was das Gerät als Identitätsorientierung betrachtet. Beispiel: eine Anwendung im Querformat auf einem Smartphone im Hochformat oder ein Smartphone im Hochformat auf einem Tablet im Querformat.

AndroidManifest.xml modifizieren

Damit die Gerätedrehung in Ihrer App gehandhabt wird, ändern Sie zuerst AndroidManifest.xml, um Android mitzuteilen, dass deine App die Ausrichtung verarbeitet und Bildschirmgrößen ändern. Dadurch wird verhindert, dass Android Daten löscht und neu erstellt das Android-Activity und das Aufrufen der onDestroy()-Funktion in der vorhandene Fensteroberfläche, wenn sich die Ausrichtung ändert. Dies geschieht durch Hinzufügen der Attribute orientation (zur Unterstützung von API-Level <13) und screenSize auf die Abschnitt configChanges:

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

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

Auflösung des Identitätsbildschirms und Kameraparameter abrufen

Bildschirmauflösung des Geräts ermitteln mit dem Wert VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR verknüpft ist. Dieses hängt von der Identitätsausrichtung des Geräts ab also diejenige, auf die die Auslagerungschain immer eingestellt sein muss. Die meisten können Sie dies zuverlässig erreichen, indem Sie vkGetPhysicalDeviceSurfaceCapabilitiesKHR() beim Start der Anwendung und den zurückgegebenen Ausmaß zu speichern. Vertauschen Sie Breite und Höhe basierend auf dem currentTransform zurückgegeben, um sicherzustellen, dass Auflösung des Identitätsbildschirms:

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, die wir zum Speichern der genannten Identität verwenden. Auflösung der Fensteroberfläche der App in der natürlichen Ausrichtung des Bildschirms.

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

Die zuverlässigste Methode zum Erkennen einer Ausrichtungsänderung in Ihrer Anwendung ist um zu prüfen, ob die Funktion vkQueuePresentKHR() VK_SUBOPTIMAL_KHR. Beispiel:

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

Hinweis:Diese Lösung funktioniert nur auf Geräten, auf denen Android 10 oder höher. Diese Android-Versionen VK_SUBOPTIMAL_KHR von vkQueuePresentKHR(). Wir speichern das Ergebnis dieser orientationChanged überprüfen, eine boolean, die über die Anwendungen eine Haupt-Rendering-Schleife.

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

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

Abfragen verwenden

Auf Geräten mit älteren Versionen als Android 10 können Sie die aktuelle Gerätetransformation alle abfragen. pollingInterval Frames, wobei pollingInterval ein Detaillierungsgrad ist, der durch den Programmierer. Rufen Sie hierzu vkGetPhysicalDeviceSurfaceCapabilitiesKHR() und vergleicht dann die zurückgegebenen Werte Feld currentTransform mit dem Wert der aktuell gespeicherten Oberfläche Transformation (in diesem in pretransformFlag gespeicherten Codebeispiel).

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 werden die vkGetPhysicalDeviceSurfaceCapabilitiesKHR() dauerte zwischen 0,120 und 0,250 ms und auf einer Bei einem Pixel 1 XL mit Android 8 dauerte die Abfrage 0,110 bis 0,350 ms.

Rückrufe verwenden

Eine zweite Möglichkeit für Geräte mit einer älteren Version unter Android 10 ist die Registrierung eines onNativeWindowResized()-Callback, um eine Funktion aufzurufen, die den Parameter orientationChanged-Flag, das der Anwendung eine Ausrichtungsänderung signalisiert ist aufgetreten:

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() erfordert eine 90-Grad-Ausrichtungsänderung, zum Beispiel das Wechseln von Quer- ins Hochformat oder und umgekehrt. Andere Ausrichtungsänderungen lösen die Neuerstellung der Auslagerungschain nicht aus. Beim Wechsel vom Querformat zum umgekehrten Querformat nicht auslösen, sodass der Android-Compositor das Umdrehen für Ihr .

Umgang mit der Ausrichtungsänderung

Um die Änderung der Ausrichtung zu verarbeiten, rufen Sie die Routine zum Ändern der Ausrichtung am oberhalb der Haupt-Renderingschleife, wenn orientationChanged auf "true" gesetzt ist. Beispiel:

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

Sie erledigen alles, was nötig ist, um die Swapchain in die Funktion OnOrientationChange(). Das bedeutet Folgendes:

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

  2. Baue die Auslagerungskette neu auf der alten Swapchain (wird als Nächstes besprochen)

  3. Erstelle die Framebuffers mit DisplayImages der neuen Swapchain neu. Hinweis: Bilder im Anhang (z. B. Tiefenbilder oder Schablonenbilder) haben in der Regel müssen neu erstellt werden, basieren auf der Identitätsauflösung der vorgedrehten Swapchain-Images.

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“ zurück. um zu zeigen, dass Sie die Ausrichtungsänderung vorgenommen haben.

Swapchain-Freizeit

Im vorherigen Abschnitt haben wir erwähnt, dass die Swapchain neu erstellt werden muss. Zunächst müssen die neuen Eigenschaften Rendering-Oberfläche:

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 überprüfen, ob eine Änderung der Ausrichtung vorgenommen wurde. currentTransform. Du speicherst ihn für später im pretransformFlag da Sie diese später benötigen, wenn Sie Anpassungen MVP-Matrix.

Geben Sie dazu die folgenden Attribute an: in der Struktur 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);
}

Das Feld imageExtent wird mit dem displaySizeIdentity-Wert gefüllt, der die Sie beim Start der Anwendung gespeichert haben. Das Feld preTransform wird automatisch durch die Variable pretransformFlag (die auf das Feld "currentTransform" festgelegt ist des surfaceCapabilities). Außerdem setzen Sie das Feld oldSwapchain auf den die gelöscht wird.

MVP-Matrixanpassung

Als Letztes müssen Sie die Transformation vor der Transformation indem Sie eine Rotationsmatrix auf Ihre MVP-Matrix anwenden. Im Wesentlichen bedeutet dies, die Drehung im Clipbereich anwenden, sodass das Bild gedreht wird mit der aktuellen Ausrichtung des Geräts. Sie können diese aktualisierte MVP-Matrix in Ihren Vertex-Shader einfügen und ihn wie gewohnt verwenden, ohne Shader.

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 (nicht Vollbild)

Wenn Ihre Anwendung einen nicht bildschirmfüllenden Darstellungsbereich/Scherenbereich verwendet, werden muss entsprechend der Ausrichtung des Geräts aktualisiert werden. Dieses müssen Sie die Optionen "Dynamischer Darstellungsbereich" und "Schere" während der Pipelineerstellung:

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 des Darstellungsbereichs während der Aufnahme 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 Darstellungsbereich, während w und h die Breite bzw. Höhe des Darstellungsbereichs definieren. Dieselbe Berechnung kann auch für den Scherentest verwendet werden. hier der Vollständigkeit halber:

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 – Ableitungen von Fragment-Shadern

Wenn Ihre Anwendung abgeleitete Berechnungen wie dFdx und dFdy verwendet, Möglicherweise sind weitere Transformationen erforderlich, um die gedrehte Koordinate zu berücksichtigen. da diese Berechnungen im Pixelbereich ausgeführt werden. Dazu ist die App um eine Angabe der preTransform an den Fragment-Shader zu übergeben (z. B. für die aktuelle Ausrichtung des Geräts. Verwenden Sie diese Ganzzahl, um die Ableitungsberechnungen korrekt ausführen:

  • Für einen um 90 Grad gedrehten Rahmen <ph type="x-smartling-placeholder">
      </ph>
    • dFdx muss dFdy zugeordnet sein.
    • dFdy muss -dFdx zugeordnet sein.
  • Für einen um 270 Grad gedrehten Rahmen <ph type="x-smartling-placeholder">
      </ph>
    • dFdx muss -dFdy zugeordnet sein.
    • dFdy muss dFdx zugeordnet sein.
  • Für einen um 180 Grad gedrehten Rahmen <ph type="x-smartling-placeholder">
      </ph>
    • dFdx muss -dFdx zugeordnet sein.
    • dFdy muss -dFdy zugeordnet sein.

Fazit

Damit Sie Vulkan auf Android optimal nutzen können, ist die Implementierung der Vor-Rotation ein Muss. Die wichtigsten Erkenntnisse aus dieser Artikel:

  • Achten Sie darauf, dass beim Erstellen oder Wiederherstellen der Swapchain das Pretransform-Flag so eingestellt, dass sie mit dem Flag übereinstimmt, das vom Android-Betriebssystem zurückgegeben wird. Dadurch werden für den Compositor-Overhead.
  • Die Größe der Swapchain muss an die Identitätsauflösung des App-Fensters angepasst bleiben in der natürlichen Ausrichtung des Bildschirms.
  • Drehen Sie die MVP-Matrix im Clipbereich, um die Ausrichtung des Geräts zu berücksichtigen. da die Auflösung/Erweiterung der Swapchain nicht mehr mit der Ausrichtung aktualisiert wird. des Bildschirms.
  • Aktualisieren Sie den Darstellungsbereich und die Scherenrechtecke nach Bedarf für Ihre Anwendung.

Beispiel-App:Minimale Android-Vorrotation