In diesem Artikel wird beschrieben, wie Sie die Geräteausrichtung in Ihrer Vulkan-Anwendung durch die Implementierung von Pre-Rotation effizient verarbeiten können.
Mit Vulkan können Sie viel mehr Informationen zum Rendering-Status angeben als mit OpenGL. Bei Vulkan müssen Sie Dinge, die vom Treiber in OpenGL verarbeitet werden, explizit implementieren, z. B. die Geräteausrichtung und ihre Beziehung zur Ausrichtung der Renderoberfläche. Es gibt drei Möglichkeiten, wie Android die Renderfläche des Geräts mit der Geräteausrichtung in Einklang bringen kann:
- Das Android-Betriebssystem kann die DPU (Display Processing Unit) des Geräts verwenden, die die Oberflächenrotation effizient in der Hardware verarbeiten kann. Nur auf unterstützten Geräten verfügbar.
- Das Android-Betriebssystem kann die Oberflächenrotation durch Hinzufügen eines Compositor-Passes verarbeiten. Dies führt zu Leistungskosten, je nachdem, wie der Compositor das Ausgabebild drehen muss.
- 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 kann eine Anwendung nicht erkennen, ob die Oberflächenrotation, die außerhalb der Anwendung erfolgt, kostenlos ist. Auch wenn es eine DPU gibt, die sich darum kümmert, wird es wahrscheinlich immer noch einen messbaren Leistungsverlust geben. Wenn Ihre Anwendung CPU-gebunden ist, wird dies zu einem Stromversorgungsproblem, da der Android Compositor, der normalerweise mit einer erhöhten Frequenz ausgeführt wird, mehr GPU-Leistung benötigt. Wenn Ihre Anwendung GPU-gebunden ist, kann der Android-Compositor die GPU-Arbeit Ihrer Anwendung unterbrechen, was zu zusätzlichen Leistungseinbußen führt.
Bei der Ausführung von Versandtiteln auf dem Pixel 4XL haben wir Folgendes beobachtet: SurfaceFlinger (die Aufgabe mit höherer Priorität, die den Android-Compositor steuert):
Die Arbeit der Anwendung wird regelmäßig unterbrochen, was zu Frametime-Treffern von 1 bis 3 ms führt.
Das 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 Verarbeitung der Ausrichtung wird die GPU-Unterbrechung durch SurfaceFlinger fast vollständig verhindert, während die GPU-Frequenz um 40% sinkt, da die vom Android Compositor verwendete erhöhte Frequenz nicht mehr benötigt wird.
Damit Oberflächenrotationen wie im vorherigen Fall mit möglichst geringem Aufwand richtig verarbeitet werden, sollten Sie Methode 3 implementieren. Dies wird als Vorrotation bezeichnet. Damit wird dem Android-Betriebssystem mitgeteilt, dass Ihre App die Oberflächenrotation übernimmt. Dazu können Sie Flags für die Oberflächenumwandlung übergeben, die die Ausrichtung während der Swapchain-Erstellung angeben. Dadurch wird verhindert, dass der Android-Compositor die Drehung selbst ausführt.
Das Festlegen des Surface-Transformations-Flags ist für jede Vulkan-Anwendung wichtig. Anwendungen unterstützen in der Regel entweder mehrere Ausrichtungen oder eine einzelne Ausrichtung, bei der die Renderoberfläche eine andere Ausrichtung hat als die, die das Gerät als seine Identitätsausrichtung betrachtet. Beispiele: Eine App, die nur im Querformat funktioniert, auf einem Smartphone, das nur im Hochformat verwendet werden kann, oder eine App, die nur im Hochformat funktioniert, auf einem Tablet, das nur im Querformat verwendet werden kann.
AndroidManifest.xml ändern
Wenn Sie die Geräteausrichtung in Ihrer App verarbeiten möchten, müssen Sie zuerst die AndroidManifest.xml
-Datei der Anwendung ändern, um Android mitzuteilen, dass Ihre App Änderungen an der Ausrichtung und Bildschirmgröße verarbeitet. Dadurch wird verhindert, dass Android das Activity
zerstört und neu erstellt und die Funktion onDestroy()
auf der vorhandenen Fensteroberfläche aufgerufen wird, wenn sich die Ausrichtung ändert. Dazu werden die Attribute orientation
(zur Unterstützung von API-Level <13) und screenSize
zum Abschnitt configChanges
der Aktivität hinzugefügt:
<activity android:name="android.app.NativeActivity"
android:configChanges="orientation|screenSize">
Wenn Ihre Anwendung die Bildschirmausrichtung mit dem Attribut screenOrientation
festlegt, müssen Sie dies nicht tun. Wenn Ihre Anwendung eine feste Ausrichtung verwendet, muss die Swapchain nur einmal beim Starten/Fortsetzen der Anwendung eingerichtet werden.
Auflösung des Identitätsbildschirms und Kameraparameter abrufen
Ermitteln Sie als Nächstes die Bildschirmauflösung des Geräts, die dem Wert VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR
zugeordnet ist. Diese Auflösung ist mit der Ausrichtung des Geräts verknüpft und muss daher immer für die Swapchain festgelegt werden. Am zuverlässigsten ist es, beim Start der Anwendung vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
aufzurufen und die zurückgegebene Ausdehnung zu speichern. Tauschen Sie die Breite und Höhe basierend auf der currentTransform
, die ebenfalls zurückgegeben wird, 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, in der wir die Identitätsauflösung der Fensteroberflä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, um eine Änderung der Ausrichtung 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 oder 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 Hauptrendering-Schleife der Anwendungen 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.
Polling verwenden
Auf Geräten vor Android 10 können Sie die aktuelle Geräte-Transformation alle pollingInterval
Frames abrufen, wobei pollingInterval
eine vom Programmierer festgelegte Granularität ist. Dazu rufen Sie vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
auf und vergleichen das zurückgegebene Feld currentTransform
mit dem der aktuell gespeicherten Oberflächen-Transformation (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 das Polling zwischen 0,120 und 0,250 ms. Auf einem Pixel 1XL mit Android 8 dauerte das Polling zwischen 0,110 und 0,350 ms.vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
Callbacks verwenden
Eine zweite Option für Geräte mit einer Android-Version unter Android 10 besteht darin, einen onNativeWindowResized()
-Callback zu registrieren, um eine Funktion aufzurufen, die das orientationChanged
-Flag festlegt. Dadurch wird der Anwendung 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 Änderungen der Ausrichtung um 90 Grad aufgerufen wird, z. B. beim Wechsel vom Quer- zum Hochformat oder umgekehrt. Bei anderen Änderungen der Ausrichtung wird die Swapchain nicht neu erstellt.
Eine Änderung von Querformat zu umgekehrtem Querformat löst sie beispielsweise nicht aus. In diesem Fall muss der Android-Compositor die Drehung für Ihre Anwendung vornehmen.
Umgang mit der Ausrichtungsänderung
Rufen Sie die Routine für die Ausrichtungsänderung oben im Hauptwiedergabedurchlauf auf, wenn die Variable orientationChanged
auf „true“ gesetzt ist. Beispiel:
bool VulkanDrawFrame() {
if (orientationChanged) {
OnOrientationChange();
}
Sie führen alle erforderlichen Schritte aus, um die Swapchain in der Funktion OnOrientationChange()
neu zu erstellen. Das bedeutet:
Zerstören Sie alle vorhandenen Instanzen von
Framebuffer
undImageView
.Erstellen Sie die Swapchain neu, während Sie die alte Swapchain zerstören (was als Nächstes behandelt wird).
Erstellen Sie die Framebuffer mit den DisplayImages der neuen Swapchain neu. Hinweis:Bilder für Anhänge (z. B. Tiefen-/Schablonenbilder) müssen in der Regel nicht neu erstellt werden, da sie auf der Identitätsauflösung der vorab gedrehten 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 orientationChanged
-Flag auf „false“ zurück, um anzugeben, dass Sie die Änderung der Ausrichtung verarbeitet haben.
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 VkSurfaceCapabilities
-Struktur mit den neuen Informationen gefüllt wurde, können Sie prüfen, ob eine Änderung der Ausrichtung stattgefunden hat, indem Sie das Feld currentTransform
prüfen. 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 im VkSwapchainCreateInfo
-Struct 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
-Bereich 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“ des surfaceCapabilities
festgelegt ist. Außerdem legen Sie das Feld oldSwapchain
auf die Swapchain fest, die zerstört werden soll.
Anpassung der MVP-Matrix
Als Letztes müssen Sie die Vorabtransformation anwenden, indem Sie eine Rotationsmatrix auf Ihre MVP-Matrix anwenden. Dadurch wird die Drehung im Clip-Space angewendet, sodass das resultierende Bild an die aktuelle Geräteausrichtung angepasst 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;
Überlegung – Nicht-Vollbild-Viewport und Scissor
Wenn in Ihrer Anwendung ein Viewport oder eine Scissor-Region verwendet wird, die nicht den gesamten Bildschirm abdeckt, muss sie entsprechend der Ausrichtung des Geräts aktualisiert werden. Dazu müssen Sie beim Erstellen der Vulkan-Pipeline die dynamischen Viewport- und Scissor-Optionen 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 Viewport-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 bzw. Höhe des Darstellungsbereichs definieren.
Dieselbe Berechnung kann auch verwendet werden, um den Scheren-Test festzulegen. Sie 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-Derivate
Wenn in Ihrer Anwendung abgeleitete Berechnungen wie dFdx
und dFdy
verwendet werden, 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 Vorabtransformation an den Fragment-Shader übergeben (z. B. eine Ganzzahl, die die aktuelle Geräteausrichtung darstellt) und damit die Ableitungsberechnungen richtig zuordnen:
- Für einen um 90 Grad vorab gedrehten Frame
- dFdx muss dFdy zugeordnet werden.
- dFdy muss -dFdx zugeordnet werden.
- Für einen um 270 Grad vorab gedrehten Frame
- dFdx muss -dFdy zugeordnet werden.
- dFdy muss dFdx zugeordnet werden.
- Bei einem um 180 Grad vorab gedrehten Frame:
- dFdx muss -dFdx zugeordnet werden.
- dFdy muss -dFdy zugeordnet werden.
Fazit
Damit Ihre Anwendung Vulkan auf Android optimal nutzen kann, ist die Implementierung der Vorrotation unerlässlich. Die wichtigsten Erkenntnisse aus diesem Artikel sind:
- Achten Sie darauf, dass das Pretransform-Flag beim Erstellen oder Neuerstellen der Swapchain auf den Wert gesetzt wird, der vom Android-Betriebssystem zurückgegeben wird. Dadurch wird der Compositor-Overhead vermieden.
- Die Swapchain-Größe muss in der natürlichen Ausrichtung des Displays auf die Identitätsauflösung der Fensteroberfläche der App festgelegt sein.
- Drehen Sie die MVP-Matrix im Clip-Space, um die Ausrichtung des Geräts zu berücksichtigen, da die Auflösung/der Umfang der Swapchain nicht mehr mit der Ausrichtung des Displays aktualisiert wird.
- Aktualisieren Sie die Viewport- und Scissor-Rechtecke nach Bedarf für Ihre Anwendung.
Beispiel-App:Minimale Vorrotation für Android