Vulkan 사전 회전으로 기기 방향 처리

이 문서에서는 사전 회전을 구현하여 Vulkan 애플리케이션의 기기 회전을 효율적으로 처리하는 방법을 설명합니다.

Vulkan을 사용하면 다음 작업을 할 수 있습니다. OpenGL을 사용할 때보다 렌더링 상태에 관해 훨씬 더 많은 정보를 지정할 수 있습니다. Vulkan의 경우 기기 방향 및 OpenGL과 렌더링 표면 방향을 정의합니다. Android는 세 가지 방법으로 기기 방향에 따라 기기의 렌더링 표면을 조정하는 작업을 처리합니다.

  1. Android OS는 기기의 디스플레이 처리 장치 (DPU)를 사용할 수 있으며, 하드웨어에서 표면 회전을 효율적으로 처리할 수 있습니다. 시청 가능한 곳 지원되는 기기만 사용할 수 있습니다.
  2. Android OS는 컴포지터 패스를 추가하여 노출 영역 회전을 처리할 수 있습니다. 이 컴포지터가 컴포지터를 처리하는 방법에 따라 성능 비용이 발생합니다. 출력 이미지 회전
  3. 애플리케이션 자체는 이미지를 현재 방향과 일치하는 렌더링 표면으로 합니다.

다음 중 어떤 방법을 사용해야 하나요?

현재는 애플리케이션이 노출 영역 회전을 알 수 있는 방법이 없습니다. 무료로 처리됩니다. 이를 처리하기 위한 DPU가 있더라도 측정 가능한 성능에 불이익이 있을 가능성이 있습니다. 애플리케이션이 CPU의 제약을 받는 경우에는 GPU 사용량 증가를 막을 수 있습니다. Android Compositor는 게재빈도를 높일 수 있습니다. 애플리케이션이 GPU에 종속되면 Android Compositor는 애플리케이션의 GPU 작업을 선점하여 추가 성능 손실이 발생할 수도 있습니다.

Pixel 4XL에서 배송 서비스를 출시하면서 SurfaceFlinger (Android를 구동하는 높은 우선순위의 작업)는 컴포지터):

  • 정기적으로 애플리케이션 작업을 선점하여 1~3ms 소요 프레임 시간을 측정합니다.

  • GPU의 압력을 증가시켜 꼭짓점/텍스처 메모리에 저장됩니다. 이는 컴포지터가 전체 이미지를 읽어야 하기 때문입니다. framebuffer를 사용하여 합성 작업을 실행합니다.

방향을 제대로 처리하면 SurfaceFlinger에 의한 GPU 선점이 거의 중지되는 반면, Android Compositor에서 사용하는 부스트 주파수는 더 이상 필요하지 않으므로 GPU 주파수는 40% 감소합니다.

표면 회전이 최소한의 오버헤드로 적절하게 처리되도록 하기 위해 메서드 3을 구현해야 합니다. 이를 사전 회전이라고 합니다. 이렇게 하면 Android OS에서 이 표면 회전을 처리합니다. 또한 swapchain 생성 중에 방향을 지정하는 표시 영역 변환 플래그를 전달하여 수행할 수 있습니다. 이로 인해 중지됨 Android Compositor가 자체 회전을 실행하지 않음

노출 영역 변환 플래그를 설정하는 방법을 아는 것은 모든 Vulkan에 중요합니다. 애플리케이션입니다. 애플리케이션은 여러 방향을 지원하는 경향이 있으며 렌더링 표면이 다른 위치에 있는 단일 방향을 지원할 수 있습니다. 기기의 ID 방향이라고 간주합니다. 예를 들어 세로 모드 휴대전화에서 가로 모드 전용 애플리케이션이나 가로 모드 태블릿의 세로 모드 전용 애플리케이션은 사용할 수 없습니다.

AndroidManifest.xml 수정

앱에서 기기 회전을 처리하려면 먼저 앱의 AndroidManifest.xml 파일을 변경하여 Android에 앱이 방향과 화면 크기 변경을 처리한다고 알립니다. 이렇게 하면 Android가 Android Activity를 삭제 및 재생성하고 방향 변경이 발생할 때 기존 창 표시 영역에서 onDestroy() 함수를 호출하지 못하도록 합니다. orientation(API 수준 13 미만 지원) 및 screenSize 속성을 활동의 configChanges 섹션에 추가하여 이 작업을 수행합니다.

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

애플리케이션에서 screenOrientation를 사용하여 화면 방향을 수정하는 경우 속성을 사용하지 않아도 됩니다. 또한 애플리케이션이 고정 swapchain을 한 번만 설정하면 됩니다. 애플리케이션 시작/재개

ID 화면 해상도 및 카메라 매개변수 가져오기

