Vulkan ön döndürme özelliğiyle cihaz yönünü yönetin

Bu makalede, ön döndürme uygulayarak Vulkan uygulamanızda cihaz döndürme işlemini verimli bir şekilde nasıl gerçekleştireceğiniz açıklanmaktadır.

Vulkan ile, oluşturma durumu hakkında OpenGL'de yapabileceğinizden çok daha fazla bilgi belirtebilirsiniz. Vulkan'da, OpenGL'de sürücü tarafından işlenen şeyleri (ör. cihaz yönü ve oluşturma yüzeyi yönü ile ilişkisi) açıkça uygulamanız gerekir. Android, cihazın oluşturma yüzeyini cihazın yönüyle üç şekilde eşleştirebilir:

  1. Android OS, cihazın Ekran İşleme Birimi'ni (DPU) kullanabilir. DPU, donanımdaki yüzey rotasyonunu verimli bir şekilde işleyebilir. Yalnızca desteklenen cihazlarda kullanılabilir.
  2. Android OS, bir birleştirici geçişi ekleyerek yüzey rotasyonunu işleyebilir. Bu, birleştiricinin çıkış görüntüsünü döndürme işlemini nasıl gerçekleştirdiğine bağlı olarak performans maliyetine neden olur.
  3. Uygulama, döndürülmüş bir görüntüyü ekranın mevcut yönüyle eşleşen bir oluşturma yüzeyine oluşturarak yüzey döndürme işlemini kendisi gerçekleştirebilir.

Aşağıdaki yöntemlerden hangisini kullanmalısınız?

Şu anda bir uygulamanın, uygulama dışında işlenen yüzey rotasyonunun ücretsiz olup olmayacağını bilmesi mümkün değildir. Bu işlemi sizin için yapacak bir DPU olsa bile ölçülebilir bir performans kaybı yaşanabilir. Uygulamanız CPU'ya bağlıysa bu durum, genellikle daha yüksek bir frekansta çalışan Android Compositor'ın GPU kullanımının artması nedeniyle güç sorununa yol açar. Uygulamanız GPU'ya bağlıysa Android Compositor, uygulamanızın GPU çalışmasını da önleyerek ek performans kaybına neden olabilir.

