杂项功能

服务器状态

CloudXR 服务器通过 OpenVR 向其他进程报告其状态

auto *pSystem = vr::VR_Init(&eError, vr::VRApplication_Utility);
if (eError == vr::VRInitError_None)
{
   cxrServerState state = (cxrServerState)(pSystem->GetInt32TrackedDeviceProperty(
                 vr::k_unTrackedDeviceIndex_Hmd,
                     (vr::ETrackedDeviceProperty)cxrTrackedDeviceProperty::Prop_CloudXRServerState_Int32));
}

cxrServerState 枚举的定义位于 CloudXRCommon.h 中。

向服务器应用程序发送用户数据

CloudXR 客户端可以使用 cxrSendInputEvent() 客户端 API,并将 cxrGenericUserInputEvent 作为参数类型,向服务器上运行的应用程序发送任意数据。

数据被传递到服务器计算机,以内存映射文件的形式公开,并且对服务器上的每个进程都可见。以下示例展示了如何访问用户数据,并进行了适当的错误检查。

HANDLE hUserDataFile = OpenFileMapping(FILE_MAP_READ, FALSE, cxrUserDataFileName);

unsigned char *mappedUserData =
   (unsigned char*)MapViewOfFile(hUserDataFile, FILE_MAP_READ, 0, 0, cxrUserDataMaxSize);

HANDLE hUserDataMutex = CreateMutex(NULL, FALSE, cxrUserDataMutexName);

for (int i = 0; i < 1000; i++)
{
   WaitForSingleObject(hUserDataMutex, INFINITE);
   printf_s("%.*s\n", cxrUserDataMaxSize, mappedUserData);
   ReleaseMutex(hUserDataMutex);

   Sleep(500);
}

UnmapViewOfFile(mappedUserData);
CloseHandle(hUserDataFile);

注视点缩放

CloudXR 2.0 开始支持注视点缩放。注视点缩放考虑了观看者在视野外围较低的敏锐度,并使用可变缩放因子对这些区域进行降采样,其中纹素在中心处为 1:1,并向外边缘逐渐进行子采样。

在客户端上,此功能通过 -f N 命令行选项 启用,其中 N 是应用于客户端分辨率的比例,表示要降采样的目标大小。帧的某个区域将被保留且不进行降采样,其余部分会随着您靠近帧的边缘而逐渐缩放。因此,较大的比例因子会导致更大的流式帧分辨率,并具有更多未缩放的像素,而较小的比例因子将具有较少的原始质量像素。N 的建议默认值为 50,这意味着将 50% 的比例因子应用于提供的设备分辨率。有效值范围为 0(禁用)或 25-100(指定不同的降采样级别)。我们通常发现 50、60 或 70 足以用于测试您的内容并选择可接受的结果。

警告

在某些 Android 设备上,特别是 Meta Quest 2/3/Pro,注视点流式传输存在问题。当前默认设置使用 AImageReader 解码器,不进行注视点渲染。AImageReader + 注视点缩放目前 (CloudXR 4.0.1) 会导致扭曲效果,使其无法在实际应用中使用。如果需要注视点渲染,请使用 -disable-ir-decoder 标志禁用 AImageReader 解码器。

遗憾的是,这可能会导致出现模糊线条失真,这是 CloudXR 中 MediaCodec 解码器的已知问题。您必须为您的应用程序做出正确的权衡。

服务器中支持前向注视点渲染(实现为 DX11 着色器),而反向注视点渲染目前仅在 Windows DirectX、CUDA 客户端和 Android GLES 客户端中受支持。

计划在未来的 CloudXR 版本中支持具有眼动追踪功能的设备,届时将实时调整注视点中心。

流式传输 AR 内容

要使用 CloudXR 流式传输 AR 内容,作为 OpenVR 应用程序的服务器应用程序在左眼中提供包含 RGBA 数据的主场景。数据中的 alpha 通道指示应与实时摄像头内容混合的区域。右眼数据将被忽略,但由于 OpenVR 要求提交双眼数据,为了获得最佳性能,我们建议应用程序也提交左眼纹理作为右眼纹理,方法是使用相同的纹理句柄,或者选择性地为右眼提交一个小的虚拟纹理。

