Vulkan の事前回転でデバイスの向きを処理する

この記事では、事前回転を実装して、Vulkan アプリでデバイスの回転を効率的に処理する方法について説明します。

Vulkan を使用すると、 OpenGL の場合よりも多くの情報を指定できます。 Vulkan では、ドライバが処理するものを Vulkan で OpenGL(デバイスの向きや、 レンダリング サーフェスの向き。Android で Android の デバイスのレンダリング サーフェスとデバイスの向きを調整するハンドル:

  1. Android OS は、デバイスのディスプレイ プロセッシング ユニット(DPU)を使用できます。 ハードウェアでサーフェスの回転を効率的に処理できます提供日 のみサポートされています。
  2. Android OS は、コンポジタパスを追加することでサーフェスの回転を処理できます。この コンポジタの処理方法に応じてパフォーマンス コストが変わります。 行います。
  3. アプリケーション自体がサーフェスをレンダリングすることで、サーフェスの回転を処理できます。 画面の現在の向きと一致するレンダリング サーフェスに画像を回転させます。 クリックします。

どの方法を使用すればよいですか。

現時点では、サーフェスの回転が 無料です。アプリに対してそのような処理を行う DPU があるとしても、測定可能な量のパフォーマンス コストが依然として存在する可能性があります。アプリケーションが CPU の制約を受けている場合、 Android コンポジターによる GPU 使用量の増加。 向上しますアプリが GPU バウンドである場合は、Android コンポジターがアプリの GPU 処理のプリエンプトも行うので、さらにパフォーマンスが低下することがあります。

Google Pixel 4 XL で出荷タイトルを運用する場合、 SurfaceFlinger が動作し、Android アプリの Compositor):

  • アプリケーションの処理を定期的にプリエンプトし、1 ~ 3 ミリ秒かかる フレーム時間までのヒット数、

  • GPU の負荷が高くなる コンポジターは頂点またはテクスチャ メモリ全体を読み取る必要があるため、 フレームバッファを使って、合成処理を行います。

向きを適切に処理すると、SurfaceFlinger による GPU のプリエンプションがほぼ完全に停止する一方で、GPU 周波数は 40% 低下します。これは、Android コンポジターが使用するブースト周波数が不要になるためです。

サーフェスの回転ができるだけ少ないオーバーヘッドで適切に処理されるようにするには、 上記の例のように、メソッド 3 を実装する必要があります。 これは事前回転と呼ばれます。これにより、アプリが サーフェスの回転を処理します。これを行うには、スワップチェーンの作成時に、向きを指定するサーフェス変換フラグを渡します。これにより、停止 Android コンポジタが自身で回転を実行しないようにする。

サーフェス変換フラグの設定方法を知ることは、すべての Vulkan で重要 説明します。アプリケーションは複数の画面の向きをサポートする傾向がある レンダリング サーフェスが異なる 1 つの向きで 方向性をデバイスが認識したものになります。たとえば、本来は縦向きのスマートフォンにおける横向き専用アプリと、本来は横向きのタブレットにおける縦向き専用アプリを考えてみてください。

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 を使用して画面の向きを修正する場合 属性がある場合、この操作を行う必要はありません。また、アプリケーションで固定の スワップチェーンをセットアップする必要があるのは、 アプリの起動/再開。

本来の画面解像度とカメラ パラメータを取得する

次に、デバイスの画面解像度を検出します。 VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR 値に関連付けられている必要があります。この解像度はデバイスの本来の向きと関連付けられているため、スワップチェーンを常にこの解像度に設定する必要があります。最も これを取得する確実な方法は、 アプリケーション起動時の 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。この結果を保存して チェックイン orientationChangedboolean メイン レンダリング ループを介して行われます。

デバイスの向きの変更を検出する(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.250 ミリ秒でした。また、Android 8 を搭載した Pixel 1XL では、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 コンポジタが切り替えを 説明します。

向きの変更を処理する

向きの変更を処理するには、orientationChanged 変数が true に設定されているときに、メイン レンダリング ループの冒頭で向きの変更ルーチンを呼び出します。例:

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

スワップチェーンを再作成するために必要なすべての作業は、 OnOrientationChange() 関数を使用します。具体的には、次のようになります。

  1. FramebufferImageView の既存のインスタンスを破棄します。

  2. 破棄中にスワップチェーンを再作成 古いスワップチェーン(これについては後述)

  3. 新しいスワップチェーンの 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 にリセットして、向きの変更の処理を終えたことを示します。

スワップチェーンの再作成

前のセクションでは、スワップチェーンを再作成する必要があることを説明しました。そのための最初のステップは、レンダリング サーフェスの新しい特性を取得することです。

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 フィールドには、pretransformFlag 変数(surfaceCapabilities の currentTransform フィールドに設定されています)が入力されます。また、破棄されるスワップチェーンに 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;

考慮事項 - 非全画面表示のビューポートとシザー

アプリが非全画面表示のビューポート / シザー領域を使用している場合は、デバイスの向きに応じてそれらを更新する必要があります。そのためには、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 変数はビューポートの左上隅の座標を定義し、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 を最大限に活用するためには、事前回転を実装することが必要です。この記事の最も重要な結論は次のとおりです。

  • スワップチェーンの作成中または再作成中に、事前変換フラグが Android オペレーティング システムから返されるフラグと一致するように設定します。これにより コンポジタのオーバーヘッドが生じます
  • スワップチェーン サイズをアプリ ウィンドウの ID 解像度に固定する 画面の自然な向きに合わせます。
  • デバイスの向きを考慮してクリップ空間で MVP マトリックスを回転させます。 スワップチェーンの解像度/範囲が向きに合わせて更新されなくなったため クリックします。
  • アプリケーションの必要に応じて、ビューポートとシザーの長方形を更新します。

サンプルアプリ: 最小限の Android の事前回転