Pixel 4XL'de sevkiyat başlıklarını çalıştırırken SurfaceFlinger'ın (Android Compositor'ı çalıştıran daha yüksek öncelikli görev) şunları yaptığını gördük:

  • Uygulamanın çalışmasını düzenli olarak kesintiye uğratarak kare sürelerinde 1-3 ms'lik gecikmelere neden olur ve

  • Oluşturucu, oluşturma işlemini gerçekleştirmek için tüm çerçeve arabelleğini okumak zorunda olduğundan GPU'nun köşe/doku belleği üzerindeki baskıyı artırır.

Yönü doğru şekilde işlemek, SurfaceFlinger tarafından GPU önceliklendirmesini neredeyse tamamen durdurur. Android Compositor tarafından kullanılan artırılmış frekansa artık ihtiyaç duyulmadığından GPU frekansı% 40 düşer.

Yüzey döndürme işlemlerinin, yukarıdaki örnekte görüldüğü gibi mümkün olduğunca az ek yükle düzgün bir şekilde işlenmesini sağlamak için 3. yöntemi uygulamanız gerekir. Bu, ön döndürme olarak bilinir. Bu, Android OS'e yüzey rotasyonunu uygulamanızın işlediğini bildirir. Bunu, takas zinciri oluşturma sırasında yönü belirten yüzey dönüştürme işaretlerini ileterek yapabilirsiniz. Bu, Android Compositor'ın döndürme işlemini kendisi yapmasını engeller.

Yüzey dönüştürme işaretinin nasıl ayarlanacağını bilmek her Vulkan uygulaması için önemlidir. Uygulamalar genellikle birden fazla yönü destekler veya oluşturma yüzeyinin, cihazın kimlik yönü olarak kabul ettiği yönden farklı bir yönde olduğu tek bir yönü destekler. Örneğin, dikey kimlikli bir telefonda yalnızca yatay uygulama veya yatay kimlikli bir tablette yalnızca dikey uygulama.

AndroidManifest.xml dosyasını değiştirme

Uygulamanızda cihaz döndürme işlemini yönetmek için uygulamanın AndroidManifest.xml dosyasını değiştirerek Android'e uygulamanızın yön ve ekran boyutu değişikliklerini yöneteceğini bildirin. Bu sayede Android, yön değişikliği olduğunda Android Activity'yi silip yeniden oluşturmaz ve mevcut pencere yüzeyinde onDestroy() işlevini çağırmaz. Bu işlem, etkinliğin configChanges bölümüne orientation (API düzeyi <13'ü desteklemek için) ve screenSize özelliklerini ekleyerek yapılır:

<activity android:name="android.app.NativeActivity"
          android:configChanges="orientation|screenSize">

Uygulamanız, screenOrientation özelliğini kullanarak ekran yönünü sabitliyorsa bunu yapmanız gerekmez. Ayrıca, uygulamanız sabit bir yön kullanıyorsa swapchain'i yalnızca uygulama başlatıldığında/devam ettirildiğinde bir kez ayarlaması gerekir.

Kimlik Ekranı Çözünürlüğü ve Kamera Parametrelerini Alma

Ardından, VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR değeriyle ilişkili cihazın ekran çözünürlüğünü tespit edin. Bu çözünürlük, cihazın kimlik yönüyle ilişkilidir ve bu nedenle, swapchain'in her zaman ayarlanması gereken çözünürlüktür. Bunu elde etmenin en güvenilir yolu, uygulama başlatılırken vkGetPhysicalDeviceSurfaceCapabilitiesKHR() yöntemini çağırıp döndürülen kapsamı depolamaktır. Kimlik ekranı çözünürlüğünü depoladığınızdan emin olmak için genişlik ve yüksekliği, döndürülen currentTransform değerine göre değiştirin:

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, uygulamanın pencere yüzeyinin ekranın doğal yönündeki kimlik çözümlemesini depolamak için kullandığımız bir VkExtent2D yapısıdır.

Cihaz yönü değişikliklerini algılama (Android 10+)

Uygulamanızda yön değişikliğini algılamanın en güvenilir yolu, vkQueuePresentKHR() işlevinin VK_SUBOPTIMAL_KHR değerini döndürüp döndürmediğini doğrulamaktır. Örneğin:

auto res = vkQueuePresentKHR(queue_, &present_info);
if (res == VK_SUBOPTIMAL_KHR){
  orientationChanged = true;
}

Not: Bu çözüm yalnızca Android 10 ve sonraki sürümleri çalıştıran cihazlarda kullanılabilir. Android'in bu sürümleri, vkQueuePresentKHR() üzerinden VK_SUBOPTIMAL_KHR döndürür. Bu kontrolün sonucunu, uygulamanın ana oluşturma döngüsünden erişilebilen bir boolean olan orientationChanged içinde saklarız.

Cihaz yönü değişikliklerini algılama (Android 10 öncesi)

Android 10 veya daha eski sürümlerin yüklü olduğu cihazlarda VK_SUBOPTIMAL_KHR desteklenmediği için farklı bir uygulama gerekir.

Anketleri kullanma

Android 10 öncesi cihazlarda, mevcut cihaz dönüşümünü her pollingInterval karede bir yoklayabilirsiniz. Burada pollingInterval, programcı tarafından belirlenen bir ayrıntı düzeyidir. Bunu yapmak için vkGetPhysicalDeviceSurfaceCapabilitiesKHR() yöntemini çağırın ve döndürülen currentTransform alanını, o anda depolanan yüzey dönüşümünün alanıyla (bu kod örneğinde pretransformFlag içinde depolanır) karşılaştırın.

currFrameCount++;
if (currFrameCount >= pollInterval){
  VkSurfaceCapabilitiesKHR capabilities;
  vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);

  if (pretransformFlag != capabilities.currentTransform) {
    window_resized = true;
  }
  currFrameCount = 0;
}

Android 10 yüklü bir Pixel 4'te yoklama vkGetPhysicalDeviceSurfaceCapabilitiesKHR() 0,120-0,250 ms, Android 8 yüklü bir Pixel 1XL'de ise yoklama 0,110-0,350 ms sürdü.

Geri çağırmaları kullanma

Android 10'dan önceki sürümleri çalıştıran cihazlar için ikinci bir seçenek, onNativeWindowResized() geri çağırma işlevi kaydederek orientationChanged işaretini ayarlayan bir işlevi çağırmaktır. Bu, uygulamaya yön değişikliği olduğunu bildirir:

void android_main(struct android_app *app) {
  ...
  app->activity->callbacks->onNativeWindowResized = ResizeCallback;
}

ResizeCallback şu şekilde tanımlanır:

void ResizeCallback(ANativeActivity *activity, ANativeWindow *window){
  orientationChanged = true;
}

Bu çözümün sorunu, onNativeWindowResized() yönteminin yalnızca 90 derecelik yön değişiklikleri (ör. yataydan dikeye veya dikeyden yataya geçiş) için çağrılmasıdır. Diğer yön değişiklikleri, takas zincirinin yeniden oluşturulmasını tetiklemez. Örneğin, yataydan ters yataya geçiş bu işlemi tetiklemez. Android oluşturucunun uygulamanız için çevirme işlemini yapması gerekir.

Yön Değişikliğini İşleme

Yön değişikliğini işlemek için orientationChanged değişkeni true olarak ayarlandığında ana oluşturma döngüsünün en üstünde yön değişikliği rutinini çağırın. Örneğin:

bool VulkanDrawFrame() {
 if (orientationChanged) {
   OnOrientationChange();
}

Takas zincirini yeniden oluşturmak için gereken tüm işlemleri OnOrientationChange() işlevinde yaparsınız. Bu, şu anlama gelir:

  1. Framebuffer ve ImageView'in mevcut tüm örneklerini silin.

  2. Eski takas zincirini yok ederken takas zincirini yeniden oluşturun (bu konuyu daha sonra ele alacağız) ve

  3. Yeni takas zincirinin DisplayImages'ı ile Framebuffer'ları yeniden oluşturun. Not: Ek resimlerin (ör. derinlik/kalıp resimleri) önceden döndürülmüş takas zinciri resimlerinin kimlik çözünürlüğüne dayalı oldukları için genellikle yeniden oluşturulmaları gerekmez.

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;
}

İşlevin sonunda, yön değişikliğini işlediğinizi göstermek için orientationChanged işaretini false olarak sıfırlarsınız.

Swapchain Yeniden Oluşturma

Önceki bölümde, takas zincirini yeniden oluşturmanız gerektiğinden bahsetmiştik. Bunun için ilk adım, oluşturma yüzeyinin yeni özelliklerini almaktır:

void createSwapChain(VkSwapchainKHR oldSwapchain) {
   VkSurfaceCapabilitiesKHR capabilities;
   vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
   pretransformFlag = capabilities.currentTransform;

VkSurfaceCapabilities yapısı yeni bilgilerle doldurulduktan sonra currentTransform alanını kontrol ederek yön değişikliği olup olmadığını kontrol edebilirsiniz. MVP matrisinde düzenlemeler yaparken daha sonra ihtiyacınız olacağı için bu bilgiyi pretransformFlag alanına kaydedeceksiniz.

Bunun için VkSwapchainCreateInfo yapısında aşağıdaki özellikleri belirtin:

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);
}

