本文介绍了如何通过实现预旋转来高效处理 Vulkan 应用中的设备旋转。
借助 Vulkan,您可以 与 OpenGL 相比,可以指定更多有关渲染状态的信息。 使用 Vulkan 时,您必须显式实现由驱动程序处理的内容, OpenGL,例如设备方向及其与 Android 可通过三种方式 处理使设备的渲染 Surface 与设备屏幕方向协调一致:
- Android 操作系统可以使用设备的显示处理器 (DPU), 它可以高效地处理硬件中的 Surface 旋转。播放设备 受支持的设备。
- Android OS 可以通过添加合成器通道来处理 Surface 旋转。这个 会产生性能成本,具体取决于合成器处理 旋转输出图像。
- 应用本身可以通过渲染 将图片旋转至与 显示屏上。
您应该使用下面哪种方法?
目前,应用还不知道表面是否旋转 也完全免费即使有 DPU 会为您处理 Surface 旋转,仍然可能会使性能大幅降低。如果您的应用受 CPU 限制,这会造成电源问题,因为 Android 合成器(通常以 频率已提升。如果您的应用受到 GPU 的限制,Android 合成器也会抢占应用的 GPU 工作,导致额外的性能损失。
我们发现,在 Pixel 4XL 上推出推出后的应用或游戏时,我们发现 SurfaceFlinger(促成 Android 运行线程的 合成器):
定期抢占应用的工作,占用 1-3 毫秒的时间 帧时间命中次数
给 GPU 的 因为合成器必须读取整个 帧缓冲区来完成合成工作。
以适当方式处理屏幕方向几乎能够完全阻止 SurfaceFlinger 抢占 GPU,并且 GPU 频率会下降 40%,因为 Android 合成器不再需要以加快的频率运行。
为了确保表面旋转得到正确处理,并且开销 如前面的案例所示,您应该实现方法 3。 这称为“预旋转”。这会告知 Android OS 您的应用 负责处理表面旋转。为此,您可以通过在交换链创建期间传递指定屏幕方向的 Surface 转换标志。这会停止 Android 合成器执行旋转操作。
对于每个 Vulkan 来说,了解如何设置 Surface 转换标志非常重要 应用。应用往往支持多种屏幕方向 或支持呈现表面位于不同方向的单一方向 与设备认为自身的方向一致。竖版手机上的只支持横向显示的应用,或在横版平板电脑上的只支持纵向显示的应用。
修改 AndroidManifest.xml
如需在应用中处理设备旋转,请先更改应用的 AndroidManifest.xml
文件,以告知 Android 您的应用将处理屏幕方向和屏幕尺寸变化。这可以防止 Android 在屏幕方向发生变化时执行以下操作:销毁并重新创建 Android Activity
,并对现有窗口Surface 调用 onDestroy()
函数。向 activity 的 configChanges
部分添加 orientation
(用于支持 API 13 以下级别)和 screenSize
属性,可以做到这一点:
<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
结构,我们用它来存储应用窗口 Surface 在屏幕自然方向的上述自身分辨率。
检测设备屏幕方向变化(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
。我们存储这个
签入 orientationChanged
,它是一个 boolean
,可通过
应用的主渲染循环
检测设备屏幕方向的变化(Android 10 以下版本)
对于搭载 Android 10 或更低版本的设备,
需要进行实现,因为 VK_SUBOPTIMAL_KHR
不受支持。
使用轮询
在搭载 Android 10 之前版本的设备上,您可以每隔 1 次轮询一次当前设备转换
pollingInterval
帧,其中 pollingInterval
是由确定的粒度
由程序员生成为此,可调用 vkGetPhysicalDeviceSurfaceCapabilitiesKHR()
,然后将返回的 currentTransform
字段与当前存储的 Surface 转换(在此代码示例中,存储在 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()
需要 .120-.250 毫秒,在搭载 Android 8 的 Pixel 1XL 上,轮询需要 .110-.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()
函数。这意味着:
销毁
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,以表示您已处理屏幕方向变化。
重新创建交换链
在上一部分中,我们提到需要重新创建交换链。如要实现该目标,第一步需要获取呈现 Surface 的新特征:
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
变量(设置为 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
变量用于定义视口左上角的坐标,而 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 着色器导数
如果您的应用使用的是导数计算(例如 dFdx
和 dFdy
),则可能需要进行其他转换才能将旋转后的坐标系统纳入考虑,因为这些计算是在像素空间中执行的。为此,需要应用将 preTransform 的某些指示元素(例如表示当前设备屏幕方向的整数)传递到 fragment 着色器中,并使用相应指示元素来适当映射导数计算:
- 对于 90 度的预旋转帧
- dFdx 必须映射到 dFdy
- dFdy 必须映射到 -dFdx
- 对于 270 度的预旋转帧
- dFdx 必须映射到 -dFdy
- dFdy 必须映射到 dFdx
- 对于 180 度的预旋转帧
- dFdx 必须映射到 -dFdx
- dFdy 必须映射到 -dFdy
总结
为了使应用在 Android 上充分利用 Vulkan,必须实现预旋转。本文中最重要的内容如下:
- 确保在交换链创建或重新创建期间, 与 Android 操作系统返回的标记相匹配。这样可以避免 合成器开销
- 将交换链大小固定为应用窗口的身份分辨率 自然屏幕方向的表面。
- 在裁剪空间中旋转 MVP 矩阵,以考虑设备屏幕方向, 因为交换链分辨率/范围不再随方向更新, 。
- 根据应用的需要更新视口和剪刀矩形。