توضح هذه المقالة كيفية التعامل مع دوران الجهاز بفعالية في تطبيق Vulkan من خلال تنفيذ التدوير المسبق.
باستخدام Vulkan، يمكنك تحديد معلومات أكثر بكثير عن حالة العرض مقارنةً بما يمكنك فعله باستخدام OpenGL. باستخدام Vulkan، عليك تنفيذ الإجراءات التي يعالجها برنامج التشغيل في OpenGL بشكل صريح، مثل اتجاه الجهاز وعلاقتَه باتجاه سطح العرض. هناك ثلاث طرق يمكن لنظام التشغيل Android من خلالها مواءمة سطح العرض على الجهاز مع اتجاه الجهاز:
- يمكن لنظام التشغيل Android استخدام وحدة معالجة الشاشة (DPU) في الجهاز، التي يمكنها التعامل بكفاءة مع دوران السطح في الأجهزة. تتوفّر هذه الميزة على الأجهزة المتوافقة فقط.
- يمكن لنظام التشغيل Android التعامل مع دوران السطح من خلال إضافة تمريرة مركبة. سيؤدي ذلك إلى انخفاض الأداء حسب كيفية تعامل المكوّن مع تدوير الصورة الناتجة.
- يمكن للتطبيق نفسه التعامل مع دوران السطح من خلال عرض صورة مُدرَجة على سطح عرض يتطابق مع الاتجاه الحالي للشاشة.
ما هي الطريقة التي يجب استخدامها؟
لا تتوفّر حاليًا طريقة لمعرفة ما إذا كان تطبيق معيّن سيتمكن من استخدام ميزة تدوير السطح التي تتم خارج التطبيق بدون أي رسوم. حتى إذا كان لديك وحدة معالجة بيانات (DPU) تهتم بهذه المسألة نيابةً عنك، من المرجّح أن تدفع غرامة قياسية متعلقة بالأداء. إذا كان تطبيقك يعتمد على وحدة المعالجة المركزية، تصبح هذه مشكلة في الطاقة بسبب زيادة استخدام وحدة معالجة الرسومات من خلال أداة Android Compositor التي تعمل عادةً بتردد مُعزَّز. إذا كان التطبيق مرتبطًا بوحدة معالجة الرسومات، يمكن لـ Android Compositor أيضًا إيقاف عمل وحدة معالجة الرسومات في تطبيقك بشكل استباقي، ما يؤدي إلى فقدان مزيد من الأداء.
عند تشغيل عناوين الإصدار على هاتف Pixel 4XL، لاحظنا أنّ SurfaceFlinger (المهمة ذات الأولوية الأعلى التي تشغّل Android Compositor):
يسبق التطبيق بانتظام في تنفيذ مهامه، ما يؤدي إلى رصد زمن عرض اللقطة بقيم تتراوح بين 1 و3 مللي ثانية
تؤدي إلى زيادة الضغط على ذاكرة النقاط/الملمس في وحدة معالجة الرسومات، لأنّ "أداة الدمج" يجب أن تقرأ ملف ذاكرة الإطار بالكامل للقيام بعملية الدمج.
يؤدي التعامل مع الاتجاه بشكل صحيح إلى إيقاف استيلاء SurfaceFlinger على وحدة معالجة الرسومات بشكل كامل تقريبًا، بينما ينخفض معدّل تكرار وحدة معالجة الرسومات بنسبة% 40 لأنّه لم يعُد هناك حاجة إلى معدّل التكرار المحسَّن الذي يستخدمه "مُركِّب Android".
لضمان معالجة عمليات تدوير السطح بشكل صحيح وبأقل قدر ممكن من عمليات التحميل الزائد، كما هو موضّح في الحالة السابقة، يجب تنفيذ الطريقة 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;
}
ملاحظة: لا يعمل هذا الحل إلا على الأجهزة التي تعمل بالإصدار 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
على "صحيح". مثلاً:
bool VulkanDrawFrame() {
if (orientationChanged) {
OnOrientationChange();
}
عليك تنفيذ كل الأعمال اللازمة لإعادة إنشاء سلسلة التبديل داخل الدالة OnOrientationChange()
. وهذا يعني أنّه يمكنك:
إزالة أي نُسخ حالية من
Framebuffer
وImageView
إعادة إنشاء سلسلة التبديل مع تدمير سلسلة التبديل القديمة (التي سيتم مناقشتها بعد ذلك)،
أعِد إنشاء 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
لأنّك ستحتاج إليها لاحقًا عند إجراء تعديلات على ملف تعريف العميل المستهدف في مرحلة الطرح المبكر (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
(الذي يتم ضبطه على الحقل الحالي Transform
في 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;
ملاحظة: إطار العرض والقص غير المخصصَين للعرض بملء الشاشة
إذا كان تطبيقك يستخدم إطار عرض/منطقة مقصّ غير ملء الشاشة، يجب تعديلهما وفقًا لاتجاه الجهاز. يتطلب ذلك تفعيل خيارَي 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
،
قد تكون هناك حاجة إلى عمليات تحويل إضافية لمراعاة نظام الإحداثيات المُدار أثناء تنفيذ هذه العمليات الحسابية في مساحة البكسل. وهذا يتطلب من التطبيق تمرير مؤشر ما قبل التحويل إلى أداة تظليل الأجزاء (مثل عدد صحيح يمثل الاتجاه الحالي للجهاز) واستخدام ذلك لتعيين العمليات الحسابية الاشتقاقية بشكل صحيح:
- بالنسبة إلى لقطة تم تدويرها مسبقًا بمقدار 90 درجة
- يجب ربط dFdx بـ dFdy.
- يجب ربط dFdy بـ -dFdx
- للحصول على إطار تم تدويره مسبقًا بزاوية 270 درجة
- يجب ربط dFdx بـ -dFdy.
- يجب ربط dFdy بـ dFdx.
- بالنسبة إلى إطار تم تدويره مسبقًا بمقدار 180 درجة،
- يجب ربط dFdx بـ -dFdx.
- يجب ربط dFdy بـ -dFdy
الخاتمة
لكي يستفيد تطبيقك إلى أقصى حد من Vulkan على Android، يجب تنفيذ عملية "التدوير المُسبَق". في ما يلي أهم النقاط التي يمكن استخلاصها من هذه المقالة:
- تأكَّد من أنّه أثناء إنشاء سلسلة التبديل أو إعادة إنشائها، يتم ضبط علامة التحويل المُسبَق لتتطابق مع العلامة التي يعرضها نظام التشغيل Android. سيؤدي ذلك إلى تجنُّب التأثيرات الجانبية لبرنامج "المركّب".
- يجب إبقاء حجم سلسلة الاستبدال ثابتًا وفقًا لدرجة دقة هوية سطح نافذة التطبيق في الاتجاه الطبيعي للشاشة.
- يمكنك تدوير مصفوفة MVP في مساحة المقطع لضبط اتجاه الأجهزة، لأنّ درجة دقة/مدى سلسلة التبديل لم تعُد تتغيّر مع اتجاه الشاشة.
- عدِّل مستطيلات إطار العرض والمقص حسب الحاجة في تطبيقك.
نموذج تطبيق: الحد الأدنى من الدوران المُسبَق على Android