imageExtent alanı, uygulama başlangıcında depoladığınız displaySizeIdentity kapsamıyla doldurulur. preTransform alanı, pretransformFlag değişkeniyle (surfaceCapabilities öğesinin currentTransform alanına ayarlanır) doldurulur. Ayrıca oldSwapchain alanını, yok edilecek takas zincirine ayarlarsınız.

MVP Matrisi Düzenlemesi

Son olarak, MVP matrisinize bir rotasyon matrisi uygulayarak ön dönüşümü uygulamanız gerekir. Bu işlem, kırpma alanında döndürme uygulayarak elde edilen görüntünün mevcut cihaz yönüne döndürülmesini sağlar. Ardından, bu güncellenmiş MVP matrisini köşe gölgelendiricinize aktarabilir ve gölgelendiricilerinizi değiştirmenize gerek kalmadan normal şekilde kullanabilirsiniz.

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;

Dikkat Edilmesi Gerekenler: Tam Ekran Olmayan Görüntü Alanı ve Makas

Uygulamanız tam ekran olmayan bir görüntü alanı/makas bölgesi kullanıyorsa bunların cihazın yönüne göre güncellenmesi gerekir. Bunun için Vulkan'ın ardışık düzen oluşturma işlemi sırasında dinamik görüntü alanı ve makas seçeneklerini etkinleştirmeniz gerekir:

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);