发送 AR 光照信息

Android CloudXR AR 客户端发送对颜色、主场景光线方向以及环境光球谐函数的估计。服务器上运行的 OpenVR 应用程序可以通过以下方式查询这些估计值

vr::ETrackedPropertyError error;
m_pHMD->GetArrayTrackedDeviceProperty(
   vr::k_unTrackedDeviceIndex_Hmd,
       (vr::ETrackedDeviceProperty)Prop_ArLightColor_Vector3,
       vr::k_unHmdVector3PropertyTag,
       &m_mainLightColor,
       sizeof(m_mainLightColor),
       &error);
m_pHMD->GetArrayTrackedDeviceProperty(
   vr::k_unTrackedDeviceIndex_Hmd,
       (vr::ETrackedDeviceProperty)Prop_ArLightDirection_Vector3,
       vr::k_unHmdVector3PropertyTag,
       &m_mainLightDir,
       sizeof(m_mainLightDir),
       &error);

音频支持

CloudXR 支持在服务器和客户端之间发送和接收音频数据。Windows 和 Android 示例客户端展示了如何使用 CloudXR cxrSendAudio() API 和 cxrClientCallbacks.RenderAudio() 回调来实现此目的。

注意

默认情况下,禁用从客户端向服务器发送音频。要启用此功能,必须设置 -sa 客户端选项和 -ra 服务器选项。有关更多信息,请参阅 命令行选项

CloudXR 文件存储

CloudXR 在许多地方需要输出日志或其他数据,因此需要一个“输出目录”来工作。从历史上看,CloudXR 一直使用硬编码的默认目录。在新版本中,我们已转向应用程序“拥有”目录路径,只需让 CloudXR 知道将内容放在哪里即可。从客户端来看,它在传递到 cxrCreateReceiver()cxrReceiverDesc 结构的 appOutputPath 成员中设置。一般来说,我们建议此路径指向类似 logs 子目录的路径,但命名和位置由您决定。

Windows 存储

在 Windows 上,示例代码使用标准方法查找 AppData 路径,然后创建一个特定于供应商和应用程序的路径,并在其下创建一个 logs 子目录。我们通常使用 NVIDIA 基础文件夹和 CloudXR 子文件夹作为根目录,然后使用基于客户端名称的特定于应用程序的文件夹。它看起来像这样

char userDir[CXR_MAX_PATH + 1] = "";
std::string appBaseDir, appOutputDir;
HRESULT result = SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, SHGFP_TYPE_CURRENT, userDir);
if (!SUCCEEDED(result) && !GetTempPath(sizeof(userDir), userDir))
    strcpy(userDir, "C:\Temp"); // super-fallback case.
appBaseDir = userDir;
appBaseDir += "\NVIDIA\CloudXR\SampleClient\";
appOutputDir = appBaseDir + "logs\";

之后,您有责任确保创建并可访问完整路径。

在调用 cxrCreateReceiver() 之前,作为接收器描述符的一部分,您提供您选择的输出路径。 类似

strncpy(desc.appOutputPath, appOutputDir.c_str(), CXR_MAX_PATH - 1);
desc.appOutputPath[CXR_MAX_PATH - 1] = 0;

所有其他日志、跟踪和捕获文件都将写入到该位置。

Android 分区存储

