关于 Vulkan 初始化/取消初始化和 Android 应用程序生命周期事件的问题

Question about Vulkan Initialization / Deinitialization and Android App life cycle event

我研究了很多平台厂商提供的Vulkan示例应用程序(Qucomm Adreno SDK、PowerVR SDK、ARM Mali SDK和Google的android NDK示例)。我注意到所有示例都按照以下代码模式执行 Vulkan 初始化和取消初始化:

void android_main(struct android_app* androidApp)
{
    ...
    androidApp->onAppCmd = [](struct android_app* androidApp, int32_t cmd) -> void //Event handle (Lambda)
    {
        VulkanApp* app = (VulkanApp*)androidApp->userData;
        switch (cmd) 
        {
        case APP_CMD_INIT_WINDOW:   
            initVulkan(...);    //Initialize vulkan: layers, extension, instance, surface, device, swapchain, ...
            break;
        case APP_CMD_TERM_WINDOW:
            deinitVulkan(...);    //Deinitialize vulkan: in reversed order...
            break;
        ...
        }
        ...
    }
    ...
}

基本上,应用程序在 NDK 事件 APP_CMD_INIT_WINDOW 中初始化 Vulkan 组件,并在事件 APP_CMD_TERM_WINDOW 中销毁它们。这段代码在app被用户启动运行一段时间后被用户退出时还是比较合理的。


但是,当用户将 android 应用程序切换到后台(通过主页或菜单按钮)然后将其调回来多次时,配对事件 APP_CMD_TERM_WINDOWAPP_CMD_INIT_WINDOW 将被触发多次,因此函数 initVulkan()deinitVulkan() 会被多次调用。

在这种情况下,代码对我来说似乎是不合理的:既然应用程序只是暂时被推到后台又被带回前台,为什么我们要销毁所有 Vulkan 组件比如层、扩展、实例、设备、表面、交换链、管道……然后重新创建它们?最多,也许唯一需要重新创建的组件是交换链和管道。但为什么所有 SDK 的示例应用程序都执行这种繁重的娱乐活动?

顺便说一句,当我与其他平台上的 Vulkan 示例源代码进行比较时,例如 Windows、Linux、macOS 和 iOS、none 的性能如此繁重的娱乐活动。

我曾尝试使用 "initialize once" 解决方案,但 android 应用程序在从后台返回到前台时崩溃。

所以可能的问题是: 当 Android 应用程序在后台和前台之间切换时,我们是否必须销毁并重新创建所有 Vulkan 组件?如果没有,我们怎么办?

-----

更新: 我收到的关于我的问题的建议很少,我知道在 Android 应用程序的 "swapped-away" 期间,我们最好限制应用程序占用的系统资源(特别是在收到低内存警告时),同时, Vulkan 组件上的细粒度 pause/resume 机制有助于在轻内存使用和快速应用程序恢复之间保持良好平衡。

