يمكنك معالجة اتجاه الجهاز باستخدام تدوير Vulkan المُسبَق.

توضّح هذه المقالة كيفية التعامل بكفاءة مع دوران الجهاز في تطبيق Vulkan من خلال تنفيذ الدوران المُسبَق.

باستخدام Vulkan، يمكنك تحديد معلومات أكثر بكثير عن حالة العرض مقارنةً بما يمكنك فعله باستخدام OpenGL. باستخدام Vulkan، عليك تنفيذ الإجراءات التي يعالجها برنامج التشغيل في OpenGL بشكل صريح، مثل اتجاه الجهاز وعلاقتَه باتجاه سطح العرض. هناك ثلاث طرق يمكن لنظام التشغيل Android من خلالها مواءمة سطح العرض على الجهاز مع اتجاه الجهاز:

  1. يمكن لنظام التشغيل Android استخدام وحدة معالجة الشاشة (DPU) في الجهاز، التي يمكنها التعامل بكفاءة مع دوران السطح في الأجهزة. تتوفّر هذه الميزة على الأجهزة المتوافقة فقط.
  2. يمكن لنظام التشغيل Android التعامل مع دوران السطح من خلال إضافة تمريرة مركبة. سيؤدي ذلك إلى انخفاض الأداء حسب كيفية تعامل المكوّن مع تدوير الصورة الناتجة.
  3. يمكن للتطبيق نفسه التعامل مع دوران السطح من خلال عرض صورة مُدرَجة على سطح عرض يتطابق مع الاتجاه الحالي للشاشة.

ما هي الطريقة التي يجب استخدامها؟

لا تتوفّر حاليًا طريقة لمعرفة ما إذا كان تطبيق معيّن سيتمكن من استخدام ميزة "تدوير السطح" التي تتم إدارتها خارج التطبيق مجانًا. حتى إذا كان لديك وحدة معالجة بيانات (DPU) تهتم بهذه المسألة نيابةً عنك، من المرجّح أن تدفع غرامة قابلة للقياس تتعلّق بالأداء. إذا كان تطبيقك يعتمد على وحدة المعالجة المركزية، تصبح هذه مشكلة في الطاقة بسبب زيادة استخدام وحدة معالجة الرسومات من خلال أداة Android Compositor التي تعمل عادةً بتردد مُعزَّز. إذا كان تطبيقك يعتمد على وحدة معالجة الرسومات، يمكن أن يسبق مجمع الرسومات في Android أيضًا عمل وحدة معالجة الرسومات في تطبيقك، ما يؤدي إلى مزيد من فقدان الأداء.

عند تشغيل عناوين الإصدار على هاتف Pixel 4XL، لاحظنا أنّ SurfaceFlinger (المهمة ذات الأولوية الأعلى التي تشغّل Android Compositor):

  • يسبق بانتظام عمل التطبيق، ما يؤدي إلى رصد قياسات تتراوح بين 1 و3 مللي ثانية لمدة عرض اللقطة.

  • تؤدي إلى زيادة الضغط على ذاكرة النقاط/الملمس في وحدة معالجة الرسومات، لأنّ "أداة الدمج" يجب أن تقرأ ملف ذاكرة الإطار بالكامل للقيام بعملية الدمج.

يؤدي التعامل مع الاتجاه بشكل صحيح إلى إيقاف استيلاء SurfaceFlinger على وحدة معالجة الرسومات بشكل كامل تقريبًا، بينما ينخفض معدّل تكرار وحدة معالجة الرسومات بنسبة% 40 لأنّه لم يعُد هناك حاجة إلى معدّل التكرار المحسَّن الذي يستخدمه "مُركِّب Android".

