Gestisci l'orientamento del dispositivo con la pre-rotazione Vulkan

Questo articolo descrive come gestire in modo efficiente la rotazione dei dispositivi nella tua applicazione Vulkan implementando la pre-rotazione.

Con Vulkan puoi: specificare molte più informazioni sullo stato di rendering rispetto a quelle che puoi utilizzare con OpenGL. Con Vulkan, devi implementare esplicitamente gli elementi gestiti dal driver in OpenGL, ad esempio l'orientamento del dispositivo e la sua relazione con dell'orientamento della superficie di rendering. Android può aiutarti in tre modi gestire la riconciliazione della superficie di rendering del dispositivo con l'orientamento del dispositivo:

  1. Il sistema operativo Android può utilizzare l'unità di elaborazione display (DPU) del dispositivo, in grado di gestire in modo efficiente la rotazione delle superfici nell'hardware. Disponibile su solo su dispositivi supportati.
  2. Il sistema operativo Android può gestire la rotazione della superficie aggiungendo una tessera del compositore. Questo avrà un costo in termini di prestazioni in base a come il compositore deve affrontare la rotazione dell'immagine di output.
  3. L'applicazione stessa può gestire la rotazione della superficie eseguendo il rendering di immagine ruotata su una superficie di rendering che corrisponde all'orientamento corrente sul display.

Quale di questi metodi dovresti usare?

Al momento, un'applicazione non può sapere se la rotazione della superficie gestiti al di fuori dell'applicazione sono senza costi. Anche se è presente una DPU ci sarà comunque una penalità misurabile per pagare. Se l'applicazione è vincolata alla CPU, si tratta di un problema di alimentazione a causa all'aumento dell'utilizzo della GPU da parte del Compositor Android, che solitamente è in esecuzione frequenza aumentata. Se la tua applicazione è legata alla GPU, il compositore Android può anche prerilasciare il lavoro della GPU dell'applicazione, aumentando le prestazioni o una perdita di dati.

