Menangani orientasi perangkat dengan pra-rotasi Vulkan

Artikel ini menjelaskan cara menangani rotasi perangkat secara efisien di aplikasi Vulkan dengan menerapkan pra-rotasi.

Dengan Vulkan, Anda dapat menentukan informasi status rendering yang jauh lebih lengkap daripada yang dapat Anda lakukan dengan OpenGL. Dengan Vulkan, Anda harus menerapkan secara eksplisit hal-hal yang ditangani oleh driver di OpenGL, seperti orientasi perangkat dan hubungannya dengan orientasi platform render. Ada tiga cara yang dapat dilakukan Android untuk menangani rekonsiliasi platform render pada perangkat dengan orientasi perangkat:

  1. Android OS dapat menggunakan Unit Pemrosesan Display (DPU) perangkat, yang dapat menangani rotasi platform di hardware secara efisien. Hanya tersedia di perangkat yang didukung.
  2. Android OS dapat menangani rotasi platform dengan menambahkan file compositor. Hal ini akan memakan resource performa, bergantung pada cara compositor menangani rotasi gambar output tersebut.
  3. Aplikasi itu sendiri dapat menangani rotasi platform dengan merender gambar yang dirotasi ke platform render yang cocok dengan orientasi layar saat ini.

Metode mana yang harus Anda gunakan?

Saat ini, aplikasi tidak memiliki cara untuk mengetahui apakah ada beban resource untuk rotasi platform yang ditangani di luar aplikasi. Meskipun ada DPU yang membantu Anda menangani masalah ini, kemungkinan masih ada penalti performa terukur yang harus diatasi. Jika aplikasi Anda mengandalkan CPU, hal ini akan menyebabkan masalah daya karena peningkatan penggunaan GPU oleh Android Compositor, yang biasanya berjalan pada frekuensi yang ditingkatkan. Jika aplikasi Anda mengandalkan GPU, Android Compositor juga dapat menghentikan pekerjaan GPU aplikasi Anda sehingga menyebabkan penurunan performa.

Saat menjalankan judul pengiriman di Pixel 4XL, kami telah melihat bahwa SurfaceFlinger (tugas prioritas yang lebih tinggi yang mendorong Android Compositor):

  • Secara rutin menghentikan sementara pekerjaan aplikasi, sehingga menyebabkan hit 1-3 md pada waktu render frame, dan

  • Memberikan tekanan yang lebih besar pada memori vertex/tekstur GPU, karena Compositor harus membaca seluruh framebuffer untuk melakukan pekerjaan komposisinya.

Proses penanganan orientasi hampir menghentikan sepenuhnya preemption GPU oleh SurfaceFlinger, sementara frekuensi GPU turun 40% karena peningkatan frekuensi yang digunakan oleh Android Compositor tidak diperlukan lagi.

Untuk memastikan rotasi platform ditangani dengan benar menggunakan overhead seminimal mungkin, seperti yang terlihat pada kasus sebelumnya, Anda harus menerapkan metode 3. Hal ini dikenal sebagai pra-rotasi. Kode ini memberi tahu Android OS bahwa aplikasi Anda menangani rotasi platform. Anda dapat melakukannya dengan meneruskan tanda transformasi platform yang menentukan orientasi selama pembuatan swapchain. Tindakan ini akan menghentikan Android Compositor agar tidak melakukan rotasi itu sendiri.

Sangat penting untuk mengetahui cara menyetel tanda transformasi platform untuk setiap aplikasi Vulkan. Aplikasi cenderung mendukung beberapa orientasi atau mendukung satu orientasi jika platform render berada dalam orientasi lain yang dipertimbangkan oleh objek perangkat sebagai orientasi bawaannya. Misalnya, aplikasi khusus lanskap pada ponsel berorientasi bawaan potret, atau aplikasi khusus potret pada tablet berorientasi bawaan 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 level API 13) dan screenSize ke bagian configChanges aktivitas:

<activity android:name="android.app.NativeActivity"
          android:configChanges="orientation|screenSize">

