Questo articolo descrive come gestire in modo efficiente la rotazione del dispositivo nell'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 elementi gestiti dal driver in OpenGL, come 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 l'unità di elaborazione del display (DPU) del dispositivo, che può gestire in modo efficiente la rotazione della superficie nell'hardware. Disponibile solo sui dispositivi supportati.
- Il sistema operativo Android può gestire la rotazione della superficie aggiungendo un passaggio del compositore. Questa operazione comporterà 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, un'applicazione non può sapere se la rotazione della superficie gestita al di fuori dell'applicazione sarà senza costi. Anche se esiste una DPU che si occupa di questo per te, è probabile che ci sia comunque una penalità di rendimento misurabile da pagare. Se la tua applicazione è vincolata alla CPU, il problema diventa di alimentazione a causa dell'aumento dell'utilizzo della GPU da parte del compositore Android, che di solito viene eseguito a una frequenza maggiore. Se la tua applicazione è legata alla GPU, anche il compositore Android può interrompere il lavoro della GPU dell'applicazione, causando un'ulteriore perdita di prestazioni.
Quando eseguiamo titoli di spedizione su Pixel 4 XL, 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 nei tempi di frame e
Aumenta la pressione sulla memoria di vertici/texture della GPU, perché il compositore deve leggere l'intero framebuffer per svolgere il suo lavoro di composizione.
La gestione corretta dell'orientamento impedisce quasi del tutto la preemption della GPU da parte di SurfaceFlinger, mentre la frequenza della GPU scende del 40% perché la frequenza aumentata utilizzata dal compositore Android non è più necessaria.
Per garantire la corretta gestione delle rotazioni delle superfici con il minor overhead possibile, come illustrato nel caso precedente, devi implementare il metodo 3. Questa operazione è nota come pre-rotazione. In questo modo, il sistema operativo Android comunica che la tua app gestisce la rotazione della superficie. A questo scopo, passa i flag di trasformazione della superficie che specificano l'orientamento durante la creazione della swapchain. In questo modo, il compositore Android interrompe la rotazione autonoma.
Sapere come impostare il flag di trasformazione della superficie è importante per ogni applicazione Vulkan. Le app tendono a supportare più orientamenti o un singolo orientamento in cui la superficie di rendering ha un orientamento diverso da quello che il dispositivo considera il suo orientamento di identità. Ad esempio, un'applicazione solo orizzontale su uno smartphone con identità verticale o un'applicazione solo 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 comunicare ad Android che la tua app gestirà l'orientamento
e le modifiche alle dimensioni dello schermo. In questo modo, Android non distrugge e ricrea
l'Activity
di Android e non chiama la
funzione onDestroy()
sulla
superficie della finestra esistente quando si verifica un cambio di orientamento. A questo scopo, aggiungi 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 corregge l'orientamento dello schermo utilizzando l'attributo screenOrientation
, non è necessario eseguire questa operazione. Inoltre, se la tua applicazione utilizza un orientamento fisso,
dovrà configurare la swapchain una sola volta all'avvio/ripristino dell'applicazione.
Ottenere la risoluzione dello schermo dell'identità e i parametri della videocamera
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 a cui deve sempre essere impostata la swapchain. Il modo più
affidabile per ottenerlo è effettuare una chiamata a
vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
all'avvio dell'applicazione e
memorizzare l'extent restituito. Scambia la larghezza e l'altezza in base a
currentTransform
restituito anche 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 archiviare la risoluzione dell'identità
della superficie della finestra dell'app nell'orientamento naturale del display.
Rilevare le modifiche all'orientamento del dispositivo (Android 10+)
Il modo più affidabile per rilevare una modifica dell'orientamento nella tua 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
ciclo di rendering principale delle applicazioni.
Rilevare le modifiche all'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.
Utilizzo del polling
Sui dispositivi con versioni 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 polling
vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
ha richiesto tra 0,120 e 0,250 ms, mentre su
Pixel 1XL con Android 8, il polling ha richiesto 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
flag orientationChanged
, segnalando all'applicazione che si è verificato un cambio di 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 i cambiamenti di orientamento di 90 gradi, ad esempio il passaggio da orizzontale a verticale o viceversa. Altri cambiamenti di orientamento non attiveranno la ricreazione della swapchain.
Ad esempio, un cambiamento da orizzontale a orizzontale inverso non lo attiverà, richiedendo al compositore Android di eseguire l'inversione per la tua applicazione.
Gestione del cambio di orientamento
Per gestire il cambio di orientamento, chiama la routine di cambio di orientamento all'inizio del ciclo di rendering principale quando la variabile orientationChanged
è impostata su true. Ad esempio:
bool VulkanDrawFrame() {
if (orientationChanged) {
OnOrientationChange();
}
Esegui tutto il lavoro necessario per ricreare la swapchain all'interno
della funzione OnOrientationChange()
. Ciò significa che:
Elimina tutte le istanze esistenti di
Framebuffer
eImageView
.Ricrea la swapchain distruggendo la vecchia swapchain (che verrà discussa di seguito) e
Ricrea i Framebuffer con le DisplayImage della nuova swapchain. Nota:le immagini degli allegati (ad esempio immagini di profondità/stencil) in genere non devono essere ricreate perché si basano sulla risoluzione dell'identità delle immagini della swapchain pre-rotazione.
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, reimposti il flag orientationChanged
su false
per indicare che hai gestito il cambio di orientamento.
Swapchain Recreation
Nella sezione precedente abbiamo menzionato la 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 struct VkSurfaceCapabilities
compilata con le nuove informazioni, ora puoi verificare se si è verificato un cambio di orientamento controllando il campo currentTransform
. Lo memorizzerai per un secondo momento nel campo pretransformFlag
in quanto ti servirà in seguito quando apporterai modifiche alla
matrice MVP.
Per farlo, specifica i seguenti attributi
nella struttura 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
hai memorizzato all'avvio dell'applicazione. Il campo preTransform
verrà compilato
con la variabile pretransformFlag
(impostata sul campo currentTransform
di surfaceCapabilities
). Imposta anche il campo oldSwapchain
sulla
swapchain che verrà eliminata.
Aggiustamento della matrice MVP
L'ultima cosa da fare è applicare la pre-trasformazione applicando una matrice di rotazione alla matrice MVP. In sostanza, questa operazione applica la rotazione nello spazio clip in modo che l'immagine risultante venga ruotata in base all'orientamento attuale del dispositivo. Puoi quindi passare questa matrice MVP aggiornata nel vertex shader 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 non a schermo intero e forbici
Se la tua applicazione utilizza una regione di visualizzazione/ritaglio non a schermo intero, dovrà essere aggiornata in base all'orientamento del dispositivo. Per questo è necessario attivare le opzioni dinamiche Viewport e Scissor 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 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 - Derivate dello shader di framenti
Se la tua applicazione utilizza calcoli derivati come dFdx
e dFdy
,
potrebbero essere necessarie trasformazioni aggiuntive per tenere conto del sistema di coordinate ruotato,
poiché questi calcoli vengono eseguiti nello spazio dei pixel. A questo scopo, l'app
deve passare un'indicazione della pre-trasformazione allo shader dei frammenti (ad esempio un
numero intero che rappresenta l'orientamento attuale del dispositivo) e utilizzarla per mappare
correttamente i calcoli delle derivate:
- Per un frame prerotato di 90 gradi
- dFdx deve essere mappato a dFdy
- dFdy deve essere mappato a -dFdx
- Per un frame prerotato di 270 gradi
- dFdx deve essere mappato a -dFdy
- dFdy deve essere mappato su dFdx
- Per un frame prerotato di 180 gradi,
- dFdx deve essere mappato a -dFdx
- dFdy deve essere mappato su -dFdy
Conclusione
Per sfruttare al meglio Vulkan su Android, la tua applicazione deve implementare la pre-rotazione. I concetti più importanti da ricordare di questo articolo sono:
- Assicurati che durante la creazione o la ricreazione della swapchain, il flag di pre-trasformazione sia impostato in modo che corrisponda al flag restituito dal sistema operativo Android. In questo modo si eviterà il sovraccarico del compositore.
- Mantieni le dimensioni della swapchain 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, perché la risoluzione/l'estensione della swapchain non si aggiorna più in base all'orientamento del display.
- Aggiorna i rettangoli della finestra e di ritaglio in base alle esigenze della tua applicazione.
App di esempio: pre-rotazione minima di Android