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

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

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

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

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

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

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

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

رصد التغييرات في اتجاه الجهاز (الإصدار 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)

بالنسبة إلى الأجهزة التي تعمل بنظام التشغيل 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() ما بين 120.250 و250 ملّي ثانية وفي هاتف Pixel 1XL الذي يعمل بنظام التشغيل Android 8، واستغرقت عملية الاستطلاع بين 0.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 يتم تعيين المتغير على true. مثلاً:

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 على القيمة "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 (الذي يتم تعيينه على الحقل 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;

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

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

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