Hal ini tidak perlu dilakukan jika aplikasi Anda memperbaiki orientasi layarnya menggunakan atribut screenOrientation. Selain itu, jika aplikasi Anda menggunakan orientasi tetap, Anda hanya perlu menyiapkan swapchain sekali saat memulai/melanjutkan aplikasi.

Mendapatkan Resolusi Layar dan Parameter Kamera Bawaan

Selanjutnya, deteksi resolusi layar perangkat yang terkait dengan nilai VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR. Resolusi ini berkaitan dengan orientasi bawaan perangkat sehingga harus selalu disetel ke swapchain. Cara terbaik untuk mendapatkan ini adalah melakukan panggilan ke vkGetPhysicalDeviceSurfaceCapabilitiesKHR() saat memulai aplikasi, dan menyimpan extent yang ditampilkan. Tukar lebar dan tinggi berdasarkan currentTransform yang juga ditampilkan untuk memastikan Anda menyimpan resolusi layar bawaan:

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 bawaan 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 di perangkat yang menjalankan Android 10 dan yang lebih baru. Versi Android ini menampilkan VK_SUBOPTIMAL_KHR dari vkQueuePresentKHR(). Kami menyimpan hasil pemeriksaan ini di orientationChanged, boolean yang dapat diakses dari loop rendering utama aplikasi.

Mendeteksi Perubahan Orientasi Perangkat (Pra-Android 10)

Untuk perangkat yang menjalankan Android 10 atau yang lebih lama, diperlukan implementasi yang berbeda karena VK_SUBOPTIMAL_KHR tidak didukung.

Menggunakan Polling

Pada perangkat pra-Android 10, Anda dapat melakukan polling untuk mengubah perangkat saat ini setiap pollingInterval frame, dengan pollingInterval yang merupakan detail yang ditentukan oleh pemrogram. 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() agar memanggil fungsi yang menetapkan tanda 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 dipanggil untuk 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 tidak akan memicunya, sehingga compositor Android harus membalikkan aplikasi Anda.

Menangani Perubahan Orientasi

Untuk menangani perubahan orientasi, panggil rutinitas perubahan orientasi di bagian atas loop rendering utama saat variabel orientationChanged disetel ke benar. Contoh:

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

Anda melakukan semua pekerjaan yang diperlukan untuk membuat ulang swapchain dalam fungsi OnOrientationChange(). Artinya, Anda:

  1. Menghancurkan instance Framebuffer dan ImageView yang ada,

  2. Membuat ulang swapchain saat menghancurkan swapchain lama (yang akan dibahas berikutnya), dan

  3. Membuat ulang Framebuffer dengan DisplayImage swapchain baru. Catatan: Gambar lampiran (misalnya, gambar kedalaman/stensil) biasanya tidak perlu dibuat ulang karena didasarkan pada resolusi identitas gambar swapchain yang telah dirotasi 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 tanda 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 diperlukan nanti saat Anda 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 menyiapkan pra-transformasi dengan menerapkan matriks rotasi ke matriks MVP. Pada dasarnya, yang dilakukan adalah 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 wilayah scissor/area tampilan 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 extent area tampilan 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 area tampilan sedangkan w dan h masing-masing menentukan lebar dan tinggi area tampilan. Komputasi yang sama juga dapat digunakan untuk menyetel pengujian scissor, dan disertakan di sini untuk tujuan 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, tanda pra-transformasi telah ditetapkan agar cocok dengan tanda yang ditampilkan oleh sistem operasi Android. Tindakan ini akan menghindari overhead pada compositor.
  • Ukuran swapchain harus tetap sesuai dengan resolusi bawaan platform jendela aplikasi dalam orientasi alami layar.
  • Merotasi matriks MVP di ruang klip untuk memperhitungkan orientasi perangkat, karena resolusi/extent swapchain tidak lagi diperbarui dengan orientasi layar.
  • Perbarui kotak area tampilan dan scissor sesuai kebutuhan menggunakan aplikasi Anda.

Contoh Aplikasi: Pra-rotasi minimal Android