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

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

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

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

أيّ من هذه الطرق يجب استخدامها؟

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

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

  • يوقف بانتظام عمل التطبيق، ما يؤدي إلى حدوث تأخيرات في أوقات الإطارات تتراوح بين 1 و3 ملي ثانية، و

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

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

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

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

استخدام عمليات إعادة الاستدعاء

هناك خيار ثانٍ للأجهزة التي تعمل بإصدار أقدم من 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 Compositor إجراء عملية التدوير لتطبيقك.

التعامل مع تغيير الاتجاه

للتعامل مع تغيير الاتجاه، استدعِ روتين تغيير الاتجاه في أعلى حلقة العرض الرئيسية عندما يتم ضبط المتغيّر orientationChanged على "صحيح". على سبيل المثال:

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

يمكنك إجراء جميع الأعمال اللازمة لإعادة إنشاء سلسلة التبديل ضمن الدالة OnOrientationChange(). يعني ذلك ما يلي:

  1. إتلاف أيّ مثيلات حالية من Framebuffer وImageView،

  2. إعادة إنشاء سلسلة التبديل أثناء إتلاف سلسلة التبديل القديمة (سيتم تناول ذلك لاحقًا)، و

  3. إعادة إنشاء مخازن الإطارات باستخدام 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 على "خطأ" للإشارة إلى أنّك تعاملت مع تغيير الاتجاه.

إعادة إنشاء سلسلة التبديل

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

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

تعديل مصفوفة MVP

آخر ما عليك فعله هو تطبيق عملية التحويل المُسبَق من خلال تطبيق مصفوفة تدوير على مصفوفة 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;

اعتبارات: منفذ العرض والمقص غير بملء الشاشة

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

اعتبارات: مشتقات برنامج تظليل الأجزاء

إذا كان تطبيقك يستخدم عمليات حسابية مشتقة مثل dFdx وdFdy، قد تكون هناك حاجة إلى عمليات تحويل إضافية لمراعاة نظام الإحداثيات المُدوَّر لأنّ عمليات الحساب هذه يتم تنفيذها في مساحة البكسل. يتطلّب ذلك من التطبيق تمرير بعض المؤشرات إلى أداة تظليل الأجزاء (مثل عدد صحيح يمثّل اتجاه الجهاز الحالي) واستخدام ذلك لربط عمليات الحساب الاشتقاقية بشكلٍ سليم:

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

الخاتمة

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

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

تطبيق نموذجي: التدوير المُسبَق البسيط على Android