我查看了 Google NDK OpenGL ES "teapot" 示例,我注意到这个 NDK gl 示例使用高度查找调整机制来暂停/恢复 OpenGL 上下文: 在事件处理部分,代码如下:

    switch (cmd)
    {
        case APP_CMD_INIT_WINDOW:
            // The window is being shown, get it ready.
            if (app->window != NULL)
                eng->InitDisplay(app);
            break;
        case APP_CMD_TERM_WINDOW:
            // The window is being hidden or closed, clean it up.
            eng->TermDisplay();
            break;
        case APP_CMD_LOW_MEMORY:
            // Free up GL resources
            eng->TrimMemory();
            break;

这是函数 InitDisplay() 的代码:

int Engine::InitDisplay(android_app *app)
{
    if (!initialized_resources_)  // THIS IS FIRST TIME THE EVENT IS TRIGGERED WHEN APP IS LAUNCHED
    {
        gl_context_->Init(app_->window);    //Initialize OpenGL
        LoadResources();
        ...
    }
    else  // TRIGGERED WHEN APP IS BROUGHT BACK FROM BACKGROUND TO FOREGROUND
    {
        // On some devices, ANativeWindow is re-created when the app is resumed
        if (app->window != gl_context_->GetANativeWindow())
        {
            // Re-initialize ANativeWindow.
            assert(gl_context_->GetANativeWindow());
            UnloadResources();
            gl_context_->Invalidate();
            gl_context_->Init(app->window); //Initialize OpenGL again
            LoadResources();
            ...
        }
        // Normal case, only need to resume OpenGL
        else
        {
            // initialize OpenGL ES and EGL
            if (EGL_SUCCESS == gl_context_->Resume(app_->window))//Resume OpenGL
            {
                UnloadResources();
                LoadResources();
            }
            ...
        }
    }
    ...    

从代码中可以看出,在"good cases"的大部分情况下,只有一小部分OpenGL资源被卸载和重新加载;只有在 "bad cases" 下,OpenGL 上下文被完全销毁并重新创建,才能生成快速应用恢复。

所以我的问题可以扩展为:有谁知道一个好的 Vulkan/Android 模板应用程序,它使用这个 find-grined Vulkan 暂停/恢复机制?或者想分享您自己的代码来做到这一点?我目前正在为此工作,但进展不顺利。

Under this scenario the code appear to be unreasonable to me though: since the app is just temporarily pushed to background and brought back to foreground, why should we destroy all Vulkan components ...

定义 "temporary"。一个典型的用户可能有数十个应用程序打开但处于空闲状态并且 运行 在后台运行,如果它们都保留所有资源,那将是一个巨大的内存消耗。

使用图形的应用程序 API 几乎总是大量占用内存,因此期望后台应用程序释放资源供前台应用程序使用是合理的。

By the way, when I compare with Vulkan sample source code on other platforms, like Windows, Linux, macOS and iOS, none of them perform such heavy recreations.

iOS 不强制执行,但在开发人员最佳实践中强烈建议应用程序释放大量内存资源作为 applicationDidEnterBackground 处理程序的一部分。

其余的都是桌面平台,应用程序使用模型完全不同(打开 -> 关闭,不打开 -> 挂起),因此它们具有不同的编程模型也就不足为奇了。

在后台保留大部分 Vulkan 是可能的,但是你必须重新创建交换链(以及任何依赖对象,如交换链图像的图像视图)是对的,因为你回到前台时获得一个新的原生 window/surface。遗憾的是,我没有任何示例代码可供您参考。

为了简单起见,现有示例可能采用这种方式,因为在大多数其他平台上,不需要支持与其他顶级 Vulkan 对象具有不同生命周期的交换链。

实际上建议不要在进入后台时立即丢弃太多内存。但是如果没有,就要注意onTrimMemory回调,拿到大数据就释放。这允许您在用户短暂切换时快速恢复(并且无需耗电重新加载纹理等),但仍然允许系统回收内存,而无需在需要时完全杀死您的应用程序。

多亏了我的一些建议和this article at Gamasutra,我把所有的事情都说清楚了,终于找到了应用暂停和恢复期间Vulkan资源管理的完美解决方案。

基本思想是:应用程序从暂停状态恢复后,无需再次重新创建所有 Vulkan 资源。这样做会导致重新创建所有与 Vulkan 相关的应用程序资源,并且很难编码。只有以下 Vulkan 对象需要重新创建:

  • 表面
  • 渲染通道
  • 交换链及相关

NDK 事件处理代码应如下所示:

 switch (cmd)
    {
        case APP_CMD_INIT_WINDOW:
            if( this is triggered by app launch)
                initVulkan();
            else //This is triggered by app resumption
                resetVulkan(); // recreateSurface, RenderPass, Swapchain and related
            break;
            ...

我的 Vulkan 应用程序现在可以非常顺利地自行切换。