توضّح هذه المقالة كيفية التعامل بكفاءة مع تدوير الجهاز في تطبيق Vulkan من خلال تنفيذ ميزة "التدوير المُسبَق".
باستخدام Vulkan، يمكنك تحديد معلومات أكثر بكثير عن حالة العرض مقارنةً بما يمكنك تحديده باستخدام OpenGL. باستخدام Vulkan، يجب تنفيذ العناصر التي يعالجها برنامج التشغيل في OpenGL بشكل صريح، مثل اتجاه الجهاز وعلاقته باتجاه سطح العرض. هناك ثلاث طرق يمكن لنظام Android من خلالها التوفيق بين سطح العرض واتجاه الجهاز:
- يمكن لنظام التشغيل Android استخدام وحدة معالجة العرض (DPU) في الجهاز، والتي يمكنها التعامل بكفاءة مع تدوير السطح في الأجهزة. تتوفّر هذه الميزة على الأجهزة المتوافقة فقط.
- يمكن لنظام التشغيل Android التعامل مع تدوير السطح من خلال إضافة خطوة التجميع بواسطة أداة التركيب. سيؤدي ذلك إلى تكلفة أداء اعتمادًا على طريقة تعامل أداة التركيب مع تدوير الصورة الناتجة.
- يمكن للتطبيق نفسه التعامل مع تدوير السطح من خلال عرض صورة تم تدويرها على سطح عرض يتطابق مع الاتجاه الحالي للشاشة.
أي من هذه الطرق يجب أن تستخدمها؟
في الوقت الحالي، ليس هناك طريقة يمكن للتطبيق من خلالها معرفة ما إذا كان سيتم تدوير الشاشة خارج التطبيق بدون أي رسوم. وحتى إذا كان هناك وحدة معالجة مركزية مخصصة (DPU) تتولى هذه المهمة، من المحتمل أن يكون هناك تأثير سلبي ملحوظ على الأداء. إذا كان تطبيقك يعتمد بشكل كبير على وحدة المعالجة المركزية، ستصبح هذه مشكلة متعلّقة بالطاقة بسبب زيادة استخدام وحدة معالجة الرسومات من خلال Android Compositor الذي يعمل عادةً بتردد أعلى. إذا كان تطبيقك يعتمد على وحدة معالجة الرسومات، يمكن أن يوقف Android Composito أيضًا عمل وحدة معالجة الرسومات في تطبيقك، ما يؤدي إلى انخفاض إضافي في الأداء.
عند تشغيل ألعاب رائجة على هاتف Pixel 4XL، لاحظنا أنّ SurfaceFlinger (المهمة ذات الأولوية الأعلى التي تشغّل مُركِّب Android):
يستبق عمل التطبيق بانتظام، ما يتسبب في حدوث تأثيرات تتراوح بين 1 و3 ملي ثانية على أوقات عرض اللقطات،
يؤدي ذلك إلى زيادة الضغط على ذاكرة الرؤوس/الزخرفة في وحدة معالجة الرسومات، لأنّ "المُركِّب" يحتاج إلى قراءة المخزن المؤقت للإطارات بالكامل لإجراء عملية التركيب.
تؤدي معالجة الاتجاه بشكل صحيح إلى إيقاف استباق وحدة معالجة الرسومات من خلال 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;
}
ملاحظة: لا يعمل هذا الحلّ إلا على الأجهزة التي تعمل بنظام التشغيل 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(). ويعني ذلك أنّك:
تدمير أي نُسخ حالية من
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 على "خطأ"
للإشارة إلى أنّك عالجت تغيير الاتجاه.
إعادة إنشاء Swapchain
في القسم السابق، ذكرنا أنّه يجب إعادة إنشاء سلسلة التبديل. تتمثّل الخطوات الأولى في الحصول على الخصائص الجديدة لسطح العرض:
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 (الذي تم ضبطه على الحقل 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;
Consideration - Non-Full Screen Viewport and Scissor
إذا كان تطبيقك يستخدم إطار عرض أو منطقة اقتصاص لا تملأ الشاشة، يجب تعديلهما وفقًا لاتجاه الجهاز. يتطلّب ذلك تفعيل خيارَي 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);
Consideration - Fragment Shader Derivatives
إذا كان تطبيقك يستخدم عمليات حسابية مشتقة مثل dFdx وdFdy،
قد تكون هناك حاجة إلى عمليات تحويل إضافية لمراعاة نظام الإحداثيات
الذي تم تدويره لأنّ هذه العمليات الحسابية يتم تنفيذها في مساحة البكسل. يتطلّب ذلك من التطبيق
تمرير بعض المؤشرات لـ preTransform إلى أداة تظليل الأجزاء (مثل
عدد صحيح يمثّل اتجاه الجهاز الحالي) واستخدام ذلك لربط
عمليات حساب المشتقات بشكل صحيح:
- بالنسبة إلى لقطة تم تدويرها مسبقًا بزاوية 90 درجة
- يجب ربط dFdx بـ dFdy
- dFdy يجب ربطها بـ -dFdx
- بالنسبة إلى إطار تم تدويره مسبقًا بمقدار 270 درجة
- يجب ربط dFdx بـ -dFdy
- يجب ربط dFdy بـ dFdx
- بالنسبة إلى الإطار الذي تم تدويره مسبقًا بمقدار 180 درجة،
- يجب ربط dFdx بـ -dFdx
- يجب ربط dFdy بـ -dFdy
الخاتمة
للاستفادة إلى أقصى حد من Vulkan على Android في تطبيقك، يجب تنفيذ ميزة "التدوير المُسبَق". أهم النقاط التي يمكن استخلاصها من هذه المقالة هي:
- تأكَّد من ضبط علامة التحويل المُسبق أثناء إنشاء سلسلة التبديل أو إعادة إنشائها لمطابقة العلامة التي يعرضها نظام التشغيل Android. سيؤدي ذلك إلى تجنُّب العبء الإضافي للمُركِّب.
- يجب الحفاظ على حجم سلسلة التبديل ثابتًا وفقًا للدقة الأساسية لمساحة نافذة التطبيق في الاتجاه الطبيعي للشاشة.
- يجب تدوير مصفوفة MVP في مساحة الاقتصاص لمراعاة اتجاه الأجهزة، لأنّ دقة/نطاق سلسلة التبديل لم يعُد يتم تعديلهما مع اتجاه الشاشة.
- عدِّل إطار العرض ومستطيلات القص حسب الحاجة في تطبيقك.
نموذج تطبيق: الحد الأدنى من التدوير المُسبَق على Android