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ürmeyi verimli bir şekilde nasıl yöneteceğ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 yönetilen öğeleri (ör. cihaz yönü ve görüntüleme yüzeyi yönü ile ilişkisi) açıkça uygulamanız gerekir. Android'in cihazın oluşturma yüzeyini cihaz yönüyle uyumlu hale getirmenin üç yolu vardır:

  1. Android OS, cihazın ekran işleme birimini (DPU) kullanabilir. Bu birim, donanımda yüzey dönüşünü verimli bir şekilde yönetebilir. Yalnızca desteklenen cihazlarda kullanılabilir.
  2. Android OS, bir derleyici geçişi ekleyerek yüzey rotasyonunu yönetebilir. Bu, birleştiricinin çıkış görüntüsünü döndürmesiyle ilgili olarak nasıl işlem yapması gerektiğine bağlı olarak performans maliyeti oluşturur.
  3. Uygulama, döndürülmüş bir resmi ekranın mevcut yönüyle eşleşen bir oluşturma yüzeyinde oluşturarak yüzey döndürmeyi kendi başına halledebilir.

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

Şu anda, uygulama dışında yönetilen yüzey dönüşümünün ücretsiz olup olmayacağının uygulama tarafından bilinmesi mümkün değildir. Bu işlemi sizin için yapacak bir DPU olsa bile ödemeniz gereken ölçülebilir bir performans cezası olabilir. Uygulamanız CPU'ya bağlıysa Android Compositor'un GPU kullanımındaki artış (genellikle artırılmış bir sıklıkta çalışır) nedeniyle güç sorunu ortaya çıkar. Uygulamanız GPU'ya bağlıysa Android Compositor, uygulamanızın GPU çalışmasını da önleyebilir ve ek performans kaybına neden olabilir.

