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

Bu makalede, ön döndürme uygulayarak Vulkan uygulamanızda cihaz rotasyonunun nasıl etkili bir şekilde yönetileceği açıklanmaktadır.

Vulkan ile, oluşturma durumu hakkında OpenGL ile belirtebileceğinizden çok daha fazla bilgi belirleyebilirsiniz. Vulkan ile OpenGL'de sürücü tarafından işlenen cihaz yönü ve bunun yüzey yönünü oluşturma ile ilişkisi gibi şeyleri açıkça uygulamanız gerekir. Android, cihazın oluşturma yüzeyini cihaz yönüyle eşleştirmenin üç yolu vardır:

  1. Android OS, donanımda yüzey dönüşünü verimli bir şekilde işleyebilen cihazın Ekran İşleme Birimini (DPU) kullanabilir. Yalnızca desteklenen cihazlarda kullanılabilir.
  2. Android OS, bir toplayıcı geçişi ekleyerek yüzey döndürme işlemini işleyebilir. Bunun performans maliyeti, oluşturucunun çıkış görüntüsünü döndürmek için ne yapması gerektiğine bağlı olarak değişir.
  3. Uygulamanın kendisi, ekranın mevcut yönüyle eşleşen oluşturma yüzeyine döndürülmüş bir resim oluşturarak yüzey döndürmesini işleyebilir.

Bu yöntemlerden hangisini kullanmalısınız?

Şu anda bir uygulamanın, uygulama dışında işlenen yüzey rotasyonunun serbest olup olmayacağını bilmesi mümkün değildir. Sizin yerinize bununla ilgilenecek bir DPU olsa bile, ödemeniz gereken ölçülebilir bir performans cezası olabilir. Uygulamanız CPU'ya bağlıysa bu durum, genellikle artırılmış bir sıklıkta çalışan Android Birleştirici tarafından daha fazla GPU kullanımı nedeniyle güç sorunu haline gelir. Uygulamanız GPU'ya bağlıysa Android Birleştirici, uygulamanızın GPU çalışmasını da geçici olarak keserek ek performans kaybına neden olabilir.

Pixel 4XL'de kargo başlıkları yayınlarken SurfaceFlinger'ın (Android Birleştirici'yi artıran daha yüksek öncelikli görev) olduğunu gördük:

  • Uygulamanın çalışmasını düzenli olarak önceden keserek 1-3 ms. isabetin kare sürelerine neden olmasını sağlar.

  • Kompozitörün, bileşim çalışmasını yapmak için çerçeve arabelleğinin tamamını okuması gerektiğinden GPU'nun köşe/doku belleğine daha fazla baskı uygular.

Yönün işlenmesi, SurfaceFlinger tarafından yapılan GPU önceliklendirmesini neredeyse tamamen durdurur ve Android Compositor tarafından kullanılan artırılmış frekansa artık ihtiyaç duyulmadığından GPU sıklığı% 40 düşer.

Önceki örnekte görüldüğü gibi, yüzey dönüşlerinin 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 uygulamanız gerekir. Bu işleme, rotasyon adı verilir. Bu, Android OS'ye uygulamanızın yüzey dönüşünü işlediğini bildirir. Bunu, takas zinciri oluşturulurken yönü belirten yüzey dönüşüm işaretlerini ileterek yapabilirsiniz. Bu işlem, Android Birleştirici'nin rotasyonu kendisi yapmasını durdurur.

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

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

Uygulamanızda cihazın döndürülmesini sağlamak için, uygulamanın AndroidManifest.xml dosyasını değiştirerek Android'e, yön ve ekran boyutu değişikliklerini yapacağını bildirin. Bu, bir yön değişikliği olduğunda Android'in Android Activity'i silip yeniden oluşturmasını ve mevcut pencere yüzeyinde onDestroy() işlevini çağırmasını engeller. Bu işlem, etkinliğin configChanges bölümüne orientation (API düzeyini <13'ü 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üzeltirse bunu yapmanız gerekmez. Ayrıca, uygulamanız sabit yön kullanıyorsa uygulama başlatma/devam ettirme sırasında değişim zincirini yalnızca bir kez ayarlaması gerekir.

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

Ardından, cihazın VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR değeriyle ilişkilendirilmiş ekran çözünürlüğünü tespit edin. Bu çözünürlük cihazın kimlik yönüyle ilişkilendirilir ve değişim zincirinin her zaman ayarlanması gereken çözümdür. Bunu almanın en güvenilir yolu, uygulama başlatılırken vkGetPhysicalDeviceSurfaceCapabilitiesKHR() öğesine bir çağrı yapmak ve döndürülen kapsamı depolamaktır. Kimlik ekran çözünürlüğünü depoladığınızdan emin olmak için döndürülen currentTransform öğesine göre genişliği ve yüksekliği 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 söz konusu kimlik çözümlemesini ekranın doğal yönünde 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 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() tarihinde VK_SUBOPTIMAL_KHR tutarında iade yaptı. Bu kontrolün 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 Öncesi)

Android 10 veya önceki sürümleri çalıştıran cihazlarda VK_SUBOPTIMAL_KHR desteklenmediğinden farklı bir uygulama gerekir.

