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