جهت گیری دستگاه را با پیش چرخش Vulkan کنترل کنید

این مقاله نحوه مدیریت کارآمد چرخش دستگاه در برنامه Vulkan خود را با اجرای پیش چرخش توضیح می دهد.

با Vulkan ، می توانید اطلاعات بسیار بیشتری را در مورد وضعیت رندر نسبت به OpenGL تعیین کنید. با Vulkan، باید به صراحت مواردی را که توسط درایور در OpenGL مدیریت می‌شود، پیاده‌سازی کنید، مانند جهت‌گیری دستگاه و ارتباط آن با جهت‌گیری سطحی رندر . سه راه وجود دارد که Android می‌تواند سطح رندر دستگاه را با جهت‌گیری دستگاه تطبیق دهد:

  1. سیستم عامل اندروید می تواند از واحد پردازش نمایشگر (DPU) دستگاه استفاده کند که می تواند چرخش سطح را در سخت افزار به طور موثر انجام دهد. فقط در دستگاه های پشتیبانی شده موجود است.
  2. سیستم‌عامل اندروید می‌تواند با اضافه کردن یک پاس ترکیبی، چرخش سطح را کنترل کند. این هزینه عملکرد بسته به نحوه برخورد سازنده با چرخش تصویر خروجی خواهد داشت.
  3. خود برنامه می‌تواند با رندر کردن یک تصویر چرخانده شده بر روی سطح رندر که با جهت فعلی نمایشگر مطابقت دارد، چرخش سطح را انجام دهد.

از کدام یک از این روش ها باید استفاده کرد؟

در حال حاضر، هیچ راهی برای یک برنامه وجود ندارد که بداند آیا چرخش سطحی که خارج از برنامه انجام می شود رایگان است یا خیر. حتی اگر یک DPU وجود داشته باشد که این کار را برای شما انجام دهد، باز هم احتمالاً جریمه عملکرد قابل اندازه گیری برای پرداخت وجود دارد. اگر برنامه شما به CPU متصل است، به دلیل افزایش استفاده از GPU توسط Android Compositor، که معمولاً با فرکانس تقویت شده اجرا می شود، این یک مشکل برق می شود. اگر برنامه شما محدود به GPU است، Android Compositor همچنین می تواند از کار GPU برنامه شما جلوگیری کند و باعث کاهش عملکرد بیشتر شود.

هنگام اجرای عناوین حمل و نقل در Pixel 4XL، SurfaceFlinger (وظیفه با اولویت بالاتری که Android Compositor را هدایت می‌کند) را دیده‌ایم:

  • به طور منظم از کار برنامه جلوگیری می کند و باعث بازدید 1-3 میلی ثانیه در فریم تایم ها می شود و

  • فشار بیشتری بر حافظه رأس/بافت GPU وارد می کند، زیرا Compositor باید کل فریم بافر را بخواند تا کار ترکیب بندی خود را انجام دهد.

جهت‌گیری مدیریت به‌درستی پیش‌دستی GPU توسط SurfaceFlinger را تقریباً به طور کامل متوقف می‌کند، در حالی که فرکانس GPU 40٪ کاهش می‌یابد زیرا فرکانس تقویت‌شده مورد استفاده توسط Android Compositor دیگر مورد نیاز نیست.

برای اطمینان از اینکه چرخش های سطحی به درستی با کمترین هزینه ممکن انجام می شوند، همانطور که در مورد قبل مشاهده شد، باید روش 3 را اجرا کنید. این به عنوان پیش چرخش شناخته می شود. این به سیستم عامل اندروید می گوید که برنامه شما چرخش سطح را انجام می دهد. شما می توانید این کار را با ارسال پرچم های تبدیل سطحی که جهت را در حین ایجاد swapchain مشخص می کنند، انجام دهید. این کار باعث می‌شود Android Compositor خود چرخش را انجام ندهد.

