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:
- 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.
- 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.
- 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:
Elimina tutte le istanze esistenti di
Framebuffer
eImageView
,Ricrea la swapchain durante l'eliminazione la vecchia swapchain (di cui parleremo dopo)
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