Pixel 4XL'de gönderim başlıkları çalıştırıldığında SurfaceFlinger'ın (Android Compositor'ı çalıştıran daha yüksek öncelikli görev) aşağıdakileri yaptığı tespit edildi:

  • Uygulamanın çalışmasını düzenli olarak önler, kare sürelerinde 1-3 ms'lik artışlara neden olur ve

  • GPU'nun köşe/doku belleğine daha fazla baskı uygular. Bunun nedeni, derleyicinin kompozisyon işini yapmak için çerçeve önbellüğünün tamamını okuması gerekmesidir.

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

Yüzey dönmelerinin mümkün olduğunca az ek yükle doğru şekilde işlenmesi için önceki örnekte görüldüğü gibi 3. yöntemi uygulamanız gerekir. Buna ön rotasyon denir. Bu, Android OS'e yüzey dönme işlemini uygulamanızın yönettiğ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 Birleştirici'nin rotasyonu kendi kendine yapmasını engeller.

Yüzey dönüştürme işaretini nasıl ayarlayacağınızı 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ğinden farklı bir yönde olduğu tek bir yönü destekler. Örneğin, dikey kimlikli bir telefonda yalnızca yatay olarak çalışan bir uygulama veya yatay kimlikli bir tablette yalnızca dikey olarak çalışan bir uygulama.

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

Uygulamanızda cihaz döndürmeyi işlemek için uygulamanın AndroidManifest.xml dosyasını değiştirerek Android'e uygulamanızın yön ve ekran boyutu değişikliklerini işleyeceğini bildirin. Bu, Android'in Android Activity öğesini yok edip yeniden oluşturmasını ve yön değişikliği olduğunda mevcut pencere yüzeyinde onDestroy() işlevini çağırmasını engeller. Bu işlem, etkinliğin configChanges bölümüne orientation (API düzeyi 13'ten düşük olan sürümleri desteklemek için) ve screenSize özellikleri eklenerek yapılır:

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

Uygulamanız ekran yönünü screenOrientation özelliğini kullanarak düzeltiyorsa bunu yapmanız gerekmez. Ayrıca, uygulamanız sabit bir yönde kullanılıyorsa uygulama başlatılırken/devam ettirilirken takas zincirinin yalnızca bir kez ayarlanması gerekir.

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

Ardından, VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR değeriyle ilişkili cihazın ekran çözünürlüğünü algılayın. Bu çözünürlük, cihazın kimlik yönelimiyle ilişkilidir ve bu nedenle, takas zincirinin her zaman ayarlanması gereken çözünürlüktür. Bu işlemi yapmanın en güvenilir yolu, uygulama başlatılırken vkGetPhysicalDeviceSurfaceCapabilitiesKHR() işlevine çağrı yapmak ve 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önelimindeki söz konusu kimlik çözünürlüğünü depolamak için kullandığımız bir VkExtent2D yapısıdır.

Cihaz Yön Değişikliklerini Algılama (Android 10 ve sonraki sürümler)

Uygulamanızdaki 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. Örnek:

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 çalışır. Android'in bu sürümleri, vkQueuePresentKHR() kaynağından VK_SUBOPTIMAL_KHR döndürür. Bu kontrolün sonucunu, uygulamaların ana oluşturma döngüsünden erişilebilen bir boolean olan orientationChanged içinde depolarız.

Cihaz Yön Değişikliklerini Algılama (Android 10'dan Önceki Sürümler)

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ü pollingInterval karede bir (pollingInterval, programcı tarafından belirlenen bir ayrıntı düzeyidir) anketleyebilirsiniz. Bunu yapmak için vkGetPhysicalDeviceSurfaceCapabilitiesKHR() işlevini çağırıp döndürülen currentTransform alanını, şu anda depolanan yüzey dönüşümüyle (bu kod örneğinde pretransformFlag içinde depolanır) karşılaştırırsınız.

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

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

Android 10 çalıştıran bir Pixel 4'te vkGetPhysicalDeviceSurfaceCapabilitiesKHR() anketi 0,120-0,250 ms, Android 8 çalıştıran bir Pixel 1XL'de ise 0,110-0,350 ms sürdü.

Geri çağırma işlevini kullanma

Android 10'un altındaki sürümleri çalıştıran cihazlar için ikinci seçenek, orientationChanged işaretini ayarlayan bir işlevi çağırmak üzere onNativeWindowResized() geri çağırma işlevi kaydetmektir. Bu işlev, uygulamaya bir yön değişikliğinin gerçekleştiğini bildirir:

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

Burada ResizeCallback şu şekilde tanımlanır:

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

Bu çözümün sorunu, onNativeWindowResized() işlevinin yalnızca 90 derecelik yön değişiklikleri için (ör. yataydan dikeye veya tam tersi) çağrılmasıdır. Diğer yön değişiklikleri, takas zincirinin yeniden oluşturulmasını tetiklemez. Örneğin, yatay moddan ters yatay moda geçiş bu özelliği tetiklemez. Bu durumda Android derleyicinin uygulamanız için çevirme işlemini yapması gerekir.

Yön Değişikliğini Ele Alma

Yönlendirme değişikliğini işlemek için orientationChanged değişkeni doğru olarak ayarlandığında ana oluşturma döngüsünün üst kısmındaki yön değişikliği rutinini çağırın. Örnek:

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

Değişim zincirini yeniden oluşturmak için gereken tüm işlemleri OnOrientationChange() işlevinde yaparsınız. Bu durumda:

  1. Mevcut tüm Framebuffer ve ImageView örneklerini yok edin.

  2. Eski takas zincirini yok ederken takas zincirini yeniden oluşturun (sonraki bölümde ele alınacaktır) ve

  3. Yeni takas zincirinin DisplayImages özelliğiyle Framebuffer'ları yeniden oluşturun. Not: Önceden döndürülmüş takas zinciri resimlerinin kimlik çözünürlüğünü temel aldıklarından, ek resimlerin (ör. derinlik/şablon resimleri) genellikle yeniden oluşturulması 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 hallettiğinizi göstermek için orientationChanged işaretini false olarak sıfırlayın.

Değişim zinciri yeniden oluşturma

Önceki bölümde, takas zincirinin yeniden oluşturulması gerektiğinden bahsetmiştik. Bunu yapmanın ilk adımları, oluşturma yüzeyinin yeni özelliklerini elde etmeyi içerir:

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

VkSurfaceCapabilities yapısı yeni bilgilerle doldurulduğunda, currentTransform alanını kontrol ederek bir yön değişikliğinin olup olmadığını kontrol edebilirsiniz. MVP matrisinde düzenlemeler yaptığınızda daha sonra bu değere ihtiyacınız olacağından, pretransformFlag alanında saklayın.

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şlatılırken depoladığınız displaySizeIdentity kapsamıyla doldurulur. preTransform alanı, pretransformFlag değişkeniyle doldurulur (pretransformFlag, surfaceCapabilities nesnesinin currentTransform alanına ayarlanır). Ayrıca oldSwapchain alanını, yok edilecek swapchain olarak ayarlarsınız.

MVP Matrisi Düzenlemesi

Son olarak, MVP matrisinize bir dönme matrisi uygulayarak ön dönüşümü uygulamanız gerekir. Bu işlem, döndürmeyi klip alanında uygulayarak ortaya çıkan resmin mevcut cihaz yönüne döndürülmesini sağlar. Ardından, bu güncellenmiş MVP matrisini verteks gölgelendiricinize aktarabilir ve gölgelendiricilerinizi değiştirmek zorunda 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 gereken nokta: 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ı, w ve h ise sırasıyla 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 dahil edilmiş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);

Üzerinde Düşünme - Fragment Shader Türevleri

Uygulamanız dFdx ve dFdy gibi türev hesaplamaları kullanıyorsa bu hesaplamalar piksel alanında yürütüldüğü için döndürülmüş koordinat sistemini hesaba katmak üzere ek dönüşümler gerekebilir. Bunun için uygulamanın, ön dönüştürmeyle ilgili bir göstergeyi (mevcut cihaz yönünü temsil eden bir tam sayı gibi) parçacık gölgelendiriciye iletmesi ve türev hesaplamalarını düzgün şekilde eşlemek için bunu kullanması gerekir:

  • Önceden 90 derece döndürülmüş bir kare için
    • dFdx, dFdy ile eşlenmelidir.
    • dFdy, -dFdx ile eşlenmelidir
  • Önceden döndürülmüş 270 derece kare için
    • dFdx, -dFdy ile eşlenmelidir
    • dFdy, dFdx ile eşlenmelidir.
  • Önceden döndürülmüş 180 derece 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 döndürme özelliğini uygulamanız gerekir. Bu makaleden çıkarılabilecek en önemli noktalar şunlardır:

  • Değişim zinciri oluşturulurken veya yeniden oluşturulurken, ön dönüştürme işaretinin Android işletim sistemi tarafından döndürülen işaretle eşleşecek şekilde ayarlandığından emin olun. Bu sayede, derleyicinin ek yükünü önleyebilirsiniz.
  • Değişim zinciri boyutunu, ekranın doğal yönündeki uygulama pencere yüzeyinin kimlik çözünürlüğüne sabitleyin.
  • Değişim zinciri çözünürlüğü/kapsamı artık ekranın yönelimiyle güncellenmediği için cihazın yönelimini hesaba katmak üzere MVP matrisini klip alanında döndürün.
  • Uygulamanız tarafından gerektiği şekilde görüntü alanı ve makas dikdörtgenlerini güncelleyin.

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