טיפול בכיוון המכשיר עם סיבוב מראש של Vulkan

במאמר הזה מוסבר איך לטפל ביעילות ברוטציית מכשירים באפליקציית Vulkan על ידי הטמעה של רוטציה מראש.

באמצעות Vulkan אפשר לציין הרבה יותר מידע על מצב עיבוד מאשר עם OpenGL. ב-Vulkan, עליך להטמיע באופן מפורש דברים שהנהג או הנהגת מטפלים בהם OpenGL, כמו כיוון המכשיר והקשר שלו אל לעבד את הכיוון של פני השטח. יש שלוש דרכים שבהן Android יכול נקודת אחיזה להתאמת משטח העיבוד של המכשיר לכיוון המכשיר:

  1. מערכת ההפעלה Android יכולה להשתמש ביחידת עיבוד המסך (DPU) של המכשיר, שיכול לטפל ביעילות בסיבוב של משטחים בחומרה. זמין אצל מכשירים נתמכים בלבד.
  2. מערכת ההפעלה Android יכולה לטפל בסיבוב של משטחים על ידי הוספת אישור קומפוזבילי. הזה תהיה עלות ביצועים בהתאם לאופן שבו המחבר צריך להתמודד סיבוב של תמונת הפלט.
  3. האפליקציה עצמה יכולה להתמודד עם סיבוב פני השטח על ידי עיבוד של מסובבת את התמונה למשטח עיבוד שתואם לכיוון הנוכחי של המסך.

באילו מהשיטות הבאות כדאי לך להשתמש?

בשלב זה, אין לאפליקציה דרך לדעת אם סיבוב פני השטח יטופלו מחוץ לאפליקציה ללא תשלום. גם אם יש DPU שצריך לקחת לטפל בזה בשבילך, עדיין ייתכן שקיים עונש ניתן למדידה לשלם. אם האפליקציה קשורה למעבד (CPU), זו בעיית חשמל שנגרמת את השימוש המוגבר ב-GPU על ידי הרכיב Android Compositor, שפועל בדרך כלל הגברת התדירות. אם האפליקציה קשורה ל-GPU, הרכיב Android Compositor, יכול גם למנוע את עבודת ה-GPU של האפליקציה, וכך לשפר את הביצועים .

כשמשלוחים של טלפון Pixel 4XL, SurfaceFlinger (המשימה בעלת העדיפות הגבוהה יותר שפועלת ב-Android Compositor):

  • מכבה באופן קבוע את עבודת האפליקציה, וכתוצאה מכך נמשכת 1-3 אלפיות השנייה מלהיטים למסגרות זמן של רינדור פריים,

  • מגביר את הלחץ על ה-GPU זיכרון קודקוד/טקסטור, כי המרכיב צריך לקרוא את כל של framebuffer כדי לבצע את עבודת ההרכבה.

כיוון הטיפול עוצר באופן תקין את הקדם-GPU על ידי SurfaceFlinger כמעט. לחלוטין, בעוד שתדירות ה-GPU יורדת ב-40% כשהתדר המוגבר של אין יותר צורך ב-Android Compositor.

כדי להבטיח שסיבובי פני השטח יטופלו כראוי עם תקורה קטנה מדי כפי שאפשר לראות במקרה הקודם, מומלץ ליישם את שיטה 3. התהליך הזה נקרא רוטציה מראש. מערכת ההפעלה Android מציינת שהאפליקציה שלך שמטפל בסיבוב של פני השטח. אפשר לעשות זאת על ידי העברת דגלים לטרנספורמציה של פני השטח שמציין את הכיוון במהלך יצירת החלפה. פעולה זו מסתיימת Android Compositor מביצוע הסיבוב בעצמי.

