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 ile OpenGL'de sürücü tarafından işlenen cihaz yönü ve bunun oluşturma yüzey yönü ile ilişkisi gibi şeyleri 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 işleyebilir. Yalnızca desteklenen cihazlarda kullanılabilir.
  2. Android OS, bir karıştırıcı 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öndürmenin ücretsiz olup olmayacağının uygulama tarafından bilinmesi mümkün değildir. Bu sorunu sizin yerinize halledecek bir DPU olsa bile muhtemelen ödemeniz gereken ölçülebilir bir performans cezası olacaktır. Uygulamanız CPU'ya bağlıysa Android Compositor'un genellikle artırılmış bir sıklıkta çalıştığından GPU kullanımının artması 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 isabetlere neden olur ve

  • GPU'nun köşe/doku belleğine daha fazla baskı uygular. Bunun nedeni, derleyicinin oluşturma işlemini 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öndürmelerin önceki örnekte görüldüğü gibi mümkün olduğunca az ek yük ile düzgün bir şekilde yapılmasını sağlamak için 3. yöntemi uygulamalısınız. Buna ön rotasyon denir. Bu, Android OS'e uygulamanızın yüzey dönüşünü işlediğini bildirir. Bunu, değişim zinciri oluşturma sırasında yönü belirten yüzey dönüşümü 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ü veya oluşturma yüzeyinin, cihazın kimlik yönünün 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önüşünü 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, Android'in, Android Activity cihazını kaldırıp 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ı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. Ö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() tarihinden itibaren VK_SUBOPTIMAL_KHR olarak döndürülüyor. Bu denetimin sonucunu, uygulamaları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'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ğırmaları 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;
}

Yeniden boyutlandırma Geri çağırması şu şekilde tanımlanır:

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

Bu çözümün sorunu, onNativeWindowResized() özelliğinin yalnızca yataydan dikeye (veya tam tersi) 90 derecelik yön değişiklikleri için ç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 true 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();
}

Takas zincirini yeniden OnOrientationChange() işlevi içinde yeniden oluşturmak için gereken tüm işleri 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 Oluşturma

Önceki bölümde takas zincirini yeniden oluşturmanız gerektiğinden bahsedeceğiz. 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 yaparken daha sonra bu değere ihtiyacınız olacağından, daha sonra kullanmak üzere pretransformFlag alanında depolarsınız.

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 (surfaceCapabilities'nin 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 görüntünün 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ı tanımlarken w ve h değişkenleri 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
  • 270 derece önceden döndürülmüş 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 sabit tutun.
  • 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