دانستن نحوه تنظیم پرچم تبدیل سطح برای هر برنامه Vulkan مهم است. برنامه‌ها تمایل دارند یا از جهت‌گیری‌های متعدد پشتیبانی کنند یا از یک جهت پشتیبانی کنند که در آن سطح رندر در جهتی متفاوت از جهت‌گیری هویت دستگاه است. به عنوان مثال، یک برنامه فقط افقی در یک تلفن با هویت عمودی، یا یک برنامه کاربردی فقط عمودی در یک رایانه لوحی با هویت منظره.

AndroidManifest.xml را تغییر دهید

برای مدیریت چرخش دستگاه در برنامه خود، با تغییر فایل AndroidManifest.xml برنامه شروع کنید تا به Android بگویید که برنامه شما تغییر جهت و اندازه صفحه نمایش را انجام می دهد. این مانع از آن می‌شود که Android Activity از بین ببرد و دوباره ایجاد کند و هنگام تغییر جهت، تابع onDestroy() روی سطح پنجره موجود فراخوانی کند. این کار با افزودن orientation (برای پشتیبانی از سطح API <13) و صفات screenSize به بخش configChanges فعالیت انجام می شود:

<activity android:name="android.app.NativeActivity"
          android:configChanges="orientation|screenSize">

اگر برنامه شما جهت صفحه نمایش خود را با استفاده از ویژگی screenOrientation اصلاح می کند، نیازی به انجام این کار ندارید. همچنین، اگر برنامه شما از جهت گیری ثابتی استفاده می کند، تنها یک بار در راه اندازی/رزومه برنامه، swapchain را راه اندازی می کند.

رزولوشن صفحه نمایش هویت و پارامترهای دوربین را دریافت کنید

سپس، وضوح صفحه نمایش دستگاه مرتبط با مقدار VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR را شناسایی کنید. این رزولوشن با جهت گیری هویت دستگاه مرتبط است و بنابراین همان وضوحی است که swapchain همیشه باید روی آن تنظیم شود. مطمئن ترین راه برای به دست آوردن این، تماس با 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 است که ما از آن برای ذخیره وضوح هویت سطح پنجره برنامه در جهت طبیعی نمایشگر استفاده می کنیم.

تشخیص تغییرات جهت دستگاه (Android 10+)

مطمئن ترین راه برای تشخیص تغییر جهت در برنامه شما این است که بررسی کنید آیا تابع vkQueuePresentKHR() VK_SUBOPTIMAL_KHR را برمی گرداند یا خیر. به عنوان مثال:

auto res = vkQueuePresentKHR(queue_, &present_info);
if (res == VK_SUBOPTIMAL_KHR){
  orientationChanged = true;
}

توجه: این راه حل فقط روی دستگاه های دارای Android 10 و بالاتر کار می کند. این نسخه‌های اندروید VK_SUBOPTIMAL_KHR از vkQueuePresentKHR() برمی‌گردانند. ما نتیجه این بررسی را در orientationChanged ذخیره می‌کنیم، یک boolean که از حلقه رندر اصلی برنامه‌ها قابل دسترسی است.

تشخیص تغییرات جهت گیری دستگاه (پیش از اندروید 10)

برای دستگاه‌هایی که Android 10 یا بالاتر دارند، به پیاده‌سازی متفاوتی نیاز است، زیرا VK_SUBOPTIMAL_KHR پشتیبانی نمی‌شود.

با استفاده از نظرسنجی

در دستگاه‌های پیش از اندروید 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-.250 میلی‌ثانیه طول کشید و در Pixel 1XL دارای Android 8، نظرسنجی بین 0.110-.350 میلی‌ثانیه طول کشید.

استفاده از Callbacks