Per la spedizione di titoli su Pixel 4XL, abbiamo riscontrato che SurfaceFlinger (l'attività con priorità più elevata che guida Compositore):

  • Prerilascia regolarmente il lavoro dell'applicazione, causando 1-3 ms dagli hit alle durate frame

  • Aumenta la pressione sulle prestazioni delle GPU vertex/texture, poiché il compositore deve leggere l'intera framebuffer per svolgere le sue composizioni.

Se gestisci correttamente l'orientamento, il prerilascio della GPU da parte di SurfaceFlinger viene quasi interrotto completamente, mentre la frequenza GPU scende del 40% poiché la frequenza potenziata utilizzata Android Compositor non è più necessario.

Per una corretta gestione delle rotazioni delle superfici con il minimo sforzo possibile, come abbiamo visto nel caso precedente, devi implementare il metodo 3. Questo processo è noto come pre-rotazione. Questo indica al sistema operativo Android che la tua app gestisce la rotazione della superficie. Puoi farlo passando i flag della trasformazione della superficie. che specificano l'orientamento durante la creazione della swapchain. Questo interrompe Android Compositor non esegua se stesso la rotazione.

Sapere come impostare il flag della trasformazione di superficie è importante per ogni Vulkan un'applicazione. Le applicazioni tendono a supportare più orientamenti o supportare un singolo orientamento in cui la superficie di rendering è in una posizione diversa orientato all'identità del dispositivo. Ad esempio: un'applicazione che supporta solo l'orientamento orizzontale su un telefono con identità verticale su un tablet con identità orizzontale.

Modifica AndroidManifest.xml

Per gestire la rotazione dei dispositivi nella tua app, inizia modificando la parte AndroidManifest.xml file per comunicare ad Android che l'app gestirà l'orientamento e alle dimensioni dello schermo. In questo modo Android non può distruggere e ricreare Android Activity e chiamando il metodo onDestroy() nel superficie esistente della finestra quando si verifica un cambio di orientamento. Ciò viene fatto aggiungendo gli attributi orientation (per supportare un livello API inferiore a 13) e screenSize alle impostazioni Sezione configChanges:

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

Se l'applicazione corregge l'orientamento dello schermo utilizzando screenOrientation non è necessario eseguire questa operazione. Inoltre, se la tua applicazione utilizza un modello orientamento, dovrai configurare la catena una sola volta l'avvio/il ripristino dell'applicazione.

Recupero della risoluzione della schermata di identità e dei parametri della fotocamera

Dopodiché, rileva la risoluzione dello schermo del dispositivo associati al valore VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR. Questo risoluzione è associata all'orientamento dell'identità del dispositivo ed è quindi quello su cui dovrà essere sempre impostato la funzione di swapchain. Il più il modo più affidabile per ottenerlo è effettuare una chiamata vkGetPhysicalDeviceSurfaceCapabilitiesKHR() all'avvio dell'applicazione e archiviare l'estensione restituita. Inverti larghezza e altezza in base al currentTransform viene restituito anche per garantire l'archiviazione la risoluzione della schermata dell'identità:

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 è una struttura VkExtent2D che utilizziamo per archiviare detta identità risoluzione della superficie della finestra dell'app nell'orientamento naturale del display.

Rilevamento dei cambiamenti di orientamento del dispositivo (Android 10 e versioni successive)

Il modo più affidabile per rilevare un cambiamento di orientamento nell'applicazione è per verificare se la funzione vkQueuePresentKHR() restituisce VK_SUBOPTIMAL_KHR. Ad esempio:

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

Nota: questa soluzione funziona solo sui dispositivi con Android 10 e versioni successive. Queste versioni di Android restituiscono VK_SUBOPTIMAL_KHR da vkQueuePresentKHR(). Memorizziamo i risultati fai il check-in orientationChanged, un boolean accessibile dal delle applicazioni il ciclo di rendering principale.

Rilevare i cambiamenti nell'orientamento del dispositivo (prima di Android 10)

Per i dispositivi con Android 10 o versioni precedenti, viene perché VK_SUBOPTIMAL_KHR non è supportato.

Uso dei sondaggi

Sui dispositivi precedenti ad Android 10, puoi eseguire il polling del dispositivo attuale e trasformare ogni pollingInterval frame, dove pollingInterval rappresenta una granularità decisa dal programmatore. Per farlo, ti basta chiamare vkGetPhysicalDeviceSurfaceCapabilitiesKHR() e confrontando poi il valore restituito Campo currentTransform con quello della superficie attualmente archiviata (in questo esempio di codice archiviato in pretransformFlag).

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

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

Su Pixel 4 con Android 10, il sondaggio vkGetPhysicalDeviceSurfaceCapabilitiesKHR() ha impiegato tra 0,120 e 0,250 ms e su una Pixel 1XL con Android 8; il polling ha richiesto 0,110-0,350 ms.

Uso dei callback

Una seconda opzione, per i dispositivi con versioni precedenti ad Android 10, è registrare un onNativeWindowResized() per chiamare una funzione che imposta la Flag orientationChanged, che segnala all'applicazione un cambiamento di orientamento si è verificato:

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

Dove RidimensionaCallback è definito come:

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

Il problema di questa soluzione è che onNativeWindowResized() ottiene solo richiede modifiche dell'orientamento di 90 gradi, ad esempio per passare da orizzontale a verticale vice versa. Altri cambiamenti di orientamento non attiveranno la ricreazione della catena di scambio. Ad esempio, una modifica da orizzontale a orizzontale non lo attivi, il compositore Android deve eseguire il un'applicazione.

Gestione del cambiamento di orientamento

Per gestire la modifica dell'orientamento, chiama la routine di modifica dell'orientamento su superiore del loop di rendering principale quando orientationChanged sia impostata su true. Ad esempio:

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

Devi fare tutto il lavoro necessario per ricreare la catena la funzione OnOrientationChange(). Ciò significa che:

  1. Elimina tutte le istanze esistenti di Framebuffer e ImageView,

  2. Ricrea la swapchain durante l'eliminazione la vecchia swapchain (di cui parleremo dopo)

  3. Ricrea i Framebuffer con i DisplayImages della nuova catena di swap. Nota:le immagini allegate (ad esempio, immagini di profondità o stencil) di solito non devono essere ricreati poiché si basano sulla risoluzione dell'identità delle immagini swapchain pre-ruotate.

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

E alla fine della funzione reimposti il flag orientationChanged su false per dimostrare di aver gestito il cambio di orientamento.

Divertimento a catena

Nella sezione precedente abbiamo parlato di dover ricreare la catena di swap. Il primo passo per farlo è acquisire le nuove caratteristiche del superficie di rendering:

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

Con lo struct VkSurfaceCapabilities compilato con le nuove informazioni, Ora puoi controllare se è stato effettuato un cambiamento di orientamento controllando le currentTransform. Lo archivierai per dopo in pretransformFlag campo perché ti servirà in un secondo momento, quando apporti modifiche al la matrice MVP.

Per farlo, specifica i seguenti attributi nello 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);
}

