Bu makalede, önceden rotasyon uygulayarak Vulkan uygulamanızda cihaz rotasyonunu nasıl verimli bir şekilde yönetebileceğ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 oluşturma yüzeyi yönü ile ilişkisi) açıkça uygulamanız gerekir. Android, cihazın oluşturma yüzeyini cihaz yönüyle hizalamanın üç yolu vardır:
- 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.
- Android OS, bir karıştırıcı geçişi ekleyerek yüzey rotasyonunu yönetebilir. Bu işlemin, birleştiricinin, çıkış görüntüsünü döndürmeyle nasıl başa çıktığına bağlı olarak bir performans maliyeti olacaktır.
- 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 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 genellikle artırılmış bir frekansta çalışan GPU kullanımının artması nedeniyle bu durum bir güç sorunu haline gelir. 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
Bileşik'in kompozisyon işini yapmak için çerçeve arabelleğinin tamamını okuması gerektiğinden, GPU'nun tepe/doku belleği üzerinde daha fazla basınç uygular.
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, 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ü destekler veya oluşturma yüzeyinin, cihazın kimlik yönüne göre 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'ü 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ü sakladığınızdan emin olmak için genişliği ve yüksekliği de döndürülen currentTransform
öğesine 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 söz konusu kimlik çözünürlüğünü 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ı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()
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 ö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ü 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 Yönetme
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 ise yön değişikliğini uyguladığınızı göstermek için orientationChanged
işaretini yanlış değerine sıfırlarsınız.
Değişim zinciri yeniden 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 almaktır:
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ı, surfaceCapabilities
değişkeninin currentTransform alanına ayarlanır pretransformFlag
değişkeniyle doldurulur. oldSwapchain
alanını da kaldırılacak değişim 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 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:
- 90 derece önceden döndürülmüş 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.
- 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 yararlanabilmesi için ön döndürme özelliğini uygulamanız gerekir. Bu makaleden en önemli çıkarımlar şunlardır:
- Takas zinciri oluşturma veya yeniden oluşturma işlemi sırasında, önceden 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, derleyicinin ek yükünü önleyebilirsiniz.
- Takas zinciri boyutunu, uygulamanın pencere yüzeyinin kimlik çözünürlüğüne, ekranın doğal yönünde sabit tutun.
- Takas zinciri çözünürlüğü/uzunluğu artık ekranın yönüne göre güncellenmediğinden, cihazların yönünü hesaba katmak için klip alanındaki MVP matrisini 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