گزینه دوم برای دستگاه‌هایی که زیر اندروید 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 درجه، مانند رفتن از افقی به عمودی یا بالعکس فراخوانی می شود. سایر تغییرات جهت گیری باعث ایجاد تفریح ​​در زنجیره مبادله نمی شود. برای مثال، تغییر از منظره به منظره معکوس باعث ایجاد آن نمی‌شود، و به آن نیاز است که کامپوزیتور اندروید برای برنامه شما حرکت کند.

مدیریت تغییر جهت

برای مدیریت تغییر جهت، زمانی که متغیر orientationChanged روی true تنظیم شده است، روال تغییر جهت را در بالای حلقه رندر اصلی فراخوانی کنید. به عنوان مثال:

bool VulkanDrawFrame() {
 if (orientationChanged) {
   OnOrientationChange();
}

شما تمام کارهای لازم برای ایجاد مجدد swapchain را در تابع OnOrientationChange() انجام می دهید. این بدان معنی است که شما:

  1. هر نمونه موجود از Framebuffer و ImageView را از بین ببرید،

  2. همزمان با از بین بردن swapchain قدیمی (که در ادامه به آن پرداخته خواهد شد)، swapchain را دوباره ایجاد کنید.

  3. با DisplayImages جدید swapchain، فریم بافرها را دوباره بسازید. توجه: تصاویر پیوست (مثلاً تصاویر عمقی/استنسیل) معمولاً نیازی به بازسازی ندارند زیرا بر اساس وضوح هویت تصاویر زنجیره مبادله از قبل چرخانده شده اند.

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

در بخش قبل به بازآفرینی swapchain اشاره کردیم. اولین گام برای انجام این کار شامل بدست آوردن ویژگی های جدید سطح رندر است:

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);

محاسبه واقعی وسعت viewport در طول ضبط بافر فرمان به صورت زیر است:

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);

در نظر گرفتن - مشتقات Shader Fragment

اگر برنامه شما از محاسبات مشتق شده مانند dFdx و dFdy استفاده می‌کند، ممکن است برای محاسبه سیستم مختصات چرخانده به تبدیل‌های اضافی نیاز باشد زیرا این محاسبات در فضای پیکسلی اجرا می‌شوند. این امر مستلزم آن است که برنامه برخی از نشانه‌های PreTransform را به سایه‌زن قطعه ارسال کند (مانند یک عدد صحیح که جهت‌گیری دستگاه فعلی را نشان می‌دهد) و از آن برای ترسیم صحیح محاسبات مشتق استفاده کند:

  • برای یک قاب از پیش چرخش 90 درجه
    • dFdx باید به dFdy نگاشت شود
    • dFdy باید به -dFdx نگاشت شود
  • برای یک قاب از پیش چرخش 270 درجه
    • dFdx باید به -dFdy نگاشت شود
    • dFdy باید به dFdx نگاشت شود
  • برای یک قاب از پیش چرخش 180 درجه ،
    • dFdx باید به -dFdx نگاشت شود
    • dFdy باید به -dFdy نگاشت شود

نتیجه گیری

برای اینکه برنامه شما بیشترین استفاده را از Vulkan در اندروید داشته باشد، اجرای پیش چرخش ضروری است. مهمترین نکات این مقاله عبارتند از:

  • اطمینان حاصل کنید که در طول ایجاد یا بازآفرینی swapchain، پرچم پیش تبدیل با پرچم بازگردانده شده توسط سیستم عامل Android مطابقت داشته باشد. با این کار از سربار کامپوزیتور جلوگیری می شود.
  • اندازه swapchain را با وضوح هویت سطح پنجره برنامه در جهت طبیعی نمایشگر ثابت نگه دارید.
  • ماتریس MVP را در فضای کلیپ بچرخانید تا جهت گیری دستگاه ها را در نظر بگیرید، زیرا وضوح/وسعت swapchain دیگر با جهت نمایشگر به روز نمی شود.
  • در صورت نیاز، مستطیل های دید و قیچی را به روز کنید.

نمونه برنامه: حداقل پیش چرخش اندروید