Este artigo descreve como processar a rotação de dispositivos de maneira eficiente no aplicativo Vulkan implementando a pré-rotação.
Com o Vulkan, é possível especificar muito mais informações sobre o estado de renderização do que com o OpenGL. Com o Vulkan, é necessário implementar explicitamente elementos que são processados pelo driver no OpenGL, como a orientação do dispositivo e a relação dela com a orientação da superfície de renderização. O Android pode processar a reconciliação da superfície de renderização do dispositivo com a orientação do dispositivo de três maneiras:
- O SO Android pode usar a unidade de processamento de tela (DPU, na sigla em inglês) do dispositivo, que pode processar a rotação da superfície em hardware. Disponível apenas em dispositivos compatíveis.
- O SO Android pode processar a rotação da superfície adicionando uma transmissão do compositor. Isso vai ter um custo de desempenho dependendo de como o compositor precisa lidar com a rotação da imagem de saída.
- O próprio aplicativo pode processar a rotação da superfície renderizando uma imagem girada em uma superfície de renderização que corresponde à orientação atual da tela.
Qual destes métodos você deve usar?
Atualmente, não há uma maneira de um aplicativo saber se a rotação da superfície processada fora do aplicativo será livre. Mesmo que haja uma DPU para resolver esse problema, ainda haverá uma perda mensurável no desempenho. Se o app for vinculado à CPU, isso se tornará um problema de energia devido ao aumento do uso da GPU pelo Android Compositor, que geralmente é executado em uma frequência elevada. Se o aplicativo for vinculado à GPU, o Android Compositor também poderá forçar a interrupção do trabalho da GPU no aplicativo, prejudicando o desempenho.
Ao executar títulos de envio no Pixel 4XL, observamos que o SurfaceFlinger (a tarefa de maior prioridade que impulsiona o Compositor do Android)
Previne regularmente o trabalho do aplicativo, causando 1 a 3 ms de acessos a frametimes e
Aumenta a pressão sobre a memória de vértice/textura da GPU, porque o compositor precisa ler todo o framebuffer para fazer o trabalho de composição.
O processamento da orientação interrompe a preempção da GPU pelo SurfaceFlinger quase totalmente. Enquanto isso, a frequência da GPU diminui em 40% porque a frequência aumentada usada pelo Android Composer não é mais necessária.
Para garantir que as rotações de superfície sejam processadas corretamente com o menor overhead possível, como mostrado no caso anterior, implemente o método 3. Isso é conhecido como pré-rotação. Isso informa ao SO Android que seu app processa a rotação da superfície. Você pode fazer isso transmitindo sinalizações de transformação da superfície que especificam a orientação durante a criação da cadeia de troca. Isso impede que o Android Composer faça a rotação por conta própria.
Saber como definir a flag de transformação da superfície é importante para cada aplicativo Vulkan. Os aplicativos tendem a oferecer suporte a várias orientações ou a uma única orientação em que a superfície de renderização está em uma orientação diferente do que o dispositivo considera a orientação de identidade. Por exemplo, um aplicativo somente em modo paisagem em um smartphone de identidade retrato ou um aplicativo somente em modo retrato em um tablet de identidade paisagem.
Modificar o AndroidManifest.xml
Para gerenciar a rotação de dispositivo no seu app, comece mudando o arquivo
AndroidManifest.xml
do aplicativo para informar ao Android que seu app processará as mudanças
de tamanho da tela e de orientação. Isso evita que o Android destrua e recrie a
Activity
do Android e chame a função
onDestroy()
na superfície
da janela existente quando ocorre uma mudança de orientação. Isso é feito
adicionando os atributos orientation
(para compatibilidade com o nível de API <13) e
screenSize
à seção
configChanges
da atividade:
<activity android:name="android.app.NativeActivity"
android:configChanges="orientation|screenSize">
Se o aplicativo corrigir a orientação da tela usando o atributo screenOrientation
, não será preciso fazer isso. Além disso, se o aplicativo usar uma orientação
fixa, ele só precisará configurar a cadeia de troca uma vez
na inicialização/retomada do aplicativo.
Acessar a resolução da tela de identidade e os parâmetros da câmera
Em seguida, detecte a resolução da tela do dispositivo
associada ao valor VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR
. Essa
resolução está associada à orientação da identidade do dispositivo e,
portanto, é aquela em que a cadeia de troca sempre precisará ser definida. A maneira mais
confiável de conseguir isso é fazendo uma chamada para
vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
na inicialização do aplicativo e
armazenando a extensão retornada. Troque a largura e a altura com base no
currentTransform
que também é retornado para garantir o armazenamento da
resolução da tela de identidade:
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 é uma estrutura de VkExtent2D
que usamos para armazenar essa resolução
de identidade da superfície da janela do app na orientação natural da tela.
Detectar mudanças de orientação do dispositivo (Android 10 e versões mais recentes)
A maneira mais confiável de detectar uma mudança de orientação no seu aplicativo é
verificar se a função vkQueuePresentKHR()
retorna
VK_SUBOPTIMAL_KHR
. Exemplo:
auto res = vkQueuePresentKHR(queue_, &present_info);
if (res == VK_SUBOPTIMAL_KHR){
orientationChanged = true;
}
Observação:essa solução só funciona em dispositivos com o
Android 10 ou versões mais recentes. Essas versões do Android retornam
VK_SUBOPTIMAL_KHR
de vkQueuePresentKHR()
. Armazenamos o resultado dessa
verificação em orientationChanged
, um boolean
que pode ser acessado
no loop de renderização principal dos aplicativos.
Detectar mudanças na orientação do dispositivo (versões anteriores ao Android 10)
Para dispositivos com o Android 10 ou versões anteriores, é necessária
uma implementação diferente, já que o VK_SUBOPTIMAL_KHR
não é compatível.
Como usar a enquete
Em dispositivos anteriores ao Android 10, você pode pesquisar a transformação atual do dispositivo a cada
pollingInterval
de frames, em que pollingInterval
é uma granularidade decidida
pelo programador. Para fazer isso, chame
vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
e compare o campo
currentTransform
retornado com o da transformação
de superfície armazenada no momento (neste exemplo de código armazenado em pretransformFlag
).
currFrameCount++;
if (currFrameCount >= pollInterval){
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
if (pretransformFlag != capabilities.currentTransform) {
window_resized = true;
}
currFrameCount = 0;
}
Em um Pixel 4 com Android 10, a pesquisa de
vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
levou de 0,120 a 0,250 ms e em um
Pixel 1XL com o Android 8, a pesquisa levou 0,110 a 0,350 ms.
Como usar callbacks
Uma segunda opção para dispositivos com execução em versões anteriores ao Android 10 é registrar um callback
onNativeWindowResized()
para chamar uma função que defina a sinalização
orientationChanged
, indicando ao aplicativo
que ocorreu uma mudança na orientação:
void android_main(struct android_app *app) {
...
app->activity->callbacks->onNativeWindowResized = ResizeCallback;
}
Em que ResizeCallback é definido como:
void ResizeCallback(ANativeActivity *activity, ANativeWindow *window){
orientationChanged = true;
}
O problema com essa solução é que onNativeWindowResized()
só é
chamado para mudanças de orientação de 90 graus, como a mudança de paisagem para retrato ou
vice-versa. Outras mudanças de orientação não vão acionar a recriação da cadeia de troca.
Por exemplo, uma mudança de paisagem para paisagem invertida
não aciona a rotação, exigindo que o compositor do Android faça a inversão para o
aplicativo.
Como processar a mudança de orientação
Para processar a mudança de orientação, chame a rotina correspondente na
parte superior do loop de renderização principal quando a variável orientationChanged
estiver definida como verdadeira. Exemplo:
bool VulkanDrawFrame() {
if (orientationChanged) {
OnOrientationChange();
}
Você faz todo o trabalho necessário para recriar a cadeia de troca na
função OnOrientationChange()
. Isso significa que você:
Destrua todas as instâncias de
Framebuffer
eImageView
.Recrie a cadeia de troca ao destruir a antiga (o que será discutido a seguir) e
Recrie os framebuffers com as DisplayImages da nova cadeia de troca. Observação:as imagens de anexo, por exemplo, imagens de profundidade/estêncil, geralmente não precisam ser recriadas porque são baseadas na resolução de identidade das imagens da cadeia de troca pré-giradas.
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, no final da função, você redefine a sinalização orientationChanged
como falsa
para mostrar que processou a mudança de orientação.
Recreação da cadeia de troca
Na seção anterior, mencionamos ter que recriar a cadeia de troca. As primeiras etapas para fazer isso envolvem o recebimento das novas características da superfície de renderização:
void createSwapChain(VkSwapchainKHR oldSwapchain) {
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
pretransformFlag = capabilities.currentTransform;
Com a estrutura VkSurfaceCapabilities
preenchida com as novas informações,
agora é possível ver se ocorreu uma mudança na orientação verificando o
campo currentTransform
. Você a armazenará no campo pretransformFlag
,
porque será necessária mais tarde, quando você fizer ajustes na
matriz de MVP.
Para fazer isso, especifique os seguintes atributos
na estrutura 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);
}
O campo imageExtent
será preenchido com a extensão displaySizeIdentity
que
você armazenou na inicialização do aplicativo. O campo preTransform
será preenchido
com a variável pretransformFlag
, que é definida como o campo currentTransform
de surfaceCapabilities
. O campo oldSwapchain
também é definido como
a cadeia de troca que será destruída.
Ajuste da matriz de MVP
O último passo é aplicar a pré-transformação aplicando uma matriz de rotação à matriz de MVP. Essencialmente, isso aplica a rotação no espaço de corte para que a imagem resultante seja girada para a orientação atual do dispositivo. Em seguida, você pode simplesmente transmitir essa matriz de MVP atualizada para o sombreador de vértice e usá-la normalmente sem precisar modificar os sombreadores.
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;
Consideração: janela de visualização de tela não cheia e tesoura
Se o aplicativo estiver usando uma região de janela de visualização/tesoura que não seja de tela cheia, ela precisará ser atualizada de acordo com a orientação do dispositivo. Isso requer que você ative as opções dinâmicas da janela de visualização e tesoura durante a criação do pipeline do 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);
O cálculo real da extensão da janela de visualização durante a gravação do buffer de comando tem esta aparência:
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);
As variáveis x
e y
definem as coordenadas do canto superior esquerdo da
janela de visualização, enquanto w
e h
definem a largura e a altura, respectivamente.
O mesmo cálculo também pode ser usado para definir o teste de tesoura e está incluído
aqui para maior abrangência:
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);
Considerações: derivados do sombreador de fragmentos
Se o seu aplicativo estiver usando cálculos derivados, como dFdx
e dFdy
,
outras transformações poderão ser necessárias para considerar o sistema de coordenadas rotacionado
conforme esses cálculos são executados no espaço de pixels. Isso exige que o app
transmita uma indicação da pré-transformação ao sombreador do fragmento, como um
número inteiro que representa a orientação atual do dispositivo, e use-a para mapear
corretamente os cálculos derivados:
- Para um frame pré-girado em 90 graus
- dFdx precisa ser mapeado para dFdy.
- dFdy precisa ser mapeado para -dFdx.
- Para um frame pré-girado em 270 graus
- dFdx precisa ser mapeado para -dFdy.
- dFdy precisa ser mapeado para dFdx.
- Para um frame pré-girado em 180 graus,
- dFdx precisa ser mapeado para -dFdx.
- dFdy precisa ser mapeado para -dFdy.
Conclusão
Para que seu aplicativo aproveite ao máximo o Vulkan no Android, a implementação da pré-rotação é imprescindível. Estas são as conclusões mais importantes deste artigo:
- Durante a criação ou a recriação da cadeia de troca, verifique se a flag de pré-transformação está definida para corresponder à flag retornada pelo sistema operacional Android. Isso vai evitar a sobrecarga do compositor.
- Mantenha o tamanho da cadeia de troca fixo na resolução de identidade da superfície da janela do app na orientação natural da tela.
- Gire a matriz de MVP no espaço de corte para considerar a orientação dos dispositivos, porque a resolução/extensão da cadeia de troca não é mais atualizada com a orientação da tela.
- Atualize retângulos de tesoura e janelas de visualização conforme a necessidade do aplicativo.
App de exemplo: pré-rotação mínima do Android (link em inglês)