哥啊,为啥你不给个 API 呢?
编写 OpenVR 驱动是一个问题,而安装 OpenVR 驱动到 SteamVR 则是一个更大的问题。许多 OpenVR 驱动要求用户手动将驱动文件夹放到那个神秘目录内,但有些比较高端的程序就不需要用户手动这么做。我们能否实现这个自动安装行为呢?
答案是可以。SteamVR 提供了 $SteamVRRoot/bin/vrpathreg.exe
来注册和反注册驱动,这样即使你的安装程序没有管理员权限,也一样可以用这个工具注册新的驱动到 SteamVR. 但是这只是将问题往下踢了一层:我们怎么知道 $SteamVRRoot
是哪?
如同所有未标准化的操作一样,实现这个功能的方法有各种野路子。下面是一些可以尝试的操作:
扫盘
如果用户没有做什么骚操作的话,SteamVR 的安装路径一定是 %SystemDrive%\Program Files (x86)\Steam\steamapps\common\SteamVR
. 没错,SteamVR 其实默认是不允许修改安装目录的。所以我们可以先检查这个路径是否存在,如果存在的话,剩下的问题就简单了。
但是有许多用户会搞骚操作把这个东西挪走——毕竟这玩意太大了,放在系统盘里是不可接受的。我们或许可以通过检查 X:\Program Files (x86)\Steam\steamapps\common\SteamVR
的方式来找,但枚举驱动器的成功率也不是很高,因为 Steam 的游戏库的路径是可以完全自定义的。
提取注册表信息
我们还可以通过查注册表来找到 SteamVR 的安装路径。SteamVR 的安装路径一般写在 HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 250820\InstallLocation
, 直接读这个键就能获得 $SteamVRRoot
.
但是在实机测试的时候,我发现有些时候这个键是读不到的。直觉上这是因为安装程序是 32 位的,所以尝试访问注册表时,所有请求会被转发到 WOW6432Node
里面去。但是即使重新打包 64 位的安装程序,该读不到还是读不到。似乎 SteamVR 的老用户尤其会遇到这种问题。当然,也有一种可能是我的 NSIS 写得稀烂,注册表操作没有成功完成或者之类的。
我不是很想问用户手动键入 SteamVR 的位置。看来要一劳永逸地解决这个问题,使用一个安装后处理程序是必须的了。
来点灰魔法
用一个安装后处理程序读取注册表当然是可以做到的,但这未免也太无聊了一点。而且我手动排查了一台安装无法进行的电脑,结论是这个注册表键不一定在每台电脑上都存在。毕竟,这个注册表键是 Windows 的安装与卸载信息,即使没有这个信息 SteamVR 的各个组件也能正常工作。一些所谓的注册表清理软件可能会毫无理由地删除这些信息,或者 Steam 在安装这个东西的时候没有写入这些信息也不一定。
事已至此,要开始弄一些灰魔法了。
灰魔法,不是黑魔法,因为这些东西严格意义上来说是有理论支持的,而且里面并不需要太多整活。
vr::VR_Init
究竟干了什么?
因为我们仍然有 OpenVR API 可用,我们或许可以从这个方面着手,看看它有没有提供一个「获取安装路径」的能力。结论自然是「没有」,因为技术上一个 OpenVR Runtime 甚至可以在一个没有文件系统概念的环境中运行。
不过我们知道这个程序一定是在标准 Windows 下运行的。有没有其他好方法呢?
vr::VRInit(vr::EVRInitError *, vr::EVRApplicationType)
可以指定当前 VR 应用的类型。其中,vr::EVRApplicationType::VRApplication_Utility
可以在不唤起 SteamVR 或者其他 OpenVR Runtime 的情况下初始化 OpenVR 客户端程序接口,当然,只有 IVRSettings
和 IVRApplication
这两个接口能用。
不过这两个接口也没有提供任何有用的信息。调转一下思路:它是怎么知道和哪个 OpenVR Runtime 通信的?拆开头文件,可以看到这样一句话:
namespace vr {
/// <SNIP> ///
/** Finds the active installation of vrclient.dll and initializes it */
inline IVRSystem *VR_Init( EVRInitError *peError, EVRApplicationType eApplicationType, const char *pStartupInfo )
{
/// <SNIP> ///
}
}
这个 vrclient.dll
似乎是由 OpenVR 运行时提供的。如果我们搜索 vrclient.dll
的话,会发现它就在 $SteamVRRoot/bin/vrclient.dll
.
嗯,答案接近了。
获取当前进程加载的 DLL 列表
我们可以初始化 OpenVR 之后,检查当前进程加载的 DLL 列表,其中一定能找到 vrclient.dll
. 这之后,我们再拿到这个文件的路径,就可以进一步推断出剩下的路径了。
当然,实际使用的时候会发现在 x64 上加载的是 vrclient_x64.dll
. 所以我们需要照顾两种情况:
const TCHAR* targetModuleName = TEXT("vrclient.dll");
const TCHAR* targetModuleName64 = TEXT("vrclient_x64.dll");
static bool findVrClientModule(TCHAR* path, size_t len) {
auto currentPid = GetCurrentProcessId();
HMODULE hMods[1024];
HANDLE process;
DWORD cbNeeded;
process = OpenProcess(
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, currentPid);
if (process == nullptr) {
logger->error("Failed to open process: {}", GetLastError());
return false;
}
if (!EnumProcessModules(process, hMods, sizeof(hMods), &cbNeeded)) {
return false;
}
const TCHAR* foundModuleName = nullptr;
TCHAR szModName[MAX_PATH];
for (unsigned int i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) {
if (GetModuleFileNameEx(process,
hMods[i],
szModName,
sizeof(szModName) / sizeof(TCHAR))) {
logger->trace(TEXT("Module name: {}"), szModName);
if (StrStr(szModName, targetModuleName) != nullptr) {
logger->info(TEXT("Found module: {}"), szModName);
foundModuleName = targetModuleName;
break;
}
if (StrStr(szModName, targetModuleName64) != nullptr) {
logger->info(TEXT("Found module: {}"), szModName);
foundModuleName = targetModuleName64;
break;
}
}
}
if (foundModuleName == nullptr) {
return false;
}
result = StringCchCopy(path, len, szModName);
if (FAILED(result)) {
return false;
}
return true;
}
获取 $SteamVRRoot
获得了 SteamVR 客户端 DLL 所在位置之后,只需要擦去 \bin\vrclient(_x64)?.dll
就可以了:
static bool extractSteamVrRoot(TCHAR* path,
size_t len,
const TCHAR* vrPathRegPath) {
if (path == nullptr || vrPathRegPath == nullptr) {
return false;
}
size_t vrPathRegPathLen = 0;
auto result = StringCchLength(vrPathRegPath, MAX_PATH, &vrPathRegPathLen);
if (FAILED(result)) {
return false;
}
size_t backtickIndex = 0;
bool firstBackTickFound = false;
bool secondBackTickFound = false;
for (size_t i = vrPathRegPathLen - 1; i > 0; i--) {
if (vrPathRegPath[i] == TEXT('\\')) {
if (!firstBackTickFound) {
firstBackTickFound = true;
continue;
}
secondBackTickFound = true;
backtickIndex = i;
break;
}
}
if (!secondBackTickFound) {
return false;
}
auto length = vrPathRegPathLen - backtickIndex;
if (length >= len) {
return false;
}
result = StringCchCopyN(path, len, vrPathRegPath, backtickIndex);
if (FAILED(result)) {
return false;
}
return true;
}
Your comments will be submitted to a human moderator and will only be shown publicly after approval. The moderator reserves the full right to not approve any comment without reason. Please be civil.