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

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

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

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

أي من الطرق التالية ينبغي أن تستخدمها؟

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

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

  • يستبعد عمل التطبيق بانتظام، ويتسبب في حدوث نقرات تتراوح بين 1 و3 ملي ثانية في أوقات عرض اللقطات،

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

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

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

من المهم معرفة كيفية ضبط علامة تحويل السطح لكل تطبيق من تطبيقات 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;
}

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

رصد التغيُّرات في اتجاه الجهاز (الإصدارات الأقدم من Android 10)

بالنسبة إلى الأجهزة التي تعمل بالإصدار 10 من نظام التشغيل Android أو الإصدارات الأقدم، يجب تنفيذ طريقة مختلفة، لأنّ 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 الذي يعمل بالإصدار 10 من نظام التشغيل Android، استغرقت استطلاع vkGetPhysicalDeviceSurfaceCapabilitiesKHR() بين .120 و.250 ملي ثانية، وعلى هاتف Pixel 1XL الذي يعمل بنظام التشغيل Android 8، استغرقت الاستطلاع مدة تتراوح بين 110 و.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 تنفيذ الدوران في التطبيق.

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

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

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 على "خطأ" لإظهار أنّك تعاملت مع تغيير الاتجاه.

ترفيه

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

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 (الذي يتم ضبطه على حقل التحويل الحالي في 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;

التفكير في الشراء - إطار عرض ومقص غير بملء الشاشة

إذا كان تطبيقك يستخدم إطار عرض/منطقة مقصّة وغير بملء الشاشة، فيجب تعديلها وفقًا لاتجاه الجهاز. يتطلب هذا تفعيل خياري إطار العرض ومقص الديناميكي أثناء إنشاء مسار 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.
    • يجب ربط dFd بـ dFdx.
  • للحصول على إطار تم تدويره مسبقًا 180 درجة، يجب اتّباع الخطوات التالية:
    • يجب ربط dFdx بـ dFdx.
    • يجب ربط dFdy بـ -dFdy.

الخاتمة

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

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

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