لضمان معالجة عمليات تدوير السطح بشكل صحيح وبأقل قدر ممكن من عمليات التحميل الزائد، كما هو موضّح في الحالة السابقة، يجب تنفيذ الطريقة 3. ويُعرف ذلك باسم العرض المُسبَق. يُعلم ذلك نظام التشغيل Android بأنّ تطبيقك يتولى إدارة دوران السطح. يمكنك إجراء ذلك من خلال تمرير علامات تحويل السطح التي تحدّد الاتجاه أثناء إنشاء سلسلة التبديل. يؤدي ذلك إلى إيقاف Android Compositor عن إجراء عملية التدوير نفسها.

من المهم معرفة كيفية ضبط علامة تحويل السطح لكل تطبيق Vulkan. تميل التطبيقات إلى أن تكون متوافقة مع أوضاع متعددة أو متوافقة مع وضع واحد يكون فيه سطح العرض في وضع مختلف عن الوضع الذي يعتبره الجهاز هو وضع الهوية. على سبيل المثال، تطبيق مخصّص للعرض في الوضع الأفقي فقط على هاتف مخصّص للعرض في الوضع العمودي فقط، أو تطبيق مخصّص للعرض في الوضع العمودي فقط على جهاز لوحي مخصّص للعرض في الوضع الأفقي فقط

تعديل ملف AndroidManifest.xml

للتعامل مع عملية تدوير الجهاز في تطبيقك، ابدأ بتغيير ملف AndroidManifest.xml للتطبيق لإعلام Android بأنّ تطبيقك سيتولى التعامل مع اتجاه الشاشة وتغييرات حجمها. يمنع ذلك نظام Android من تدمير رمز Android Activity وإعادة إنشائه واستدعاء الدالة onDestroy() على سطح النافذة الحالي عند حدوث تغيير في الاتجاه. ويتم ذلك من خلال إضافة السمتَين orientation (لتتوافق مع المستوى 13 أو أقل من واجهة برمجة التطبيقات) وscreenSize إلى قسم configChanges للنشاط:

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

إذا كان تطبيقك يحدّد اتجاه الشاشة باستخدام السمة screenOrientation ، لن تحتاج إلى إجراء ذلك. بالإضافة إلى ذلك، إذا كان تطبيقك يستخدم اتجاهًا ثابتًا، لن يحتاج إلا إلى إعداد سلسلة التبديل مرة واحدة عند بدء تشغيل التطبيق أو استئنافه.

الحصول على درجة دقة شاشة الهوية ومَعلمات الكاميرا

بعد ذلك، عليك رصد درجة دقة شاشة الجهاز المرتبطة بالقيمة VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR. يرتبط هذا القرار باتجاه هوية الجهاز، وبالتالي هو القرار الذي يجب ضبط سلسلة التبديل عليه دائمًا. إنّ الطريقة الأكثر موثوقية للحصول على هذا النطاق هي إجراء مكالمة إلى 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 نستخدمها لتخزين هوية درجة دقة سطح نافذة التطبيق في الاتجاه الطبيعي للشاشة.

رصد تغييرات اتجاه الجهاز (الإصدار 10 من نظام التشغيل Android والإصدارات الأحدث)

إنّ الطريقة الأكثر موثوقية لرصد تغيير في الاتجاه في تطبيقك هي التحقّق مما إذا كانت دالة vkQueuePresentKHR() تعرِض VK_SUBOPTIMAL_KHR. مثلاً:

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

ملاحظة: لا يعمل هذا الحل إلا على الأجهزة التي تعمل بالإصدار 10 من نظام التشغيل Android والإصدارات الأحدث. تعرض إصدارات 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 و0.250 ملي ثانية، وعلى هاتف Pixel 1XL الذي يعمل بنظام التشغيل Android 8، استغرقت عملية الاستطلاع مدة تتراوح بين 0 .110 و0.350 ملي ثانية.

استخدام طلبات إعادة الاتصال

الخيار الثاني للأجهزة التي تعمل بإصدار أقدم من Android 10 هو تسجيل رمز برمجي للرجوع إلى دالة برمجية تضبط العلامة orientationChanged، ما يشير إلى التطبيق أنّه حدث تغيير في الاتجاه: onNativeWindowResized()

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. أعِد إنشاء Framebuffers باستخدام 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 لأنّك ستحتاج إليها لاحقًا عند إجراء تعديلات على ملف تعريف العميل المستهدف في مرحلة الطرح.

