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 cihaz yönü ve oluşturma yüzeyi yönü gibi öğeleri açıkça uygulamanız gerekir. Android'in cihazın oluşturma yüzeyini cihaz yönüyle uyumlu hale getirmenin üç yolu vardır:
- Android OS, cihazın ekran işleme birimini (DPU) kullanabilir. Bu birim, donanımda yüzey dönüşümünü verimli bir şekilde yönetebilir. Yalnızca desteklenen cihazlarda kullanılabilir.
- 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.
- 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, uygulamanın dışında yönetilen yüzey döndürmenin ü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 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ö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 işlem, 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ö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
öğ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ırmanız gerekir.
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 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();
}
Değişim zincirini yeniden oluşturmak için gereken tüm işlemleri OnOrientationChange()
işlevinde yaparsınız. Bu durumda:
Mevcut tüm
Framebuffer
veImageView
örneklerini yok edin.Eski takas zincirini yok ederken takas zincirini yeniden oluşturun (sonraki bölümde ele alınacaktır) ve
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 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ı, imha edilecek takas zincirine 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 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