Il campo imageExtent verrà compilato con l'estensione displaySizeIdentity che memorizzati all'avvio dell'applicazione. Il campo preTransform verrà compilato con la variabile pretransformFlag (impostata sul campo currentTransform) di surfaceCapabilities). Inoltre, imposti il campo oldSwapchain sul di swapchain che verrà distrutta.

Aggiustamento della matrice MVP

L'ultima cosa da fare è applicare la pre-trasformazione applicando una matrice di rotazione alla matrice MVP. In sostanza, la rotazione nello spazio del clip in modo che l'immagine risultante venga ruotata l'orientamento attuale del dispositivo. Puoi quindi passare semplicemente questa matrice MVP aggiornata in Vertex Streamer e utilizzarlo normalmente senza dover modificare Shaper.

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;

Considerazione: area visibile non a schermo intero e forbice

Se la tua applicazione utilizza un'area visibile o forbice non a schermo intero, dovrà essere aggiornato in base all'orientamento del dispositivo. Questo richiede di attivare le opzioni dinamiche Viewport e Scissor durante la creazione della pipeline:

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

Il calcolo effettivo dell'estensione dell'area visibile durante la registrazione del buffer dei comandi è simile al seguente:

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

Le variabili x e y definiscono le coordinate dell'angolo in alto a sinistra della area visibile, mentre w e h definiscono rispettivamente la larghezza e l'altezza dell'area visibile. Lo stesso calcolo può essere usato anche per impostare il test delle forbici, ed è incluso qui per completezza:

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

Considerazione: elementi derivati di Fragment Shader

Se la tua applicazione utilizza calcoli derivati come dFdx e dFdy, Potrebbero essere necessarie ulteriori trasformazioni per tenere conto della coordinata ruotata quando questi calcoli vengono eseguiti nello spazio di pixel. Questa operazione richiede l'app per passare alcune indicazioni di preTransform nello shaker dei frammenti (come numero intero che rappresenta l'orientamento corrente del dispositivo) e utilizzalo per mappare i calcoli delle derivate correttamente:

  • Per un fotogramma prerotto a 90 gradi
      .
    • dFdx deve essere mappato a dFdy
    • dFdy deve essere mappato a -dFdx
  • Per un fotogramma pre-rotto a 270 gradi
      .
    • dFdx deve essere mappato a -dFdy
    • dFdy deve essere mappato a dFdx
  • Per un fotogramma prerotto a 180 gradi,
      .
    • dFdx deve essere mappato a -dFdx
    • dFdy deve essere mappato a -dFdy

Conclusione

Per fare in modo che la tua applicazione sfrutti al meglio Vulkan su Android, è fondamentale implementare la pre-rotazione. I concetti più importanti di questo sono:

  • Assicurati che durante la creazione o la ricreazione di swapchain, il flag di pretrasformazione sia impostato in modo che corrisponda al flag restituito dal sistema operativo Android. In questo modo eviterai l'overhead del compositore.
  • Mantieni le dimensioni della swapchain sulla risoluzione dell'identità della finestra dell'app superficie nell'orientamento naturale del display.
  • Ruota la matrice MVP nello spazio del clip per tenere conto dell'orientamento del dispositivo. perché la risoluzione/estensione swapchain non si aggiorna più con l'orientamento del display.
  • Aggiorna l'area visibile e i rettangoli con forbici in base alle esigenze dell'applicazione.

App di esempio: pre-rotazione minima di Android