Komut arabelleği kaydı sırasında görüntü alanı kapsamının gerçek hesaplaması şu şekildedir:

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);

x ve y değişkenleri, görüntü alanının sol üst köşesinin koordinatlarını tanımlarken w ve h, görüntü alanının genişliğini ve yüksekliğini tanımlar. Aynı hesaplama, makas testini ayarlamak için de kullanılabilir ve eksiksiz olması için buraya eklenmiştir:

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);

Değerlendirme - Parça Gölgelendirici Türevleri

Uygulamanız dFdx ve dFdy gibi türev hesaplamalar kullanıyorsa bu hesaplamalar piksel alanında yürütüldüğünden, döndürülen koordinat sistemini hesaba katmak için ek dönüşümler gerekebilir. Bu durumda uygulamanın, preTransform'un bir göstergesini (ör. mevcut cihaz yönünü temsil eden bir tam sayı) parça gölgelendiriciye iletmesi ve türev hesaplamalarını doğru şekilde eşlemek için bunu kullanması gerekir:

  • 90 derece önceden döndürülmüş bir kare için
    • dFdx, dFdy ile eşlenmelidir.
    • dFdy, -dFdx ile eşlenmelidir.
  • 270 derece önceden döndürülmüş bir çerçeve için
    • dFdx, -dFdy ile eşlenmelidir.
    • dFdy, dFdx ile eşlenmelidir.
  • 180 derece önceden döndürülmüş bir kare için:
    • dFdx, -dFdx ile eşlenmelidir.
    • dFdy, -dFdy ile eşlenmelidir.

Sonuç

Uygulamanızın Android'de Vulkan'dan en iyi şekilde yararlanabilmesi için ön rotasyonun uygulanması gerekir. Bu makaleden çıkarılacak en önemli sonuçlar şunlardır:

  • Takas zinciri oluşturma veya yeniden oluşturma sırasında, ön dönüşüm işaretinin Android işletim sistemi tarafından döndürülen işaretle eşleşecek şekilde ayarlandığından emin olun. Bu sayede birleştirici ek yükü önlenir.
  • Takas zinciri boyutunu, ekranın doğal yönünde uygulama penceresi yüzeyinin kimlik çözünürlüğüne sabit tutun.
  • Takas zinciri çözünürlüğü/kapsamı artık ekranın yönüyle güncellenmediğinden, cihazın yönünü hesaba katmak için kırpma alanındaki MVP matrisini döndürün.
  • Görüntü alanını ve makas dikdörtgenlerini uygulamanızın gerektirdiği şekilde güncelleyin.

Örnek uygulama: Minimal Android ön döndürme