חשוב לדעת איך להגדיר את הדגל של טרנספורמציה של פני השטח לכל Vulkan תרגום מכונה. אפליקציות בדרך כלל תומכות בכמה כיוונים או לתמוך בכיוון אחד שבו משטח העיבוד נמצא בכיוון אחר לכיוון שאליו המכשיר מחשיב את כיוון הזהות שלו. לדוגמה, אפליקציה עם תצוגה לרוחב בלבד בטלפון עם זיהוי אנכי, או בפריסה לאורך בלבד בטאבלט עם זהות העסק לרוחב.

שינוי AndroidManifest.xml

כדי לטפל בסיבוב של מכשירים באפליקציה, צריך להתחיל על ידי שינוי קובץ AndroidManifest.xml כדי להודיע ל-Android שהאפליקציה תטפל בכיוון וגודל המסך משתנה. הפעולה הזו מונעת מ-Android להשמיד וליצור מחדש את האפליקציה את Android Activity וקוראים onDestroy() על פני המסך של החלון הקיים כשמשנים את הכיוון. הפעולה מתבצעת על ידי הוספת המאפיינים orientation (כדי לתמוך ברמת ה-API <13) ו-screenSize לפעילות הקטע configChanges:

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

אם האפליקציה מתקנת את כיוון המסך באמצעות screenOrientation אין צורך לעשות זאת. כמו כן, אם האפליקציה משתמשת בקובץ ולכן יהיה צורך להגדיר את ההחלפה רק פעם אחת בהפעלה/המשך של אפליקציה.

מקבלים את הפרמטרים של רזולוציית מסך הזהות והמצלמה

בשלב הבא, מזהים את רזולוציית המסך של המכשיר שמשויך לערך של VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR. הזה משויך לכיוון הזהות של המכשיר, לכן צריך להגדיר תמיד את ה-replacementchain. במידה הרבה ביותר הדרך הבטוחה הזו היא להתקשר vkGetPhysicalDeviceSurfaceCapabilitiesKHR() בזמן הפעלת האפליקציה. מאחסנים את המידה שמוחזרת. להחליף את הרוחב והגובה בהתאם גם מוחזר currentTransform כדי לוודא שהאחסון רזולוציית מסך הזהות:

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 הוא מבנה VkExtent2D שמשמש לשמירת הזהות שצוינה הרזולוציה של משטח החלון של האפליקציה בכיוון הטבעי של המסך.

זיהוי שינויים בכיוון המכשיר (Android 10 ואילך)

הדרך הבטוחה ביותר לזהות שינוי כיוון באפליקציה היא כדי לבדוק אם הפונקציה vkQueuePresentKHR() מחזירה VK_SUBOPTIMAL_KHR. לדוגמה:

auto res = vkQueuePresentKHR(queue_, &present_info);
if (res == VK_SUBOPTIMAL_KHR){
  orientationChanged = true;
}

הערה: הפתרון הזה פועל רק במכשירים פועלים Android 10 ואילך. הגרסאות האלה של Android חוזרות VK_SUBOPTIMAL_KHR מ-vkQueuePresentKHR(). אנחנו מאחסנים את התוצאה עושה צ'ק-אין ב-orientationChanged, boolean שאפשר לגשת אליו אפליקציות לולאת רינדור ראשית.

זיהוי שינויים בכיוון המכשיר (לפני Android 10)

במכשירים עם Android מגרסה 10 ומעלה, נדרש, כי אין תמיכה ב-VK_SUBOPTIMAL_KHR.

שימוש בסקרים

במכשירים עם מערכת הפעלה ישנה יותר מ-Android 10 אפשר לדגום את המכשיר הנוכחי בכל טרנספורמציה pollingInterval פריימים, כאשר רמת הפירוט של pollingInterval נקבעת על ידי המתכנת. כדי לעשות את זה, פשוט מתקשרים. vkGetPhysicalDeviceSurfaceCapabilitiesKHR() ולאחר מכן השוואה של הערכים שהוחזרו שדה currentTransform עם גודל המשטח שמאוחסן כרגע (בדוגמה הזו של הקוד, ששמורה ב-pretransformFlag).

