Gérer l'orientation de l'appareil avec la prérotation Vulkan

Cet article explique comment gérer efficacement la rotation des appareils dans votre application Vulkan en implémentant la prérotation.

Avec Vulkan, vous pouvez spécifier beaucoup plus d'informations qu'OpenGL sur l'état du rendu. Avec Vulkan, vous devez explicitement mettre en œuvre les éléments gérés par le pilote dans OpenGL, tels que l'orientation de l'appareil et son lien avec l'orientation de la surface de rendu. Il existe trois façons pour Android de gérer la réconciliation de la surface de rendu avec l'orientation de l'appareil :

  1. L'OS Android peut utiliser l'unité de traitement d'affichage (Display Processing Unit, DPU) de l'appareil, qui peut gérer efficacement la rotation de la surface du matériel. Disponible uniquement sur les appareils compatibles.
  2. L'OS Android peut gérer la rotation de la surface en ajoutant une carte compositeur. Cela aura un coût en termes de performances selon la manière dont le compositeur doit gérer la rotation de l'image de sortie.
  3. L'application elle-même peut gérer la rotation de la surface en affichant sur une surface de rendu une image orientée dans le même sens que l'écran.

Parmi ces méthodes, lesquelles devriez-vous utiliser ?

Actuellement, rien ne permet à une application de savoir si la rotation de la surface gérée en dehors de l'application aura un coût ou non. Même si une DPU s'en occupe pour vous, une pénalité de performance mesurable s'appliquera probablement. Si votre application utilise le processeur de manière intensive, un problème d'alimentation risque de se poser en raison de l'utilisation accrue du GPU par le compositeur Android, qui s'exécute généralement à une fréquence accrue. Si votre application utilise le processeur de manière intensive, le compositeur Android peut également préempter le travail du GPU de votre application, ce qui entraîne une perte de performance supplémentaire.

En exécutant des jeux sur le Pixel 4XL, nous avons constaté que SurfaceFlinger (la tâche à priorité la plus élevée qui pilote le compositeur Android) :

  • préempte régulièrement le travail de l'application, ce qui rallonge le temps de rendu de 1 à 3 ms ;

  • et accroît la pression sur la mémoire requise par le GPU pour les sommets/textures, car le compositeur doit lire l'intégralité du tampon d'images pour effectuer son travail de composition.

Une gestion appropriée de l'orientation met presque entièrement fin à la préemption du GPU par SurfaceFlinger, tandis que la fréquence du GPU chute de 40 %, car la fréquence accrue utilisée par le compositeur Android n'est plus nécessaire.

Pour veiller à ce que les rotations d'une surface soient correctement gérées avec le moins de surcharge possible (comme dans le cas ci-dessus), nous vous recommandons d'appliquer la méthode 3- C'est ce qu'on appelle la prérotation. Cette méthode indique à l'OS Android que votre application gère la rotation de la surface. Pour ce faire, vous pouvez transmettre des indicateurs de transformation de surface qui spécifient l'orientation lors de la création de la chaîne de permutation. Cela empêche le compositeur Android de réaliser la rotation lui-même.

Il est important de savoir comment définir l'indicateur de transformation de surface pour chaque application Vulkan. Les applications ont tendance à prendre en charge soit plusieurs orientations, soit une seule orientation dans laquelle la surface de rendu n'est pas orientée dans le même sens que ce que l'appareil considère comme son orientation d'identité. Par exemple, une application en mode paysage uniquement sur un téléphone doté d'une identité portrait ou une application en mode portrait uniquement sur une tablette dotée d'une identité paysage.

Modifier le fichier AndroidManifest.xml

