Artikel ini menjelaskan cara menangani rotasi perangkat secara efisien di aplikasi Vulkan dengan menerapkan pra-rotasi.
Dengan Vulkan, Anda dapat menentukan informasi yang jauh lebih banyak tentang status rendering dibandingkan dengan yang bisa Anda lakukan pada OpenGL. Dengan Vulkan, Anda harus secara eksplisit mengimplementasikan hal-hal yang ditangani oleh {i>driver<i} di OpenGL, seperti orientasi perangkat dan hubungannya dengan orientasi platform render. Android dapat menggunakan tiga cara menangani rekonsiliasi permukaan render perangkat dengan orientasi perangkat:
- OS Android dapat menggunakan Display Processing Unit (DPU) perangkat, yang dapat menangani rotasi permukaan secara efisien di perangkat keras. Tersedia pada hanya perangkat yang didukung.
- OS Android dapat menangani rotasi permukaan dengan menambahkan compositor pass. Ini akan memiliki biaya kinerja tergantung pada bagaimana compositor harus menangani memutar gambar output.
- Aplikasi itu sendiri dapat menangani rotasi permukaan dengan merender gambar yang diputar ke platform render yang sesuai dengan orientasi saat ini layar.
Manakah dari metode ini yang harus Anda gunakan?
Saat ini, tidak ada cara bagi aplikasi untuk mengetahui apakah rotasi platform yang ditangani di luar aplikasi secara gratis. Meskipun ada DPU yang membantu Anda menangani masalah ini, kemungkinan masih ada penalti performa terukur yang harus dibayarkan. Jika aplikasi Anda terikat dengan CPU, ini akan menjadi masalah daya karena peningkatan penggunaan GPU oleh Android Compositor, yang biasanya berjalan pada frekuensi yang ditingkatkan. Jika aplikasi Anda terikat dengan GPU, Android Compositor juga dapat menghentikan pekerjaan GPU aplikasi Anda sehingga menyebabkan hilangnya performa.
Saat menjalankan judul pengiriman di Pixel 4XL, kita telah melihat SurfaceFlinger (tugas dengan prioritas lebih tinggi yang menggerakkan Android Penyusun):
Mengantisipasi pekerjaan aplikasi secara rutin, menyebabkan 1-3 md hingga waktu render frame, dan
Meningkatkan tekanan pada GPU memori verteks/tekstur, karena Compositor harus membaca seluruh {i>framebuffer<i} untuk melakukan pekerjaan komposisinya.
Orientasi penanganan hampir secara total menghentikan preemption GPU oleh SurfaceFlinger, sementara frekuensi GPU turun 40% karena frekuensi yang telah dikuatkan dan digunakan oleh Android Compositor tidak diperlukan lagi.
Untuk memastikan rotasi permukaan ditangani dengan benar dengan overhead yang seminimal mungkin memungkinkan, seperti yang terlihat dalam kasus sebelumnya, Anda harus mengimplementasikan metode 3. Tindakan ini dikenal sebagai pra-rotasi. Kode ini memberi tahu Android OS bahwa aplikasi Anda akan menangani rotasi permukaan. Anda dapat melakukannya dengan meneruskan flag transformasi platform yang menentukan orientasi selama pembuatan swapchain. Tindakan ini menghentikan Android Compositor agar tidak melakukan rotasi sendiri.
Mengetahui cara menyetel tanda transformasi permukaan penting untuk setiap Vulkan aplikasi. Aplikasi cenderung mendukung beberapa orientasi atau mendukung satu orientasi dengan permukaan render berada di orientasi terhadap apa yang dianggap perangkat sebagai orientasi identitasnya. Misalnya, aplikasi khusus lanskap pada ponsel identitas potret, atau aplikasi khusus potret pada tablet identitas lanskap.
Mengubah AndroidManifest.xml
Untuk menangani rotasi perangkat di aplikasi Anda, mulailah dengan mengubah file
AndroidManifest.xml
aplikasi untuk memberi tahu Android bahwa aplikasi Anda akan
menangani perubahan orientasi dan ukuran layar. Hal ini akan mencegah Android menghancurkan dan membuat ulang
Activity
Android dan memanggil fungsi
onDestroy()
pada platform jendela yang ada
saat terjadi perubahan orientasi. Hal ini dilakukan dengan
menambahkan atribut orientation
(untuk mendukung API level 13) dan screenSize
ke bagian
configChanges
aktivitas:
<activity android:name="android.app.NativeActivity"
android:configChanges="orientation|screenSize">
Jika aplikasi Anda memperbaiki orientasi layarnya menggunakan screenOrientation
, Anda tidak perlu melakukan ini. Selain itu, jika aplikasi Anda menggunakan
maka orientasi hanya perlu
menyiapkan {i>swapchain <i}sekali
memulai/melanjutkan aplikasi.
Mendapatkan Resolusi Layar Identitas dan Parameter Kamera
Berikutnya, deteksi resolusi layar perangkat
yang terkait dengan nilai VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR
. Resolusi
ini berkaitan dengan orientasi identitas perangkat sehingga resolusi
tersebut harus selalu disetel ke swapchain. Paling sering
cara yang dapat diandalkan untuk mendapatkannya
adalah dengan melakukan panggilan ke
vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
saat aplikasi dimulai, dan
menyimpan luasan yang ditampilkan. Tukar lebar dan tinggi berdasarkan
currentTransform
yang juga ditampilkan untuk memastikan bahwa Anda menyimpan
resolusi layar identitas:
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 adalah struktur VkExtent2D
yang digunakan untuk menyimpan
resolusi identitas platform jendela aplikasi dalam orientasi layar yang alami
Mendeteksi Perubahan Orientasi Perangkat (Android 10+)
Cara terbaik untuk mendeteksi perubahan orientasi dalam aplikasi Anda adalah
dengan memverifikasi apakah fungsi vkQueuePresentKHR()
menampilkan
VK_SUBOPTIMAL_KHR
atau tidak. Contoh:
auto res = vkQueuePresentKHR(queue_, &present_info);
if (res == VK_SUBOPTIMAL_KHR){
orientationChanged = true;
}
Catatan: Solusi ini hanya berfungsi pada perangkat yang menjalankan
Android 10 dan yang lebih baru. Versi Android ini menampilkan
VK_SUBOPTIMAL_KHR
dari vkQueuePresentKHR()
. Kita menyimpan hasil
periksa di orientationChanged
, boolean
yang dapat diakses dari
dalam loop rendering utama.
Mendeteksi Perubahan Orientasi Perangkat (Pra-Android 10)
Untuk perangkat yang menjalankan Android 10 atau yang lebih lama,
penerapan diperlukan, karena VK_SUBOPTIMAL_KHR
tidak didukung.
Menggunakan Polling
Pada perangkat pra-Android 10, Anda dapat memeriksa transformasi perangkat saat ini setiap
frame pollingInterval
, dengan pollingInterval
adalah perincian yang diputuskan
oleh {i>programmer<i}. Cara melakukannya dengan memanggil
vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
, lalu membandingkan kolom currentTransform
yang ditampilkan dengan transformasi platform
yang saat ini disimpan (dalam contoh kode ini disimpan di pretransformFlag
-nya).
currFrameCount++;
if (currFrameCount >= pollInterval){
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
if (pretransformFlag != capabilities.currentTransform) {
window_resized = true;
}
currFrameCount = 0;
}
Pada Pixel 4 yang menjalankan -Android 10, polling
vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
memerlukan waktu sekitar 120-250 md, dan pada
Pixel 1XL yang menjalankan Android 8, polling memerlukan waktu sekitar 110-350 md.
Menggunakan Callback
Opsi kedua untuk perangkat yang berjalan di Android 10 ke bawah adalah mendaftarkan callback
onNativeWindowResized()
untuk memanggil fungsi yang menetapkan
flag orientationChanged
yang memberi tahu aplikasi ke orientasi jika telah
terjadi perubahan:
void android_main(struct android_app *app) {
...
app->activity->callbacks->onNativeWindowResized = ResizeCallback;
}
Dengan ResizeCallback didefinisikan sebagai:
void ResizeCallback(ANativeActivity *activity, ANativeWindow *window){
orientationChanged = true;
}
Masalah dengan solusi ini adalah onNativeWindowResized()
hanya mendapatkan
membutuhkan perubahan orientasi 90 derajat, seperti beralih dari lanskap ke potret atau
sebaliknya. Perubahan orientasi lainnya tidak akan memicu pembuatan ulang swapchain.
Misalnya, perubahan dari lanskap ke lanskap terbalik akan
tidak memicunya, membutuhkan compositor Android untuk melakukan membalik untuk
aplikasi.
Menangani Perubahan Orientasi
Untuk menangani perubahan orientasi, panggil rutinitas perubahan orientasi di bagian
atas loop perenderan utama saat variabel orientationChanged
disetel ke benar. Contoh:
bool VulkanDrawFrame() {
if (orientationChanged) {
OnOrientationChange();
}
Anda melakukan semua pekerjaan yang diperlukan untuk membuat ulang swapchain di dalam
fungsi OnOrientationChange()
. Ini berarti Anda:
Hancurkan instance
Framebuffer
danImageView
yang ada,Buat ulang swapchain saat menghancurkan swapchain lama (yang akan dibahas selanjutnya), dan
Buat ulang Framebuffer dengan DisplayImages swapchain baru. Catatan: Gambar lampiran (misalnya, gambar kedalaman/stensil) biasanya tidak perlu dibuat ulang karena didasarkan pada resolusi identitas gambar swapchain yang telah diputar sebelumnya.
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;
}
Dan di akhir fungsi, Anda akan mereset flag orientationChanged
ke salah
untuk menunjukkan bahwa Anda telah menangani perubahan orientasi.
Pembuatan Ulang Swapchain
Di bagian sebelumnya, kita harus membuat ulang swapchain. Langkah pertama untuk melakukannya adalah memperoleh karakteristik baru dari platform rendering:
void createSwapChain(VkSwapchainKHR oldSwapchain) {
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
pretransformFlag = capabilities.currentTransform;
Dengan struktur VkSurfaceCapabilities
yang telah diisi dengan informasi baru, Anda
sekarang dapat memeriksa kolom currentTransform
untuk melihat apakah perubahan
orientasi telah terjadi atau belum. Anda akan menyimpannya nanti di kolom pretransformFlag
karena Anda akan memerlukannya untuk nanti saat melakukan penyesuaian pada
matriks MVP.
Untuk melakukannya, tentukan atribut
berikut di struktur VkSwapchainCreateInfo
:
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);
}
Kolom imageExtent
akan diisi dengan extent displaySizeIdentity
yang Anda simpan saat aplikasi dimulai. Kolom preTransform
akan diisi
dengan variabel pretransformFlag
(yang ditetapkan ke kolom
currentTransform pada surfaceCapabilities
). Anda juga menyetel kolom oldSwapchain
ke
swapchain yang akan dihancurkan.
Penyesuaian Matriks MVP
Hal terakhir yang harus Anda lakukan adalah menerapkan pra-transformasi dengan menerapkan matriks rotasi ke matriks MVP Anda. Pada dasarnya yang dilakukan ialah menerapkan rotasi dalam ruang klip sehingga gambar yang dihasilkan diputar ke orientasi perangkat saat ini. Kemudian Anda dapat meneruskan matriks MVP yang telah diperbarui ini ke dalam shader vertex dan menggunakannya seperti biasa tanpa perlu mengubah shader.
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;
Pertimbangan - Area Pandang dan Tampilan Layar yang Tidak Penuh Layar
Jika aplikasi Anda menggunakan region scissor/viewport layar tidak penuh, aplikasi tersebut harus diperbarui sesuai dengan orientasi perangkat. Hal ini mengharuskan Anda mengaktifkan opsi Viewport dan Scissor dinamis selama pembuatan pipeline Vulkan:
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);
Komputasi aktual area extent viewport selama perekaman buffering perintah terlihat seperti ini:
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);
Variabel x
dan y
menentukan koordinat sudut kiri atas
viewport sedangkan w
dan h
masing-masing menentukan lebar dan tinggi viewport.
Komputasi yang sama juga dapat digunakan
untuk mengatur tes gunting, dan disertakan
di sini untuk kelengkapan:
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);
Pertimbangan - Derivatif Shader Fragmen
Jika aplikasi Anda menggunakan komputasi turunan seperti dFdx
dan dFdy
,
transformasi tambahan mungkin diperlukan untuk memperhitungkan sistem koordinat
yang dirotasi karena komputasi ini dieksekusi di ruang piksel. Aplikasi
harus meneruskan beberapa indikasi preTransform ke dalam shader fragmen (seperti
integer yang mewakili orientasi perangkat saat ini) dan menggunakannya untuk memetakan
komputasi turunan dengan benar:
- Untuk frame yang telah diputar sebelumnya sebesar 90 derajat
- dFdx harus dipetakan ke dFdy
- dFdy harus dipetakan ke -dFdx
- Untuk frame yang telah diputar sebelumnya sebesar 270 derajat
- dFdx harus dipetakan ke -dFdy
- dFdy harus dipetakan ke dFdx
- Untuk frame yang telah diputar sebelumnya sebesar 180 derajat
- dFdx harus dipetakan ke -dFdx
- dFdy harus dipetakan ke -dFdy
Kesimpulan
Agar aplikasi dapat memaksimalkan Vulkan di Android, Anda harus menerapkan pra-rotasi. Hal yang paling penting dari artikel ini adalah:
- Pastikan bahwa selama pembuatan atau pembuatan ulang swapchain, flag pra-transformasi diatur agar sesuai dengan penanda yang dikembalikan oleh sistem operasi Android. Hal ini akan menghindari overhead compositor.
- Pertahankan ukuran swapchain tetap sesuai dengan resolusi identitas jendela aplikasi muncul di orientasi alami tampilan.
- Putar matriks MVP dalam ruang klip untuk memperhitungkan orientasi perangkat, karena resolusi/extent swapchain tidak lagi diperbarui dengan orientasi layar.
- Perbarui persegi panjang area pandang dan gunting sesuai kebutuhan aplikasi Anda.
Contoh Aplikasi: Pra-rotasi minimal Android