currFrameCount++;
if (currFrameCount >= pollInterval){
  VkSurfaceCapabilitiesKHR capabilities;
  vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);

  if (pretransformFlag != capabilities.currentTransform) {
    window_resized = true;
  }
  currFrameCount = 0;
}

בטלפון Pixel 4 עם Android 10, עריכת סקרים vkGetPhysicalDeviceSurfaceCapabilitiesKHR() נמשכה בין 0 .120 ל-.250 אלפיות השנייה וב- מכשירי Pixel 1XL עם Android 8 .השימוש בסקרים נמשך 110-.350 אלפיות השנייה.

שימוש בקריאות חוזרות (callbacks)

האפשרות השנייה למכשירים שבהם פועלת מערכת Android פחות מ-10 היא לרשום onNativeWindowResized() כדי להפעיל פונקציה שמגדירה דגל orientationChanged, שמסמן לאפליקציה שינוי כיוון התרחש:

void android_main(struct android_app *app) {
  ...
  app->activity->callbacks->onNativeWindowResized = ResizeCallback;
}

כאשר ResizeCallback מוגדר כך:

void ResizeCallback(ANativeActivity *activity, ANativeWindow *window){
  orientationChanged = true;
}

הבעיה בפתרון הזה היא ש-onNativeWindowResized() מקבל רק לשינויי כיוון של 90 מעלות, כמו מעבר מפריסה לאורך לפריסה לאורך, להיפך. שינויים אחרים בכיוון לא יפעילו מחדש את פעולת ההחלפה. לדוגמה, שינוי מפורמט לרוחב להפך לא להפעיל אותו, דבר שמחייב את רכיב ה-Android לבצע את ההיפוך תרגום מכונה.

התמודדות עם שינוי הכיוון

כדי לטפל בשינוי הכיוון, צריך להפעיל את התרחיש של שינוי הכיוון בחלק העליון של לולאת הרינדור הראשית כאשר orientationChanged מוגדר כ-true. לדוגמה:

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

עליך לבצע את כל העבודה הדרושה כדי ליצור מחדש את ההחלפה בתוך את הפונקציה OnOrientationChange(). כלומר:

  1. להשמיד את כל המופעים הקיימים של Framebuffer ו-ImageView,

  2. יצירה מחדש של שרשרת ההחלפה תוך כדי השמדה את ההחלפה הישנה (שאליו יתואר בהמשך), וגם

  3. יצירה מחדש של מאגרי הפריים עם תמונות DisplayImages של ה-Displayimages של רשת ההחלפה החדשה. הערה: בדרך כלל תמונות קבצים מצורפים (לדוגמה, תמונות עומק/סטנסיל) לא צריך ליצור אותן מחדש, מבוססות על רזולוציית הזהות של התמונות שסובבו מראש.

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;
}

ובסוף הפונקציה מאפסים את הדגל orientationChanged ל-False כדי להראות שטיפלתם בשינוי הכיוון.

בילוי סוואפצ'יין

בקטע הקודם הזכרנו שצריך ליצור מחדש את ההחלפה. השלבים הראשונים לשם כך כוללים קבלת המאפיינים החדשים שטח העיבוד:

void createSwapChain(VkSwapchainKHR oldSwapchain) {
   VkSurfaceCapabilitiesKHR capabilities;
   vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
   pretransformFlag = capabilities.currentTransform;

כשהמבנה של VkSurfaceCapabilities מאוכלס במידע החדש, יכול עכשיו לבדוק אם התרחש שינוי בכיוון, על ידי בדיקת שדה currentTransform. המערכת תאחסן אותו למועד מאוחר יותר בpretransformFlag בשדה הזה, כי תצטרכו אותו מאוחר יותר כשמבצעים שינויים מטריצת ה-MVP.

כדי לעשות זאת, צריך לציין את המאפיינים הבאים. במבנה 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);
}

השדה imageExtent יאוכלס במידה displaySizeIdentity שבה שאחסנת בעת ההפעלה של האפליקציה. השדה preTransform יאוכלס עם המשתנה pretransformFlag (שמוגדר לשדה הטרנספורמציה הנוכחי). של surfaceCapabilities). בנוסף מגדירים את השדה oldSwapchain שיושמד.

