Bu makalede, ön döndürme uygulayarak Vulkan uygulamanızda cihaz döndürmeyi nasıl verimli bir şekilde ele alacağınız açıklanmaktadır.
Vulkan ile, OpenGL'de belirtebileceğinizden çok daha fazla bilgi belirtebilirsiniz. Vulkan'da, OpenGL'de sürücü tarafından işlenen cihaz yönü ve render yüzeyi yönü ile ilişkisi gibi öğeleri açıkça uygulamanız gerekir. Android, cihazın oluşturma yüzeyini cihaz yönüyle eşleştirme işlemini üç şekilde gerçekleştirebilir:
- Android işletim sistemi, cihazın ekran işleme birimini (DPU) kullanabilir. Bu birim, yüzey döndürme işlemini donanımda verimli bir şekilde gerçekleştirebilir. Yalnızca desteklenen cihazlarda kullanılabilir.
- Android işletim sistemi, bir birleştirme geçişi ekleyerek yüzey döndürmeyi işleyebilir. Bu, birleştiricinin çıkış görüntüsünü döndürmekle nasıl başa çıkması gerektiğine bağlı olarak performans maliyetine neden olur.
- Uygulama, ekranın mevcut yönüne uygun bir oluşturma yüzeyine döndürülmüş bir resim oluşturarak yüzey döndürme işlemini kendisi gerçekleştirebilir.
Bu yöntemlerden hangisini kullanmalısınız?
Şu anda, bir uygulamanın, uygulama dışında işlenen yüzey döndürmenin ücretsiz olup olmayacağını bilmesinin bir yolu yoktur. Bu işlemi sizin için yapacak bir DPU olsa bile, muhtemelen ölçülebilir bir performans cezası ödemeniz gerekir. Uygulamanız CPU ile sınırlıysa bu durum, genellikle artırılmış bir frekansta çalışan Android Compositor'ın GPU kullanımının artması nedeniyle güç sorunu haline gelir. Uygulamanız GPU'ya bağlıysa Android Compositor, uygulamanızın GPU çalışmasını da önceliklendirebilir ve bu da ek performans kaybına neden olur.
Pixel 4XL'de gönderim başlıkları çalıştırılırken SurfaceFlinger'ın (Android Compositor'ı çalıştıran daha yüksek öncelikli görev) aşağıdaki gibi davrandığı görülmüştür:
Uygulamanın çalışmasını düzenli olarak kesintiye uğratarak kare sürelerinde 1-3 ms'lik artışlara neden olur ve
Compositor, kompozisyon çalışmasını yapmak için tüm çerçeve arabelleğini okumak zorunda olduğundan GPU'nun köşe/doku belleği üzerinde daha fazla baskı oluşturur.
Yönlendirmenin düzgün şekilde yapılması, SurfaceFlinger'ın 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ürmelerinin mümkün olduğunca az ek yükle düzgün şekilde işlenmesini sağlamak için (önceki örnekte gösterildiği gibi) 3. yöntemi uygulamanız gerekir. Bu durum önceden döndürme olarak bilinir. Bu, Android işletim sistemine uygulamanızın yüzey döndürme işlemini yaptığını bildirir. Bunu, swapchain oluşturma sırasında yönü belirten yüzey dönüştürme işaretlerini ileterek yapabilirsiniz. Bu işlem, Android Compositor'ın rotasyonu kendiliğinden 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 fazla yönü destekler veya tek bir yönü destekler. Bu durumda, oluşturma yüzeyi cihazın kimlik yönü olarak kabul ettiği yönden farklı bir yöndedir. Örneğin, dikey kimlikli bir telefonda yalnızca yatay yönlü bir uygulama veya yatay kimlikli bir tablette yalnızca dikey yönlü bir uygulama.
AndroidManifest.xml dosyasını değiştirme
Uygulamanızda cihaz döndürmeyi işlemek için öncelikle 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, yön değişikliği olduğunda Android'in Android Activity
öğesini yok edip 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üzeyi <13'ü desteklemek için) ve screenSize
özelliklerinin eklenmesiyle yapılır:
<activity android:name="android.app.NativeActivity"
android:configChanges="orientation|screenSize">
Uygulamanız ekran yönünü screenOrientation
özelliğiyle düzeltiyorsa bu işlemi yapmanız gerekmez. Ayrıca, uygulamanız sabit bir yön kullanıyorsa uygulama başlatıldığında/devam ettirildiğinde yalnızca bir kez takas zinciri oluşturması 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ü tespit edin. Bu çözünürlük, cihazın kimlik yönüyle ilişkilidir ve bu nedenle, takas zincirinin her zaman ayarlanması gereken çözünürlüktür. Bu değeri almanın en güvenilir yolu, uygulama başlatılırken vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
numarasına bir ç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ş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 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 tespit etmenin en güvenilir yolu, vkQueuePresentKHR()
işlevinin VK_SUBOPTIMAL_KHR
değerini 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. Bu Android sürümleri, vkQueuePresentKHR()
'den VK_SUBOPTIMAL_KHR
döndürür. Bu kontrolün sonucunu, uygulamaların ana oluşturma döngüsünden erişilebilen bir orientationChanged
olan boolean
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.
Yoklamayı kullanma
Android 10'dan önceki cihazlarda, mevcut cihaz dönüşümünü her pollingInterval
karede bir yoklayabilirsiniz. Burada pollingInterval
, programcı tarafından belirlenen bir ayrıntı düzeyidir. 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 Pixel 4'te yoklama vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
0,120-0,250 ms sürerken Android 8 çalıştıran Pixel 1 XL'de yoklama 0,110-0,350 ms sürdü.
Geri aramaları kullanma
Android 10'dan eski 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 kaydetmektir. Bu işaret, uygulamaya yön değişikliği olduğunu 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ümdeki sorun, onNativeWindowResized()
işlevinin yalnızca 90 derecelik yön değişiklikleri (ör. yataydan dikeye geçiş veya bunun tersi) için çağrılmasıdır. Diğer yön değişiklikleri, swapchain'in yeniden oluşturulmasını tetiklemez.
Örneğin, yataydan ters yataya geçiş bunu tetiklemez ve Android birleştiricinin uygulamanız için çevirme işlemini yapması gerekir.
Yön Değişikliğini Ele Alma
Yön 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 en üstünde yön değişikliği yordamını çağırın. Örneğin:
bool VulkanDrawFrame() {
if (orientationChanged) {
OnOrientationChange();
}
OnOrientationChange()
işlevi içinde takas zincirini yeniden oluşturmak için gereken tüm işleri yaparsınız. Bu durumda:
Framebuffer
veImageView
'nin mevcut tüm örneklerini yok edin.Eski değişim zincirini yok ederken değişim zincirini yeniden oluşturun (bu konu bir sonraki bölümde ele alınacaktır) ve
Yeni swapchain'in DisplayImage'leriyle Framebuffer'ları yeniden oluşturun. Not: Ek resimlerin (ör. derinlik/şablon resimleri) genellikle yeniden oluşturulması gerekmez. Bu resimler, önceden döndürülmüş takas zinciri resimlerinin kimlik çözümlemesine dayanır.
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 işlediğinizi göstermek için orientationChanged
işaretini false olarak sıfırlarsınız.
Swapchain Recreation
Ö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 almayı içerir:
void createSwapChain(VkSwapchainKHR oldSwapchain) {
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
pretransformFlag = capabilities.currentTransform;
VkSurfaceCapabilities
yapısı yeni bilgilerle doldurulduktan sonra currentTransform
alanını kontrol ederek yön değişikliği olup olmadığını kontrol edebilirsiniz. MVP matrisinde daha sonra düzenlemeler yaparken ihtiyacınız olacağı için bu bilgiyi pretransformFlag
alanında saklayacaksınız.
Bunu yapmak 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 (surfaceCapabilities
öğesinin currentTransform alanına ayarlanır) doldurulur. Ayrıca oldSwapchain
alanını, yok edilecek swapchain'e ayarlarsınız.
MVP Matrix Adjustment
Yapmanız gereken son şey, MVP matrisinize bir döndürme matrisi uygulayarak ön dönüşümü uygulamaktır. Bu işlem, sonuçta elde edilen görüntünün mevcut cihaz yönüne döndürülmesi için klip alanında döndürme uygular. Ardından, bu güncellenmiş MVP matrisini köşe gölgelendiricinize aktarabilir ve gölgelendiricilerinizi değiştirmenize gerek 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;
Değerlendirme - Tam ekran olmayan görünüm alanı ve kırpma
Uygulamanız tam ekran olmayan bir görünüm penceresi/kırpma bölgesi kullanıyorsa bunların cihazın yönüne göre güncellenmesi gerekir. Bunun için Vulkan'ın işlem hattı oluşturma işlemi sırasında dinamik görünüm alanı ve kırpma 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ı tanımlarken w
ve h
değişkenleri 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 - Parça Shader Türevleri
Uygulamanız dFdx
ve dFdy
gibi türetilmiş hesaplamalar kullanıyorsa bu hesaplamalar piksel alanında yürütüldüğünden, döndürülmüş koordinat sistemini hesaba katmak için ek dönüşümler gerekebilir. Bu, uygulamanın ön dönüştürme ile ilgili bazı bilgileri parça gölgelendiricisine (ör. mevcut cihaz yönünü temsil eden bir tam sayı) iletmesini ve türev hesaplamalarını düzgün şekilde eşlemek için bu bilgileri kullanmasını gerektirir:
- 90 derece önceden 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üş bir çerçeve için
- dFdx, -dFdy ile eşlenmelidir.
- dFdy, dFdx ile eşlenmelidir.
- 180 derece önceden döndürülmüş 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 uygulanması gerekir. Bu makaleden çıkarılacak en önemli sonuçlar şunlardır:
- Swapchain oluşturma veya yeniden oluşturma sırasında, ö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 işlem, birleştirme programının ek yükünü önler.
- Takas zinciri boyutunu, uygulamanın pencere yüzeyinin ekranın doğal yönündeki kimlik çözünürlüğüne sabit tutun.
- Takas zinciri çözünürlüğü/boyutu artık ekranın yönüne göre güncellenmediğinden, cihazın yönünü hesaba katmak için MVP matrisini kırpma alanında döndürün.
- Görünüm alanı ve makas dikdörtgenlerini uygulamanızın ihtiyaçlarına göre güncelleyin.
Örnek uygulama: Minimal Android ön döndürme