จัดการการวางแนวอุปกรณ์ด้วยการหมุนล่วงหน้าของ Vulkan

บทความนี้อธิบายวิธีจัดการการหมุนอุปกรณ์อย่างมีประสิทธิภาพในแอปพลิเคชัน Vulkan โดยการใช้การหมุนล่วงหน้า

Vulkan ช่วยให้คุณระบุข้อมูลเกี่ยวกับสถานะการแสดงผลได้มากกว่า OpenGL มาก เมื่อใช้ Vulkan คุณต้องใช้สิ่งต่างๆ ที่ไดรเวอร์จัดการใน OpenGL อย่างชัดเจน เช่น การวางแนวอุปกรณ์และความสัมพันธ์กับการวางแนวพื้นผิวการแสดงผล Android มี 3 วิธีในการจัดการการปรับพื้นผิวการแสดงผลของอุปกรณ์ให้สอดคล้องกับการวางแนวของอุปกรณ์

  1. ระบบปฏิบัติการ Android สามารถใช้หน่วยประมวลผลการแสดงผล (DPU) ของอุปกรณ์ ซึ่งจัดการการหมุนพื้นผิวในฮาร์ดแวร์ได้อย่างมีประสิทธิภาพ พร้อมใช้งานใน อุปกรณ์ที่รองรับเท่านั้น
  2. ระบบปฏิบัติการ Android สามารถจัดการการหมุนพื้นผิวได้โดยการเพิ่มการส่งผ่าน Compositor การดำเนินการนี้ จะมีค่าใช้จ่ายด้านประสิทธิภาพขึ้นอยู่กับวิธีที่เครื่องมือทำ Composite ต้องจัดการ กับการหมุนรูปภาพเอาต์พุต
  3. แอปพลิเคชันสามารถจัดการการหมุนพื้นผิวได้ด้วยการแสดงผลรูปภาพที่หมุนแล้วบนพื้นผิวการแสดงผลที่ตรงกับการวางแนวปัจจุบันของจอแสดงผล

คุณควรใช้วิธีใด

ปัจจุบันแอปพลิเคชันไม่สามารถทราบได้ว่าการหมุนพื้นผิวที่จัดการภายนอกแอปพลิเคชันจะฟรีหรือไม่ แม้ว่าจะมี DPU ที่ดูแลเรื่องนี้ให้คุณ แต่ก็ยังอาจมีค่าปรับด้านประสิทธิภาพที่วัดได้ ที่คุณต้องจ่าย หากแอปพลิเคชันของคุณขึ้นอยู่กับ CPU ปัญหานี้จะกลายเป็นปัญหาด้านพลังงานเนื่องจาก Android Compositor ใช้ GPU มากขึ้น ซึ่งมักจะทำงานที่ ความถี่ที่เพิ่มขึ้น หากแอปพลิเคชันของคุณขึ้นอยู่กับ GPU คอมโพสิตเตอร์ Android ก็สามารถขัดจังหวะการทำงานของ GPU ของแอปพลิเคชันได้เช่นกัน ซึ่งจะทำให้ประสิทธิภาพลดลงเพิ่มเติม

เมื่อเรียกใช้การจัดส่งใน Pixel 4XL เราพบว่า SurfaceFlinger (งานที่มีลำดับความสำคัญสูงกว่าซึ่งขับเคลื่อน Android Compositor) มีลักษณะดังนี้

  • ขัดจังหวะการทำงานของแอปพลิเคชันเป็นประจำ ทำให้เกิดการเข้าชม 1-3 มิลลิวินาที ในเวลาเฟรม และ

  • เพิ่มแรงกดดันต่อหน่วยความจำของจุดยอด/พื้นผิวของ GPU เนื่องจาก Compositor ต้องอ่านทั้งเฟรมบัฟเฟอร์เพื่อทำงานการจัดองค์ประกอบ

การจัดการการวางแนวอย่างเหมาะสมจะหยุดการขัดจังหวะชั่วคราว GPU โดย SurfaceFlinger เกือบทั้งหมด ในขณะที่ความถี่ของ GPU จะลดลง 40% เนื่องจากไม่จำเป็นต้องใช้ความถี่ที่เพิ่มขึ้นซึ่งใช้โดยตัวจัดองค์ประกอบของ Android อีกต่อไป

คุณควรใช้วิธีที่ 3 เพื่อให้มั่นใจว่าการหมุนพื้นผิวได้รับการจัดการอย่างถูกต้องโดยมีค่าใช้จ่ายน้อยที่สุดเท่าที่จะเป็นไปได้ ดังที่เห็นในกรณีข้างต้น ซึ่งเรียกว่าการหมุนเวียนล่วงหน้า ซึ่งจะบอกระบบปฏิบัติการ Android ว่าแอปของคุณ จัดการการหมุนพื้นผิว คุณทำได้โดยส่งแฟล็กการเปลี่ยนรูปแบบพื้นผิว ที่ระบุการวางแนวในระหว่างการสร้าง Swapchain ซึ่งจะหยุดไม่ให้ Compositor ของ Android ทำการหมุนด้วยตัวเอง

การทราบวิธีตั้งค่าแฟล็กการแปลงพื้นผิวเป็นสิ่งสำคัญสำหรับแอปพลิเคชัน Vulkan ทุกแอปพลิเคชัน โดยทั่วไปแล้ว แอปพลิเคชันมักจะรองรับการวางแนวหลายแบบ หรือรองรับการวางแนวแบบเดียวที่พื้นผิวการแสดงผลมีการวางแนวแตกต่าง จากการวางแนวที่อุปกรณ์ถือเป็นเอกลักษณ์ของตน เช่น แอปพลิเคชันที่ใช้ได้เฉพาะในแนวนอนบนโทรศัพท์ที่ระบุตัวตนเป็นแนวตั้ง หรือแอปพลิเคชันที่ใช้ได้เฉพาะในแนวตั้ง บนแท็บเล็ตที่ระบุตัวตนเป็นแนวนอน

