mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-17 19:00:48 +02:00
support alvr and opengl
This commit is contained in:
1 parent
b187efbedf
commit
3d89f128ce
14 files changed
+1711
-107
No files matched your search
@@ -1,5 +1,125 @@
|
||||
This is a dev branch for vr support in Mocktail.
|
||||
# Mocktail VR (experimental)
|
||||
|
||||
idk if I can do this.
|
||||
Build this checkout, start WiVRn or SteamVR/ALVR, then connect your headset before launching:
|
||||
|
||||
But it will be fun :)
|
||||
```sh
|
||||
git submodule update --init --recursive
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DMOCKTAIL_ENABLE_VR=ON
|
||||
cmake --build build -j4
|
||||
./build/mocktail --vr
|
||||
```
|
||||
|
||||
## Automatic runtime selection
|
||||
|
||||
Normally just run `./build/mocktail --vr`. The default `auto` mode detects a
|
||||
running SteamVR server (also used by ALVR) or a listening WiVRn IPC socket for
|
||||
the current desktop user. A stale socket file does not count as a running
|
||||
server. If exactly one server is detected, its runtime takes priority over an
|
||||
old registration for the other server. If both or neither are detected, the
|
||||
registered OpenXR runtime has priority.
|
||||
|
||||
SteamVR is also found through `openvr/openvrpaths.vrpath`, standard Steam
|
||||
locations, and the running server's executable path. WiVRn development builds
|
||||
are discovered near the running server executable; a matching registered
|
||||
WiVRn manifest is preferred, including a Flatpak registration. The selected
|
||||
path and the reason for selecting it are printed at startup.
|
||||
|
||||
Explicit `XR_RUNTIME_JSON` and `MOCKTAIL_VR_RUNTIME` overrides remain respected.
|
||||
To return to automatic selection after manually choosing a runtime:
|
||||
|
||||
```sh
|
||||
unset XR_RUNTIME_JSON MOCKTAIL_VR_RUNTIME
|
||||
./build/mocktail --vr
|
||||
```
|
||||
|
||||
Detection does not start servers, connect a headset, or modify system runtime
|
||||
registration. It observes the current process/user namespace; container and
|
||||
host runtimes still need correctly shared libraries and IPC access.
|
||||
|
||||
## WiVRn
|
||||
|
||||
Start WiVRn and connect its headset client first. Run WiVRn and Mocktail as the
|
||||
same desktop user, with the same runtime environment; do not start Mocktail
|
||||
from a root shell while WiVRn runs in your user session. For containers/chroots,
|
||||
the host runtime library and IPC socket must both be accessible inside.
|
||||
|
||||
```sh
|
||||
MOCKTAIL_VR_RUNTIME=wivrn ./build/mocktail --vr
|
||||
```
|
||||
|
||||
If WiVRn disconnects and exits, reconnect/restart it before retrying Mocktail.
|
||||
`Connection refused` on `wivrn/comp_ipc` means the selected server is not
|
||||
accepting the connection. Installing `dbus-launch` alone does not fix that.
|
||||
|
||||
## ALVR through SteamVR
|
||||
|
||||
Install SteamVR and ALVR, register the ALVR driver using its dashboard, then
|
||||
connect the headset and confirm SteamVR sees it. Follow the upstream
|
||||
[ALVR Linux instructions](https://github.com/alvr-org/ALVR/wiki/Linux-Troubleshooting),
|
||||
including the SteamVR launch-options workaround when required.
|
||||
|
||||
```sh
|
||||
env -u XR_RUNTIME_JSON MOCKTAIL_VR_RUNTIME=alvr \
|
||||
MOCKTAIL_GRAPHICS_BACKEND=direct-vulkan ./build/mocktail --vr
|
||||
```
|
||||
|
||||
The command clears an inherited WiVRn manifest override. `steamvr` is an alias
|
||||
for `alvr`. The selection uses SteamVR's OpenXR runtime;
|
||||
ALVR handles the streaming. If automatic discovery cannot find a custom
|
||||
installation, specify its actual manifest:
|
||||
|
||||
```sh
|
||||
XR_RUNTIME_JSON="/path/to/SteamVR/steamxr_linux64.json" ./build/mocktail --vr
|
||||
```
|
||||
|
||||
`XR_RUNTIME_JSON` always wins. `MOCKTAIL_VR_RUNTIME=system` delegates selection
|
||||
to the OpenXR loader. The default `auto` uses the server detection described above, then registered
|
||||
and installed runtimes. Selection affects only this process.
|
||||
|
||||
## OpenGL ES
|
||||
|
||||
The `opengl`/`system` renderer uses the guest's real EGL/OpenGL ES context:
|
||||
|
||||
```sh
|
||||
MOCKTAIL_GRAPHICS_BACKEND=opengl ./build/mocktail --vr
|
||||
```
|
||||
|
||||
The runtime must provide `XR_KHR_opengl_es_enable` and `XR_MNDX_egl_enable`, and
|
||||
support sharing images with the guest EGL device. Both eye copies stay on the
|
||||
GPU; no Python viewer or CPU readback is required. Camera tracking uses the
|
||||
same OpenXR frame/pose pipeline as Vulkan. If the runtime cannot support this
|
||||
EGL binding, use `MOCKTAIL_GRAPHICS_BACKEND=direct-vulkan`; the ALVR command
|
||||
above deliberately selects Vulkan.
|
||||
|
||||
## Current validation and limitations
|
||||
|
||||
- Native VR camera/eye hooks are verified for Roblox build **2998**, Build ID
|
||||
`ade08266c67aee88ec9c1d00902150e1684dad3a`. Approval of a newer payload for
|
||||
ordinary rendering does **not** establish VR compatibility. Do not bypass
|
||||
the Build-ID checks to run the old VR hooks on build 3092.
|
||||
- GLES framebuffer copying, distinct eyes, state restoration, resource
|
||||
invalidation, and an EGL OpenXR session with session-loss recovery have been
|
||||
tested. A local unauthenticated Roblox run on hardware GLES submitted over
|
||||
240 projection frames through a simulated Monado headset.
|
||||
- This host's software GLES/Vulkan combination could create XR texture names
|
||||
but not usable framebuffer attachments. The copy failure is reported and
|
||||
XR output stops; this is not counted as a successful rendering test.
|
||||
- The local Monado test runtime reports `u_graphics_buffer_unref: Bad file
|
||||
descriptor (fd: 0)` during GLES teardown, including successful hardware
|
||||
runs. This remains an integration caveat; clean headset teardown is not
|
||||
claimed from those simulator runs. During auto-selection validation, one
|
||||
run also failed a later EGL session recreation with
|
||||
`vkCreateFence: VK_ERROR_OUT_OF_HOST_MEMORY`; a fresh repeat passed.
|
||||
Runtime selection and initial frame submission succeeded in both runs.
|
||||
- Real headset acceptance through WiVRn and ALVR/SteamVR is still pending.
|
||||
Controller input and canted eye orientations are not implemented.
|
||||
- Use `./build/mocktail --no-vr` for ordinary rendering. No helper script is
|
||||
needed for normal VR startup.
|
||||
|
||||
The GLES OpenXR integration test is opt-in and needs an already connected
|
||||
runtime (or a separately prepared simulator). Run from a graphical desktop:
|
||||
|
||||
```sh
|
||||
MOCKTAIL_TEST_XR_GLES=1 ./build/tests/gles_vr_transport_test \
|
||||
--gtest_filter='*OpenXrEglSession*'
|
||||
```
|
||||
@@ -14,7 +14,7 @@ add_library(mocktail_vr STATIC
|
||||
add_library(Mocktail::VR ALIAS mocktail_vr)
|
||||
target_include_directories(mocktail_vr PUBLIC "${CMAKE_SOURCE_DIR}/include")
|
||||
target_include_directories(mocktail_vr PRIVATE "${CMAKE_SOURCE_DIR}/src")
|
||||
target_link_libraries(mocktail_vr PUBLIC Vulkan::Headers)
|
||||
target_link_libraries(mocktail_vr PUBLIC Vulkan::Headers PRIVATE nlohmann_json::nlohmann_json)
|
||||
# shm_open for the desktop eye mirror lives in librt on glibc.
|
||||
find_library(MOCKTAIL_RT_LIBRARY rt)
|
||||
if(MOCKTAIL_RT_LIBRARY)
|
||||
@@ -47,12 +47,14 @@ if(MOCKTAIL_ENABLE_VR)
|
||||
endfunction()
|
||||
mocktail_add_openxr_sdk()
|
||||
target_sources(mocktail_vr PRIVATE src/vr/openxr_probe.cc src/vr/openxr_preview.cc
|
||||
src/vr/openxr_backend.cc)
|
||||
src/vr/openxr_backend.cc src/vr/gles_transport.cc)
|
||||
# Error unwinding is confined to these native session implementations; their
|
||||
# public entry points catch errors before returning to the guest runtime.
|
||||
set_source_files_properties(src/vr/openxr_preview.cc src/vr/openxr_backend.cc
|
||||
PROPERTIES COMPILE_OPTIONS -fexceptions)
|
||||
target_compile_definitions(mocktail_vr PRIVATE XR_USE_GRAPHICS_API_VULKAN)
|
||||
set_source_files_properties(src/vr/openxr_backend.cc PROPERTIES COMPILE_DEFINITIONS "XR_USE_GRAPHICS_API_OPENGL_ES;XR_USE_PLATFORM_EGL")
|
||||
target_include_directories(mocktail_vr PRIVATE ${MOCKTAIL_EGL_INCLUDE_DIR} ${MOCKTAIL_GLES3_INCLUDE_DIR})
|
||||
target_link_libraries(mocktail_vr PRIVATE OpenXR::openxr_loader Vulkan::Headers ${CMAKE_DL_LIBS})
|
||||
else()
|
||||
target_sources(mocktail_vr PRIVATE src/vr/openxr_disabled.cc)
|
||||
|
||||
@@ -25,10 +25,21 @@ enum class VrBackendMode {
|
||||
kXrOutput,
|
||||
};
|
||||
|
||||
// Explicit XR_RUNTIME_JSON wins. Otherwise prefer a native WiVRn manifest,
|
||||
// then let the OpenXR loader use the registered runtime (including Flatpak WiVRn).
|
||||
// Explicit XR_RUNTIME_JSON wins. Runtime preference can select SteamVR (ALVR),
|
||||
// WiVRn, or the registered OpenXR runtime without changing system registration.
|
||||
std::string SelectVrRuntimeManifest(const std::string& explicit_manifest,
|
||||
const std::vector<std::string>& candidates);
|
||||
std::vector<std::string> VrRuntimeManifestCandidates(
|
||||
const std::string &preference, const std::string &home,
|
||||
const std::string &config_home, const std::string &config_dirs);
|
||||
struct VrRuntimeDiscovery {
|
||||
std::vector<std::string> candidates;
|
||||
std::string reason;
|
||||
};
|
||||
VrRuntimeDiscovery DiscoverVrRuntime(
|
||||
const std::string& preference, const std::string& home,
|
||||
const std::string& config_home, const std::string& config_dirs,
|
||||
const std::string& runtime_dir, const std::string& proc_root = "/proc");
|
||||
std::string VrRuntimeUnavailableHint(bool user_selected, const std::string& manifest);
|
||||
VrBackendMode ResolveVrBackendMode(bool vr_enabled);
|
||||
const char* VrBackendModeName(VrBackendMode mode);
|
||||
@@ -58,6 +69,10 @@ struct ScriptedPoseSample {
|
||||
// frame N+1 and publishing the scripted/runtime head pose that the device
|
||||
// bridge injects into Roblox before its eye passes run.
|
||||
//
|
||||
// EGL/OpenGL ES uses XR_MNDX_egl_enable on the guest SDL context and GPU
|
||||
// framebuffer blits into GLES swapchain textures. It shares the same frame
|
||||
// scheduling, tracking, pose application and session recovery below.
|
||||
//
|
||||
// Eye image provenance is recorded in the adapter during the guest eye
|
||||
// initializer (owner = DebugDeviceVR object) and bound to an eye index only
|
||||
// through the real per-frame eye-getter request plus the actual render-pass
|
||||
@@ -67,6 +82,9 @@ struct ScriptedPoseSample {
|
||||
// All public entry points are exception-free at their boundary; the
|
||||
// implementation is compiled with exceptions like openxr_preview.cc and
|
||||
// translates failures into Status/log outcomes.
|
||||
class GlesTransport;
|
||||
enum class VrGraphicsApi { kVulkan, kOpenGles };
|
||||
|
||||
class OpenXrBackend final {
|
||||
public:
|
||||
OpenXrBackend() = default;
|
||||
@@ -75,7 +93,14 @@ class OpenXrBackend final {
|
||||
OpenXrBackend& operator=(const OpenXrBackend&) = delete;
|
||||
|
||||
// Claims the process-wide backend slot and creates XrInstance/system.
|
||||
Status Arm();
|
||||
Status Arm(VrGraphicsApi graphics_api = VrGraphicsApi::kVulkan);
|
||||
// EGL context must be current on the calling thread. Resolver bypasses guest
|
||||
// hooks.
|
||||
Status AttachGlesContext(void *display, void *config, void *context,
|
||||
void *egl_get_proc, void *(*resolve)(const char *));
|
||||
void DetachGlesContext(void *context);
|
||||
void NoteGlesPresent();
|
||||
void *WrapGlesProcAddress(const char *name, void *raw);
|
||||
// Idempotent full teardown (XR resources first, then the instance).
|
||||
void Disarm();
|
||||
bool armed() const { return armed_.load(std::memory_order_acquire); }
|
||||
@@ -161,6 +186,7 @@ class OpenXrBackend final {
|
||||
struct EyeSwapchain {
|
||||
void* swapchain = nullptr; // XrSwapchain (opaque to keep the header XR-free)
|
||||
std::vector<VkImage> images;
|
||||
std::vector<std::uint32_t> gl_images;
|
||||
std::uint32_t acquired_index = 0;
|
||||
bool image_acquired = false;
|
||||
std::uint32_t width = 0;
|
||||
@@ -172,6 +198,9 @@ class OpenXrBackend final {
|
||||
VkDevice device,
|
||||
const VkDeviceCreateInfo* create_info,
|
||||
std::string* error);
|
||||
bool InitializeGlesSessionLocked(std::string *error);
|
||||
bool InitializeSessionResourcesLocked(std::string *error);
|
||||
bool CopyGlesEyesIntoSwapchains();
|
||||
void DisarmLocked();
|
||||
void* ResolveFunction(const char* name) const;
|
||||
void SetupDeviceAfterCreationLocked(VkPhysicalDevice physical_device,
|
||||
@@ -201,6 +230,11 @@ class OpenXrBackend final {
|
||||
void EnsureMirrorForExtent(std::uint32_t width, std::uint32_t height);
|
||||
MirrorVkProcs MirrorProcs() const;
|
||||
|
||||
VrGraphicsApi graphics_api_ = VrGraphicsApi::kVulkan;
|
||||
GlesTransport *gles_ = nullptr;
|
||||
std::uint64_t gl_min_version_ = 0;
|
||||
std::uint64_t gl_max_version_ = 0;
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::atomic<bool> armed_{false};
|
||||
std::atomic<bool> recording_{false};
|
||||
|
||||
+15
-8
@@ -351,7 +351,8 @@ int main(int argc, char* argv[]) {
|
||||
"debug device (exact Build ID 2998 only)";
|
||||
if (vr_backend_mode == mocktail::vr::VrBackendMode::kXrOutput) {
|
||||
std::cerr << "; eye images are submitted to the active OpenXR runtime "
|
||||
"as a projection layer. Start WiVRn and connect the headset "
|
||||
"as a projection layer. Start WiVRn or SteamVR/ALVR and "
|
||||
"connect the headset "
|
||||
"before launching.\n";
|
||||
} else {
|
||||
std::cerr << "; OpenXR output is disabled in this diagnostic mode "
|
||||
@@ -894,14 +895,20 @@ int main(int argc, char* argv[]) {
|
||||
mocktail::vr::VrBackendMode::kXrOutput) {
|
||||
// Fail closed and loudly when no OpenXR runtime is reachable; the
|
||||
// native-stereo diagnostic mode must be requested explicitly.
|
||||
const mocktail::Status xr_status = xr_backend.Arm();
|
||||
const mocktail::Status xr_status =
|
||||
xr_backend.Arm(runtime_config.config.graphics_backend() ==
|
||||
mocktail::runtime::GraphicsBackend::kVulkan
|
||||
? mocktail::vr::VrGraphicsApi::kVulkan
|
||||
: mocktail::vr::VrGraphicsApi::kOpenGles);
|
||||
if (!xr_status.ok()) {
|
||||
std::cerr << "[FATAL] Cannot arm the OpenXR backend for --vr: "
|
||||
<< xr_status.message() << '\n'
|
||||
<< " Start WiVRn, connect your headset, then run mocktail -vr again.\n"
|
||||
<< " For a custom runtime set XR_RUNTIME_JSON to its manifest.\n"
|
||||
<< " Set MOCKTAIL_VR_BACKEND=native for the "
|
||||
"native-stereo diagnostic mode without a runtime.\n";
|
||||
std::cerr
|
||||
<< "[FATAL] Cannot arm the OpenXR backend for --vr: "
|
||||
<< xr_status.message() << '\n'
|
||||
<< " Start WiVRn or SteamVR/ALVR, connect your headset, then "
|
||||
"run mocktail -vr again.\n"
|
||||
<< " For a custom runtime set XR_RUNTIME_JSON to its manifest.\n"
|
||||
<< " Set MOCKTAIL_VR_BACKEND=native for the "
|
||||
"native-stereo diagnostic mode without a runtime.\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
#include "vr/gles_transport.h"
|
||||
#include "mocktail/vr/openxr_backend.h"
|
||||
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES3/gl3.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
extern "C" void *mocktail_vr_initializing_object();
|
||||
extern "C" bool mocktail_vr_current_eye(void **, int *, void **);
|
||||
extern "C" void mocktail_vr_consume_eye();
|
||||
|
||||
namespace mocktail::vr {
|
||||
namespace {
|
||||
// The window owns a single EGL context. Function pointers are loaded from
|
||||
// that exact SDL EGL/GLES implementation, never from libGL or the guest shim.
|
||||
#define GL_PROCS(X) \
|
||||
X(GetIntegerv, PFNGLGETINTEGERVPROC) \
|
||||
X(IsEnabled, PFNGLISENABLEDPROC) \
|
||||
X(Enable, PFNGLENABLEPROC) \
|
||||
X(Disable, PFNGLDISABLEPROC) \
|
||||
X(GenFramebuffers, PFNGLGENFRAMEBUFFERSPROC) X(DeleteFramebuffers, \
|
||||
PFNGLDELETEFRAMEBUFFERSPROC) \
|
||||
X(BindFramebuffer, PFNGLBINDFRAMEBUFFERPROC) \
|
||||
X(CheckFramebufferStatus, PFNGLCHECKFRAMEBUFFERSTATUSPROC) X( \
|
||||
GetFramebufferAttachmentParameteriv, \
|
||||
PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC) \
|
||||
X(FramebufferTexture2D, PFNGLFRAMEBUFFERTEXTURE2DPROC) X( \
|
||||
FramebufferTextureLayer, PFNGLFRAMEBUFFERTEXTURELAYERPROC) \
|
||||
X(FramebufferRenderbuffer, PFNGLFRAMEBUFFERRENDERBUFFERPROC) \
|
||||
X(TexImage2D, PFNGLTEXIMAGE2DPROC) X( \
|
||||
TexStorage2D, PFNGLTEXSTORAGE2DPROC) \
|
||||
X(RenderbufferStorage, PFNGLRENDERBUFFERSTORAGEPROC) \
|
||||
X(RenderbufferStorageMultisample, \
|
||||
PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC) \
|
||||
X(DeleteTextures, PFNGLDELETETEXTURESPROC) \
|
||||
X(DeleteRenderbuffers, \
|
||||
PFNGLDELETERENDERBUFFERSPROC) \
|
||||
X(BlitFramebuffer, \
|
||||
PFNGLBLITFRAMEBUFFERPROC) \
|
||||
X(Flush, PFNGLFLUSHPROC)
|
||||
struct Procs {
|
||||
#define DECLARE(name, type) type name = nullptr;
|
||||
GL_PROCS(DECLARE)
|
||||
#undef DECLARE
|
||||
} gl;
|
||||
thread_local GlesTransport *current = nullptr;
|
||||
GlesTransport *Current() {
|
||||
return current && current->IsCurrent() ? current : nullptr;
|
||||
}
|
||||
void GL_APIENTRY GenFramebuffers(GLsizei n, GLuint *ids) {
|
||||
gl.GenFramebuffers(n, ids);
|
||||
if (auto *t = Current())
|
||||
t->Generated(n, ids);
|
||||
}
|
||||
void GL_APIENTRY DeleteFramebuffers(GLsizei n, const GLuint *ids) {
|
||||
if (auto *t = Current())
|
||||
t->Deleted(n, ids);
|
||||
gl.DeleteFramebuffers(n, ids);
|
||||
}
|
||||
void GL_APIENTRY BindFramebuffer(GLenum target, GLuint id) {
|
||||
gl.BindFramebuffer(target, id);
|
||||
if (auto *t = Current())
|
||||
t->Bind(target, id);
|
||||
}
|
||||
void GL_APIENTRY FramebufferTexture2D(GLenum target, GLenum attachment,
|
||||
GLenum tex_target, GLuint texture,
|
||||
GLint level) {
|
||||
gl.FramebufferTexture2D(target, attachment, tex_target, texture, level);
|
||||
if (auto *t = Current())
|
||||
t->AttachmentChanged(target);
|
||||
}
|
||||
void GL_APIENTRY FramebufferTextureLayer(GLenum target, GLenum attachment,
|
||||
GLuint texture, GLint level,
|
||||
GLint layer) {
|
||||
gl.FramebufferTextureLayer(target, attachment, texture, level, layer);
|
||||
if (auto *t = Current())
|
||||
t->AttachmentChanged(target);
|
||||
}
|
||||
void GL_APIENTRY FramebufferRenderbuffer(GLenum target, GLenum attachment,
|
||||
GLenum rb_target, GLuint rb) {
|
||||
gl.FramebufferRenderbuffer(target, attachment, rb_target, rb);
|
||||
if (auto *t = Current())
|
||||
t->AttachmentChanged(target);
|
||||
}
|
||||
void GL_APIENTRY TexImage2D(GLenum target, GLint level, GLint internal_format,
|
||||
GLsizei w, GLsizei h, GLint border, GLenum format,
|
||||
GLenum type, const void *data) {
|
||||
gl.TexImage2D(target, level, internal_format, w, h, border, format, type,
|
||||
data);
|
||||
if (target == GL_TEXTURE_2D && level == 0)
|
||||
if (auto *t = Current())
|
||||
t->Storage(true, w, h, internal_format);
|
||||
}
|
||||
void GL_APIENTRY TexStorage2D(GLenum target, GLsizei levels, GLenum format,
|
||||
GLsizei w, GLsizei h) {
|
||||
gl.TexStorage2D(target, levels, format, w, h);
|
||||
if (target == GL_TEXTURE_2D)
|
||||
if (auto *t = Current())
|
||||
t->Storage(true, w, h, format);
|
||||
}
|
||||
void GL_APIENTRY RenderbufferStorage(GLenum target, GLenum format, GLsizei w,
|
||||
GLsizei h) {
|
||||
gl.RenderbufferStorage(target, format, w, h);
|
||||
if (target == GL_RENDERBUFFER)
|
||||
if (auto *t = Current())
|
||||
t->Storage(false, w, h, format);
|
||||
}
|
||||
void GL_APIENTRY RenderbufferStorageMultisample(GLenum target, GLsizei samples,
|
||||
GLenum format, GLsizei w,
|
||||
GLsizei h) {
|
||||
gl.RenderbufferStorageMultisample(target, samples, format, w, h);
|
||||
if (target == GL_RENDERBUFFER)
|
||||
if (auto *t = Current())
|
||||
t->Storage(false, w, h, format, samples);
|
||||
}
|
||||
void GL_APIENTRY DeleteTextures(GLsizei n, const GLuint *ids) {
|
||||
if (auto *t = Current())
|
||||
t->ObjectsDeleted(true, n, ids);
|
||||
gl.DeleteTextures(n, ids);
|
||||
}
|
||||
void GL_APIENTRY DeleteRenderbuffers(GLsizei n, const GLuint *ids) {
|
||||
if (auto *t = Current())
|
||||
t->ObjectsDeleted(false, n, ids);
|
||||
gl.DeleteRenderbuffers(n, ids);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool GlesTransport::Initialize(void *display, void *config, void *context,
|
||||
void *egl_get_proc,
|
||||
void *(*resolve)(const char *),
|
||||
std::string *error) {
|
||||
if (!display || !config || !context || !egl_get_proc || !resolve) {
|
||||
*error =
|
||||
"OpenGL VR requires a real current EGL context, config and display";
|
||||
return false;
|
||||
}
|
||||
#define LOAD(name, type) \
|
||||
gl.name = reinterpret_cast<type>(resolve("gl" #name)); \
|
||||
if (!gl.name) { \
|
||||
*error = "OpenGL VR is missing gl" #name; \
|
||||
return false; \
|
||||
}
|
||||
GL_PROCS(LOAD)
|
||||
#undef LOAD
|
||||
using GetProc = __eglMustCastToProperFunctionPointerType (*)(const char *);
|
||||
get_current_context_ = reinterpret_cast<void *(*)()>(
|
||||
reinterpret_cast<GetProc>(egl_get_proc)("eglGetCurrentContext"));
|
||||
if (!get_current_context_ || get_current_context_() != context) {
|
||||
*error = "OpenGL VR must initialize on the current EGL render context";
|
||||
return false;
|
||||
}
|
||||
GLint major = 0, minor = 0;
|
||||
gl.GetIntegerv(GL_MAJOR_VERSION, &major);
|
||||
gl.GetIntegerv(GL_MINOR_VERSION, &minor);
|
||||
if (major < 3) {
|
||||
*error = "OpenGL VR requires OpenGL ES 3.0 or newer";
|
||||
return false;
|
||||
}
|
||||
version_ = (std::uint64_t(major) << 48) | (std::uint64_t(minor) << 32);
|
||||
display_ = display;
|
||||
config_ = config;
|
||||
context_ = context;
|
||||
egl_get_proc_ = egl_get_proc;
|
||||
gl.GenFramebuffers(2, copy_fbos_);
|
||||
if (!copy_fbos_[0] || !copy_fbos_[1]) {
|
||||
*error = "Cannot allocate OpenGL VR copy framebuffers";
|
||||
return false;
|
||||
}
|
||||
MakeCurrent();
|
||||
return true;
|
||||
}
|
||||
bool GlesTransport::IsCurrent() const {
|
||||
return get_current_context_ && get_current_context_() == context_;
|
||||
}
|
||||
void GlesTransport::MakeCurrent() { current = this; }
|
||||
void GlesTransport::ReleaseCurrent() { current = nullptr; }
|
||||
void GlesTransport::Destroy() {
|
||||
if (IsCurrent())
|
||||
gl.DeleteFramebuffers(2, copy_fbos_);
|
||||
copy_fbos_[0] = copy_fbos_[1] = 0;
|
||||
if (current == this)
|
||||
current = nullptr;
|
||||
owners_.clear();
|
||||
textures_.clear();
|
||||
renderbuffers_.clear();
|
||||
ResetEyes();
|
||||
context_ = nullptr;
|
||||
}
|
||||
void *GlesTransport::Wrap(const char *name, void *raw) {
|
||||
if (!raw || !name)
|
||||
return raw;
|
||||
#define WRAP(name_) \
|
||||
if (std::strcmp(name, "gl" #name_) == 0) \
|
||||
return reinterpret_cast<void *>(&name_);
|
||||
WRAP(GenFramebuffers)
|
||||
WRAP(DeleteFramebuffers)
|
||||
WRAP(BindFramebuffer) WRAP(FramebufferTexture2D) WRAP(FramebufferRenderbuffer)
|
||||
WRAP(FramebufferTextureLayer) WRAP(TexImage2D) WRAP(TexStorage2D)
|
||||
WRAP(RenderbufferStorage) WRAP(RenderbufferStorageMultisample)
|
||||
WRAP(DeleteTextures) WRAP(DeleteRenderbuffers)
|
||||
#undef WRAP
|
||||
return raw;
|
||||
}
|
||||
void GlesTransport::ResetEyes() {
|
||||
eyes_[0] = {};
|
||||
eyes_[1] = {};
|
||||
}
|
||||
void GlesTransport::Generated(int count, const unsigned *ids) {
|
||||
void *owner = mocktail_vr_initializing_object();
|
||||
if (!owner || !ids)
|
||||
return;
|
||||
for (int i = 0; i < count; ++i)
|
||||
if (ids[i])
|
||||
owners_[ids[i]] = owner;
|
||||
}
|
||||
void GlesTransport::Deleted(int count, const unsigned *ids) {
|
||||
if (!ids)
|
||||
return;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
owners_.erase(ids[i]);
|
||||
for (auto &eye : eyes_)
|
||||
if (eye.framebuffer == ids[i])
|
||||
eye = {};
|
||||
}
|
||||
}
|
||||
void GlesTransport::ObjectsDeleted(bool texture, int count,
|
||||
const unsigned *ids) {
|
||||
if (!ids)
|
||||
return;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
(texture ? textures_ : renderbuffers_).erase(ids[i]);
|
||||
for (auto &eye : eyes_)
|
||||
if (eye.texture == texture && eye.object == ids[i])
|
||||
eye = {};
|
||||
}
|
||||
}
|
||||
void GlesTransport::Storage(bool texture, int w, int h, unsigned format,
|
||||
int samples) {
|
||||
GLint id = 0;
|
||||
gl.GetIntegerv(texture ? GL_TEXTURE_BINDING_2D : GL_RENDERBUFFER_BINDING,
|
||||
&id);
|
||||
if (!id)
|
||||
return;
|
||||
// Storage redefinition invalidates previously rendered eye content.
|
||||
const unsigned object = static_cast<unsigned>(id);
|
||||
ObjectsDeleted(texture, 1, &object);
|
||||
(texture ? textures_ : renderbuffers_)[object] = {w, h, format, samples};
|
||||
}
|
||||
void GlesTransport::AttachmentChanged(unsigned target) {
|
||||
GLint id = 0;
|
||||
gl.GetIntegerv(target == GL_READ_FRAMEBUFFER ? GL_READ_FRAMEBUFFER_BINDING
|
||||
: GL_DRAW_FRAMEBUFFER_BINDING,
|
||||
&id);
|
||||
for (auto &eye : eyes_)
|
||||
if (eye.framebuffer == static_cast<unsigned>(id))
|
||||
eye = {};
|
||||
}
|
||||
void GlesTransport::Bind(unsigned target, unsigned framebuffer,
|
||||
std::uint64_t frame) {
|
||||
if (!frame)
|
||||
return;
|
||||
if (target != GL_FRAMEBUFFER && target != GL_DRAW_FRAMEBUFFER)
|
||||
return;
|
||||
auto found = owners_.find(framebuffer);
|
||||
if (found == owners_.end())
|
||||
return;
|
||||
void *owner = nullptr;
|
||||
void *guest_fb = nullptr;
|
||||
int eye = -1;
|
||||
if (!mocktail_vr_current_eye(&owner, &eye, &guest_fb) || eye < 0 || eye > 1 ||
|
||||
found->second != owner)
|
||||
return;
|
||||
if (frame == UINT64_MAX) {
|
||||
auto *backend = ActiveVrBackend();
|
||||
const auto pose =
|
||||
backend ? backend->PublishedHeadPose() : ScriptedPoseSample{};
|
||||
if (!pose.valid)
|
||||
return;
|
||||
frame = pose.frame;
|
||||
}
|
||||
GLint bound = 0;
|
||||
gl.GetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &bound);
|
||||
if (static_cast<unsigned>(bound) != framebuffer)
|
||||
return;
|
||||
if (gl.CheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
return;
|
||||
GLint type = 0, object = 0, level = 0, face = 0;
|
||||
gl.GetFramebufferAttachmentParameteriv(
|
||||
GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &type);
|
||||
if (type != GL_TEXTURE && type != GL_RENDERBUFFER)
|
||||
return;
|
||||
gl.GetFramebufferAttachmentParameteriv(
|
||||
GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &object);
|
||||
if (type == GL_TEXTURE) {
|
||||
gl.GetFramebufferAttachmentParameteriv(
|
||||
GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL, &level);
|
||||
gl.GetFramebufferAttachmentParameteriv(
|
||||
GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE, &face);
|
||||
if (level != 0 || face != 0)
|
||||
return;
|
||||
}
|
||||
const auto &storage = type == GL_TEXTURE ? textures_ : renderbuffers_;
|
||||
auto it = storage.find(object);
|
||||
if (it == storage.end())
|
||||
return;
|
||||
const auto &size = it->second;
|
||||
// DebugDeviceVR currently creates single-sample 500x500 RGBA eyes.
|
||||
// Reject unsupported formats/extents instead of copying unrelated pixels.
|
||||
if (size.width != 500 || size.height != 500 || size.samples != 0 ||
|
||||
(size.format != GL_RGBA8 && size.format != GL_RGBA &&
|
||||
size.format != GL_SRGB8_ALPHA8))
|
||||
return;
|
||||
|
||||
if ((eyes_[0].owner && eyes_[0].owner != owner) ||
|
||||
(eyes_[1].owner && eyes_[1].owner != owner))
|
||||
ResetEyes();
|
||||
const bool changed = eyes_[eye].framebuffer != framebuffer;
|
||||
eyes_[eye] = {framebuffer,
|
||||
static_cast<unsigned>(object),
|
||||
type == GL_TEXTURE,
|
||||
size.width,
|
||||
size.height,
|
||||
owner,
|
||||
frame};
|
||||
mocktail_vr_consume_eye();
|
||||
if (changed)
|
||||
std::fprintf(
|
||||
stderr,
|
||||
" [vr-backend] GLES eye-bound eye=%d owner=%p guest_fb=%p gl_fb=%u "
|
||||
"color=%u extent=%dx%d provenance=guest_initializer_and_eye_getter\n",
|
||||
eye, owner, guest_fb, framebuffer, object, size.width, size.height);
|
||||
}
|
||||
bool GlesTransport::Copy(const unsigned textures[2], unsigned width,
|
||||
unsigned height) {
|
||||
if (!IsCurrent() || !textures[0] || !textures[1] || !eyes_[0].object ||
|
||||
!eyes_[1].object ||
|
||||
(eyes_[0].texture == eyes_[1].texture &&
|
||||
eyes_[0].object == eyes_[1].object))
|
||||
return false;
|
||||
GLint read_fb = 0, draw_fb = 0;
|
||||
gl.GetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &read_fb);
|
||||
gl.GetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &draw_fb);
|
||||
const bool scissor = gl.IsEnabled(GL_SCISSOR_TEST);
|
||||
gl.Disable(GL_SCISSOR_TEST);
|
||||
bool copied = true;
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
gl.BindFramebuffer(GL_READ_FRAMEBUFFER, copy_fbos_[0]);
|
||||
if (eyes_[i].texture)
|
||||
gl.FramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_TEXTURE_2D, eyes_[i].object, 0);
|
||||
else
|
||||
gl.FramebufferRenderbuffer(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_RENDERBUFFER, eyes_[i].object);
|
||||
gl.BindFramebuffer(GL_DRAW_FRAMEBUFFER, copy_fbos_[1]);
|
||||
gl.FramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_TEXTURE_2D, textures[i], 0);
|
||||
const auto read_status = gl.CheckFramebufferStatus(GL_READ_FRAMEBUFFER);
|
||||
const auto draw_status = gl.CheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
|
||||
if (read_status != GL_FRAMEBUFFER_COMPLETE ||
|
||||
draw_status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
static bool reported = false;
|
||||
if (!reported)
|
||||
std::fprintf(stderr,
|
||||
" [vr-backend] GLES copy framebuffer incomplete: "
|
||||
"read=0x%x draw=0x%x XR texture=%u\n",
|
||||
read_status, draw_status, textures[i]);
|
||||
reported = true;
|
||||
copied = false;
|
||||
break;
|
||||
}
|
||||
gl.BlitFramebuffer(0, 0, eyes_[i].width, eyes_[i].height, 0, 0, width,
|
||||
height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
}
|
||||
// Release references to runtime textures before releasing XR swapchain
|
||||
// images.
|
||||
gl.BindFramebuffer(GL_READ_FRAMEBUFFER, copy_fbos_[0]);
|
||||
gl.FramebufferRenderbuffer(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_RENDERBUFFER, 0);
|
||||
gl.BindFramebuffer(GL_DRAW_FRAMEBUFFER, copy_fbos_[1]);
|
||||
gl.FramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_TEXTURE_2D, 0, 0);
|
||||
gl.BindFramebuffer(GL_READ_FRAMEBUFFER, read_fb);
|
||||
gl.BindFramebuffer(GL_DRAW_FRAMEBUFFER, draw_fb);
|
||||
if (scissor)
|
||||
gl.Enable(GL_SCISSOR_TEST);
|
||||
// OpenXR's GL binding synchronizes the flushed work at image release.
|
||||
gl.Flush();
|
||||
return copied;
|
||||
}
|
||||
} // namespace mocktail::vr
|
||||
@@ -0,0 +1,69 @@
|
||||
#ifndef MOCKTAIL_VR_GLES_TRANSPORT_H_
|
||||
#define MOCKTAIL_VR_GLES_TRANSPORT_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace mocktail::vr {
|
||||
// One guest EGL context. GL callbacks and copies run on its current thread.
|
||||
// Only framebuffers created inside the verified DebugDeviceVR initializer
|
||||
// can become eyes; a live eye-getter request supplies the eye index.
|
||||
class GlesTransport {
|
||||
public:
|
||||
struct Eye {
|
||||
unsigned framebuffer = 0;
|
||||
unsigned object = 0;
|
||||
bool texture = false;
|
||||
int width = 0, height = 0;
|
||||
void *owner = nullptr;
|
||||
std::uint64_t frame = 0;
|
||||
};
|
||||
bool Initialize(void *display, void *config, void *context,
|
||||
void *egl_get_proc, void *(*resolve)(const char *),
|
||||
std::string *error);
|
||||
bool IsCurrent() const;
|
||||
void Destroy(); // Requires this context current; call before EGL destruction.
|
||||
void MakeCurrent();
|
||||
static void ReleaseCurrent();
|
||||
static void *Wrap(const char *name, void *raw);
|
||||
void ResetEyes();
|
||||
bool Copy(const unsigned textures[2], unsigned width, unsigned height);
|
||||
const Eye &eye(int index) const { return eyes_[index]; }
|
||||
void *display() const { return display_; }
|
||||
void *config() const { return config_; }
|
||||
void *context() const { return context_; }
|
||||
void *egl_get_proc() const { return egl_get_proc_; }
|
||||
std::uint64_t version() const { return version_; }
|
||||
|
||||
// Called after the real GL operation, never while the backend mutex is held.
|
||||
void Generated(int count, const unsigned *ids);
|
||||
void Deleted(int count, const unsigned *ids);
|
||||
void Storage(bool texture, int width, int height, unsigned format,
|
||||
int samples = 0);
|
||||
void ObjectsDeleted(bool texture, int count, const unsigned *ids);
|
||||
void AttachmentChanged(unsigned target);
|
||||
void Bind(unsigned target, unsigned framebuffer,
|
||||
std::uint64_t frame = UINT64_MAX);
|
||||
|
||||
private:
|
||||
friend struct GlesTransportTestAccess;
|
||||
struct StorageInfo {
|
||||
int width = 0, height = 0;
|
||||
unsigned format = 0;
|
||||
int samples = 0;
|
||||
};
|
||||
void *display_ = nullptr;
|
||||
void *config_ = nullptr;
|
||||
void *context_ = nullptr;
|
||||
void *egl_get_proc_ = nullptr;
|
||||
std::uint64_t version_ = 0;
|
||||
void *(*get_current_context_)() = nullptr;
|
||||
unsigned copy_fbos_[2] = {};
|
||||
std::unordered_map<unsigned, void *> owners_;
|
||||
std::unordered_map<unsigned, StorageInfo> textures_;
|
||||
std::unordered_map<unsigned, StorageInfo> renderbuffers_;
|
||||
Eye eyes_[2];
|
||||
};
|
||||
} // namespace mocktail::vr
|
||||
#endif
|
||||
+334
-65
@@ -2,10 +2,15 @@
|
||||
#include "mocktail/vr/vr_perf.h"
|
||||
#include "mocktail/vr/vr_pose_math.h"
|
||||
|
||||
#include "vr/gles_transport.h"
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES3/gl3.h>
|
||||
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_platform.h>
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -219,7 +224,7 @@ OpenXrBackend* ActiveVrBackend() {
|
||||
return g_backend;
|
||||
}
|
||||
|
||||
Status OpenXrBackend::Arm() {
|
||||
Status OpenXrBackend::Arm(VrGraphicsApi graphics_api) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (armed_.load(std::memory_order_acquire)) {
|
||||
return Status::Ok();
|
||||
@@ -234,36 +239,74 @@ Status OpenXrBackend::Arm() {
|
||||
}
|
||||
try {
|
||||
const std::string user_manifest = EnvString("XR_RUNTIME_JSON", "");
|
||||
const auto manifest = SelectVrRuntimeManifest(user_manifest,
|
||||
{"/usr/local/share/openxr/1/openxr_wivrn.json",
|
||||
"/usr/share/openxr/1/openxr_wivrn.json"});
|
||||
const auto preference = EnvString("MOCKTAIL_VR_RUNTIME", "auto");
|
||||
Require(
|
||||
preference == "auto" || preference == "system" ||
|
||||
preference == "wivrn" || preference == "steamvr" ||
|
||||
preference == "alvr",
|
||||
"MOCKTAIL_VR_RUNTIME must be auto, system, wivrn, steamvr, or alvr");
|
||||
const auto discovery =
|
||||
user_manifest.empty()
|
||||
? DiscoverVrRuntime(
|
||||
preference, EnvString("HOME", ""),
|
||||
EnvString("XDG_CONFIG_HOME", ""),
|
||||
EnvString("XDG_CONFIG_DIRS", ""),
|
||||
EnvString("XDG_RUNTIME_DIR",
|
||||
("/run/user/" + std::to_string(getuid())).c_str()))
|
||||
: VrRuntimeDiscovery{{}, "explicit XR_RUNTIME_JSON override"};
|
||||
const auto manifest =
|
||||
SelectVrRuntimeManifest(user_manifest, discovery.candidates);
|
||||
Log(" [vr-backend] runtime selection: %s\n", discovery.reason.c_str());
|
||||
Require(!manifest.empty() ||
|
||||
(preference != "steamvr" && preference != "alvr" &&
|
||||
preference != "wivrn"),
|
||||
"Selected VR runtime was not found. For ALVR, install SteamVR and "
|
||||
"set XR_RUNTIME_JSON "
|
||||
"to its steamxr_linux64.json if automatic discovery cannot find "
|
||||
"the installation. "
|
||||
"For WiVRn, install its native runtime or select its manifest "
|
||||
"explicitly.");
|
||||
if (!manifest.empty()) {
|
||||
Require(setenv("XR_RUNTIME_JSON", manifest.c_str(), 1) == 0,
|
||||
"Cannot select the OpenXR runtime");
|
||||
Log(" [vr-backend] runtime manifest: %s\n", manifest.c_str());
|
||||
} else {
|
||||
Log(" [vr-backend] using registered OpenXR runtime; connect the headset in WiVRn first\n");
|
||||
Log(" [vr-backend] using registered OpenXR runtime; connect the headset "
|
||||
"in WiVRn or SteamVR/ALVR first\n");
|
||||
}
|
||||
vk_loader_ = dlopen("libvulkan.so.1", RTLD_NOW | RTLD_LOCAL);
|
||||
Require(vk_loader_ != nullptr,
|
||||
"Cannot load the host Vulkan loader libvulkan.so.1");
|
||||
auto loader_gipa = reinterpret_cast<PFN_vkGetInstanceProcAddr>(
|
||||
dlsym(vk_loader_, "vkGetInstanceProcAddr"));
|
||||
Require(loader_gipa != nullptr, "Host vkGetInstanceProcAddr is missing");
|
||||
|
||||
const char* extension = XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME;
|
||||
graphics_api_ = graphics_api;
|
||||
if (graphics_api_ == VrGraphicsApi::kVulkan) {
|
||||
vk_loader_ = dlopen("libvulkan.so.1", RTLD_NOW | RTLD_LOCAL);
|
||||
Require(vk_loader_ != nullptr,
|
||||
"Cannot load the host Vulkan loader libvulkan.so.1");
|
||||
auto loader_gipa = reinterpret_cast<PFN_vkGetInstanceProcAddr>(
|
||||
dlsym(vk_loader_, "vkGetInstanceProcAddr"));
|
||||
Require(loader_gipa != nullptr, "Host vkGetInstanceProcAddr is missing");
|
||||
}
|
||||
const char *vk_extensions[] = {XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME};
|
||||
const char *gl_extensions[] = {XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME,
|
||||
XR_MNDX_EGL_ENABLE_EXTENSION_NAME};
|
||||
auto info = XrInfo<XrInstanceCreateInfo>(XR_TYPE_INSTANCE_CREATE_INFO);
|
||||
std::strcpy(info.applicationInfo.applicationName, "Mocktail Roblox VR");
|
||||
std::strcpy(info.applicationInfo.engineName, "Mocktail");
|
||||
info.applicationInfo.apiVersion = XR_MAKE_VERSION(1, 0, 0);
|
||||
info.enabledExtensionCount = 1;
|
||||
info.enabledExtensionNames = &extension;
|
||||
info.enabledExtensionCount =
|
||||
graphics_api_ == VrGraphicsApi::kVulkan ? 1 : 2;
|
||||
info.enabledExtensionNames =
|
||||
graphics_api_ == VrGraphicsApi::kVulkan ? vk_extensions : gl_extensions;
|
||||
XrInstance instance = XR_NULL_HANDLE;
|
||||
const XrResult instance_result = xrCreateInstance(&info, &instance);
|
||||
if (instance_result == XR_ERROR_RUNTIME_UNAVAILABLE) {
|
||||
throw std::runtime_error("xrCreateInstance: " +
|
||||
VrRuntimeUnavailableHint(!user_manifest.empty(), manifest));
|
||||
}
|
||||
if (instance_result == XR_ERROR_EXTENSION_NOT_PRESENT &&
|
||||
graphics_api_ == VrGraphicsApi::kOpenGles) {
|
||||
throw std::runtime_error(
|
||||
"OpenGL VR requires runtime extensions XR_KHR_opengl_es_enable and "
|
||||
"XR_MNDX_egl_enable; select direct-vulkan if the runtime does not "
|
||||
"support EGL.");
|
||||
}
|
||||
CheckXr(XR_NULL_HANDLE, instance_result, "xrCreateInstance");
|
||||
xr_instance_ = instance;
|
||||
|
||||
@@ -287,7 +330,8 @@ Status OpenXrBackend::Arm() {
|
||||
// state while the server runs with no headset connected.
|
||||
throw std::runtime_error(
|
||||
"The OpenXR runtime is reachable but reports no connected headset "
|
||||
"(XR_ERROR_FORM_FACTOR_UNAVAILABLE). With WiVRn: start the server "
|
||||
"(XR_ERROR_FORM_FACTOR_UNAVAILABLE). Start WiVRn or SteamVR with "
|
||||
"ALVR "
|
||||
"and connect the headset first; VR applications can only run while "
|
||||
"the headset connection is established.");
|
||||
}
|
||||
@@ -295,23 +339,36 @@ Status OpenXrBackend::Arm() {
|
||||
Require(xr_system_ != XR_NULL_SYSTEM_ID,
|
||||
"The OpenXR runtime exposes no head-mounted display system");
|
||||
|
||||
auto graphics_requirements =
|
||||
XrInfo<XrGraphicsRequirementsVulkan2KHR>(
|
||||
XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN2_KHR);
|
||||
auto get_requirements = reinterpret_cast<PFN_xrGetVulkanGraphicsRequirements2KHR>(
|
||||
ResolveFunction("xrGetVulkanGraphicsRequirements2KHR"));
|
||||
CheckXr(instance,
|
||||
get_requirements(instance, xr_system_, &graphics_requirements),
|
||||
"xrGetVulkanGraphicsRequirements2KHR");
|
||||
Log(" [vr-backend] runtime Vulkan requirement: %u.%u - %u.%u\n",
|
||||
XR_VERSION_MAJOR(graphics_requirements.minApiVersionSupported),
|
||||
XR_VERSION_MINOR(graphics_requirements.minApiVersionSupported),
|
||||
XR_VERSION_MAJOR(graphics_requirements.maxApiVersionSupported),
|
||||
XR_VERSION_MINOR(graphics_requirements.maxApiVersionSupported));
|
||||
if (graphics_api_ == VrGraphicsApi::kVulkan) {
|
||||
auto graphics_requirements = XrInfo<XrGraphicsRequirementsVulkan2KHR>(
|
||||
XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN2_KHR);
|
||||
auto get_requirements =
|
||||
reinterpret_cast<PFN_xrGetVulkanGraphicsRequirements2KHR>(
|
||||
ResolveFunction("xrGetVulkanGraphicsRequirements2KHR"));
|
||||
CheckXr(instance,
|
||||
get_requirements(instance, xr_system_, &graphics_requirements),
|
||||
"xrGetVulkanGraphicsRequirements2KHR");
|
||||
Log(" [vr-backend] runtime Vulkan requirement: %u.%u - %u.%u\n",
|
||||
XR_VERSION_MAJOR(graphics_requirements.minApiVersionSupported),
|
||||
XR_VERSION_MINOR(graphics_requirements.minApiVersionSupported),
|
||||
XR_VERSION_MAJOR(graphics_requirements.maxApiVersionSupported),
|
||||
XR_VERSION_MINOR(graphics_requirements.maxApiVersionSupported));
|
||||
|
||||
} else {
|
||||
auto requirements = XrInfo<XrGraphicsRequirementsOpenGLESKHR>(
|
||||
XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR);
|
||||
auto get = reinterpret_cast<PFN_xrGetOpenGLESGraphicsRequirementsKHR>(
|
||||
ResolveFunction("xrGetOpenGLESGraphicsRequirementsKHR"));
|
||||
CheckXr(instance, get(instance, xr_system_, &requirements),
|
||||
"xrGetOpenGLESGraphicsRequirementsKHR");
|
||||
gl_min_version_ = requirements.minApiVersionSupported;
|
||||
gl_max_version_ = requirements.maxApiVersionSupported;
|
||||
}
|
||||
armed_.store(true, std::memory_order_release);
|
||||
Log(" [vr-backend] armed: OpenXR instance/system ready; awaiting the "
|
||||
"guest Vulkan device\n");
|
||||
Log(" [vr-backend] armed: OpenXR instance/system ready; awaiting guest "
|
||||
"%s\n",
|
||||
graphics_api_ == VrGraphicsApi::kVulkan ? "Vulkan device"
|
||||
: "OpenGL ES / EGL context");
|
||||
return Status::Ok();
|
||||
} catch (const std::exception& error) {
|
||||
DisarmLocked();
|
||||
@@ -326,6 +383,10 @@ void OpenXrBackend::Disarm() {
|
||||
|
||||
void OpenXrBackend::DisarmLocked() {
|
||||
TeardownSessionLocked("disarm");
|
||||
if (gles_)
|
||||
gles_->Destroy();
|
||||
delete gles_;
|
||||
gles_ = nullptr;
|
||||
if (xr_instance_ != nullptr) {
|
||||
const auto instance = static_cast<XrInstance>(xr_instance_);
|
||||
(void)xrDestroyInstance(instance);
|
||||
@@ -611,6 +672,141 @@ bool OpenXrBackend::InitializeSessionLocked(
|
||||
"xrCreateSession");
|
||||
xr_session_ = session;
|
||||
|
||||
return InitializeSessionResourcesLocked(error);
|
||||
} catch (const std::exception &exception) {
|
||||
*error = exception.what();
|
||||
TeardownSessionLocked("session init failure", session_recovery_pending_);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Status OpenXrBackend::AttachGlesContext(void *display, void *config,
|
||||
void *context, void *egl_get_proc,
|
||||
void *(*resolve)(const char *)) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!armed_ || graphics_api_ != VrGraphicsApi::kOpenGles)
|
||||
return Status::Ok();
|
||||
try {
|
||||
if (gles_) {
|
||||
Require(gles_->context() == context && gles_->display() == display &&
|
||||
gles_->IsCurrent(),
|
||||
"OpenGL VR cannot switch to a different live EGL context");
|
||||
gles_->MakeCurrent();
|
||||
return Status::Ok();
|
||||
}
|
||||
gles_ = new GlesTransport;
|
||||
std::string error;
|
||||
Require(gles_->Initialize(display, config, context, egl_get_proc, resolve,
|
||||
&error),
|
||||
error);
|
||||
Require(gles_->version() >= gl_min_version_ &&
|
||||
gles_->version() <= gl_max_version_,
|
||||
"EGL context version is outside the OpenXR runtime's OpenGL ES "
|
||||
"requirements");
|
||||
Require(InitializeGlesSessionLocked(&error), error);
|
||||
Log(" [vr-backend] bound OpenGL ES context to OpenXR; GPU eye copies "
|
||||
"enabled, pose_source=%s\n",
|
||||
EnvString("MOCKTAIL_VR_POSE_SOURCE", "runtime").c_str());
|
||||
return Status::Ok();
|
||||
} catch (const std::exception &error) {
|
||||
TeardownSessionLocked("EGL binding failed");
|
||||
if (gles_)
|
||||
gles_->Destroy();
|
||||
delete gles_;
|
||||
gles_ = nullptr;
|
||||
return Status::Error(StatusCode::kUnavailable, error.what());
|
||||
}
|
||||
}
|
||||
|
||||
bool OpenXrBackend::InitializeGlesSessionLocked(std::string *error) {
|
||||
try {
|
||||
Require(gles_ && gles_->IsCurrent(),
|
||||
"OpenXR EGL session creation requires the current guest context");
|
||||
auto binding =
|
||||
XrInfo<XrGraphicsBindingEGLMNDX>(XR_TYPE_GRAPHICS_BINDING_EGL_MNDX);
|
||||
binding.display = gles_->display();
|
||||
binding.config = gles_->config();
|
||||
binding.context = gles_->context();
|
||||
binding.getProcAddress =
|
||||
reinterpret_cast<PFN_xrEglGetProcAddressMNDX>(gles_->egl_get_proc());
|
||||
auto info = XrInfo<XrSessionCreateInfo>(XR_TYPE_SESSION_CREATE_INFO);
|
||||
info.next = &binding;
|
||||
info.systemId = xr_system_;
|
||||
XrSession session = XR_NULL_HANDLE;
|
||||
CheckXr(
|
||||
static_cast<XrInstance>(xr_instance_),
|
||||
xrCreateSession(static_cast<XrInstance>(xr_instance_), &info, &session),
|
||||
"xrCreateSession EGL");
|
||||
xr_session_ = session;
|
||||
return InitializeSessionResourcesLocked(error);
|
||||
} catch (const std::exception &exception) {
|
||||
*error = exception.what();
|
||||
TeardownSessionLocked("EGL session init failure",
|
||||
session_recovery_pending_);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void OpenXrBackend::DetachGlesContext(void *context) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!gles_ || gles_->context() != context)
|
||||
return;
|
||||
TeardownSessionLocked("EGL context destruction");
|
||||
gles_->Destroy();
|
||||
delete gles_;
|
||||
gles_ = nullptr;
|
||||
}
|
||||
|
||||
void *OpenXrBackend::WrapGlesProcAddress(const char *name, void *raw) {
|
||||
if (!armed_ || graphics_api_ != VrGraphicsApi::kOpenGles || !gles_)
|
||||
return raw;
|
||||
return GlesTransport::Wrap(name, raw);
|
||||
}
|
||||
|
||||
void OpenXrBackend::NoteGlesPresent() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!armed_ || graphics_api_ != VrGraphicsApi::kOpenGles || !gles_ ||
|
||||
!gles_->IsCurrent())
|
||||
return;
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const auto &eye = gles_->eye(i);
|
||||
eyes_[i].valid = eye.framebuffer != 0;
|
||||
eyes_[i].frame = eye.frame;
|
||||
eyes_[i].owner = eye.owner;
|
||||
eyes_[i].width = eye.width;
|
||||
eyes_[i].height = eye.height;
|
||||
}
|
||||
const auto &left = gles_->eye(0);
|
||||
const auto &right = gles_->eye(1);
|
||||
eyes_bound_ =
|
||||
left.framebuffer && right.framebuffer &&
|
||||
left.framebuffer != right.framebuffer && left.owner == right.owner &&
|
||||
(left.texture != right.texture || left.object != right.object);
|
||||
}
|
||||
NoteHostPresent(VK_NULL_HANDLE, VK_NULL_HANDLE);
|
||||
}
|
||||
|
||||
bool OpenXrBackend::CopyGlesEyesIntoSwapchains() {
|
||||
if (!gles_)
|
||||
return false;
|
||||
unsigned textures[2] = {};
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const auto &slot = eye_swapchains_[i];
|
||||
if (!slot.image_acquired || slot.acquired_index >= slot.gl_images.size())
|
||||
return false;
|
||||
textures[i] = slot.gl_images[slot.acquired_index];
|
||||
}
|
||||
Require(gles_->Copy(textures, eye_swapchains_[0].width,
|
||||
eye_swapchains_[0].height),
|
||||
"Cannot copy GLES eyes into runtime textures; verify EGL and OpenXR "
|
||||
"use the same GPU and support image sharing");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenXrBackend::InitializeSessionResourcesLocked(std::string *error) {
|
||||
try {
|
||||
const auto session = static_cast<XrSession>(xr_session_);
|
||||
auto space_info = XrInfo<XrReferenceSpaceCreateInfo>(
|
||||
XR_TYPE_REFERENCE_SPACE_CREATE_INFO);
|
||||
space_info.poseInReferenceSpace.orientation.w = 1;
|
||||
@@ -670,13 +866,12 @@ bool OpenXrBackend::InitializeSessionLocked(
|
||||
}
|
||||
recording_.store(true, std::memory_order_release);
|
||||
return true;
|
||||
} catch (const std::exception& exception) {
|
||||
} catch (const std::exception &exception) {
|
||||
*error = exception.what();
|
||||
TeardownSessionLocked("session init failure", session_recovery_pending_);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool OpenXrBackend::CreateSwapchains(std::string* error) {
|
||||
try {
|
||||
const auto session = static_cast<XrSession>(xr_session_);
|
||||
@@ -694,10 +889,15 @@ bool OpenXrBackend::CreateSwapchains(std::string* error) {
|
||||
// The swapchain format must accept the guest eye content. Prefer the
|
||||
// common 8-bit formats; the final choice is cross-checked against the
|
||||
// recorded eye images before the first copy (blit converts if needed).
|
||||
VkFormat chosen = VK_FORMAT_UNDEFINED;
|
||||
for (const auto candidate :
|
||||
{VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM,
|
||||
VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_R8G8B8A8_SRGB}) {
|
||||
std::int64_t chosen = 0;
|
||||
const std::vector<std::int64_t> candidates =
|
||||
graphics_api_ == VrGraphicsApi::kVulkan
|
||||
? std::vector<std::int64_t>{VK_FORMAT_B8G8R8A8_UNORM,
|
||||
VK_FORMAT_R8G8B8A8_UNORM,
|
||||
VK_FORMAT_B8G8R8A8_SRGB,
|
||||
VK_FORMAT_R8G8B8A8_SRGB}
|
||||
: std::vector<std::int64_t>{GL_RGBA8, GL_SRGB8_ALPHA8};
|
||||
for (const auto candidate : candidates) {
|
||||
if (std::find(formats.begin(), formats.end(),
|
||||
static_cast<std::int64_t>(candidate)) != formats.end()) {
|
||||
chosen = candidate;
|
||||
@@ -730,30 +930,48 @@ bool OpenXrBackend::CreateSwapchains(std::string* error) {
|
||||
CheckXr(static_cast<XrInstance>(xr_instance_),
|
||||
xrCreateSwapchain(session, &create_info, &swapchain),
|
||||
"xrCreateSwapchain");
|
||||
EyeSwapchain &slot = eye_swapchains_[eye];
|
||||
slot.swapchain =
|
||||
swapchain; // Own immediately, including enumeration failures.
|
||||
std::uint32_t image_count = 0;
|
||||
CheckXr(static_cast<XrInstance>(xr_instance_),
|
||||
xrEnumerateSwapchainImages(swapchain, 0, &image_count, nullptr),
|
||||
"xrEnumerateSwapchainImages count");
|
||||
Require(image_count > 0 && image_count <= 16,
|
||||
"the runtime returned an empty swapchain");
|
||||
std::vector<XrSwapchainImageVulkanKHR> images(image_count);
|
||||
for (auto& image : images) {
|
||||
image.type = XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR;
|
||||
}
|
||||
CheckXr(static_cast<XrInstance>(xr_instance_),
|
||||
xrEnumerateSwapchainImages(
|
||||
swapchain, image_count, &image_count,
|
||||
reinterpret_cast<XrSwapchainImageBaseHeader*>(images.data())),
|
||||
"xrEnumerateSwapchainImages");
|
||||
EyeSwapchain& slot = eye_swapchains_[eye];
|
||||
slot.swapchain = swapchain;
|
||||
slot.images.clear();
|
||||
for (const auto& image : images) {
|
||||
slot.images.push_back(image.image);
|
||||
if (graphics_api_ == VrGraphicsApi::kVulkan) {
|
||||
std::vector<XrSwapchainImageVulkanKHR> images(image_count);
|
||||
for (auto &image : images) {
|
||||
image.type = XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR;
|
||||
}
|
||||
CheckXr(
|
||||
static_cast<XrInstance>(xr_instance_),
|
||||
xrEnumerateSwapchainImages(
|
||||
swapchain, image_count, &image_count,
|
||||
reinterpret_cast<XrSwapchainImageBaseHeader *>(images.data())),
|
||||
"xrEnumerateSwapchainImages");
|
||||
slot.images.clear();
|
||||
for (const auto &image : images) {
|
||||
slot.images.push_back(image.image);
|
||||
}
|
||||
} else {
|
||||
std::vector<XrSwapchainImageOpenGLESKHR> images(image_count);
|
||||
for (auto &image : images)
|
||||
image.type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR;
|
||||
CheckXr(
|
||||
static_cast<XrInstance>(xr_instance_),
|
||||
xrEnumerateSwapchainImages(
|
||||
swapchain, image_count, &image_count,
|
||||
reinterpret_cast<XrSwapchainImageBaseHeader *>(images.data())),
|
||||
"xrEnumerateSwapchainImages GLES");
|
||||
slot.gl_images.clear();
|
||||
for (const auto &image : images) {
|
||||
slot.gl_images.push_back(image.image);
|
||||
}
|
||||
}
|
||||
slot.width = width;
|
||||
slot.height = height;
|
||||
slot.format = chosen;
|
||||
slot.format = static_cast<VkFormat>(chosen);
|
||||
Log(" [vr-backend] eye%d swapchain: %ux%u format=%d images=%u\n", eye,
|
||||
width, height, static_cast<int>(chosen), image_count);
|
||||
}
|
||||
@@ -767,6 +985,7 @@ bool OpenXrBackend::CreateSwapchains(std::string* error) {
|
||||
void OpenXrBackend::DestroySwapchains() {
|
||||
for (EyeSwapchain& slot : eye_swapchains_) {
|
||||
slot.images.clear();
|
||||
slot.gl_images.clear();
|
||||
if (slot.swapchain != nullptr) {
|
||||
(void)xrDestroySwapchain(static_cast<XrSwapchain>(slot.swapchain));
|
||||
slot.swapchain = nullptr;
|
||||
@@ -817,6 +1036,8 @@ void OpenXrBackend::TeardownSessionLocked(const char* reason, bool preserve_devi
|
||||
canted_rejection_logged_ = false;
|
||||
eyes_[0] = {};
|
||||
eyes_[1] = {};
|
||||
if (gles_)
|
||||
gles_->ResetEyes();
|
||||
applied_poses_.clear();
|
||||
published_pose_.valid = false;
|
||||
if (preserve_device) {
|
||||
@@ -1264,12 +1485,19 @@ void OpenXrBackend::HandleReferenceSpaceChange(std::int64_t change_time) {
|
||||
}
|
||||
|
||||
bool OpenXrBackend::RecoverSessionLocked(std::uint64_t now_ns) {
|
||||
if (!session_recovery_pending_ || vk_device_ == VK_NULL_HANDLE || vk_ == nullptr ||
|
||||
xr_instance_ == nullptr || now_ns < next_session_retry_ns_) return false;
|
||||
if (!session_recovery_pending_ ||
|
||||
(graphics_api_ == VrGraphicsApi::kVulkan
|
||||
? (vk_device_ == VK_NULL_HANDLE || vk_ == nullptr)
|
||||
: gles_ == nullptr) ||
|
||||
xr_instance_ == nullptr || now_ns < next_session_retry_ns_)
|
||||
return false;
|
||||
next_session_retry_ns_ = now_ns + 1'000'000'000ULL;
|
||||
std::string error;
|
||||
// nullptr means reuse the verified queue family of this existing VkDevice.
|
||||
if (!InitializeSessionLocked(vk_physical_device_, vk_device_, nullptr, &error)) {
|
||||
if (!(graphics_api_ == VrGraphicsApi::kVulkan
|
||||
? InitializeSessionLocked(vk_physical_device_, vk_device_, nullptr,
|
||||
&error)
|
||||
: InitializeGlesSessionLocked(&error))) {
|
||||
Log(" [vr-backend] waiting to restore XR session: %s\n", error.c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -1400,10 +1628,13 @@ void OpenXrBackend::RunFrameCycle(VkQueue queue, VkDevice device) {
|
||||
}
|
||||
const bool copied = [&] {
|
||||
const ScopedStageTimer copy_record(perf::Stage::kCopyRecord);
|
||||
return CopyEyesIntoSwapchains(queue);
|
||||
return graphics_api_ == VrGraphicsApi::kVulkan
|
||||
? CopyEyesIntoSwapchains(queue)
|
||||
: CopyGlesEyesIntoSwapchains();
|
||||
}() && [&] {
|
||||
const ScopedStageTimer evidence_timer(perf::Stage::kEvidence);
|
||||
return CaptureEyeEvidence(queue, 0) && CaptureEyeEvidence(queue, 1);
|
||||
return graphics_api_ == VrGraphicsApi::kOpenGles ||
|
||||
(CaptureEyeEvidence(queue, 0) && CaptureEyeEvidence(queue, 1));
|
||||
}();
|
||||
for (int eye = 0; eye < 2; ++eye) {
|
||||
if (images_held[eye]) {
|
||||
@@ -1454,14 +1685,23 @@ void OpenXrBackend::RunFrameCycle(VkQueue queue, VkDevice device) {
|
||||
(!EnvString("MOCKTAIL_VR_XR_EVIDENCE_DIR", "").empty() &&
|
||||
published_pose_.frame % std::max<std::uint64_t>(1,
|
||||
EnvUnsigned("MOCKTAIL_VR_XR_EVIDENCE_INTERVAL", 20)) == 0)) {
|
||||
Log(" [vr-backend][evidence] projection submitted=%llu frame=%llu eye0_image=%p "
|
||||
"eye1_image=%p eye0_swapchain_index=%u eye1_swapchain_index=%u\n",
|
||||
static_cast<unsigned long long>(submitted),
|
||||
static_cast<unsigned long long>(published_pose_.frame),
|
||||
static_cast<void*>(eyes_[0].image),
|
||||
static_cast<void*>(eyes_[1].image),
|
||||
eye_swapchains_[0].acquired_index,
|
||||
eye_swapchains_[1].acquired_index);
|
||||
if (graphics_api_ == VrGraphicsApi::kOpenGles) {
|
||||
Log(" [vr-backend][evidence] projection submitted=%llu frame=%llu "
|
||||
"graphics=GLES eye0_texture=%u eye1_texture=%u\n",
|
||||
static_cast<unsigned long long>(submitted),
|
||||
static_cast<unsigned long long>(published_pose_.frame),
|
||||
gles_->eye(0).object, gles_->eye(1).object);
|
||||
} else {
|
||||
Log(" [vr-backend][evidence] projection submitted=%llu frame=%llu "
|
||||
"eye0_image=%p "
|
||||
"eye1_image=%p eye0_swapchain_index=%u eye1_swapchain_index=%u\n",
|
||||
static_cast<unsigned long long>(submitted),
|
||||
static_cast<unsigned long long>(published_pose_.frame),
|
||||
static_cast<void *>(eyes_[0].image),
|
||||
static_cast<void *>(eyes_[1].image),
|
||||
eye_swapchains_[0].acquired_index,
|
||||
eye_swapchains_[1].acquired_index);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
perf::ProcessCollector().Count(perf::Counter::kFramesSkipped);
|
||||
@@ -2192,3 +2432,32 @@ void mocktail_vr_xr_note_present(VkQueue queue, VkDevice device) {
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
extern "C" bool mocktail_vr_attach_egl(void *display, void *config,
|
||||
void *context, void *egl_get_proc,
|
||||
void *(*resolve)(const char *)) {
|
||||
if (auto *backend = mocktail::vr::ActiveVrBackend()) {
|
||||
const auto status = backend->AttachGlesContext(display, config, context,
|
||||
egl_get_proc, resolve);
|
||||
if (!status.ok())
|
||||
std::fprintf(stderr, " [vr-backend] %s\n", status.message().c_str());
|
||||
return status.ok();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
extern "C" void mocktail_vr_detach_egl(void *context) {
|
||||
if (auto *backend = mocktail::vr::ActiveVrBackend())
|
||||
backend->DetachGlesContext(context);
|
||||
}
|
||||
extern "C" void mocktail_vr_release_egl() {
|
||||
mocktail::vr::GlesTransport::ReleaseCurrent();
|
||||
}
|
||||
extern "C" void *mocktail_vr_wrap_gles_proc(const char *name, void *raw) {
|
||||
if (auto *backend = mocktail::vr::ActiveVrBackend())
|
||||
return backend->WrapGlesProcAddress(name, raw);
|
||||
return raw;
|
||||
}
|
||||
extern "C" void mocktail_vr_gles_present() {
|
||||
if (auto *backend = mocktail::vr::ActiveVrBackend())
|
||||
backend->NoteGlesPresent();
|
||||
}
|
||||
+238
-21
@@ -1,36 +1,253 @@
|
||||
#include "mocktail/vr/openxr_backend.h"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#define JSON_NOEXCEPTION 1
|
||||
#include <nlohmann/json.hpp>
|
||||
namespace mocktail::vr {
|
||||
std::string SelectVrRuntimeManifest(const std::string& explicit_manifest,
|
||||
const std::vector<std::string>& candidates) {
|
||||
if (!explicit_manifest.empty()) return explicit_manifest;
|
||||
for (const auto& path : candidates) {
|
||||
std::string
|
||||
SelectVrRuntimeManifest(const std::string &explicit_manifest,
|
||||
const std::vector<std::string> &candidates) {
|
||||
if (!explicit_manifest.empty())
|
||||
return explicit_manifest;
|
||||
for (const auto &path : candidates) {
|
||||
std::error_code error;
|
||||
if (std::filesystem::is_regular_file(path, error)) return path;
|
||||
if (std::filesystem::is_regular_file(path, error))
|
||||
return path;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string VrRuntimeUnavailableHint(bool user_selected, const std::string& manifest) {
|
||||
std::vector<std::string> VrRuntimeManifestCandidates(
|
||||
const std::string &preference, const std::string &home,
|
||||
const std::string &config_home, const std::string &config_dirs) {
|
||||
if (preference == "steamvr" || preference == "alvr") {
|
||||
if (home.empty())
|
||||
return {};
|
||||
std::vector<std::string> paths;
|
||||
const auto config = config_home.empty() ? home + "/.config" : config_home;
|
||||
const auto registry = config + "/openvr/openvrpaths.vrpath";
|
||||
std::error_code error;
|
||||
if (std::filesystem::file_size(registry, error) <= 1024 * 1024 && !error) {
|
||||
std::ifstream input(registry);
|
||||
const auto json = nlohmann::json::parse(input, nullptr, false);
|
||||
if (json.is_object() && json.contains("runtime") &&
|
||||
json["runtime"].is_array()) {
|
||||
for (const auto &item : json["runtime"]) {
|
||||
if (item.is_string()) {
|
||||
const auto root = std::filesystem::path(item.get<std::string>());
|
||||
if (root.is_absolute())
|
||||
paths.push_back((root / "steamxr_linux64.json").string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const std::vector<std::string> standard = {
|
||||
home +
|
||||
"/.local/share/Steam/steamapps/common/SteamVR/steamxr_linux64.json",
|
||||
home + "/.steam/steam/steamapps/common/SteamVR/steamxr_linux64.json",
|
||||
home + "/.steam/debian-installation/steamapps/common/SteamVR/"
|
||||
"steamxr_linux64.json"};
|
||||
paths.insert(paths.end(), standard.begin(), standard.end());
|
||||
return paths;
|
||||
}
|
||||
const std::vector<std::string> wivrn = {
|
||||
"/usr/local/share/openxr/1/openxr_wivrn.json",
|
||||
"/usr/share/openxr/1/openxr_wivrn.json"};
|
||||
if (preference == "wivrn")
|
||||
return wivrn;
|
||||
if (preference == "system")
|
||||
return {}; // Use the loader's full discovery rules.
|
||||
std::vector<std::string> candidates;
|
||||
const auto add_config = [&](const std::string &path) {
|
||||
if (path.empty() || path[0] != '/')
|
||||
return;
|
||||
candidates.push_back(path + "/openxr/1/active_runtime.x86_64.json");
|
||||
candidates.push_back(path + "/openxr/1/active_runtime.json");
|
||||
};
|
||||
add_config(!config_home.empty() ? config_home
|
||||
: (home.empty() ? "" : home + "/.config"));
|
||||
std::istringstream directories(config_dirs.empty() ? "/etc/xdg"
|
||||
: config_dirs);
|
||||
std::string directory;
|
||||
while (std::getline(directories, directory, ':'))
|
||||
add_config(directory);
|
||||
add_config("/etc");
|
||||
candidates.insert(candidates.end(), wivrn.begin(), wivrn.end());
|
||||
return candidates;
|
||||
}
|
||||
|
||||
VrRuntimeDiscovery DiscoverVrRuntime(const std::string &preference,
|
||||
const std::string &home,
|
||||
const std::string &config_home,
|
||||
const std::string &config_dirs,
|
||||
const std::string &runtime_dir,
|
||||
const std::string &proc_root) {
|
||||
VrRuntimeDiscovery result;
|
||||
auto steam =
|
||||
VrRuntimeManifestCandidates("steamvr", home, config_home, config_dirs);
|
||||
auto wivrn =
|
||||
VrRuntimeManifestCandidates("wivrn", home, config_home, config_dirs);
|
||||
bool steam_running = false;
|
||||
bool wivrn_listening = false;
|
||||
// Read the kernel's socket table: a leftover socket pathname alone is not
|
||||
// evidence of a listening server. Do not connect and create a dummy XR
|
||||
// client.
|
||||
const auto ipc = (std::filesystem::path(runtime_dir) / "wivrn/comp_ipc")
|
||||
.lexically_normal();
|
||||
if (!runtime_dir.empty()) {
|
||||
std::ifstream sockets(std::filesystem::path(proc_root) / "net/unix");
|
||||
std::string line;
|
||||
while (std::getline(sockets, line)) {
|
||||
std::istringstream row(line);
|
||||
std::string num, refs, protocol, flags, type, state, inode, path;
|
||||
if (!(row >> num >> refs >> protocol >> flags >> type >> state >> inode))
|
||||
continue;
|
||||
std::getline(row >> std::ws, path);
|
||||
if (flags == "00010000" && type == "0001" &&
|
||||
std::filesystem::path(path).lexically_normal() == ipc) {
|
||||
struct stat socket_info{};
|
||||
if (stat(path.c_str(), &socket_info) == 0 &&
|
||||
S_ISSOCK(socket_info.st_mode) && socket_info.st_uid == getuid())
|
||||
wivrn_listening = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Restrict process discovery to this user. In particular, never select a
|
||||
// root server for a desktop client or another user's SteamVR installation.
|
||||
std::error_code error;
|
||||
std::filesystem::directory_iterator processes(proc_root, error);
|
||||
for (; !error && processes != std::filesystem::directory_iterator();
|
||||
processes.increment(error)) {
|
||||
const auto name = processes->path().filename().string();
|
||||
if (name.empty() || !std::all_of(name.begin(), name.end(), [](char c) {
|
||||
return c >= '0' && c <= '9';
|
||||
}))
|
||||
continue;
|
||||
struct stat metadata{};
|
||||
if (stat(processes->path().c_str(), &metadata) != 0 ||
|
||||
metadata.st_uid != getuid())
|
||||
continue;
|
||||
std::ifstream comm_file(processes->path() / "comm");
|
||||
std::string comm;
|
||||
std::getline(comm_file, comm);
|
||||
if (comm != "vrserver" && comm != "wivrn-server")
|
||||
continue;
|
||||
std::error_code exe_error;
|
||||
const auto executable =
|
||||
std::filesystem::read_symlink(processes->path() / "exe", exe_error);
|
||||
if (exe_error || !executable.is_absolute() || executable.filename() != comm)
|
||||
continue;
|
||||
if (comm == "vrserver") {
|
||||
steam_running = true;
|
||||
steam.insert(steam.begin(),
|
||||
(executable.parent_path().parent_path().parent_path() /
|
||||
"steamxr_linux64.json")
|
||||
.string());
|
||||
} else {
|
||||
// Native developer builds keep this manifest at their build root.
|
||||
auto directory = executable.parent_path();
|
||||
for (int i = 0; i < 3 && directory.has_relative_path();
|
||||
++i, directory = directory.parent_path()) {
|
||||
wivrn.insert(wivrn.begin(),
|
||||
(directory / "openxr_wivrn-dev.json").string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (preference == "steamvr" || preference == "alvr") {
|
||||
result.candidates = steam;
|
||||
result.reason = "explicit SteamVR/ALVR preference";
|
||||
} else if (preference == "wivrn") {
|
||||
result.candidates = wivrn;
|
||||
result.reason = "explicit WiVRn preference";
|
||||
} else if (preference == "system") {
|
||||
result.reason = "system OpenXR loader selection";
|
||||
} else {
|
||||
const auto registered =
|
||||
VrRuntimeManifestCandidates("auto", home, config_home, config_dirs);
|
||||
if (steam_running != wivrn_listening) {
|
||||
const std::string runtime_tag = steam_running ? "steamvr" : "wivrn";
|
||||
for (const auto &path : registered) {
|
||||
// Only registered entries, not native fallback paths.
|
||||
if (std::filesystem::path(path).filename().string().find(
|
||||
"active_runtime") != 0)
|
||||
continue;
|
||||
std::error_code manifest_error;
|
||||
if (std::filesystem::file_size(path, manifest_error) > 1024 * 1024 ||
|
||||
manifest_error)
|
||||
continue;
|
||||
auto identity =
|
||||
std::filesystem::weakly_canonical(path, manifest_error).string();
|
||||
std::ifstream input(path);
|
||||
const auto json = nlohmann::json::parse(input, nullptr, false);
|
||||
if (json.is_object() && json.contains("runtime") &&
|
||||
json["runtime"].is_object()) {
|
||||
for (const auto *key : {"name", "library_path"}) {
|
||||
const auto &runtime = json["runtime"];
|
||||
if (runtime.contains(key) && runtime[key].is_string())
|
||||
identity += " " + runtime[key].get<std::string>();
|
||||
}
|
||||
}
|
||||
std::transform(identity.begin(), identity.end(), identity.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
if (identity.find(runtime_tag) != std::string::npos)
|
||||
result.candidates.push_back(path);
|
||||
}
|
||||
const auto &active = steam_running ? steam : wivrn;
|
||||
result.candidates.insert(result.candidates.end(), active.begin(),
|
||||
active.end());
|
||||
result.reason = steam_running
|
||||
? "running SteamVR detected (including ALVR)"
|
||||
: "listening WiVRn server detected";
|
||||
} else {
|
||||
result.reason =
|
||||
steam_running
|
||||
? "both servers detected; registered runtime has priority"
|
||||
: "no single running server detected; registered runtime has "
|
||||
"priority";
|
||||
}
|
||||
result.candidates.insert(result.candidates.end(), registered.begin(),
|
||||
registered.end());
|
||||
result.candidates.insert(result.candidates.end(), steam.begin(),
|
||||
steam.end());
|
||||
result.candidates.insert(result.candidates.end(), wivrn.begin(),
|
||||
wivrn.end());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string VrRuntimeUnavailableHint(bool user_selected,
|
||||
const std::string &manifest) {
|
||||
if (user_selected) {
|
||||
return "User-selected XR_RUNTIME_JSON '" + manifest +
|
||||
"' is unavailable. Check the manifest/library and that its runtime server "
|
||||
"is running with a connected headset; this error alone does not distinguish them.";
|
||||
"' is unavailable. Check the manifest/library and that its runtime "
|
||||
"server "
|
||||
"is running with a connected headset; this error alone does not "
|
||||
"distinguish them.";
|
||||
}
|
||||
if (!manifest.empty()) {
|
||||
return "Auto-selected WiVRn runtime is unavailable. Start WiVRn and connect "
|
||||
"the headset, then retry. If it remains unavailable, check the runtime installation.";
|
||||
return "Selected OpenXR runtime '" + manifest +
|
||||
"' is unavailable. Start its server "
|
||||
"(WiVRn, or SteamVR with ALVR) and connect the headset. Run the "
|
||||
"server and "
|
||||
"Mocktail as the same desktop user in the same runtime environment.";
|
||||
}
|
||||
return "The registered OpenXR runtime is unavailable. Install/start WiVRn and "
|
||||
"connect the headset, or explicitly select an installed runtime with XR_RUNTIME_JSON.";
|
||||
return "The registered OpenXR runtime is unavailable. Start WiVRn or SteamVR "
|
||||
"with ALVR and "
|
||||
"connect the headset, or explicitly select an installed runtime with "
|
||||
"XR_RUNTIME_JSON.";
|
||||
}
|
||||
|
||||
VrBackendMode ResolveVrBackendMode(bool vr_enabled) {
|
||||
if (!vr_enabled) {
|
||||
return VrBackendMode::kDisabled;
|
||||
}
|
||||
const char* value = std::getenv("MOCKTAIL_VR_BACKEND");
|
||||
const char *value = std::getenv("MOCKTAIL_VR_BACKEND");
|
||||
const std::string mode = value ? value : "xr";
|
||||
if (mode == "native" || mode == "native-stereo") {
|
||||
return VrBackendMode::kNativeOnly;
|
||||
@@ -38,16 +255,16 @@ VrBackendMode ResolveVrBackendMode(bool vr_enabled) {
|
||||
return VrBackendMode::kXrOutput;
|
||||
}
|
||||
|
||||
const char* VrBackendModeName(VrBackendMode mode) {
|
||||
const char *VrBackendModeName(VrBackendMode mode) {
|
||||
switch (mode) {
|
||||
case VrBackendMode::kDisabled:
|
||||
return "disabled";
|
||||
case VrBackendMode::kNativeOnly:
|
||||
return "native-stereo-diagnostic";
|
||||
case VrBackendMode::kXrOutput:
|
||||
return "openxr-output";
|
||||
case VrBackendMode::kDisabled:
|
||||
return "disabled";
|
||||
case VrBackendMode::kNativeOnly:
|
||||
return "native-stereo-diagnostic";
|
||||
case VrBackendMode::kXrOutput:
|
||||
return "openxr-output";
|
||||
}
|
||||
return "disabled";
|
||||
}
|
||||
|
||||
}
|
||||
} // namespace mocktail::vr
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace mocktail::vr {
|
||||
OpenXrBackend::~OpenXrBackend() = default;
|
||||
Status OpenXrBackend::Arm() {
|
||||
Status OpenXrBackend::Arm(VrGraphicsApi) {
|
||||
return Status::Error(StatusCode::kUnavailable, "OpenXR support is disabled in this build");
|
||||
}
|
||||
void OpenXrBackend::Disarm() {}
|
||||
|
||||
+59
-2
@@ -40,6 +40,9 @@
|
||||
|
||||
namespace mocktail {
|
||||
namespace window {
|
||||
namespace {
|
||||
bool AttachVrEgl();
|
||||
}
|
||||
|
||||
// Local EGL declarations avoid conflicts with the stub headers.
|
||||
|
||||
@@ -1309,6 +1312,16 @@ bool Init(int width, int height, const char* title) {
|
||||
}
|
||||
return CreateSoftwareWaitingWindow(width, height, title);
|
||||
}
|
||||
if (!AttachVrEgl()) {
|
||||
SDL_GL_DestroyContext(sdl_context);
|
||||
SDL_DestroyWindow(g_state.sdl_window);
|
||||
g_state.sdl_window = nullptr;
|
||||
g_state.egl_context = nullptr;
|
||||
g_state.egl_display = nullptr;
|
||||
g_state.egl_config = nullptr;
|
||||
g_state.egl_surface = nullptr;
|
||||
return false;
|
||||
}
|
||||
fprintf(stderr, " [window] EGL context and surface initialized via SDL3\n");
|
||||
if (WindowTraceEnabled()) {
|
||||
fprintf(
|
||||
@@ -1342,6 +1355,23 @@ bool Init(int width, int height, const char* title) {
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool AttachVrEgl() {
|
||||
using Attach =
|
||||
bool (*)(void *, void *, void *, void *, void *(*)(const char *));
|
||||
auto attach =
|
||||
reinterpret_cast<Attach>(dlsym(RTLD_DEFAULT, "mocktail_vr_attach_egl"));
|
||||
if (!attach)
|
||||
return true;
|
||||
return attach(
|
||||
g_state.egl_display, g_state.egl_config, g_state.egl_context,
|
||||
reinterpret_cast<void *>(SDL_EGL_GetProcAddress("eglGetProcAddress")),
|
||||
+[](const char *name) -> void * {
|
||||
return reinterpret_cast<void *>(SDL_GL_GetProcAddress(name));
|
||||
});
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool MakeCurrentOnThread() {
|
||||
if (!g_state.initialised || !g_state.egl_context) {
|
||||
if (WindowTraceEnabled()) {
|
||||
@@ -1359,7 +1389,7 @@ bool MakeCurrentOnThread() {
|
||||
return false;
|
||||
}
|
||||
fprintf(stderr, " [window] EGL context now current on engine thread\n");
|
||||
return true;
|
||||
return AttachVrEgl();
|
||||
}
|
||||
|
||||
bool ReleaseCurrentOnThread() {
|
||||
@@ -1374,6 +1404,11 @@ bool ReleaseCurrentOnThread() {
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
using Release = void (*)();
|
||||
auto release =
|
||||
reinterpret_cast<Release>(dlsym(RTLD_DEFAULT, "mocktail_vr_release_egl"));
|
||||
if (release)
|
||||
release();
|
||||
if (WindowTraceEnabled()) {
|
||||
fprintf(stderr, " [window] EGL context released on current thread\n");
|
||||
}
|
||||
@@ -1516,7 +1551,11 @@ void* GetGLProcAddress(const char* name) {
|
||||
if (!g_state.initialised || name == nullptr || name[0] == '\0') {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<void*>(SDL_GL_GetProcAddress(name));
|
||||
void *raw = reinterpret_cast<void *>(SDL_GL_GetProcAddress(name));
|
||||
using Wrap = void *(*)(const char *, void *);
|
||||
static auto wrap =
|
||||
reinterpret_cast<Wrap>(dlsym(RTLD_DEFAULT, "mocktail_vr_wrap_gles_proc"));
|
||||
return wrap ? wrap(name, raw) : raw;
|
||||
}
|
||||
|
||||
void ShowIfHidden() {
|
||||
@@ -1575,6 +1614,16 @@ bool SwapBuffers() {
|
||||
return false;
|
||||
}
|
||||
|
||||
using Notify = void (*)();
|
||||
static auto vr_begin = reinterpret_cast<Notify>(
|
||||
dlsym(RTLD_DEFAULT, "mocktail_vr_note_host_present_begin"));
|
||||
static auto vr_present =
|
||||
reinterpret_cast<Notify>(dlsym(RTLD_DEFAULT, "mocktail_vr_gles_present"));
|
||||
if (vr_begin)
|
||||
vr_begin();
|
||||
if (vr_present)
|
||||
vr_present();
|
||||
|
||||
if (g_gles_text_overlay == nullptr) {
|
||||
graphics::GlesTextOverlaySource source;
|
||||
source.may_present = reinterpret_cast<decltype(source.may_present)>(
|
||||
@@ -2356,6 +2405,14 @@ void Shutdown() {
|
||||
// resources below.
|
||||
g_gles_text_overlay.reset();
|
||||
if (!g_state.direct_vulkan && g_state.egl_context != nullptr) {
|
||||
// Drain the XR session while its EGL context still exists and is current.
|
||||
SDL_GL_MakeCurrent(g_state.sdl_window,
|
||||
static_cast<SDL_GLContext>(g_state.egl_context));
|
||||
using Detach = void (*)(void *);
|
||||
auto detach =
|
||||
reinterpret_cast<Detach>(dlsym(RTLD_DEFAULT, "mocktail_vr_detach_egl"));
|
||||
if (detach)
|
||||
detach(g_state.egl_context);
|
||||
SDL_GL_MakeCurrent(g_state.sdl_window, nullptr);
|
||||
}
|
||||
if (!g_state.direct_vulkan && g_state.egl_context) {
|
||||
|
||||
@@ -29,7 +29,7 @@ file(GENERATE
|
||||
}
|
||||
]=])
|
||||
|
||||
add_executable(vr_mirror_test vr/vr_mirror_test.cc)
|
||||
add_executable(vr_mirror_test vr/vr_mirror_test.cc vr/runtime_detection_test.cc)
|
||||
target_link_libraries(vr_mirror_test PRIVATE Mocktail::VR GTest::gtest_main)
|
||||
mocktail_apply_compile_options(vr_mirror_test)
|
||||
add_executable(vr_pose_math_test vr/vr_pose_math_test.cc)
|
||||
@@ -57,6 +57,11 @@ if(MOCKTAIL_ENABLE_VR)
|
||||
target_include_directories(openxr_backend_test PRIVATE "${PROJECT_SOURCE_DIR}/third_party/OpenXR-SDK/include")
|
||||
target_link_libraries(openxr_backend_test PRIVATE Mocktail::VR GTest::gtest_main)
|
||||
gtest_discover_tests(openxr_backend_test)
|
||||
add_executable(gles_vr_transport_test vr/gles_transport_test.cc)
|
||||
target_include_directories(gles_vr_transport_test PRIVATE "${PROJECT_SOURCE_DIR}/src" "${PROJECT_SOURCE_DIR}/third_party/OpenXR-SDK/include" ${MOCKTAIL_GLES3_INCLUDE_DIR})
|
||||
target_link_libraries(gles_vr_transport_test PRIVATE Mocktail::VR SDL3::SDL3 GTest::gtest_main)
|
||||
gtest_discover_tests(gles_vr_transport_test PROPERTIES ENVIRONMENT "SDL_VIDEODRIVER=offscreen")
|
||||
|
||||
add_library(mocktail_test_openxr_runtime MODULE
|
||||
vr/fake_openxr_runtime.cc vr/fake_openxr_preview.cc)
|
||||
target_link_libraries(mocktail_test_openxr_runtime PRIVATE
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
#include "mocktail/vr/openxr_backend.h"
|
||||
#include "mocktail/vr/roblox_vr_device_bridge.h"
|
||||
#include "vr/gles_transport.h"
|
||||
#include <GLES3/gl3.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <gtest/gtest.h>
|
||||
#include <openxr/openxr.h>
|
||||
|
||||
namespace mocktail::vr {
|
||||
namespace internal {
|
||||
struct VrBridgeTestAccess {
|
||||
static void Enable(RobloxVrDeviceBridge &bridge) { bridge.active_ = true; }
|
||||
};
|
||||
} // namespace internal
|
||||
struct GlesTransportTestAccess {
|
||||
static void Owner(GlesTransport &t, unsigned fb, void *owner) {
|
||||
t.owners_[fb] = owner;
|
||||
}
|
||||
};
|
||||
struct OpenXrBackendTestAccess {
|
||||
static GlesTransport &Transport(OpenXrBackend &b) { return *b.gles_; }
|
||||
static void LoseSession(OpenXrBackend &b) {
|
||||
b.HandleSessionState(XR_SESSION_STATE_LOSS_PENDING);
|
||||
}
|
||||
};
|
||||
namespace {
|
||||
#define GL(name) \
|
||||
reinterpret_cast<decltype(&gl##name)>(SDL_GL_GetProcAddress("gl" #name))
|
||||
class GlesVrTransportTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
SDL_SetHint(SDL_HINT_VIDEO_FORCE_EGL, "1");
|
||||
ASSERT_TRUE(SDL_Init(SDL_INIT_VIDEO)) << SDL_GetError();
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
||||
window = SDL_CreateWindow("Mocktail GLES VR test", 64, 64,
|
||||
SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN);
|
||||
ASSERT_NE(window, nullptr) << SDL_GetError();
|
||||
context = SDL_GL_CreateContext(window);
|
||||
ASSERT_NE(context, nullptr) << SDL_GetError();
|
||||
ASSERT_TRUE(SDL_GL_MakeCurrent(window, context));
|
||||
internal::VrBridgeTestAccess::Enable(bridge);
|
||||
}
|
||||
void TearDown() override {
|
||||
transport.Destroy();
|
||||
if (context)
|
||||
SDL_GL_DestroyContext(context);
|
||||
if (window)
|
||||
SDL_DestroyWindow(window);
|
||||
SDL_Quit();
|
||||
}
|
||||
void InitTransport(GlesTransport &t) {
|
||||
std::string error;
|
||||
ASSERT_TRUE(t.Initialize(
|
||||
SDL_EGL_GetCurrentDisplay(), SDL_EGL_GetCurrentConfig(), context,
|
||||
reinterpret_cast<void *>(SDL_EGL_GetProcAddress("eglGetProcAddress")),
|
||||
Resolve, &error))
|
||||
<< error;
|
||||
}
|
||||
static void *Resolve(const char *name) {
|
||||
return reinterpret_cast<void *>(SDL_GL_GetProcAddress(name));
|
||||
}
|
||||
GLuint Texture(GlesTransport *t, float r, float g, float b) {
|
||||
GLuint texture = 0, fb = 0;
|
||||
GL(GenTextures)(1, &texture);
|
||||
GL(BindTexture)(GL_TEXTURE_2D, texture);
|
||||
GL(TexStorage2D)(GL_TEXTURE_2D, 1, GL_RGBA8, 500, 500);
|
||||
if (t)
|
||||
t->Storage(true, 500, 500, GL_RGBA8);
|
||||
GL(GenFramebuffers)(1, &fb);
|
||||
GL(BindFramebuffer)(GL_FRAMEBUFFER, fb);
|
||||
GL(FramebufferTexture2D)(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_TEXTURE_2D, texture, 0);
|
||||
GL(ClearColor)(r, g, b, 1);
|
||||
GL(Clear)(GL_COLOR_BUFFER_BIT);
|
||||
GL(DeleteFramebuffers)(1, &fb);
|
||||
return texture;
|
||||
}
|
||||
GLuint Eye(GlesTransport &t, int eye, GLuint texture, std::uint64_t frame = 7,
|
||||
void *owner = reinterpret_cast<void *>(123)) {
|
||||
GLuint fb = 0;
|
||||
GL(GenFramebuffers)(1, &fb);
|
||||
GL(BindFramebuffer)(GL_FRAMEBUFFER, fb);
|
||||
GL(FramebufferTexture2D)(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_TEXTURE_2D, texture, 0);
|
||||
GlesTransportTestAccess::Owner(t, fb, owner);
|
||||
BindEye(t, eye, fb, frame, owner);
|
||||
return fb;
|
||||
}
|
||||
void BindEye(GlesTransport &t, int eye, GLuint fb, std::uint64_t frame,
|
||||
void *owner) {
|
||||
GL(BindFramebuffer)(GL_FRAMEBUFFER, fb);
|
||||
bridge.OnEyeGetterCall(owner, eye,
|
||||
reinterpret_cast<void *>(std::uintptr_t(fb)));
|
||||
t.Bind(GL_FRAMEBUFFER, fb, frame);
|
||||
}
|
||||
std::array<unsigned char, 4> Pixel(GLuint texture, int x = 250, int y = 250) {
|
||||
GLuint fb = 0;
|
||||
GL(GenFramebuffers)(1, &fb);
|
||||
GL(BindFramebuffer)(GL_FRAMEBUFFER, fb);
|
||||
GL(FramebufferTexture2D)(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_TEXTURE_2D, texture, 0);
|
||||
std::array<unsigned char, 4> rgba{};
|
||||
GL(ReadPixels)(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data());
|
||||
GL(DeleteFramebuffers)(1, &fb);
|
||||
return rgba;
|
||||
}
|
||||
SDL_Window *window = nullptr;
|
||||
SDL_GLContext context = nullptr;
|
||||
GlesTransport transport;
|
||||
RobloxVrDeviceBridge bridge;
|
||||
};
|
||||
TEST_F(GlesVrTransportTest, CopiesDistinctEyesAndRestoresGuestState) {
|
||||
InitTransport(transport);
|
||||
const GLuint red = Texture(&transport, 1, 0, 0),
|
||||
green = Texture(&transport, 0, 1, 0);
|
||||
Eye(transport, 0, red);
|
||||
Eye(transport, 1, green);
|
||||
const GLuint targets[2] = {Texture(nullptr, 0, 0, 0),
|
||||
Texture(nullptr, 0, 0, 0)};
|
||||
// A tiny guest scissor must not crop the eye copy, and bindings must survive.
|
||||
GLuint fbs[2];
|
||||
GL(GenFramebuffers)(2, fbs);
|
||||
GL(BindFramebuffer)(GL_READ_FRAMEBUFFER, fbs[0]);
|
||||
GL(BindFramebuffer)(GL_DRAW_FRAMEBUFFER, fbs[1]);
|
||||
GL(Enable)(GL_SCISSOR_TEST);
|
||||
GL(Scissor)(0, 0, 1, 1);
|
||||
GL(Viewport)(2, 3, 17, 19);
|
||||
ASSERT_TRUE(transport.Copy(targets, 500, 500));
|
||||
GLint read = 0, draw = 0, viewport[4];
|
||||
GL(GetIntegerv)(GL_READ_FRAMEBUFFER_BINDING, &read);
|
||||
GL(GetIntegerv)(GL_DRAW_FRAMEBUFFER_BINDING, &draw);
|
||||
GL(GetIntegerv)(GL_VIEWPORT, viewport);
|
||||
EXPECT_EQ(static_cast<GLuint>(read), fbs[0]);
|
||||
EXPECT_EQ(static_cast<GLuint>(draw), fbs[1]);
|
||||
EXPECT_TRUE(GL(IsEnabled)(GL_SCISSOR_TEST));
|
||||
EXPECT_EQ(viewport[0], 2);
|
||||
EXPECT_EQ(viewport[1], 3);
|
||||
EXPECT_EQ(viewport[2], 17);
|
||||
EXPECT_EQ(viewport[3], 19);
|
||||
EXPECT_EQ(Pixel(targets[0]), (std::array<unsigned char, 4>{255, 0, 0, 255}));
|
||||
EXPECT_EQ(Pixel(targets[1]), (std::array<unsigned char, 4>{0, 255, 0, 255}));
|
||||
EXPECT_EQ(GL(GetError)(), GL_NO_ERROR);
|
||||
}
|
||||
TEST_F(GlesVrTransportTest, RejectsUnownedAndAliasedEyesAndTracksDeletion) {
|
||||
InitTransport(transport);
|
||||
GLuint texture = Texture(&transport, 1, 0, 0);
|
||||
GLuint fbs[2];
|
||||
GL(GenFramebuffers)(2, fbs);
|
||||
GL(BindFramebuffer)(GL_FRAMEBUFFER, fbs[0]);
|
||||
GL(FramebufferTexture2D)(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||
texture, 0);
|
||||
BindEye(transport, 0, fbs[0], 7, reinterpret_cast<void *>(123));
|
||||
EXPECT_EQ(transport.eye(0).framebuffer, 0u);
|
||||
Eye(transport, 0, texture);
|
||||
Eye(transport, 1, texture);
|
||||
const GLuint targets[2] = {Texture(nullptr, 0, 0, 0),
|
||||
Texture(nullptr, 0, 0, 0)};
|
||||
EXPECT_FALSE(transport.Copy(targets, 500, 500));
|
||||
transport.ObjectsDeleted(true, 1, &texture);
|
||||
EXPECT_EQ(transport.eye(0).framebuffer, 0u);
|
||||
EXPECT_EQ(transport.eye(1).framebuffer, 0u);
|
||||
}
|
||||
TEST_F(GlesVrTransportTest, NewOwnerAndFramebufferDeletionInvalidateOldPair) {
|
||||
InitTransport(transport);
|
||||
const GLuint red = Texture(&transport, 1, 0, 0),
|
||||
green = Texture(&transport, 0, 1, 0);
|
||||
Eye(transport, 0, red);
|
||||
Eye(transport, 1, green);
|
||||
const auto fb = Eye(transport, 0, red, 8, reinterpret_cast<void *>(456));
|
||||
EXPECT_EQ(transport.eye(1).framebuffer, 0u);
|
||||
EXPECT_EQ(transport.eye(0).frame, 8u);
|
||||
transport.Deleted(1, &fb);
|
||||
EXPECT_EQ(transport.eye(0).framebuffer, 0u);
|
||||
}
|
||||
TEST_F(GlesVrTransportTest, RenderbufferEyesAndAttachmentMutation) {
|
||||
InitTransport(transport);
|
||||
GLuint rb = 0, fb = 0;
|
||||
GL(GenRenderbuffers)(1, &rb);
|
||||
GL(BindRenderbuffer)(GL_RENDERBUFFER, rb);
|
||||
GL(RenderbufferStorage)(GL_RENDERBUFFER, GL_RGBA8, 500, 500);
|
||||
transport.Storage(false, 500, 500, GL_RGBA8);
|
||||
GL(GenFramebuffers)(1, &fb);
|
||||
GL(BindFramebuffer)(GL_FRAMEBUFFER, fb);
|
||||
GL(FramebufferRenderbuffer)(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_RENDERBUFFER, rb);
|
||||
GL(ClearColor)(0, 0, 1, 1);
|
||||
GL(Clear)(GL_COLOR_BUFFER_BIT);
|
||||
GlesTransportTestAccess::Owner(transport, fb, reinterpret_cast<void *>(123));
|
||||
BindEye(transport, 0, fb, 7, reinterpret_cast<void *>(123));
|
||||
EXPECT_EQ(transport.eye(0).object, rb);
|
||||
EXPECT_FALSE(transport.eye(0).texture);
|
||||
const GLuint green = Texture(&transport, 0, 1, 0);
|
||||
Eye(transport, 1, green);
|
||||
const GLuint targets[2] = {Texture(nullptr, 0, 0, 0),
|
||||
Texture(nullptr, 0, 0, 0)};
|
||||
ASSERT_TRUE(transport.Copy(targets, 500, 500));
|
||||
EXPECT_EQ(Pixel(targets[0]), (std::array<unsigned char, 4>{0, 0, 255, 255}));
|
||||
GL(BindFramebuffer)(GL_FRAMEBUFFER, fb);
|
||||
transport.AttachmentChanged(GL_FRAMEBUFFER);
|
||||
EXPECT_EQ(transport.eye(0).framebuffer, 0u);
|
||||
EXPECT_EQ(GL(GetError)(), GL_NO_ERROR);
|
||||
}
|
||||
TEST_F(GlesVrTransportTest, OpenXrEglSessionSubmitsAndRecovers) {
|
||||
if (!std::getenv("MOCKTAIL_TEST_XR_GLES"))
|
||||
GTEST_SKIP() << "Opt-in: requires a connected or simulated OpenXR runtime";
|
||||
OpenXrBackend backend;
|
||||
auto status = backend.Arm(VrGraphicsApi::kOpenGles);
|
||||
ASSERT_TRUE(status.ok()) << status.message();
|
||||
status = backend.AttachGlesContext(
|
||||
SDL_EGL_GetCurrentDisplay(), SDL_EGL_GetCurrentConfig(), context,
|
||||
reinterpret_cast<void *>(SDL_EGL_GetProcAddress("eglGetProcAddress")),
|
||||
Resolve);
|
||||
ASSERT_TRUE(status.ok()) << status.message();
|
||||
auto &t = OpenXrBackendTestAccess::Transport(backend);
|
||||
const GLuint red = Texture(&t, 1, 0, 0), green = Texture(&t, 0, 1, 0);
|
||||
auto *owner = reinterpret_cast<void *>(123);
|
||||
const GLuint fbs[2] = {Eye(t, 0, red), Eye(t, 1, green)};
|
||||
bool lost_session = false;
|
||||
for (int i = 0; i < 200 && backend.submitted_frames() < 20; ++i) {
|
||||
if (!lost_session && backend.submitted_frames() >= 10) {
|
||||
OpenXrBackendTestAccess::LoseSession(backend);
|
||||
EXPECT_FALSE(backend.session_running());
|
||||
EXPECT_FALSE(backend.PublishedHeadPose().valid);
|
||||
lost_session = true;
|
||||
}
|
||||
backend.NoteGlesPresent();
|
||||
auto pose = backend.PublishedHeadPose();
|
||||
if (!pose.valid) {
|
||||
SDL_Delay(10);
|
||||
continue;
|
||||
}
|
||||
backend.NotePoseApplied(owner, pose.frame);
|
||||
BindEye(t, 0, fbs[0], pose.frame, owner);
|
||||
BindEye(t, 1, fbs[1], pose.frame, owner);
|
||||
EXPECT_EQ(t.eye(0).frame, pose.frame);
|
||||
EXPECT_EQ(t.eye(1).frame, pose.frame);
|
||||
EXPECT_TRUE(t.IsCurrent());
|
||||
}
|
||||
EXPECT_TRUE(lost_session);
|
||||
EXPECT_GE(backend.submitted_frames(), 20u);
|
||||
EXPECT_EQ(GL(GetError)(), GL_NO_ERROR);
|
||||
backend.DetachGlesContext(context);
|
||||
EXPECT_FALSE(backend.session_running());
|
||||
status = backend.AttachGlesContext(
|
||||
SDL_EGL_GetCurrentDisplay(), SDL_EGL_GetCurrentConfig(), context,
|
||||
reinterpret_cast<void *>(SDL_EGL_GetProcAddress("eglGetProcAddress")),
|
||||
Resolve);
|
||||
EXPECT_TRUE(status.ok()) << status.message();
|
||||
if (status.ok()) {
|
||||
EXPECT_EQ(OpenXrBackendTestAccess::Transport(backend).eye(0).framebuffer,
|
||||
0u);
|
||||
backend.DetachGlesContext(context);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
} // namespace mocktail::vr
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "mocktail/vr/openxr_backend.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace mocktail::vr {
|
||||
namespace {
|
||||
class VrRuntimeDetectionTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
char path[] = "/tmp/mocktail-xr-detect-XXXXXX";
|
||||
ASSERT_NE(mkdtemp(path), nullptr);
|
||||
root = path;
|
||||
std::filesystem::create_directories(root / "proc/net");
|
||||
std::filesystem::create_directories(root / "config/openxr/1");
|
||||
std::filesystem::create_directories(root / "run/wivrn");
|
||||
std::filesystem::create_symlink("/proc/net/unix", root / "proc/net/unix");
|
||||
}
|
||||
void TearDown() override {
|
||||
if (socket >= 0)
|
||||
close(socket);
|
||||
std::error_code error;
|
||||
if (!root.empty())
|
||||
std::filesystem::remove_all(root, error);
|
||||
}
|
||||
void File(const std::filesystem::path &path, const std::string &data = "{}") {
|
||||
std::filesystem::create_directories(path.parent_path());
|
||||
std::ofstream(path) << data;
|
||||
}
|
||||
std::string Steam() {
|
||||
const auto install = root / "custom-library/SteamVR";
|
||||
File(install / "steamxr_linux64.json");
|
||||
File(install / "bin/linux64/vrserver", "test executable");
|
||||
File(root / "proc/100/comm", "vrserver\n");
|
||||
std::filesystem::create_symlink(install / "bin/linux64/vrserver",
|
||||
root / "proc/100/exe");
|
||||
return (install / "steamxr_linux64.json").string();
|
||||
}
|
||||
std::string WiVRn(bool listen) {
|
||||
const auto install = root / "wivrn-build";
|
||||
File(install / "openxr_wivrn-dev.json");
|
||||
File(install / "server/wivrn-server", "test executable");
|
||||
File(root / "proc/200/comm", "wivrn-server\n");
|
||||
std::filesystem::create_symlink(install / "server/wivrn-server",
|
||||
root / "proc/200/exe");
|
||||
const auto path = (root / "run/wivrn/comp_ipc").string();
|
||||
socket = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
EXPECT_GE(socket, 0);
|
||||
sockaddr_un address{};
|
||||
address.sun_family = AF_UNIX;
|
||||
EXPECT_LT(path.size(), sizeof(address.sun_path));
|
||||
std::strcpy(address.sun_path, path.c_str());
|
||||
EXPECT_EQ(
|
||||
bind(socket, reinterpret_cast<sockaddr *>(&address), sizeof(address)),
|
||||
0);
|
||||
if (listen)
|
||||
EXPECT_EQ(::listen(socket, 1), 0);
|
||||
else {
|
||||
close(socket);
|
||||
socket = -1;
|
||||
} // Leave a stale socket inode.
|
||||
return (install / "openxr_wivrn-dev.json").string();
|
||||
}
|
||||
VrRuntimeDiscovery Discover(const std::string &preference = "auto") {
|
||||
return DiscoverVrRuntime(preference, (root / "home").string(),
|
||||
(root / "config").string(),
|
||||
(root / "etc/xdg").string(),
|
||||
(root / "run").string(), (root / "proc").string());
|
||||
}
|
||||
std::string Selected(const std::string &preference = "auto") {
|
||||
return SelectVrRuntimeManifest("", Discover(preference).candidates);
|
||||
}
|
||||
std::filesystem::path root;
|
||||
int socket = -1;
|
||||
};
|
||||
TEST_F(VrRuntimeDetectionTest, RunningSteamOverridesOldRegisteredWiVRn) {
|
||||
File(root / "config/openxr/1/active_runtime.json");
|
||||
EXPECT_EQ(Selected(),
|
||||
(root / "config/openxr/1/active_runtime.json").string());
|
||||
const auto steam = Steam();
|
||||
EXPECT_EQ(Selected(), steam);
|
||||
EXPECT_NE(Discover().reason.find("running SteamVR"), std::string::npos);
|
||||
}
|
||||
TEST_F(VrRuntimeDetectionTest, ListeningWiVRnFindsDevelopmentManifest) {
|
||||
File(root / "config/openxr/1/active_runtime.json");
|
||||
const auto wivrn = WiVRn(true);
|
||||
EXPECT_EQ(Selected(), wivrn);
|
||||
EXPECT_NE(Discover().reason.find("listening WiVRn"), std::string::npos);
|
||||
}
|
||||
TEST_F(VrRuntimeDetectionTest, RegisteredWiVRnBuildWinsOverNativeFallback) {
|
||||
const auto registered =
|
||||
(root / "config/openxr/1/active_runtime.json").string();
|
||||
File(
|
||||
registered,
|
||||
R"({"runtime":{"library_path":"/var/lib/flatpak/app/wivrn/libwivrn.so"}})");
|
||||
WiVRn(true);
|
||||
EXPECT_EQ(Selected(), registered);
|
||||
}
|
||||
TEST_F(VrRuntimeDetectionTest, StaleWiVRnSocketDoesNotOverrideSteam) {
|
||||
const auto steam = Steam();
|
||||
WiVRn(false);
|
||||
EXPECT_EQ(Selected(), steam);
|
||||
}
|
||||
TEST_F(VrRuntimeDetectionTest,
|
||||
BothServersRespectRegisteredRuntimeAndExplicitOverrides) {
|
||||
const auto registered =
|
||||
(root / "config/openxr/1/active_runtime.json").string();
|
||||
File(registered);
|
||||
const auto steam = Steam();
|
||||
const auto wivrn = WiVRn(true);
|
||||
EXPECT_EQ(Selected(), registered);
|
||||
EXPECT_EQ(Selected("alvr"), steam);
|
||||
EXPECT_EQ(Selected("wivrn"), wivrn);
|
||||
EXPECT_TRUE(Discover("system").candidates.empty());
|
||||
EXPECT_EQ(
|
||||
SelectVrRuntimeManifest("/custom/override.json", Discover().candidates),
|
||||
"/custom/override.json");
|
||||
}
|
||||
TEST_F(VrRuntimeDetectionTest,
|
||||
OpenVrRegistryFindsCustomSteamLibraryWithoutRunningServer) {
|
||||
const auto install = root / "elsewhere/SteamVR";
|
||||
File(install / "steamxr_linux64.json");
|
||||
File(root / "config/openvr/openvrpaths.vrpath",
|
||||
"{\"runtime\":[42,\"relative/path\",\"" + install.string() + "\"]}");
|
||||
EXPECT_EQ(Selected("alvr"), (install / "steamxr_linux64.json").string());
|
||||
// No external host registration or installed WiVRn is assumed in this
|
||||
// assertion.
|
||||
const auto candidates = Discover().candidates;
|
||||
EXPECT_NE(std::find(candidates.begin(), candidates.end(),
|
||||
(install / "steamxr_linux64.json").string()),
|
||||
candidates.end());
|
||||
File(root / "config/openvr/openvrpaths.vrpath", "invalid json");
|
||||
EXPECT_NE(Selected("alvr"), (install / "steamxr_linux64.json").string());
|
||||
}
|
||||
} // namespace
|
||||
} // namespace mocktail::vr
|
||||
@@ -66,7 +66,10 @@ class SegmentReader {
|
||||
|
||||
TEST(VrRuntimeSelection, RuntimeErrorsPreserveSelectionProvenance) {
|
||||
EXPECT_NE(VrRuntimeUnavailableHint(true, "/custom.json").find("User-selected"), std::string::npos);
|
||||
EXPECT_NE(VrRuntimeUnavailableHint(false, "/usr/share/openxr/1/openxr_wivrn.json").find("Auto-selected WiVRn"), std::string::npos);
|
||||
EXPECT_NE(
|
||||
VrRuntimeUnavailableHint(false, "/usr/share/openxr/1/openxr_wivrn.json")
|
||||
.find("Selected OpenXR"),
|
||||
std::string::npos);
|
||||
EXPECT_NE(VrRuntimeUnavailableHint(false, "").find("registered"), std::string::npos);
|
||||
}
|
||||
|
||||
@@ -198,3 +201,28 @@ TEST(VrMirrorProducer, RejectsInvalidNameAndOversizedPublish) {
|
||||
|
||||
} // namespace
|
||||
} // namespace mocktail::vr
|
||||
|
||||
namespace mocktail::vr {
|
||||
TEST(VrRuntimeSelection, AlvrSelectsSteamVrWithoutFallingBackToWiVRn) {
|
||||
const auto alvr = VrRuntimeManifestCandidates("alvr", "/home/test", "", "");
|
||||
EXPECT_EQ(alvr, VrRuntimeManifestCandidates("steamvr", "/home/test", "", ""));
|
||||
ASSERT_FALSE(alvr.empty());
|
||||
for (const auto &path : alvr) {
|
||||
EXPECT_NE(path.find("SteamVR/steamxr_linux64.json"), std::string::npos);
|
||||
EXPECT_EQ(path.find("wivrn"), std::string::npos);
|
||||
}
|
||||
EXPECT_TRUE(
|
||||
VrRuntimeManifestCandidates("system", "/home/test", "", "").empty());
|
||||
EXPECT_EQ(SelectVrRuntimeManifest("/custom/steamxr_linux64.json", alvr),
|
||||
"/custom/steamxr_linux64.json");
|
||||
}
|
||||
TEST(VrRuntimeSelection, RegisteredRuntimePrecedesInstalledWiVRn) {
|
||||
const auto paths = VrRuntimeManifestCandidates(
|
||||
"auto", "/home/test", "/private/config", "/opt/xdg:/etc/xdg");
|
||||
ASSERT_GE(paths.size(), 4u);
|
||||
EXPECT_EQ(paths[0], "/private/config/openxr/1/active_runtime.x86_64.json");
|
||||
EXPECT_EQ(paths[1], "/private/config/openxr/1/active_runtime.json");
|
||||
EXPECT_EQ(paths[2], "/opt/xdg/openxr/1/active_runtime.x86_64.json");
|
||||
EXPECT_EQ(paths.back(), "/usr/share/openxr/1/openxr_wivrn.json");
|
||||
}
|
||||
} // namespace mocktail::vr
|
||||
Reference in new issue
Block a user