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 per le prestazioni a seconda di come il compositore deve affrontare 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):
prerilascia regolarmente il lavoro dell'applicazione, causando hit di 1-3 ms alla durata frame
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 minor overhead possibile, come 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 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 l'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 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 all'elemento currentTransform
restituito per assicurarti di memorizzare 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 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 su 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 del dispositivo attuale trasforma 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 flag orientationChanged
, segnalando all'applicazione che si è verificato un cambiamento di orientamento:
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()
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 viene attivata se passi dal formato orizzontale a quello verticale e viceversa, poiché il compositore Android deve 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 i DisplayImages della nuova catena di swap. Nota:in genere non è necessario ricreare le immagini degli allegati (ad esempio le immagini di profondità/stencil) perché 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 applicando 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
dell'area visibile, mentre w
e h
definiscono rispettivamente la larghezza e l'altezza dell'area visibile.
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 l'applicazione utilizza calcoli derivati, come dFdx
e dFdy
,
potrebbero essere necessarie ulteriori trasformazioni per tenere conto del sistema di coordinate ruotato
mentre questi calcoli vengono eseguiti nello spazio di pixel. Ciò richiede all'app
di passare alcune indicazioni della preTransform nello streamr dei frammenti (ad esempio un
numero intero che rappresenta l'orientamento corrente del dispositivo) e di utilizzarle per mappare correttamente i calcoli derivati:
- 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, è necessario implementare la pre-rotazione. I concetti più importanti di questo articolo sono:
- Assicurati che durante la creazione o la ricreazione di swapchain, il flag di pretrasformazione 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 swapchain sulla 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