Anket Kullanımı

Android 10 öncesi sürümlere sahip cihazlarda, mevcut cihaz dönüşümünü her pollingInterval karede sorgulayabilirsiniz. Burada pollingInterval, programcı tarafından belirlenen bir ayrıntı düzeyidir. Bunu yapmak için vkGetPhysicalDeviceSurfaceCapabilitiesKHR() yöntemini çağırıp döndürülen currentTransform alanını, halihazırda depolanan yüzey dönüşümüyle (pretransformFlag içinde depolanan bu kod örneğinde) karşılaştırarak yapabilirsiniz.

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

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

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

Geri Çağırmaları Kullanma

Android 10'un altında çalışan cihazlar için ikinci bir seçenek de orientationChanged işaretini ayarlayan ve uygulamaya yön değişikliğinin meydana geldiğini bildiren bir işlevi çağırmak için onNativeWindowResized() geri çağırması kaydetmektir:

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ümdeki sorun, onNativeWindowResized() ürününün yalnızca 90 derecelik yön değişiklikleri (yataydan dikeye veya tam tersi) için çağrılmasıdır. Diğer yön değişiklikleri, değişim zinciri yeniden oluşturma işlemini tetiklemez. Örneğin, yatay moddan ters yatay görünüme geçmek, bunu tetiklemez ve Android oluşturucunun, uygulamanız için bu işlemi yapmasını gerektirir.

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

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

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

Takas zincirini OnOrientationChange() işlevi içinde yeniden oluşturmak için gerekli tüm işleri yaparsınız. Bu durumda:

  1. Mevcut Framebuffer ve ImageView örneklerini yok edin,

  2. Eski takas zincirini yok ederken değişim zincirini yeniden oluşturun

  3. Framebuffers'ı yeni takas zincirinin DisplayImages ile yeniden oluşturun. Not: Ek resimleri (örneğin, derinlik/şablon resimleri) önceden döndürülmüş takas zinciri resimlerinin kimlik çözünürlüğüne dayalı olduğundan genellikle yeniden oluşturulmasına gerek yoktur.

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 uyguladığınızı göstermek için orientationChanged işaretini false (yanlış) değerine sıfırlarsınız.

Değiştirme Zinciri ile Eğlence

Önceki bölümde, takas zincirini yeniden oluşturmak zorunda olduğunuzdan bahsetmiştik. Bunu yapmanın ilk adımları, oluşturma yüzeyinin yeni özelliklerini edinmektir:

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

VkSurfaceCapabilities struct yeni bilgilerle doldurulduktan sonra, currentTransform alanını kontrol ederek yön değişikliği olup olmadığını kontrol edebilirsiniz. Daha sonra MVP matrisinde ayarlamalar yaparken daha sonra ihtiyacınız olacağından pretransformFlag alanında kullanmak üzere saklayacaksınız.

Bunu yapmak için VkSwapchainCreateInfo struct'da 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ı ile doldurulur. preTransform alanı, pretransformFlag değişkeniyle doldurulur (surfaceCapabilities değerinin mevcut Transform alanına ayarlanır). oldSwapchain alanını da kaldırılacak takas zincirine ayarlarsınız.

MVP Matrisi Düzenlemesi

Yapmanız gereken son şey, MVP matrisinize bir rotasyon matrisi uygulayarak ön dönüştürme işlemini uygulamaktır. Temelde, elde edilen resmin geçerli cihaz yönüne döndürülmesi için döndürmeyi klip alanına uygulamaktır. Daha sonra, bu güncellenmiş MVP matrisini, köşe gölgelendiricinize kolayca aktarabilir ve gölgelendiricilerinizi değiştirmeye gerek kalmadan, matrisi 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;

Üzerinde Düşünme - 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üzeni oluşturulurken 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 şekilde görünür:

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 değişkenleri 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 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);

Üzerinde Düşünme - 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, uygulamanın parça gölgelendiriciye bazı göstergelerini (geçerli cihaz yönünü temsil eden bir tam sayı gibi) iletmesini ve türev hesaplamaları doğru bir şekilde eşlemek için bunu kullanmasını gerektirir:

  • 90 derece önceden döndürülmüş bir çerçeve 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 çerçeve için
    • dFdx, -dFdx ile eşlenmelidir
    • dFdy, -dFdy ile eşlenmelidir

Sonuç

Uygulamanızın Android'de Vulkan'dan en iyi şekilde yararlanması için ön rotasyon uygulanması zorunludur. Bu makaleden en önemli çıkarım şudur:

  • Takas zinciri oluşturma veya yeniden oluşturma sırasında dönüşüm öncesi işaretinin, Android işletim sistemi tarafından döndürülen işaretle eşleşecek şekilde ayarlandığından emin olun. Bu işlem, toplayıcı ek yükünü önler.
  • Değiştirme zinciri boyutunu, uygulama pencere yüzeyinin ekranın doğal yönünden kimlik çözünürlüğüne sabit tutun.
  • Takas zinciri çözünürlüğü/uzantısı artık ekranın yönüyle güncellenmediğinden, cihaz yönünü hesaba katmak için klip alanında 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: Minimum Android ön rotasyonu