התאמת מטריצת ה-MVP

הדבר האחרון שצריך לעשות הוא ליישם את המודל טרום-טרנספורמציה באמצעות החלת מטריצה של סיבוב על מטריצת ה-MVP. מה שלמעשה זה עושה להחיל את הסיבוב במקום של הקליפ כך שהתמונה שמתקבלת תסובב בכיוון המכשיר הנוכחי. לאחר מכן תוכלו פשוט להעביר את המטריצה המעודכנת הזו של MVP לתוכנת ההצללה (shader) של הקודקודים ולהשתמש בו כרגיל בלי שתצטרכו לשנות את תוכנות הצללה (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;

התעניינות ברכישה – מספריים ואזור תצוגה שאינם במסך מלא

אם האפליקציה משתמשת באזור תצוגה/אזור תצוגה שלא מוצג במסך מלא, צריך לעדכן את הדפדפן בהתאם לכיוון המכשיר. הזה מחייבת שתפעילו את האפשרויות 'אזור תצוגה' ו'מספריים' במהלך יצירת צינור עיבוד נתונים:

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);

החישוב בפועל של היקף אזור התצוגה במהלך הקלטת מאגר הנתונים הזמני של הפקודות נראה כך:

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 ו-y מגדירים את הקואורדינטות של הפינה השמאלית העליונה של אזור התצוגה, ואילו w ו-h מגדירים את הרוחב והגובה של אזור התצוגה בהתאמה. ניתן להשתמש באותה חישוב גם כדי להגדיר את בדיקת המספריים, והיא כלולה לשלמות:

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);

התעניינות ברכישה - נגזרות של Fragment Shader

אם האפליקציה שלך משתמשת בחישובים נגזרים כמו dFdx ו-dFdy, ייתכן שיהיה צורך בטרנספורמציות נוספות כדי להביא בחשבון את הקואורדינטה שסובבה מכיוון שהחישובים האלה מבוצעים בשטח פיקסלים. לשם כך נדרשת האפליקציה כדי להעביר אינדיקציה מסוימת לטרנספורמציה מראש לגוון הצללה של המקטעים (למשל, מספר שלם שמייצג את כיוון המכשיר הנוכחי) ולהשתמש בו כדי למפות את בצורה נכונה, של חישובים נגזרים:

  • למסגרת מסובבת מראש ב-90 מעלות
    • צריך למפות את dFdx ל-dFdy
    • צריך למפות את dFdy ל- -dFdx
  • למסגרת מסובבת מראש ב-270 מעלות
    • צריך למפות את dFdx ל- -dFdy
    • צריך למפות את dFdy ל-dFdx
  • למסגרת מסובבת מראש ב-180 מעלות:
    • צריך למפות את dFdx ל- -dFdx
    • צריך למפות את dFdy ל- -dFdy

סיכום

כדי שהאפליקציה שלך תוכל להפיק את המרב מ-Vulkan ב-Android, חובה להטמיע רוטציה מראש. המסקנות החשובות ביותר אלה:

  • יש לוודא שבמהלך היצירה או היצירה של כלי החלפה, הדגל לפני הטרנספורמציה מוגדר שיתאים לדגל שהוחזר על ידי מערכת ההפעלה Android. פעולה זו תימנע את התקורה של המרכיב.
  • צריך להקפיד שגודל ה-החלפה יהיה קבוע לרזולוציית הזהות של חלון האפליקציה על המסך בכיוון הטבעי של המסך.
  • מסובבים את מטריצת ה-MVP במרחב הקליפ כדי להביא בחשבון את הכיוון של המכשירים, כי הרזולוציה/ההרחבה של ההחלפה לא מתעדכנים יותר בכיוון של המסך.
  • מעדכנים את אזור התצוגה ומלבנים של מספריים לפי הצורך באפליקציה.

אפליקציה לדוגמה: סבב מקדים מינימלי ל-Android