다음으로 기기의 화면 해상도를 감지합니다. VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR 값과 연결되어 있습니다. 이 해결 방법은 기기의 기본 방향에 연결되므로 swapchain이 항상 설정되어야 합니다. 가장 얻기 위한 신뢰할 수 있는 방법은 애플리케이션 시작 시 vkGetPhysicalDeviceSurfaceCapabilitiesKHR() 및 반환된 범위를 저장합니다. 너비와 높이를 currentTransform가 반환됩니다. ID 화면 해상도:

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 버전은 vkQueuePresentKHR()VK_SUBOPTIMAL_KHR입니다. 우리는 이 다음에서 액세스할 수 있는 booleanorientationChanged에 체크인 애플리케이션의 기본 렌더링 루프에 머무릅니다.

기기 방향 변경 감지 (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;
}

Android 10을 실행하는 Pixel 4에서 vkGetPhysicalDeviceSurfaceCapabilitiesKHR() 폴링은 0.120~0.250ms 사이에 발생했으며, Android 8을 실행하는 Pixel 1XL에서 폴링은 0.110~0.350ms가 걸렸습니다.

콜백 사용

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 재생성을 트리거하지 않습니다. 예를 들어 가로 모드에서 반전으로 변경하면 Android 컴포지터가 애플리케이션입니다.

방향 변경 처리

방향 변경을 처리하려면 orientationChanged 변수가 true로 설정되었을 때 기본 렌더링 루프 상단에서 방향 변경 루틴을 호출합니다. 예를 들면 다음과 같습니다.

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

내에서 swapchain을 다시 만드는 데 필요한 모든 작업을 실행합니다. OnOrientationChange() 함수 이는 다음을 의미합니다.

  1. FramebufferImageView의 기존 인스턴스 폐기

  2. 소멸 중에 swapchain 다시 만들기 이전의 swapchain (뒤에서 설명)

  3. 새 swapchain의 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 재생성

이전 섹션에서 swapchain을 다시 만들어야 한다고 언급했습니다. 이를 위한 첫 번째 단계는 렌더링 표시 영역의 새로운 특성을 얻는 것입니다.

void createSwapChain(VkSwapchainKHR oldSwapchain) {
   VkSurfaceCapabilitiesKHR capabilities;
   vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevice, surface, &capabilities);
   pretransformFlag = capabilities.currentTransform;

VkSurfaceCapabilities 구조에 새 정보가 채워져 있으면 이제 currentTransform 필드를 확인하여 방향 변경이 발생했는지 확인할 수 있습니다. 나중에 MVP 매트릭스를 조정할 때 필요하므로 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 필드는 surfaceCapabilities의 currentTransform 필드로 설정된 pretransformFlag 변수로 채워집니다. 또한 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;

고려 사항 - 전체 화면이 아닌 표시 영역 및 Scissor

애플리케이션이 전체 화면이 아닌 표시 영역/Scissor 영역을 사용 중인 경우 기기의 방향에 따라 이 영역을 업데이트해야 합니다. 이를 위해서는 Vulkan 파이프라인 생성 중에 동적 표시 영역 및 Scissor 옵션을 사용 설정해야 합니다.

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

xy 변수는 표시 영역의 왼쪽 상단 모서리의 좌표를 정의하고, wh 변수는 각 표시 영역의 너비와 높이를 정의합니다. 동일한 계산을 사용하여 시저 테스트를 설정할 수도 있으며 자세한 내용은 여기를 참조하세요.

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

고려 사항 - 프래그먼트 셰이더 파생

애플리케이션이 dFdxdFdy와 같은 파생 계산을 사용하는 경우, 이러한 계산은 픽셀 공간에서 실행되므로 회전된 좌표계를 고려하여 추가 변환이 필요할 수 있습니다. 이 경우 앱에서 preTransform의 표시(예: 현재 기기 방향을 나타내는 정수)를 프래그먼트 셰이더에 전달하고 이를 사용하여 파생 계산을 올바르게 매핑해야 합니다.

  • 90도 사전 회전된 프레임
    • dFdxdFdy에 매핑되어야 함
    • dFdy-dFdx에 매핑해야 함
  • 270도 사전 회전된 프레임
    • dFdx-dFdy에 매핑해야 함
    • dFdydFdx에 매핑해야 함
  • 180도 사전 회전된 프레임
    • dFdx-dFdx에 매핑해야 함
    • dFdy-dFdy에 매핑해야 함

결론

Android에서 애플리케이션이 Vulkan을 최대한 활용하려면 사전 회전을 구현해야 합니다. 이 도움말에서 가장 중요한 점은 다음과 같습니다.

  • swapchain 생성 또는 재생성 중에 사전 변환 플래그가 Android 운영 체제에서 반환한 플래그와 일치하도록 설정됩니다. 이렇게 하면 컴포지터 오버헤드가 발생하지 않습니다
  • swapchain 크기를 앱 창의 ID 해상도로 고정 표면의 자연스러운 방향을 나타냅니다.
  • 기기 방향을 고려하여 클립 공간에서 MVP 매트릭스를 회전합니다. swapchain 해상도/범위가 더 이상 방향에 따라 업데이트되지 않기 때문입니다. 놓을 수 없습니다.
  • 애플리케이션의 필요에 따라 표시 영역 및 시저 직사각형을 업데이트합니다.

샘플 앱: 최소한의 Android 사전 회전