Pour gérer la rotation des appareils dans votre application, commencez par modifier son fichier AndroidManifest.xml afin d'indiquer à Android que l'application gère les changements d'orientation et de taille d'écran. Cela empêche Android de détruire et de recréer l'Activity Android, et d'appeler la fonction onDestroy() sur la surface de la fenêtre existante lorsqu'un changement d'orientation se produit. Pour ce faire, ajoutez les attributs orientation (pour prendre en charge un niveau d'API <13) et screenSize dans la section configChanges de l'activité :

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

Si votre application corrige l'orientation de l'écran à l'aide de l'attribut screenOrientation, cette étape n'est pas nécessaire. De même, si votre application utilise une orientation fixe, la chaîne de permutation ne doit être configurée qu'une seule fois au démarrage/à la reprise de l'application.

Obtenir la résolution de l'écran d'identité et les paramètres de la caméra

Détectez ensuite la résolution d'écran de l'appareil associée à la valeur VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR. Cette résolution est associée à l'orientation d'identité de l'appareil. Il s'agit donc de celle sur laquelle la chaîne de permutation doit toujours être définie. Pour obtenir cette résolution, le moyen le plus fiable consiste à appeler vkGetPhysicalDeviceSurfaceCapabilitiesKHR() au démarrage de l'application et à stocker l'étendue renvoyée. Permutez la largeur et la hauteur en fonction de la valeur currentTransform qui est également renvoyée afin de vous assurer que la résolution de l'écran d'identité sera bien stockée :

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 est une structure VkExtent2D que nous utilisons pour stocker la résolution d'identité de la surface de la fenêtre de l'application selon l'orientation naturelle de l'écran.

Détecter les changements d'orientation de l'appareil (Android 10 et versions ultérieures)

Le moyen le plus fiable de détecter un changement d'orientation dans votre application consiste à vérifier si la fonction vkQueuePresentKHR() renvoie VK_SUBOPTIMAL_KHR. Par exemple :

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

Remarque : Cette solution ne fonctionne que sur les appareils équipés d'Android 10 ou version ultérieure. Ces versions d'Android renvoient VK_SUBOPTIMAL_KHR à partir de vkQueuePresentKHR(). Nous stockons le résultat de cette vérification dans orientationChanged, une valeur boolean accessible à partir de la boucle de rendu principale des applications.

Détecter les changements d'orientation de l'appareil (versions antérieures à Android 10)

Pour les appareils équipés d'Android 10 ou version antérieure, une implémentation différente est nécessaire, car VK_SUBOPTIMAL_KHR n'est pas compatible.

Utiliser l'interrogation

Sur les appareils antérieurs à Android 10, vous pouvez interroger la transformation toutes les pollingInterval images, où pollingInterval correspond à une précision choisie par le programmeur. Pour ce faire, appelez vkGetPhysicalDeviceSurfaceCapabilitiesKHR(), puis comparez le champ currentTransform renvoyé avec celui de la transformation de surface actuellement stockée (dans cet exemple de code, stockée dans pretransformFlag).

currFrameCount++;
if (currFrameCount >= pollInterval){
  VkSurfaceCapabilitiesKHR capabilities;
  vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);

  if (pretransformFlag != capabilities.currentTransform) {
    window_resized = true;
  }
  currFrameCount = 0;
}

Sur un Pixel 4 exécutant Android 10, l'interrogation de vkGetPhysicalDeviceSurfaceCapabilitiesKHR() prend 0,120 à 0,250 ms alors que sur un Pixel 1XL exécutant Android 8, elle prend 0,110 à 0,350 ms.

Utiliser des rappels

Une deuxième option pour les appareils exécutant Android 10 consiste à enregistrer un rappel onNativeWindowResized() afin d'appeler une fonction qui définit l'indicateur orientationChanged, pour signaler à l'application qu'un changement d'orientation s'est produit :

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

Où ResizeCallback est défini comme suit :

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

Le problème avec cette solution est qu'onNativeWindowResized() n'est appelé que pour des changements d'orientation à 90 degrés, par exemple pour passer du mode portrait au mode paysage ou inversement. Les autres changements d'orientation ne déclencheront pas la recréation de la chaîne de permutation. Par exemple, le passage du mode paysage au mode paysage inversé ne la déclenche pas, ce qui oblige le compositeur Android à effectuer le retournement pour votre application.

Gérer le changement d'orientation

Pour gérer le changement d'orientation, appelez la routine de changement d'orientation en haut de la boucle de rendu principale lorsque la variable orientationChanged est définie sur "true". Par exemple :

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

Vous effectuez tout le travail nécessaire pour recréer la chaîne de permutation dans la fonction OnOrientationChange(). Par conséquent, vous :

  1. détruisez toutes les instances existantes de Framebuffer et ImageView ;

  2. recréez la chaîne de permutation tout en détruisant l'ancienne (ce que nous verrons plus loin) ; et

  3. vous recréez lesFrameBuffer avec les DisplayImages de la nouvelle chaîne de permutation. Remarque : Les images de pièces jointes (profondeur ou stencil, par exemple) n'ont généralement pas besoin d'être recréées, car elles sont basées sur la résolution d'identité des images de la chaîne de permutation qui ont fait l'objet d'une prérotation.

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

