توضّح هذه المقالة كيفية التعامل بكفاءة مع تدوير الجهاز في تطبيق Vulkan من خلال تنفيذ عملية التدوير المسبق.
باستخدام Vulkan، يمكنك تحديد معلومات أكثر بكثير عن حالة العرض مقارنةً بما يمكنك فعله باستخدام OpenGL. باستخدام Vulkan، عليك تنفيذ الإجراءات التي يتولّى برنامج التشغيل تنفيذها في OpenGL بشكل صريح، مثل اتجاه الجهاز وعلاقته باتجاه سطح العرض. هناك ثلاث طرق يمكن لنظام التشغيل Android من خلالها تسوية سطح العرض على الجهاز مع اتجاه الجهاز:
- يمكن لنظام التشغيل Android استخدام وحدة معالجة العرض (DPU) بالجهاز، والتي يمكنها التعامل بكفاءة مع تدوير السطح في الأجهزة. تتوفّر هذه الميزة على الأجهزة المتوافقة فقط.
- يمكن لنظام التشغيل Android التعامل مع تدوير السطح من خلال إضافة تمريرة مُركِّب. سيؤدي ذلك إلى تكلفة أداء اعتمادًا على طريقة تعامل المكوّن مع تدوير الصورة الناتجة.
- يمكن للتطبيق نفسه التعامل مع تدوير الشاشة من خلال عرض صورة تم تدويرها على سطح عرض يتطابق مع الاتجاه الحالي للشاشة.
أيّ من هذه الطرق يجب استخدامها؟
في الوقت الحالي، ليس هناك طريقة يمكن للتطبيق من خلالها معرفة ما إذا كان سيتم تدوير الشاشة خارج التطبيق بدون أي رسوم. وحتى إذا كان هناك وحدة معالجة مركزية مخصصة (DPU) تتولى هذه المهمة، من المحتمل أن يكون هناك تأثير سلبي ملحوظ على الأداء. إذا كان تطبيقك يعتمد بشكل كبير على وحدة المعالجة المركزية، ستصبح هذه مشكلة متعلّقة بالطاقة بسبب زيادة استخدام وحدة معالجة الرسومات من خلال Android Compositor الذي يعمل عادةً بتردد أعلى. إذا كان تطبيقك يعتمد على وحدة معالجة الرسومات، يمكن أن يوقف Compositor في Android أيضًا عمل وحدة معالجة الرسومات في تطبيقك، ما يؤدي إلى انخفاض إضافي في الأداء.
عند تشغيل عناوين الشحن على هاتف 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 يعمل بنظام التشغيل Android 10، استغرق الاستطلاع
vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
ما بين 0 .120 و0.250 ملي ثانية، بينما استغرق الاستطلاع ما بين 0 .110 و0.350 ملي ثانية على هاتف
Pixel 1XL يعمل بنظام التشغيل Android 8.
استخدام عمليات معاودة الاتصال
هناك خيار ثانٍ للأجهزة التي تعمل بإصدار أقدم من 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()
. وهذا يعني ما يلي:
إيقاف أي مثيلات حالية من
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
، وستحتاج إليها عند إجراء تعديلات على مصفوفة الحد الأدنى من المنتج القابل للتطبيق.
لإجراء ذلك، حدِّد السمات التالية
في البنية 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;
اعتبارات - إطار العرض والمقصة في وضع عدم ملء الشاشة
إذا كان تطبيقك يستخدم منطقة عرض/قص غير كاملة الشاشة، يجب تعديلها وفقًا لاتجاه الجهاز. يتطلّب ذلك تفعيل خيارات 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