虽然将输出目录传递给接收器的新方法将更多控制权转移到所有平台上的应用程序,但此更改的驱动力是 Android 新的 [https://developer.android.com.cn/training/data-storage#scoped-storage] 分区存储功能,该功能限制(或“沙箱化”)应用程序可以在文件系统中读取/写入的位置。虽然最初在 Android 10 中作为选项引入,但在 Android 11 中默认强制执行(如果在 API 级别 < 29 的 Android 11 上,有一种方法可以请求“旧版”访问权限,请参阅 https://developer.android.com.cn/about/versions/11/privacy/storage#scoped-storage)。

从 Android 12 开始,分区存储现在对所有应用程序 100% 强制执行。这意味着我们被限制在应用程序的数据目录(或媒体目录中),并且我们无法再直接访问 sdcard 的根目录。这会影响读取 CloudXRLaunchOptions.txt 文件以设置运行时选项,以及将各种日志记录和捕获写入输出 logs 文件夹 - 这两者都必须在应用程序的目录中完成。但是,有一个问题:出于安全原因,应用程序数据目录在应用程序首次运行时才会创建。因此,如果您需要使用启动选项文件方法,则 必须 运行一次您的应用程序,并干净地退出。在那之后,然后 您可以将启动选项文件复制到应用程序数据目录,或指示 CloudXR 可以写入输出文件的路径。从那时起,CloudXR 的功能基本上与以前的版本相同,只是限制在其文件夹中。

处理 Android 数据

在强制执行分区存储的较新 Android 操作系统版本中,我们需要应用程序找到应用程序数据目录,并在 cxrCreateReceiver() 期间将其提供给 CloudXR。应用程序数据目录可以通过多种不同的方式获取,下面是两种方法,一种用于 Java,另一种用于纯原生。同样,实际文件夹在应用程序首次启动之前不存在。

在您实现 main 的纯原生应用程序中,并且 可以访问原始 android_app 结构(ARCore 和 OVR 示例就是这样),您可以轻松获取应用程序数据路径,因为它Activity 结构的成员。您可以执行以下操作

std::string outPath = mAndroidApp->activity->externalDataPath;
outPath += "/logs/";
strncpy(desc.appOutputPath, outPath.c_str(), CXR_MAX_PATH - 1);
desc.appOutputPath[CXR_MAX_PATH - 1] = 0;

如果您在以 Java 为中心的应用程序中,或者在 SDK 下的原生应用程序中,该 SDK 向您隐藏了 main(),因此您无法访问 android_app,您将需要使用 JNI 调用将路径从 Java 传递到 Native。在您的 Java 主活动中,在 OnCreate` 方法中,您可以调用以下代码来获取应用程序路径并将其传递下去

path = getExternalFilesDir(null).getAbsolutePath();
someJniNativeFunction(path);

ArCore 示例应用程序在 OnCreate 中,在对 :c:func:createNativeApplication 的调用中提供了此方法的示例。它在一行中完成,将 Java 中的绝对路径通过 JNI 函数传递到原生 C 代码中,该代码执行所有操作以将 Java 字符串转换为 C 字符串,并将其传递到主应用程序对象构造函数中。应用程序缓存该应用程序路径, 通过简单地将“/logs/”附加到应用程序路径来创建“输出路径”。

所有示例都使用类似的处理方式来稍后加载 CloudXRLaunchOptions.txt 文件,引用基本应用程序路径。这使应用程序可以灵活地在其操作系统设置的访问限制内,根据需要设置其根路径和输出路径。如上所述,输出路径随后被传递到 cxrCreateReceiver(),以便 CloudXR 知道原生网络库可以在哪里写入其日志,以及 CloudXR 可以在哪里输出 QoS 数据、视频流捕获、帧转储和任何其他调试/跟踪文件。

处理 iOS 数据

通常情况下,iOS 在各个方面都略有不同。拥有一个自定义的可读/可写目录公开可能难以设置和正确注册以供用户访问。

iOS 示例代码没有使用完全自定义的目录/树,而是简单地将基本文档目录用作其根目录,并在其中添加一个 logs 子文件夹。您可以在 CXRLogger.swift 中找到该代码,它看起来像这样

appBaseDirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
appLogDirURL = appBaseDirURL.appendingPathComponent("logs")
do {
    try FileManager.default.createDirectory(at: appLogDirURL, withIntermediateDirectories: true, attributes: nil)
} catch {
    logMsg(level: cxrLL_Error, tag: LOG_TAG, msg: "Failed to create the app logs directory, logging to console only.")
    return
}

显然,您可以扩展基本目录以附加您希望创建的任何层次结构,这只是一个简化的示例。当我们即将创建接收器时,我们需要传入日志/输出目录。对于示例,我们使用一些棘手的 Swift 代码从上面获取 appLogDirURL,并将指针编组并将字符串数据复制到我们的 C 数据结构中。它看起来像这样

let nsstring = cLogger.appLogDirURL.path as NSString
if let cstr = nsstring.utf8String {
    let _ = withUnsafeMutablePointer(to: &desc.appOutputPath) {
        $0.withMemoryRebound(to: Int8.self, capacity: Int(CXR_MAX_PATH-1)) { ptr in
            strncpy( ptr, cstr, Int(CXR_MAX_PATH-1))
        }
    }
}

如何在 Android 上启动

各种可配置的启动选项在 命令行选项 中进行了深入介绍,这些方法中的大多数也是如此。以下所有三种方法都使用相同的示例启动选项,以便更清楚地了解方法之间的差异。此外,如果您查看示例,您会注意到它们通常尝试先读取启动选项文件,然后 应用通过其他方式传递到应用程序中的任何选项 - 这允许设备上的“ baked” 设置被命令行(或其他提供的方式)覆盖。

对于以下示例,我们将使用虚构的项目名称 cxrtest(APK 名称),并且完全限定名称我们将假定为 com.nvidia.cxrtest

从 ADB 启动

从 ADB,您最终运行 am start 来启动您的应用程序。

adb shell am start -S -D -n cxrtest.app/com.nvidia.cxrtest.LaunchActivity --es args '"-s 192.168.1.1 -f 50"'

您需要指定要启动的 Activity,在本例中为 LaunchActivity,然后可以提供命令行选项以传递给应用程序,这些选项位于末尾的奇怪引号字符串内。

从 Android Studio 启动

在 Android Studio 中,您可以从“运行/调试配置”对话框中设置命令行选项。有一个名为“启动标志”的文本框,您可以指定传递给 Android Studio 在后台执行的底层 am start 命令的参数。从前面的示例扩展,您将在“启动标志”文本框中输入

--es args "-s 192.168.1.1 -f 50"

请注意,这不需要将命令字符串包装在额外的单引号中,这仅在通过 adb shell 手动启动时才需要。

直接在设备上启动

当首次安装应用程序时,它处于“停止”状态,并且在用户手动启动之前无法接收任何自动 intent。另请注意,如果您以前从未在给定设备上运行过该应用程序,则应用程序数据目录尚不存在。如果您想使用启动选项文件,则必须运行一次该应用程序(如果它没有自行退出,则退出),以便 Android 创建数据目录并供您使用。

一旦应用程序数据目录存在,您就可以在您的 PC 上创建一个名为 CloudXRLaunchOptions.txt 的文件。该文件可以包含任何命令行启动选项,可以是像上面示例中的一行长行,或者为了更好的可读性,您可以将每个选项拆分到其自己的单独行。您仍然需要在每个选项之前使用破折号前缀。

然后,只需将启动选项文件复制到应用程序文件目录(对于我们的示例,这将是 /sdcard/Android/data/com.nvidia.cxrtest/files),从那时起,假设您使用单个服务器,您就可以从设备启动,而无需进一步的 PC 连接。您可以使用 PC 的文件资源管理器(取决于您选择的操作系统)拖放它,或者您可以使用类似 adb 命令

adb push CloudXRLaunchOptions.txt /sdcard/Android/data/com.nvidia.cxrtest/files/

如何在 Windows 上启动

这将在某种程度上与 Android 平行。同样,启动选项可以在 命令行选项 中找到。我们将研究在 Windows 上启动的类似三种方法。

从命令提示符启动

您可以直接从命令提示符启动客户端,并指定您想要的任何选项。作为一个原始示例,我将使用名称“cxrtest.exe”作为应用程序,只是为了与上面的 android 描述相匹配。

cxrtest -s 192.168.1.1 -f 50

就这么简单。

从 Visual Studio 启动

在 Visual Studio 中,您可以从项目属性页设置命令行选项。如果您右键单击项目,然后在末尾选择“属性”,您将获得“属性”对话框。从左侧列表中选择“调试”部分。在右侧,您现在将看到“命令参数”。在其中,您可以输入要传递给应用程序的原始命令行,例如 -s 192.168.1.1 -f 50。同样,在这里您可能会发现保留一些可用的选项但通过后缀“ZZZ”或类似的后缀来忽略它们很有用。

使用选项文件启动

总会有这样的时候,您希望输入一次内容,然后让它正常工作。在这种情况下,您可以在 SampleClient 的 main() 函数中看到它尝试加载和解析 CloudXRLaunchOptions.txt。(然后,它继续尝试解析任何命令行参数。)由于文件路径在您的控制之下,因此您可以在合理的范围内将其指向任何位置以查找文件 - 但我们建议使用 Windows API 中的 LOCAL_APPDATA 路径,然后附加“\NVIDIA\CloudXR\<SampleAppName>”。

如前所述,您可以将命令行放在文件中的一行上,也可以将每个命令放在单独的行上,以最适合您的方式为准。您可能会发现,使用长格式命令名称而不是 1-3 个字符的缩写也更容易理解该文件。它们都必须仍然以破折号为前缀,就像在命令行上一样。在这两种情况下都使用相同的解析代码。

请注意,您的应用程序控制选项解析的顺序和组合。因此,您将看到的大多数示例都尝试首先加载选项文件,并且无论“成功”与否,都将继续解析命令行。这种方法通常被证明可以以最少的代码提供最多的功能。

如何在 Linux 上启动

如果您在 Linux 而不是 Windows 上构建 Android 应用程序,请按照 如何在 Android 上启动 中的说明进行操作。

如果您正在构建 Linux 客户端,则上述 Windows 步骤非常相似。我们将省略从 IDE 启动的说明,因为在 Linux 上有很多选项,并且在许多情况下,开发人员只是使用具有直接 shell 访问权限的编辑器。我们将仅在此处介绍命令行和文件主题。

从 Linux Shell 启动

与 Windows 一样,您可以直接从 shell 启动客户端,并指定您想要的任何选项。作为一个原始示例,我将使用名称“cxrtest”作为应用程序,只是为了与之前的描述相匹配。

./cxrtest -s 192.168.1.1 -f 50

您可以将任何客户端命令行选项放入其中。

使用 Linux 选项文件启动

就像 Windows 一样,您可能更喜欢在文件中设置一次选项,而无需在命令行上指定。相同的 SampleClient 构建适用于 Windows 和 Linux,因此此处理非常相似。在 main() 函数中,它尝试加载和解析 CloudXRLaunchOptions.txt。(然后,它继续尝试解析任何命令行参数。)由于文件路径在您的控制之下,因此您可以在合理的范围内将其指向任何位置以查找文件 - 但我们建议使用 $HOME 环境变量,然后附加类似“/.CloudXR/SampleClient/”的内容作为应用程序数据根目录。您将看到构建基本应用程序目录的 Windows 和 Linux 变体。

同样,启动选项文件可以是包含多个选项的单行,就像它是命令行一样,或者您可以将每个命令分成单独的行。您可能会发现,使用长格式命令名称而不是 1-3 个字符的缩写也更容易理解该文件 - 并且与每行一个选项结合使用,它可以使复杂的选项文件更易于维护。所有选项都必须仍然以破折号为前缀,就像在命令行上一样,因为在这两种情况下都使用相同的解析代码。

大多数示例都尝试首先加载选项文件,并且无论“成功”与否,都将继续尝试解析任何命令行参数。这种方法通常被证明可以以最少的代码提供最多的功能,并允许您通过在命令行上指定来覆盖启动选项文件中的某些功能以进行给定的运行。