À la fin de la fonction, réinitialisez l'indicateur orientationChanged sur "false" pour indiquer que vous avez géré le changement d'orientation.

Recréer la chaîne de permutation

Dans la section précédente, nous avons mentionné qu'il fallait recréer la chaîne de permutation. Pour ce faire, la première étape consiste à obtenir les nouvelles caractéristiques de la surface de rendu :

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

Maintenant que la struct VkSurfaceCapabilities contient les nouvelles informations, vous pouvez vérifier si un changement d'orientation a eu lieu en consultant le champ currentTransform. Vous le stockerez pour plus tard dans le champ pretransformFlag, car vous en aurez besoin pour apporter des ajustements à la matrice MVP.

Pour ce faire, spécifiez les attributs suivants dans la struct 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);
}

Le champ imageExtent est renseigné avec l'étendue displaySizeIdentity que vous avez stockée au démarrage de l'application. Le champ preTransform est renseigné avec la variable pretransformFlag (définie sur le champ currentTransform de surfaceCapabilities). Vous définissez également le champ oldSwapchain sur la chaîne de permutation qui sera détruite.

Ajuster la matrice MVP

La dernière chose à faire est d'appliquer la pré-transformation en appliquant une matrice de rotation à votre matrice MVP. Il s'agit essentiellement d'appliquer la rotation dans l'espace des extraits afin de faire pivoter l'image obtenue en fonction de l'orientation actuelle de l'appareil. Il vous suffit ensuite de transmettre cette matrice MVP mise à jour à votre nuanceur de sommets et de l'utiliser normalement sans modifier vos nuanceurs.

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;

Considération - Fenêtre d'affichage et rectangle ciseaux non affichés en mode plein écran

Si votre application utilise une région où la fenêtre d'affichage et le rectangle ciseaux ne s'affichent pas en mode plein écran, elle doit être mise à jour en fonction de l'orientation de l'appareil. Pour ce faire, vous devez activer les options "Fenêtre d'affichage" et "Rectangle ciseaux" lors de la création du pipeline de Vulkan :

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

Le calcul de l'étendue de la fenêtre d'affichage lors de l'enregistrement du tampon de commande se présente comme suit :

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

Les variables x et y définissent les coordonnées de l'angle supérieur gauche de la fenêtre d'affichage, tandis que w et h définissent respectivement la largeur et la hauteur de la fenêtre d'affichage. Le même calcul peut également être utilisé pour définir le test du rectangle ciseaux. Il est présenté ici par souci d'exhaustivité :

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

Considération - Dérivés du nuanceur de fragments

Si votre application utilise des calculs dérivés tels que dFdx et dFdy, des transformations supplémentaires peuvent être nécessaires pour tenir compte de la rotation du système de coordonnées, car ces calculs sont exécutés dans l'espace Pixel. Pour ce faire, l'application doit transmettre une indication de prétransformation au nuanceur de fragments (par exemple, un entier représentant l'orientation actuelle de l'appareil) et l'utiliser pour mapper correctement les calculs dérivés :

  • Pour une image ayant fait l'objet d'une prérotation à 90 degrés
    • dFdx doit être mappé sur dFdy
    • dFdy doit être mappé sur -dFdx
  • Pour une image ayant fait l'objet d'une prérotation à 270 degrés
    • dFdx doit être mappé sur -dFdy
    • dFdy doit être mappé sur dFdx
  • Pour une image ayant fait l'objet d'une prérotation à 180 degrés
    • dFdx doit être mappé sur -dFdx
    • dFdy doit être mappé sur -dFdy

Conclusion

Pour que votre application tire pleinement parti de Vulkan sous Android, l'implémentation de la prérotation est indispensable. Voici les points à retenir de cet article :

  • Lors de la création ou de la recréation d'une chaîne de permutation, assurez-vous que l'indicateur de prétransformation est configuré de façon à correspondre à celui renvoyé par le système d'exploitation Android. Cela évitera une surcharge du compositeur.
  • La taille de la chaîne de permutation doit être définie sur la résolution d'identité de la surface de la fenêtre de l'application selon l'orientation naturelle de l'écran.
  • Faites pivoter la matrice MVP dans l'espace des extraits pour tenir compte de l'orientation de l'appareil, car la résolution ou l'étendue de la chaîne de permutation n'est plus mise à jour avec l'orientation de l'écran.
  • Mettez à jour la fenêtre d'affichage et les rectangles ciseaux selon les besoins de votre application.

Application exemple : Prérotation minimale d'Android