แก้ไข AndroidManifest.xml

หากต้องการจัดการการหมุนอุปกรณ์ในแอป ให้เริ่มด้วยการเปลี่ยนไฟล์ AndroidManifest.xml ของแอปพลิเคชันเพื่อบอก Android ว่าแอปของคุณจะจัดการการวางแนวและการเปลี่ยนแปลงขนาดหน้าจอ ซึ่งจะป้องกันไม่ให้ Android ทำลายและสร้างActivity ของ Android ขึ้นใหม่ รวมถึงเรียกใช้ฟังก์ชัน 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 ขึ้นไป 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 มิลลิวินาที

การใช้การโทรกลับ

ตัวเลือกที่ 2 สำหรับอุปกรณ์ที่ใช้ 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 องศา เช่น การเปลี่ยนจากแนวนอนเป็นแนวตั้งหรือในทางกลับกัน การเปลี่ยนแปลงการวางแนวอื่นๆ จะไม่ทริกเกอร์การสร้าง Swapchain ใหม่ เช่น การเปลี่ยนจากแนวนอนเป็นแนวนอนกลับด้านจะไม่ทริกเกอร์การหมุนดังกล่าว ซึ่งทำให้ Compositor ของ Android ต้องพลิกแอปพลิเคชันของคุณ

การจัดการการเปลี่ยนแปลงการวางแนว

หากต้องการจัดการการเปลี่ยนแปลงการวางแนว ให้เรียกใช้กิจวัตรการเปลี่ยนแปลงการวางแนวที่ด้านบนของลูปการแสดงผลหลักเมื่อตั้งค่าorientationChanged ตัวแปรเป็นจริง เช่น

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

คุณต้องทำงานทั้งหมดที่จำเป็นเพื่อสร้าง Swapchain ใหม่ภายในฟังก์ชัน OnOrientationChange() ซึ่งหมายความว่าคุณ

  1. ทำลายอินสแตนซ์ที่มีอยู่ของ Framebuffer และ ImageView

  2. สร้าง Swapchain ใหม่ขณะทำลาย Swapchain เก่า (ซึ่งจะกล่าวถึงในส่วนถัดไป) และ

  3. สร้าง Framebuffer ใหม่ด้วย DisplayImage ของ Swapchain ใหม่ หมายเหตุ: โดยปกติแล้วไม่จำเป็นต้องสร้างรูปภาพที่แนบมา (เช่น รูปภาพความลึก/ลายฉลุ) ใหม่ เนื่องจากรูปภาพเหล่านี้อิงตามการแก้ปัญหาข้อมูลระบุตัวตนของรูปภาพ 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 ใหม่

ในส่วนก่อนหน้า เราได้กล่าวถึงการสร้าง 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 ที่อัปเดตนี้ ไปยัง Vertex Shader และใช้ตามปกติโดยไม่ต้องแก้ไข Shader

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 และกรรไกรที่ไม่ใช่แบบเต็มหน้าจอ

หากแอปพลิเคชันของคุณใช้ Viewport/กรรไกรตัดที่ไม่ใช่แบบเต็มหน้าจอ คุณจะต้องอัปเดตตามการวางแนวของอุปกรณ์ ซึ่งคุณต้องเปิดใช้ตัวเลือก 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 ไปยัง Fragment Shader (เช่น จำนวนเต็มที่แสดงการวางแนวอุปกรณ์ปัจจุบัน) และใช้เพื่อแมปการคำนวณอนุพันธ์อย่างเหมาะสม

  • สำหรับเฟรมที่หมุนล่วงหน้า90 องศา
    • ต้องแมป dFdx กับ dFdy
    • ต้องแมป dFdy กับ -dFdx
  • สำหรับเฟรมที่หมุนล่วงหน้า270 องศา
    • dFdx ต้องแมปกับ -dFdy
    • ต้องแมป dFdy กับ dFdx
  • สำหรับเฟรมที่หมุนไว้ล่วงหน้า180 องศา
    • ต้องแมป dFdx กับ -dFdx
    • ต้องแมป dFdy กับ -dFdy

บทสรุป

หากต้องการให้แอปพลิเคชันของคุณใช้ Vulkan บน Android ได้อย่างเต็มประสิทธิภาพ คุณต้องใช้การหมุนก่อน ประเด็นสำคัญที่สุดจากบทความนี้มีดังนี้

  • ตรวจสอบว่าในระหว่างการสร้างหรือสร้าง Swapchain ใหม่ ให้ตั้งค่าแฟล็ก pretransform ให้ตรงกับแฟล็กที่ระบบปฏิบัติการ Android ส่งคืน ซึ่งจะช่วยหลีกเลี่ยง ค่าใช้จ่ายของ Compositor
  • กำหนดขนาด Swapchain ให้คงที่ตามความละเอียดของหน้าต่างพื้นผิวของแอปในแนวนอนของจอแสดงผล
  • หมุนเมทริกซ์ MVP ในพื้นที่คลิปเพื่อพิจารณาการวางแนวของอุปกรณ์ เนื่องจากความละเอียด/ขอบเขตของ Swapchain จะไม่อัปเดตตามการวางแนว ของจอแสดงผลอีกต่อไป
  • อัปเดตรูปสี่เหลี่ยมผืนผ้าของ Viewport และ Scissor ตามที่แอปพลิเคชันของคุณต้องการ

แอปตัวอย่าง: การหมุนล่วงหน้าของ Android ที่มีขนาดเล็กที่สุด