Questo articolo descrive come gestire in modo efficiente la rotazione del dispositivo nella tua applicazione Vulkan implementando la pre-rotazione.
Con Vulkan, puoi specificare molte più informazioni sullo stato di rendering rispetto a OpenGL. Con Vulkan, devi implementare esplicitamente le funzionalità gestite dal driver in OpenGL, ad esempio l'orientamento del dispositivo e la sua relazione con l'orientamento della superficie di rendering. Esistono tre modi in cui Android può gestire la riconciliazione della superficie di rendering del dispositivo con l'orientamento del dispositivo:
- Il sistema operativo Android può utilizzare la Display Processing Unit (DPU) del dispositivo, che può gestire in modo efficiente la rotazione della superficie in hardware. Disponibile solo su dispositivi supportati.
- Il sistema operativo Android può gestire la rotazione della superficie aggiungendo un passaggio del compositore. Ciò avrà un costo in termini di prestazioni, a seconda di come il compositore deve gestire la rotazione dell'immagine di output.
- L'applicazione stessa può gestire la rotazione della superficie eseguendo il rendering di un'immagine ruotata su una superficie di rendering che corrisponde all'orientamento corrente del display.
Quale di questi metodi dovresti utilizzare?
Al momento, non è possibile per un'applicazione sapere se la rotazione della superficie gestita al di fuori dell'applicazione sarà libera. Anche se esiste un DPU che si occupa di tutto per te, è probabile che dovrai comunque pagare una penalizzazione del rendimento misurabile. Se la tua applicazione è vincolata alla CPU, si tratta di un problema di alimentazione a causa dell'aumento dell'utilizzo della GPU da parte di Android Compositor, che di solito viene eseguito a una frequenza potenziata. Se la tua applicazione è vincolata alla GPU, il compositore Android può anche anticipare il lavoro della GPU dell'applicazione, causando un'ulteriore perdita di prestazioni.
Quando eseguiamo i titoli di spedizione su Pixel 4XL, abbiamo notato che SurfaceFlinger (l'attività con priorità più elevata che gestisce Android Compositor):
Interrompe regolarmente il lavoro dell'applicazione, causando picchi di 1-3 ms ai tempi di frame e
Aumenta la pressione sulla memoria vertex/texture della GPU, perché il compositore deve leggere l'intero framebuffer per eseguire la composizione.
La gestione dell'orientamento interrompe quasi completamente la preemption della GPU da parte di SurfaceFlinger, mentre la frequenza della GPU cala del 40% poiché la frequenza potenziata utilizzata dal compositore Android non è più necessaria.
Per assicurarti che le rotazioni delle superfici vengano gestite correttamente con il minimo overhead possibile, come nel caso precedente, devi implementare il metodo 3. Questa operazione è nota come pre-rotazione. In questo modo, il sistema operativo Android viene informato che la tua app gestice la rotazione della superficie. Puoi farlo passando i flag di trasformazione della superficie che specificano l'orientamento durante la creazione della swapchain. In questo modo, impedisci al compositore Android di eseguire la rotazione in prima persona.
Sapere come impostare il flag di trasformazione della superficie è importante per ogni applicazione Vulkan. Le applicazioni tendono a supportare più orientamenti o un singolo orientamento se la superficie di rendering è in un orientamento diverso da quello considerato dal dispositivo come orientamento dell'identità. Ad esempio, un'applicazione solo in orizzontale su uno smartphone con identità verticale o un'applicazione solo in verticale su un tablet con identità orizzontale.
Modificare AndroidManifest.xml
Per gestire la rotazione del dispositivo nella tua app, inizia modificando il file AndroidManifest.xml
dell'applicazione per indicare ad Android che la tua app gestirà le modifiche dell'orientamento e delle dimensioni dello schermo. In questo modo, Android non distrugge e ricrea
Activity
e non chiama la funzione
onDestroy()
sulla
superficie della finestra esistente quando si verifica un cambio di orientamento. Per farlo,
aggiungere gli attributi orientation
(per supportare il livello API <13) e screenSize
alla sezione
configChanges
dell'attività:
<activity android:name="android.app.NativeActivity"
android:configChanges="orientation|screenSize">
Se la tua applicazione fissa l'orientamento dello schermo utilizzando l'attributo screenOrientation
, non è necessario farlo. Inoltre, se la tua applicazione utilizza un orientamento fisso, dovrà configurare la swapchain una sola volta all'avvio/al riavvio dell'applicazione.
Ottenere la risoluzione dello schermo di identità e i parametri della fotocamera
Successivamente, rileva la risoluzione dello schermo del dispositivo associata al valore VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR
. Questa risoluzione è associata all'orientamento dell'identità del dispositivo ed è quindi quella su cui dovrà sempre essere impostata la swapchain. Il modo più affidabile per ottenere questo risultato è effettuare una chiamata a vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
all'avvio dell'applicazione e memorizzare l'ambito restituito. Scambia la larghezza e l'altezza in base al valore currentTransform
restituito per assicurarti di memorizzare la risoluzione dello schermo 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 memorizzare la risoluzione dell'identità della superficie della finestra dell'app nell'orientamento naturale del display.
Rilevare le modifiche all'orientamento del dispositivo (Android 10 e versioni successive)
Il modo più affidabile per rilevare una modifica dell'orientamento nell'applicazione è 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 il risultato di questo controllo in orientationChanged
, un boolean
accessibile dal loop di rendering principale delle applicazioni.
Rilevare le variazioni dell'orientamento del dispositivo (versioni precedenti ad Android 10)
Per i dispositivi con Android 10 o versioni precedenti, è necessaria un'implementazione diversa, perché VK_SUBOPTIMAL_KHR
non è supportato.
Utilizzare il polling
Sui dispositivi precedenti ad Android 10 puoi eseguire il polling della trasformazione del dispositivo corrente ogni
pollingInterval
frame, dove pollingInterval
è una granularità decisa
dal programmatore. Per farlo, chiama vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
e poi confronta il campo currentTransform
restituito con quello della trasformazione 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 pollingvkGetPhysicalDeviceSurfaceCapabilitiesKHR()
è durato tra 0,120 e 0,250 ms, mentre su Pixel 1XL con Android 8 è durato tra 0,110 e 0,350 ms.
Utilizzo dei Callback
Una seconda opzione per i dispositivi con versioni precedenti ad Android 10 è registrare un callback
onNativeWindowResized()
per chiamare una funzione che imposta il
orientationChanged
flag, segnalando all'applicazione che si è verificata una variazione dell'orientamento:
void android_main(struct android_app *app) {
...
app->activity->callbacks->onNativeWindowResized = ResizeCallback;
}
Dove ResizeCallback è definito come:
void ResizeCallback(ANativeActivity *activity, ANativeWindow *window){
orientationChanged = true;
}
Il problema di questa soluzione è che onNativeWindowResized()
viene chiamato solo per le modifiche dell'orientamento di 90 gradi, ad esempio dal formato orizzontale a quello verticale o viceversa. Altre modifiche all'orientamento non attiveranno la ricreazione della swapchain.
Ad esempio, non verrà attivata se passi dal formato orizzontale a quello verticale e viceversa, poiché il compositore Android dovrà eseguire la rotazione per l'applicazione.
Gestione della modifica dell'orientamento
Per gestire la modifica dell'orientamento, chiama la routine di modifica dell'orientamento nella parte superiore del loop di rendering principale quando la variabile orientationChanged
è impostata su true. Ad esempio:
bool VulkanDrawFrame() {
if (orientationChanged) {
OnOrientationChange();
}
Devi eseguire tutto il lavoro necessario per ricreare la swapchain all'interno della funzione OnOrientationChange()
. Ciò significa che:
Distruggi tutte le istanze esistenti di
Framebuffer
eImageView
.Ricrea la swapchain distruggendo la vecchia swapchain (di cui parleremo più avanti) e
Ricrea i framebuffer con le DisplayImages della nuova swapchain. Nota:in genere, le immagini degli allegati (ad esempio le immagini di profondità/stencil) non devono essere ricreate in quanto si basano sulla risoluzione dell'identità delle immagini della catena di scambio 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;
}
Alla fine della funzione, reimposta il flag orientationChanged
su false per indicare che hai gestito la modifica dell'orientamento.
Ricostruzione della catena di scambio
Nella sezione precedente abbiamo accennato alla necessità di ricreare la swapchain. I primi passaggi per farlo consistono nell'ottenere le nuove caratteristiche della superficie di rendering:
void createSwapChain(VkSwapchainKHR oldSwapchain) {
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
pretransformFlag = capabilities.currentTransform;
Con la struttura VkSurfaceCapabilities
compilata con le nuove informazioni, ora puoi verificare se si è verificata una modifica dell'orientamento controllando il campo currentTransform
. Lo memorizzerai per utilizzarlo in un secondo momento nel campo pretransformFlag
poiché ti servirà quando apporterai modifiche alla
matrice MVP.
A tale scopo, specifica i seguenti attributi
nella 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
memorizzata all'avvio dell'applicazione. Il campo preTransform
verrà compilato con la variabile pretransformFlag
(impostata sul campo currentTransform di surfaceCapabilities
). Imposti anche il campo oldSwapchain
sulla swapchain che verrà distrutta.
Modifica della matrice MVP
L'ultima cosa da fare è applicare la pre-trasformazione mediante una matrice di rotazione alla matrice MVP. In sostanza, viene applicata la rotazione nello spazio del clip in modo che l'immagine risultante venga ruotata in base all'orientamento corrente del dispositivo. Puoi quindi semplicemente passare questa matrice MVP aggiornata al tuo shader vertex e utilizzarla normalmente senza dover modificare gli 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;
Considerazione: viewport e scissor non a schermo intero
Se la tua applicazione utilizza una regione viewport/scissor non a schermo intero, dovrà essere aggiornata in base all'orientamento del dispositivo. Per farlo, è necessario attivare le opzioni Viewport e Scissor dinamiche durante la creazione della pipeline di 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);
Il calcolo effettivo dell'estensione dell'area visibile durante la registrazione del buffer dei comandi è il 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 del viewport, mentre w
e h
definiscono rispettivamente la larghezza e l'altezza del viewport.
Lo stesso calcolo può essere utilizzato anche per impostare il test a forbice 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: derivate di shader di frammenti
Se la tua applicazione utilizza calcoli derivati come dFdx
e dFdy
,
potrebbero essere necessarie trasformazioni aggiuntive per tenere conto del sistema di coordinate girato
poiché questi calcoli vengono eseguiti nello spazio dei pixel. Per farlo, l'app deve trasmettere un'indicazione della preTransform allo shader di frammento (ad esempio un numero intero che rappresenta l'orientamento corrente del dispositivo) e utilizzarla per mappare correttamente i calcoli delle derivate:
- Per un frame ruotato in precedenza di 90 gradi
- dFdx deve essere mappato a dFdy
- dFdy deve essere mappato a -dFdx
- Per un frame pre-rotato di 270 gradi
- dFdx deve essere mappato a -dFdy
- dFdy deve essere mappato a dFdx
- Per un fotogramma pre-ruotato di 180 gradi,
- dFdx deve essere mappato a -dFdx
- dFdy deve essere mappato a -dFdy
Conclusione
Affinché la tua applicazione possa sfruttare al meglio Vulkan su Android, l'implementazione della pre-rotazione è un must. I punti più importanti di questo articolo sono:
- Assicurati che durante la creazione o la ricreazione della catena di scambio, il flag pretransform sia impostato in modo da corrispondere al flag restituito dal sistema operativo Android. In questo modo eviterai il sovraccarico del compositore.
- Mantieni le dimensioni della catena di scambio fisse alla risoluzione dell'identità della superficie della finestra dell'app nell'orientamento naturale del display.
- Ruota la matrice MVP nello spazio clip per tenere conto dell'orientamento dei dispositivi, poiché la risoluzione/l'estensione della catena di scambio non si aggiorna più con l'orientamento del display.
- Aggiorna il viewport e i rettangoli di ritaglio in base alle esigenze della tua applicazione.
App di esempio: prerotazione Android minima