لإجراء ذلك، حدِّد السمات التالية في بنية 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 (الذي تم ضبطه على الحقل currentTransform في surfaceCapabilities). يمكنك أيضًا ضبط الحقل oldSwapchain على سلسلة التبديل التي سيتم إتلافها.

تعديل مصفوفة منتج الحد الأدنى القابل للتطبيق

الخطوة الأخيرة التي يجب اتّخاذها هي تطبيق التحويل التمهيدي من خلال تطبيق مصفوفة دوران على مصفوفة MVP. ويؤدي هذا الإجراء إلى تطبيق التدوير في مساحة المقطع لكي يتم تدوير الصورة الناتجة لتتلاءم مع اتجاه الجهاز الحالي. يمكنك بعد ذلك تمرير مصفوفة MVP المعدَّلة إلى برنامج تشفير قمة المضلّع واستخدامها كالمعتاد بدون الحاجة إلى تعديل برامج التشفير.

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;

ملاحظة: إطار العرض والقص غير المخصصَين للعرض بملء الشاشة

إذا كان تطبيقك يستخدم إطار عرض/منطقة مقصّ غير ملء الشاشة، يجب تعديلهما وفقًا لاتجاه الجهاز. يتطلب ذلك تفعيل خيارَي Viewport وScissor الديناميكيَين أثناء إنشاء مسار 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);

في ما يلي الحساب الفعلي لمدى عرض النافذة أثناء تسجيل ذاكرة التخزين المؤقت للطلبات:

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، قد تكون هناك حاجة إلى عمليات تحويل إضافية لمراعاة نظام الإحداثيات المُدار أثناء تنفيذ هذه العمليات الحسابية في مساحة البكسل. يتطلّب ذلك من التطبيق تمرير بعض المؤشرات عن preTransform إلى برنامج تظليل الشريحة (مثل عدد صحيح يمثّل اتجاه الجهاز الحالي) واستخدام ذلك لربط عمليات حسابية مشتقة بشكل صحيح:

  • بالنسبة إلى لقطة تم تدويرها مسبقًا بمقدار 90 درجة
    • يجب ربط dFdx بـ dFdy.
    • يجب ربط dFdy بـ -dFdx
  • لإطار تم تدويره مسبقًا بمقدار 270 درجة
    • يجب ربط dFdx بـ -dFdy.
    • يجب ربط dFdy بـ dFdx.
  • بالنسبة إلى إطار تم تدويره مسبقًا بمقدار 180 درجة،
    • يجب ربط dFdx بـ -dFdx.
    • يجب ربط dFdy بـ -dFdy.

الخاتمة

لكي يستفيد تطبيقك إلى أقصى حد من Vulkan على Android، يجب تنفيذ عملية "التدوير المُسبَق". في ما يلي أهم النقاط التي يمكن استخلاصها من هذه المقالة:

  • تأكَّد من أنّه أثناء إنشاء سلسلة التبديل أو إعادة إنشائها، يتم ضبط علامة التحويل المُسبَق لتتطابق مع العلامة التي يعرضها نظام التشغيل Android. سيؤدي ذلك إلى تجنُّب التأثيرات الجانبية لبرنامج "المركّب".
  • يجب إبقاء حجم سلسلة الاستبدال ثابتًا وفقًا لدرجة دقة هوية سطح نافذة التطبيق في الاتجاه الطبيعي للشاشة.
  • يمكنك تدوير مصفوفة MVP في مساحة المقطع لضبط اتجاه الأجهزة، لأنّ درجة دقة/مدى سلسلة التبديل لم تعُد تتغيّر مع اتجاه الشاشة.
  • عدِّل مستطيلات مساحة العرض والمقص حسب الحاجة في تطبيقك.

نموذج التطبيق: الحد الأدنى من مدة الدوران المُسبَق لنظام التشغيل Android