mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-27 16:07:19 +01:00
init vr
This commit is contained in:
1 parent
c48db09f13
commit
dd23f06a8b
31 files changed
+2650
-214
No files matched your search
@@ -4,3 +4,6 @@
|
||||
[submodule "third_party/Vulkan-Headers"]
|
||||
path = third_party/Vulkan-Headers
|
||||
url = https://github.com/KhronosGroup/Vulkan-Headers.git
|
||||
[submodule "third_party/OpenXR-SDK"]
|
||||
path = third_party/OpenXR-SDK
|
||||
url = https://github.com/KhronosGroup/OpenXR-SDK.git
|
||||
@@ -125,6 +125,8 @@ set(VULKAN_HEADERS_ENABLE_INSTALL OFF CACHE BOOL
|
||||
"Install vendored Vulkan-Headers" FORCE)
|
||||
add_subdirectory(third_party/Vulkan-Headers EXCLUDE_FROM_ALL)
|
||||
|
||||
include(${CMAKE_SOURCE_DIR}/cmake/MocktailVR.cmake)
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
# Package builds disable tests and do not fetch Google Test.
|
||||
@@ -627,6 +629,7 @@ target_compile_definitions(mocktail PRIVATE
|
||||
"MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST=\"${MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST}\""
|
||||
)
|
||||
target_link_libraries(mocktail PRIVATE
|
||||
Mocktail::VR
|
||||
mocktail_legacy_runtime
|
||||
mocktail_auth_runtime_composition
|
||||
mocktail_failure_dialog_runtime
|
||||
|
||||
@@ -1,192 +1,5 @@
|
||||
# Mocktail
|
||||
This is a dev branch for vr support in Mocktail.
|
||||
|
||||
[](https://github.com/komaruworld/mocktail/actions/workflows/ci.yml)
|
||||
[](https://github.com/komaruworld/mocktail/stargazers)
|
||||
[](https://github.com/komaruworld/mocktail/releases/latest)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/rhgpfcmFSD)
|
||||
idk if I can do this.
|
||||
|
||||
Mocktail runs the Android `x86_64` Roblox client on Linux, including a Linux
|
||||
userspace hosted by FreeBSD's Linuxulator. It provides the Android ABI and JNI
|
||||
pieces the client expects, then connects them to SDL3 and Vulkan or OpenGL on
|
||||
the Linux side.
|
||||
|
||||
Mocktail is an independent community project. It is not affiliated with Roblox
|
||||
Corporation or VinegarHQ and does not distribute the Roblox client.
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
Roblox APK -> signature and ABI checks -> Bionic + JNI -> SDL3 + Vulkan/OpenGL
|
||||
```
|
||||
|
||||
The APK is checked before any native code is loaded. It is downloaded on first
|
||||
launch and is not bundled with Mocktail. The last working copy is kept in case
|
||||
an update fails.
|
||||
|
||||
## Install with Flatpak
|
||||
|
||||
Install the stable release from Flathub:
|
||||
|
||||
```bash
|
||||
flatpak install flathub space.bigrat.mocktail
|
||||
flatpak run space.bigrat.mocktail
|
||||
```
|
||||
|
||||
Nightly builds are produced automatically from the latest `main` branch:
|
||||
|
||||
```bash
|
||||
flatpak install --user https://mocktail.bigrat.space/mocktail.flatpakref
|
||||
flatpak run space.bigrat.mocktail
|
||||
```
|
||||
|
||||
## Install from the AUR
|
||||
|
||||
Arch Linux users can install either the pinned source release (`mocktail`) or
|
||||
the current development version (`mocktail-git`) with an AUR helper:
|
||||
|
||||
```bash
|
||||
paru -S mocktail-git
|
||||
# or
|
||||
yay -S mocktail-git
|
||||
```
|
||||
|
||||
Use `mocktail` instead of `mocktail-git` to build the pinned release from
|
||||
source, or install the stable prebuilt package with `paru -S mocktail-bin` or
|
||||
`yay -S mocktail-bin`.
|
||||
|
||||
## Install with DNF
|
||||
|
||||
Fedora 44 users can install either the stable `mocktail` package or the
|
||||
`mocktail-nightly` package built from `main`:
|
||||
|
||||
```bash
|
||||
sudo curl -fsSL https://mocktail.bigrat.space/rpm/mocktail.repo \
|
||||
-o /etc/yum.repos.d/mocktail.repo
|
||||
sudo dnf install mocktail
|
||||
# or
|
||||
sudo dnf install mocktail-nightly
|
||||
```
|
||||
|
||||
## Install with APT
|
||||
|
||||
Ubuntu 26.04 users can install either the stable `mocktail` package or the
|
||||
`mocktail-nightly` package built from `main`:
|
||||
|
||||
```bash
|
||||
sudo install -d -m 0755 /etc/apt/keyrings
|
||||
sudo curl -fsSL https://mocktail.bigrat.space/mocktail-packages.gpg \
|
||||
-o /etc/apt/keyrings/mocktail.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/mocktail.gpg] https://mocktail.bigrat.space/apt mocktail main" | \
|
||||
sudo tee /etc/apt/sources.list.d/mocktail.list >/dev/null
|
||||
sudo apt update
|
||||
sudo apt install mocktail
|
||||
# or
|
||||
sudo apt install mocktail-nightly
|
||||
```
|
||||
|
||||
## Direct downloads
|
||||
|
||||
Direct AppImage, DEB, and RPM downloads are available from the
|
||||
[Website](https://mocktail.bigrat.space/) and
|
||||
[GitHub Releases](https://github.com/komaruworld/mocktail/releases).
|
||||
|
||||
<details>
|
||||
<summary>Screenshots</summary>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## FFlag overrides
|
||||
|
||||
Put a JSON object in `$XDG_CONFIG_HOME/mocktail/fflags.json` (usually
|
||||
`~/.config/mocktail/fflags.json`). Mocktail applies it on the next launch.
|
||||
|
||||
```json
|
||||
{
|
||||
"FFlagExample": "True",
|
||||
"DFIntExample": "120"
|
||||
}
|
||||
```
|
||||
## FAQ
|
||||
|
||||
To see the most Frequently Asked Question go check [FAQ.md](FAQ.md)
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
Linux `x86_64` is supported. Experimental FreeBSD 15.1 Linuxulator support has
|
||||
been tested with an `x86_64` Fedora 44 userspace. On FreeBSD, Mocktail runs
|
||||
inside Linuxulator; it is not a native FreeBSD binary. Building requires CMake
|
||||
3.20+, Git, pkg-config, LLD, binutils, a C++17 compiler, SDL 3.4+, SDL3_ttf,
|
||||
Vulkan, EGL, libplacebo, fontconfig, libcurl, OpenSSL, libelf, libyaml, minizip,
|
||||
Capstone 5, utf8proc, nlohmann/json, GTK4, libadwaita 1.6+, and WebKitGTK 6.0.
|
||||
|
||||
See the [FreeBSD Guide](packaging/freebsd/README.md) for setup and launch instructions.
|
||||
|
||||
<details>
|
||||
<summary>Ubuntu 26.04+</summary>
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install build-essential cmake git ninja-build pkg-config lld \
|
||||
libsdl3-dev libsdl3-ttf-dev libcurl4-openssl-dev libssl-dev \
|
||||
nlohmann-json3-dev libyaml-dev libelf-dev libminizip-dev \
|
||||
libcapstone-dev libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev \
|
||||
libutf8proc-dev libfontconfig1-dev libegl-dev libvulkan-dev \
|
||||
libplacebo-dev zlib1g-dev
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Arch Linux</summary>
|
||||
|
||||
```bash
|
||||
sudo pacman -S --needed base-devel cmake git ninja pkgconf lld sdl3 sdl3_ttf \
|
||||
curl openssl nlohmann-json libyaml libelf minizip capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig libglvnd \
|
||||
libplacebo vulkan-headers vulkan-icd-loader zlib
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Fedora 44+</summary>
|
||||
|
||||
```bash
|
||||
sudo dnf install gcc-c++ cmake git ninja-build pkgconf-pkg-config lld \
|
||||
SDL3-devel SDL3_ttf-devel libcurl-devel openssl-devel \
|
||||
nlohmann-json-devel libyaml-devel elfutils-libelf-devel minizip-ng-compat-devel \
|
||||
capstone-devel gtk4-devel libadwaita-devel webkitgtk6.0-devel \
|
||||
utf8proc-devel fontconfig-devel libglvnd-devel vulkan-headers \
|
||||
vulkan-loader-devel libplacebo-devel zlib-ng-compat-devel
|
||||
```
|
||||
</details>
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/komaruworld/mocktail.git
|
||||
cd mocktail
|
||||
make build
|
||||
./build/mocktail
|
||||
```
|
||||
|
||||
## Known limitations
|
||||
|
||||
Mocktail does not currently work with `hardened_malloc`. Using
|
||||
`hardened_malloc` may cause Mocktail to fail to start or crash during runtime.
|
||||
Run Mocktail without `hardened_malloc` enabled.
|
||||
|
||||
## License
|
||||
|
||||
[Apache License 2.0](LICENSE). Third-party components keep their own licenses.
|
||||
|
||||
## Support
|
||||
|
||||
You can support the project by giving it a star or with cryptocurrency:
|
||||
|
||||
- USDT (TON): `UQCi6Yzcc9cOctoij6n_r1K90-OdVxAT0D_xo2UzGKkQaJDY`
|
||||
- USDT (TRC20): `TNPMG9Vig2xiuo2r1QqnXRChPH7Vu28Jmx`
|
||||
But it will be fun :)
|
||||
@@ -0,0 +1,60 @@
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# Licensed under the Apache License, Version 2.0.
|
||||
|
||||
include_guard(GLOBAL)
|
||||
|
||||
option(MOCKTAIL_ENABLE_VR "Build experimental native OpenXR support" OFF)
|
||||
|
||||
add_library(mocktail_vr STATIC src/vr/diagnostics.cc)
|
||||
add_library(Mocktail::VR ALIAS mocktail_vr)
|
||||
target_include_directories(mocktail_vr PUBLIC "${CMAKE_SOURCE_DIR}/include")
|
||||
mocktail_apply_compile_options(mocktail_vr)
|
||||
|
||||
if(MOCKTAIL_ENABLE_VR)
|
||||
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
message(FATAL_ERROR "Mocktail OpenXR support currently targets Linux")
|
||||
endif()
|
||||
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/third_party/OpenXR-SDK/CMakeLists.txt")
|
||||
message(FATAL_ERROR
|
||||
"Initialize OpenXR: git submodule update --init third_party/OpenXR-SDK")
|
||||
endif()
|
||||
|
||||
# Scope upstream's generic option names to this dependency. Static linking
|
||||
# packages the pinned loader without requiring a matching system loader.
|
||||
function(mocktail_add_openxr_sdk)
|
||||
set(BUILD_LOADER ON)
|
||||
set(DYNAMIC_LOADER OFF)
|
||||
set(BUILD_WITH_SYSTEM_JSONCPP OFF)
|
||||
set(BUILD_TESTS OFF)
|
||||
set(BUILD_API_LAYERS OFF)
|
||||
set(BUILD_CONFORMANCE_TESTS OFF)
|
||||
set(BUILD_SDK_TESTS OFF)
|
||||
set(BUILD_FORCE_GENERATION OFF)
|
||||
add_subdirectory("${CMAKE_SOURCE_DIR}/third_party/OpenXR-SDK"
|
||||
"${CMAKE_BINARY_DIR}/third_party/OpenXR-SDK" EXCLUDE_FROM_ALL)
|
||||
endfunction()
|
||||
mocktail_add_openxr_sdk()
|
||||
target_sources(mocktail_vr PRIVATE src/vr/openxr_probe.cc src/vr/openxr_preview.cc)
|
||||
# Error unwinding is confined to this native session implementation; its
|
||||
# public entry point catches errors before returning to the guest runtime.
|
||||
set_source_files_properties(src/vr/openxr_preview.cc PROPERTIES
|
||||
COMPILE_OPTIONS -fexceptions)
|
||||
target_compile_definitions(mocktail_vr PRIVATE XR_USE_GRAPHICS_API_VULKAN)
|
||||
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)
|
||||
endif()
|
||||
|
||||
add_executable(mocktail_vr_probe src/vr/probe_main.cc)
|
||||
set_target_properties(mocktail_vr_probe PROPERTIES OUTPUT_NAME mocktail-vr-probe)
|
||||
target_link_libraries(mocktail_vr_probe PRIVATE Mocktail::VR)
|
||||
mocktail_apply_compile_options(mocktail_vr_probe)
|
||||
install(TARGETS mocktail_vr_probe RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
|
||||
|
||||
# Development launcher for an independently built Monado runtime. --prepare
|
||||
# keeps its shared library and a relative manifest under this build directory.
|
||||
configure_file("${CMAKE_SOURCE_DIR}/scripts/vr_simulated.py"
|
||||
"${CMAKE_BINARY_DIR}/mocktail-vr-simulated" COPYONLY)
|
||||
file(CHMOD "${CMAKE_BINARY_DIR}/mocktail-vr-simulated"
|
||||
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE)
|
||||
@@ -49,6 +49,13 @@ graphics:
|
||||
# Optional presentation synchronization override: auto, on, or off.
|
||||
# vsync: off
|
||||
|
||||
vr:
|
||||
# Boolean (default: false): launch the Roblox experimental VR path.
|
||||
# Only Build ID 2998 is currently accepted; eye resources and OpenXR output
|
||||
# are NOT yet connected. Standalone scene: mocktail-vr-probe --scene.
|
||||
# Use --no-vr to launch Roblox normally.
|
||||
enabled: false
|
||||
|
||||
performance:
|
||||
# Boolean (default: false): size Roblox scheduler/render queues from every
|
||||
# physical CPU core. A place's Lua/main thread can still remain serial.
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef MOCKTAIL_VR_OPENXR_PREVIEW_H_
|
||||
#define MOCKTAIL_VR_OPENXR_PREVIEW_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
struct PreviewOptions {
|
||||
// Zero keeps the scene open until the runtime or user requests exit.
|
||||
std::uint32_t frame_limit = 0;
|
||||
};
|
||||
|
||||
struct PreviewResult {
|
||||
bool success = false;
|
||||
std::string message;
|
||||
std::uint64_t frames = 0;
|
||||
std::uint64_t rendered_frames = 0;
|
||||
std::uint64_t tracked_frames = 0;
|
||||
std::array<float, 3> head_position{};
|
||||
std::array<float, 4> head_orientation{0, 0, 0, 1};
|
||||
};
|
||||
|
||||
// Runs a native Vulkan/OpenXR scene with a panel fixed in LOCAL space. The
|
||||
// compositor projects it into both eyes. Roblox images/input are not connected.
|
||||
// Call on the main thread: SIGINT/SIGTERM request a graceful session exit, and
|
||||
// their previous handlers are restored before returning.
|
||||
PreviewResult RunOpenXrPreview(const PreviewOptions& options = {});
|
||||
std::string FormatPreviewResult(const PreviewResult& result);
|
||||
|
||||
} // namespace mocktail::vr
|
||||
|
||||
#endif // MOCKTAIL_VR_OPENXR_PREVIEW_H_
|
||||
@@ -0,0 +1,48 @@
|
||||
#ifndef MOCKTAIL_VR_OPENXR_PROBE_H_
|
||||
#define MOCKTAIL_VR_OPENXR_PROBE_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
enum class ProbeStatus {
|
||||
kReady,
|
||||
kBuildDisabled,
|
||||
kRuntimeUnavailable,
|
||||
kHeadsetUnavailable,
|
||||
kIncompatibleRuntime,
|
||||
kError,
|
||||
};
|
||||
|
||||
struct ViewConfiguration {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::uint32_t samples = 0;
|
||||
};
|
||||
|
||||
struct ProbeReport {
|
||||
ProbeStatus status = ProbeStatus::kError;
|
||||
std::string message;
|
||||
std::string sdk_version;
|
||||
std::string runtime_name;
|
||||
std::string runtime_version;
|
||||
std::string system_name;
|
||||
std::vector<std::string> extensions;
|
||||
std::vector<ViewConfiguration> views;
|
||||
bool orientation_tracking = false;
|
||||
bool position_tracking = false;
|
||||
std::string minimum_vulkan_version;
|
||||
std::string maximum_vulkan_version;
|
||||
};
|
||||
|
||||
// Native host discovery only. Does not load Roblox, change the active runtime,
|
||||
// create a graphics device/session, or claim that VR rendering is implemented.
|
||||
ProbeReport ProbeOpenXr();
|
||||
std::string FormatProbeReport(const ProbeReport& report);
|
||||
int ProbeExitCode(ProbeStatus status);
|
||||
|
||||
} // namespace mocktail::vr
|
||||
|
||||
#endif // MOCKTAIL_VR_OPENXR_PROBE_H_
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_COMMAND_LINE_H_
|
||||
#define MOCKTAIL_RUNTIME_COMMAND_LINE_H_
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -21,6 +22,7 @@ enum class WindowMode {
|
||||
struct CommandLineOptions {
|
||||
CommandMode mode = CommandMode::kRun;
|
||||
WindowMode window_mode = WindowMode::kUnspecified;
|
||||
std::optional<bool> vr_enabled;
|
||||
std::string program_name = "mocktail";
|
||||
std::string roblox_library_path;
|
||||
std::string graphics_backend;
|
||||
|
||||
@@ -77,6 +77,8 @@ class RuntimeConfig {
|
||||
static RuntimeConfig FromEnvironment(const Environment& environment);
|
||||
|
||||
bool headless() const { return headless_; }
|
||||
bool vr_enabled() const { return vr_enabled_; }
|
||||
bool vr_valid() const { return vr_valid_; }
|
||||
const std::filesystem::path& roblox_library_path() const {
|
||||
return roblox_library_path_;
|
||||
}
|
||||
@@ -135,6 +137,8 @@ class RuntimeConfig {
|
||||
|
||||
private:
|
||||
bool headless_ = false;
|
||||
bool vr_enabled_ = false;
|
||||
bool vr_valid_ = true;
|
||||
std::filesystem::path roblox_library_path_ = "rbx_bin/libroblox.so";
|
||||
GraphicsBackend graphics_backend_ = GraphicsBackend::kVulkan;
|
||||
std::string graphics_backend_name_ = "direct-vulkan";
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Launch the VR preview with a separately built Monado simulated runtime."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import select
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def positive_integer(value):
|
||||
number = int(value)
|
||||
if not 0 < number <= 0xFFFFFFFF:
|
||||
raise argparse.ArgumentTypeError("must be a positive 32-bit integer")
|
||||
return number
|
||||
|
||||
|
||||
def prepare_runtime(source, destination):
|
||||
"""Keep the development runtime beside the build, independent of /tmp."""
|
||||
source = source.resolve()
|
||||
document = json.loads(source.read_text())
|
||||
library = Path(document["runtime"]["library_path"])
|
||||
if not library.is_absolute():
|
||||
library = source.parent / library
|
||||
if not library.is_file():
|
||||
raise ValueError(f"Runtime library does not exist: {library}")
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
output_library = destination / "libopenxr_monado.so"
|
||||
if library.resolve() != output_library.resolve():
|
||||
shutil.copy2(library, output_library)
|
||||
output_manifest = destination / "runtime.json"
|
||||
output_manifest.write_text(json.dumps({
|
||||
"file_format_version": "1.0.0",
|
||||
"runtime": {"name": "Monado (Mocktail simulator)",
|
||||
"library_path": "./libopenxr_monado.so"},
|
||||
}, indent=2) + "\n")
|
||||
print(f"Prepared simulator: {output_manifest}")
|
||||
|
||||
|
||||
def simulator_environment(parent, manifest, display, wayland, software):
|
||||
environment = parent.copy()
|
||||
# These overrides belong to this child process; no active_runtime.json is
|
||||
# written and the user's WiVRn/system runtime selection remains intact.
|
||||
environment.update(
|
||||
XR_RUNTIME_JSON=str(manifest.resolve()),
|
||||
SIMULATED_ENABLE="1",
|
||||
XRT_COMPOSITOR_NULL="0",
|
||||
XRT_COMPOSITOR_FORCE_XCB="1" if display else "0",
|
||||
XRT_COMPOSITOR_FORCE_WAYLAND="1" if not display and wayland else "0",
|
||||
XRT_COMPOSITOR_FORCE_NVIDIA="0",
|
||||
XRT_COMPOSITOR_FORCE_RANDR="0",
|
||||
XRT_COMPOSITOR_FORCE_WAYLAND_DIRECT="0",
|
||||
XRT_COMPOSITOR_FORCE_VK_DISPLAY="-1",
|
||||
XRT_COMPOSITOR_XCB_FULLSCREEN="0",
|
||||
)
|
||||
environment.setdefault("XRT_COMPOSITOR_DEFAULT_FRAMERATE", "30")
|
||||
if display:
|
||||
environment["DISPLAY"] = display
|
||||
environment.pop("WAYLAND_DISPLAY", None)
|
||||
if software:
|
||||
candidates = [directory / "lvp_icd.json" for directory in
|
||||
(Path("/usr/share/vulkan/icd.d"), Path("/usr/local/share/vulkan/icd.d"))]
|
||||
manifest_path = next((path for path in candidates if path.is_file()), None)
|
||||
if manifest_path is None:
|
||||
raise ValueError("Mesa Lavapipe is not installed; omit --software to use your GPU")
|
||||
environment["VK_DRIVER_FILES"] = str(manifest_path)
|
||||
environment.pop("VK_ICD_FILENAMES", None)
|
||||
environment["XRT_COMPOSITOR_FORCE_GPU_INDEX"] = "0"
|
||||
environment["XRT_COMPOSITOR_FORCE_CLIENT_GPU_INDEX"] = "0"
|
||||
return environment
|
||||
|
||||
|
||||
def main():
|
||||
location = Path(__file__).resolve().parent
|
||||
default_build = location.parent / "build" if location.name == "scripts" else location
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Show the Mocktail VR test scene on the desktop using a simulated Monado headset.")
|
||||
parser.add_argument("--build-dir", type=Path, default=default_build,
|
||||
help="Mocktail build directory (defaults to the directory of this launcher)")
|
||||
parser.add_argument("--runtime", type=Path,
|
||||
help="Manifest of an in-process Monado build with the simulated driver enabled")
|
||||
parser.add_argument("--prepare", type=Path, metavar="MONADO_MANIFEST",
|
||||
help="Copy an already built Monado runtime next to this build, then exit")
|
||||
parser.add_argument("--frames", type=positive_integer,
|
||||
help="Request exit after this many frames; otherwise run until Ctrl+C")
|
||||
parser.add_argument("--software", action="store_true", help="Use Mesa Lavapipe instead of a physical GPU")
|
||||
parser.add_argument("--headless", action="store_true",
|
||||
help="Use a private Xvfb display for automated checks (defaults to 180 frames)")
|
||||
args = parser.parse_args()
|
||||
build = args.build_dir.resolve()
|
||||
if args.prepare:
|
||||
prepare_runtime(args.prepare, build / "vr-simulator")
|
||||
return 0
|
||||
manifest = args.runtime or build / "vr-simulator/runtime.json"
|
||||
if not manifest.is_file():
|
||||
parser.error("No simulated Monado runtime is prepared. Use --runtime /path/to/openxr_monado-dev.json "
|
||||
"or --prepare /path/to/openxr_monado-dev.json with a separately built Monado runtime.")
|
||||
probe = build / "mocktail-vr-probe"
|
||||
if not probe.is_file():
|
||||
parser.error(f"Build mocktail_vr_probe with MOCKTAIL_ENABLE_VR=ON first: {probe}")
|
||||
display = os.environ.get("DISPLAY")
|
||||
wayland = os.environ.get("WAYLAND_DISPLAY")
|
||||
if not args.headless and not (display or wayland):
|
||||
parser.error("Run from a graphical desktop terminal to see the scene, or use --headless for an automated check.")
|
||||
|
||||
xvfb = None
|
||||
try:
|
||||
if args.headless:
|
||||
if not shutil.which("Xvfb"):
|
||||
parser.error("--headless requires Xvfb")
|
||||
read_fd, write_fd = os.pipe()
|
||||
try:
|
||||
xvfb = subprocess.Popen(
|
||||
["Xvfb", "-displayfd", str(write_fd), "-screen", "0", "1280x720x24", "-nolisten", "tcp"],
|
||||
pass_fds=(write_fd,), stdout=subprocess.DEVNULL, start_new_session=True)
|
||||
except OSError:
|
||||
os.close(read_fd)
|
||||
raise
|
||||
finally:
|
||||
os.close(write_fd)
|
||||
with os.fdopen(read_fd) as number:
|
||||
if not select.select([number], [], [], 10)[0]:
|
||||
raise ValueError("Xvfb did not start within 10 seconds")
|
||||
display_number = number.readline().strip()
|
||||
if not display_number.isdigit():
|
||||
raise ValueError("Xvfb failed to allocate a display")
|
||||
display = ":" + display_number
|
||||
wayland = None
|
||||
env = simulator_environment(os.environ, manifest, display, wayland, args.software)
|
||||
# Keep simulator settings and runtime files private and temporary. The
|
||||
# desktop's XDG_RUNTIME_DIR is retained for Wayland authentication.
|
||||
with tempfile.TemporaryDirectory(prefix="mocktail-vr-simulated-") as directory:
|
||||
root = Path(directory)
|
||||
env["XDG_CONFIG_HOME"] = str(root / "config")
|
||||
env["XDG_DATA_HOME"] = str(root / "data")
|
||||
if not env.get("XDG_RUNTIME_DIR"):
|
||||
env["XDG_RUNTIME_DIR"] = str(root)
|
||||
command = [str(probe), "--scene"]
|
||||
frames = args.frames or (180 if args.headless else None)
|
||||
if frames:
|
||||
command += ["--frames", str(frames)]
|
||||
print(f"Simulated Monado headset: {manifest.resolve()}", flush=True)
|
||||
print("The test scene will appear in a desktop window. Ctrl+C exits." if not args.headless
|
||||
else "Checking the test scene on a private Xvfb display.", flush=True)
|
||||
child = None
|
||||
previous = {}
|
||||
|
||||
def forward_signal(signum, _frame):
|
||||
if child is not None and child.poll() is None:
|
||||
child.send_signal(signum)
|
||||
|
||||
try:
|
||||
child = subprocess.Popen(command, env=env, start_new_session=True)
|
||||
for signum in (signal.SIGINT, signal.SIGTERM):
|
||||
previous[signum] = signal.signal(signum, forward_signal)
|
||||
return child.wait()
|
||||
finally:
|
||||
if child is not None and child.poll() is None:
|
||||
child.terminate()
|
||||
try:
|
||||
child.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
child.kill()
|
||||
child.wait()
|
||||
for signum, handler in previous.items():
|
||||
signal.signal(signum, handler)
|
||||
finally:
|
||||
if xvfb is not None:
|
||||
xvfb.terminate()
|
||||
try:
|
||||
xvfb.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
xvfb.kill()
|
||||
xvfb.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except (OSError, ValueError, KeyError) as error:
|
||||
print(f"VR simulator: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -2476,7 +2476,8 @@ jobject BuildAppBridgeInitParams(JNIEnv* env, jstring client_settings,
|
||||
SetStringField(env, params, "userAgent", user_agent.c_str());
|
||||
SetBooleanField(env, params, "isTablet", JNI_FALSE);
|
||||
SetBooleanField(env, params, "isPotato", JNI_FALSE);
|
||||
SetBooleanField(env, params, "isVrDevice", JNI_FALSE);
|
||||
SetBooleanField(env, params, "isVrDevice",
|
||||
IsEnabled("MOCKTAIL_VR_ENABLED") ? JNI_TRUE : JNI_FALSE);
|
||||
SetStringField(env, params, "buildVariant", "googleProdRelease");
|
||||
SetObjectField(env, params, "vrContext", "Landroid/app/Activity;",
|
||||
activity);
|
||||
@@ -4507,6 +4508,18 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
|
||||
const mocktail::compat::BuildProfile& build_profile =
|
||||
*profile_result.profile;
|
||||
// The experimental VR ABI has only been observed on the active 2998
|
||||
// payload. Do not pass VR device parameters to unrelated builds, even when
|
||||
// they have a general-purpose compatibility profile. Eye initialization and
|
||||
// OpenXR projection are not yet implemented; this is a diagnostic launch.
|
||||
if (IsEnabled("MOCKTAIL_VR_ENABLED") &&
|
||||
build_profile.elf_build_id !=
|
||||
"ade08266c67aee88ec9c1d00902150e1684dad3a") {
|
||||
std::cerr << "[FATAL] Experimental Roblox VR requires exact Build ID "
|
||||
"ade08266c67aee88ec9c1d00902150e1684dad3a; loaded "
|
||||
<< build_profile.elf_build_id << "\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const mocktail::compat::HostAbiProfile* host_abi_profile =
|
||||
mocktail::compat::FindHostAbiProfile(build_profile.elf_build_id);
|
||||
const bool experiment_allowed =
|
||||
|
||||
+30
-22
@@ -308,6 +308,36 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
mocktail::runtime::ScrubCommandLineLaunchArguments(&command_line.options,
|
||||
argc, argv);
|
||||
|
||||
const mocktail::runtime::RuntimeConfigBootstrapResult config_bootstrap =
|
||||
mocktail::runtime::EnsureRuntimeConfigFile(paths.config_file());
|
||||
if (!config_bootstrap) {
|
||||
std::cerr << "[FATAL] Cannot prepare " << paths.config_file() << ": "
|
||||
<< config_bootstrap.error << '\n';
|
||||
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
||||
(void)mocktail::runtime::ShowFailureDialog(
|
||||
environment, "Mocktail could not prepare its configuration.");
|
||||
}
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
mocktail::runtime::RuntimeConfigLoadResult runtime_config =
|
||||
mocktail::runtime::LoadRuntimeConfig(environment, paths.config_file());
|
||||
if (!runtime_config) {
|
||||
std::cerr << "[FATAL] Cannot load " << paths.config_file() << ": "
|
||||
<< runtime_config.error << '\n';
|
||||
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
||||
(void)mocktail::runtime::ShowFailureDialog(
|
||||
environment, "Mocktail could not load its configuration.");
|
||||
}
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
// The standalone OpenXR scene belongs to mocktail-vr-probe --scene.
|
||||
// Continue through Roblox startup to inspect its native VR device.
|
||||
if (runtime_config.config.vr_enabled()) {
|
||||
std::cerr << " [vr] Roblox VR initialization requested (experimental); "
|
||||
"stereo eye resources and OpenXR output are not yet connected. "
|
||||
"Standalone scene: mocktail-vr-probe --scene\n";
|
||||
}
|
||||
mocktail::runtime::SessionLog session_log;
|
||||
mocktail::runtime::FailureSupportBundleGuard support_bundle_guard(
|
||||
environment, paths,
|
||||
@@ -355,28 +385,6 @@ int main(int argc, char* argv[]) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
const mocktail::runtime::RuntimeConfigBootstrapResult config_bootstrap =
|
||||
mocktail::runtime::EnsureRuntimeConfigFile(paths.config_file());
|
||||
if (!config_bootstrap) {
|
||||
std::cerr << "[FATAL] Cannot prepare " << paths.config_file() << ": "
|
||||
<< config_bootstrap.error << '\n';
|
||||
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
||||
(void)mocktail::runtime::ShowFailureDialog(
|
||||
environment, "Mocktail could not prepare its configuration.");
|
||||
}
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
mocktail::runtime::RuntimeConfigLoadResult runtime_config =
|
||||
mocktail::runtime::LoadRuntimeConfig(environment, paths.config_file());
|
||||
if (!runtime_config) {
|
||||
std::cerr << "[FATAL] Cannot load " << paths.config_file() << ": "
|
||||
<< runtime_config.error << '\n';
|
||||
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
||||
(void)mocktail::runtime::ShowFailureDialog(
|
||||
environment, "Mocktail could not load its configuration.");
|
||||
}
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runtime_config.config.use_system_proxy()) {
|
||||
const mocktail::runtime::SystemProxyResult system_proxy =
|
||||
mocktail::runtime::ResolveSystemProxy();
|
||||
|
||||
@@ -74,6 +74,8 @@ CommandLineParseResult ParseCommandLine(int argc, const char* const argv[]) {
|
||||
&result.options.graphics_backend, &result.error)) {
|
||||
return result;
|
||||
}
|
||||
} else if (argument == "--vr" || argument == "--no-vr") {
|
||||
result.options.vr_enabled = argument == "--vr";
|
||||
} else if (argument == "--allow-unverified-build") {
|
||||
result.options.allow_unverified_build = true;
|
||||
} else if (argument == "--force-run-latest") {
|
||||
@@ -141,6 +143,7 @@ CommandLineParseResult ParseCommandLine(int argc, const char* const argv[]) {
|
||||
(!result.options.roblox_library_path.empty() ||
|
||||
result.options.window_mode != WindowMode::kUnspecified ||
|
||||
!result.options.graphics_backend.empty() ||
|
||||
result.options.vr_enabled.has_value() ||
|
||||
result.options.allow_unverified_build ||
|
||||
!result.options.launch_request_json.empty())) {
|
||||
result.error = "--force-run-latest must be used on its own";
|
||||
@@ -218,6 +221,11 @@ void ScrubCommandLineLaunchArguments(CommandLineOptions* options, int argc,
|
||||
|
||||
bool ApplyCommandLineEnvironment(const CommandLineOptions& options,
|
||||
std::string* error) {
|
||||
if (options.vr_enabled.has_value() &&
|
||||
!SetEnvironment("MOCKTAIL_VR_ENABLED", *options.vr_enabled ? "1" : "0",
|
||||
error)) {
|
||||
return false;
|
||||
}
|
||||
if (!options.roblox_library_path.empty() &&
|
||||
!SetEnvironment("ROBLOX_LIB_PATH", options.roblox_library_path, error)) {
|
||||
return false;
|
||||
@@ -248,6 +256,8 @@ std::string CommandLineUsage(const std::string& program_name) {
|
||||
"x86_64 libroblox.so\n"
|
||||
<< " --headless Run without creating an SDL window\n"
|
||||
<< " --windowed Force windowed startup (default)\n"
|
||||
<< " --vr / --no-vr Launch Roblox in experimental VR mode or "
|
||||
"disable vr.enabled\n"
|
||||
<< " --graphics <backend> direct-vulkan | opengl | system | "
|
||||
"angle-vulkan (default: direct-vulkan)\n"
|
||||
<< " --allow-unverified-build Run a known but unverified Build-ID "
|
||||
|
||||
@@ -170,6 +170,8 @@ std::string BuildNetworkProxyUrl(const NetworkProxyConfig& proxy) {
|
||||
RuntimeConfig RuntimeConfig::FromEnvironment(const Environment& environment) {
|
||||
RuntimeConfig config;
|
||||
config.headless_ = LegacyEnabled(environment, "MOCKTAIL_HEADLESS");
|
||||
config.vr_enabled_ =
|
||||
ReadBoolean(environment, "MOCKTAIL_VR_ENABLED", false, &config.vr_valid_);
|
||||
config.roblox_library_path_ = environment.GetOr(
|
||||
"ROBLOX_LIB_PATH", config.roblox_library_path_.string());
|
||||
config.graphics_backend_name_ = environment.GetOr(
|
||||
|
||||
@@ -67,6 +67,13 @@ graphics:
|
||||
# Optional presentation synchronization override: auto, on, or off.
|
||||
# vsync: off
|
||||
|
||||
vr:
|
||||
# Boolean (default: false): launch the Roblox experimental VR path.
|
||||
# Only Build ID 2998 is currently accepted; eye resources and OpenXR output
|
||||
# are NOT yet connected. Standalone scene: mocktail-vr-probe --scene.
|
||||
# Use --no-vr to launch Roblox normally.
|
||||
enabled: false
|
||||
|
||||
performance:
|
||||
# Boolean (default: false): size Roblox scheduler/render queues from every
|
||||
# physical CPU core. A place's Lua/main thread can still remain serial.
|
||||
|
||||
@@ -168,6 +168,7 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
|
||||
"device.keyboard",
|
||||
"runtime.headless",
|
||||
"runtime.roblox_library",
|
||||
"vr.enabled",
|
||||
"appearance.theme",
|
||||
"graphics.backend",
|
||||
"graphics.frame_rate_limit",
|
||||
@@ -308,6 +309,14 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
|
||||
}
|
||||
(*environment)["MOCKTAIL_THEME"] = *theme;
|
||||
}
|
||||
if (const auto enabled = value("vr.enabled"); enabled.has_value()) {
|
||||
bool parsed = false;
|
||||
if (!ParseBoolean(*enabled, &parsed)) {
|
||||
*error = "vr.enabled must be true or false";
|
||||
return false;
|
||||
}
|
||||
(*environment)["MOCKTAIL_VR_ENABLED"] = parsed ? "1" : "0";
|
||||
}
|
||||
if (const auto backend = value("graphics.backend"); backend.has_value()) {
|
||||
if (RuntimeConfig::ParseGraphicsBackend(*backend) ==
|
||||
GraphicsBackend::kUnknown) {
|
||||
@@ -656,7 +665,7 @@ bool LoadYaml(const std::filesystem::path& path, ValueMap* values, bool* loaded,
|
||||
*error = "device must be a preset scalar or detailed mapping";
|
||||
valid = false;
|
||||
}
|
||||
} else if (key == "runtime" || key == "appearance" ||
|
||||
} else if (key == "runtime" || key == "appearance" || key == "vr" ||
|
||||
key == "graphics" || key == "performance" ||
|
||||
key == "audio" || key == "window" || key == "input" ||
|
||||
key == "compatibility" || key == "network") {
|
||||
@@ -725,6 +734,9 @@ RuntimeConfigLoadResult LoadRuntimeConfig(
|
||||
result.config = RuntimeConfig::FromEnvironment(layered);
|
||||
if (!result.config.frame_rate().valid()) {
|
||||
result.error = "frame-rate policy is invalid";
|
||||
} else if (!result.config.vr_valid()) {
|
||||
result.error = "VR configuration is invalid: MOCKTAIL_VR_ENABLED must be "
|
||||
"1/0, true/false, or on/off";
|
||||
} else if (!result.config.device_profile_valid()) {
|
||||
result.error = "device profile is invalid";
|
||||
} else if (!result.config.input_capabilities().touch_enabled &&
|
||||
@@ -761,6 +773,10 @@ RuntimeConfigLoadResult LoadRuntimeConfig(
|
||||
|
||||
bool ExportRuntimeConfigEnvironment(const RuntimeConfig& config,
|
||||
std::string* error) {
|
||||
if (!config.vr_valid()) {
|
||||
if (error != nullptr) *error = "cannot export an invalid VR configuration";
|
||||
return false;
|
||||
}
|
||||
if (!config.device_profile_valid()) {
|
||||
if (error != nullptr) {
|
||||
*error = "cannot export an invalid device profile";
|
||||
@@ -813,6 +829,8 @@ bool ExportRuntimeConfigEnvironment(const RuntimeConfig& config,
|
||||
const bool base_exported =
|
||||
SetEnvironmentValue("MOCKTAIL_HEADLESS", config.headless() ? "1" : "0",
|
||||
error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_VR_ENABLED", config.vr_enabled() ? "1" : "0",
|
||||
error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DEVICE_PROFILE", std::string(device.name),
|
||||
error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DEVICE_CLASS",
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
#include "mocktail/vr/openxr_probe.h"
|
||||
#include "mocktail/vr/openxr_preview.h"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
std::string FormatPreviewResult(const PreviewResult& result) {
|
||||
std::ostringstream output;
|
||||
output << "[VR] " << result.message << '\n'
|
||||
<< "Frames: " << result.frames
|
||||
<< ", rendered: " << result.rendered_frames
|
||||
<< ", tracked: " << result.tracked_frames << '\n';
|
||||
if (result.tracked_frames) {
|
||||
output << "Head position (metres): " << result.head_position[0] << ", "
|
||||
<< result.head_position[1] << ", " << result.head_position[2] << '\n'
|
||||
<< "Head orientation (xyzw): " << result.head_orientation[0] << ", "
|
||||
<< result.head_orientation[1] << ", " << result.head_orientation[2]
|
||||
<< ", " << result.head_orientation[3] << '\n';
|
||||
}
|
||||
return output.str();
|
||||
}
|
||||
|
||||
int ProbeExitCode(ProbeStatus status) {
|
||||
switch (status) {
|
||||
case ProbeStatus::kReady:
|
||||
return 0;
|
||||
case ProbeStatus::kBuildDisabled:
|
||||
return 2;
|
||||
case ProbeStatus::kRuntimeUnavailable:
|
||||
return 3;
|
||||
case ProbeStatus::kHeadsetUnavailable:
|
||||
return 4;
|
||||
case ProbeStatus::kIncompatibleRuntime:
|
||||
return 5;
|
||||
case ProbeStatus::kError:
|
||||
return 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string FormatProbeReport(const ProbeReport& report) {
|
||||
std::ostringstream output;
|
||||
output << "Mocktail OpenXR discovery\n";
|
||||
if (!report.sdk_version.empty())
|
||||
output << "SDK: " << report.sdk_version << '\n';
|
||||
if (!report.runtime_name.empty()) {
|
||||
output << "Runtime: " << report.runtime_name << " ("
|
||||
<< report.runtime_version << ")\n";
|
||||
}
|
||||
if (!report.system_name.empty()) {
|
||||
output << "System: " << report.system_name << '\n'
|
||||
<< "Tracking: orientation=" << report.orientation_tracking
|
||||
<< " position=" << report.position_tracking << '\n';
|
||||
}
|
||||
for (std::size_t eye = 0; eye < report.views.size(); ++eye) {
|
||||
const auto& view = report.views[eye];
|
||||
output << "Eye " << eye << ": " << view.width << 'x' << view.height
|
||||
<< ", samples=" << view.samples << '\n';
|
||||
}
|
||||
if (!report.minimum_vulkan_version.empty()) {
|
||||
output << "Vulkan: minimum=" << report.minimum_vulkan_version
|
||||
<< ", highest runtime-tested=" << report.maximum_vulkan_version
|
||||
<< '\n';
|
||||
}
|
||||
output << "Extensions (" << report.extensions.size() << "):\n";
|
||||
for (const auto& extension : report.extensions)
|
||||
output << " " << extension << '\n';
|
||||
output << "Discovery: " << report.message << '\n'
|
||||
<< "Roblox VR rendering: not implemented yet.\n"
|
||||
<< "This probe does not create an XR session or render frames.\n";
|
||||
return output.str();
|
||||
}
|
||||
|
||||
} // namespace mocktail::vr
|
||||
@@ -0,0 +1,23 @@
|
||||
#include "mocktail/vr/openxr_probe.h"
|
||||
#include "mocktail/vr/openxr_preview.h"
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
PreviewResult RunOpenXrPreview(const PreviewOptions&) {
|
||||
PreviewResult result;
|
||||
result.message =
|
||||
"This build does not include OpenXR support. Use a VR-enabled build of "
|
||||
"Mocktail.";
|
||||
return result;
|
||||
}
|
||||
|
||||
ProbeReport ProbeOpenXr() {
|
||||
ProbeReport report;
|
||||
report.status = ProbeStatus::kBuildDisabled;
|
||||
report.message =
|
||||
"This build does not include OpenXR support. Use a "
|
||||
"VR-enabled build of Mocktail.";
|
||||
return report;
|
||||
}
|
||||
|
||||
} // namespace mocktail::vr
|
||||
@@ -0,0 +1,729 @@
|
||||
#include "mocktail/vr/openxr_preview.h"
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_platform.h>
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <signal.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::vr {
|
||||
namespace {
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
volatile sig_atomic_t interrupted = 0;
|
||||
void Interrupt(int) { interrupted = 1; }
|
||||
|
||||
class SignalScope {
|
||||
public:
|
||||
SignalScope() {
|
||||
interrupted = 0;
|
||||
struct sigaction action{};
|
||||
action.sa_handler = Interrupt;
|
||||
sigemptyset(&action.sa_mask);
|
||||
have_int_ = sigaction(SIGINT, &action, &old_int_) == 0;
|
||||
have_term_ = sigaction(SIGTERM, &action, &old_term_) == 0;
|
||||
}
|
||||
~SignalScope() {
|
||||
if (have_int_) sigaction(SIGINT, &old_int_, nullptr);
|
||||
if (have_term_) sigaction(SIGTERM, &old_term_, nullptr);
|
||||
}
|
||||
|
||||
private:
|
||||
struct sigaction old_int_{}, old_term_{};
|
||||
bool have_int_ = false, have_term_ = false;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
T XrInfo(XrStructureType type) {
|
||||
T value{};
|
||||
value.type = type;
|
||||
return value;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T VkInfo(VkStructureType type) {
|
||||
T value{};
|
||||
value.sType = type;
|
||||
return value;
|
||||
}
|
||||
|
||||
void Require(bool condition, const std::string& message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
void CheckVk(VkResult result, const char* operation) {
|
||||
Require(result == VK_SUCCESS,
|
||||
std::string(operation) + " failed: " + std::to_string(result));
|
||||
}
|
||||
|
||||
// Every Vulkan call uses the host loader explicitly. Linking global vk*
|
||||
// symbols here could accidentally enter Mocktail's Android Vulkan adapter.
|
||||
struct Vulkan {
|
||||
void* library = nullptr;
|
||||
PFN_vkGetInstanceProcAddr get_instance_proc = nullptr;
|
||||
PFN_vkGetDeviceProcAddr get_device_proc = nullptr;
|
||||
VkInstance instance = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice physical_device = VK_NULL_HANDLE;
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
VkQueue queue = VK_NULL_HANDLE;
|
||||
std::uint32_t queue_family = 0;
|
||||
PFN_vkDestroyInstance DestroyInstance = nullptr;
|
||||
|
||||
#define MOCKTAIL_PREVIEW_VK_FUNCTIONS(X) \
|
||||
X(DestroyDevice) \
|
||||
X(GetDeviceQueue) \
|
||||
X(DeviceWaitIdle) \
|
||||
X(CreateImageView) \
|
||||
X(DestroyImageView) \
|
||||
X(CreateRenderPass) \
|
||||
X(DestroyRenderPass) \
|
||||
X(CreateFramebuffer) \
|
||||
X(DestroyFramebuffer) \
|
||||
X(CreateCommandPool) \
|
||||
X(DestroyCommandPool) \
|
||||
X(AllocateCommandBuffers) \
|
||||
X(ResetCommandBuffer) \
|
||||
X(BeginCommandBuffer) \
|
||||
X(EndCommandBuffer) \
|
||||
X(CmdBeginRenderPass) \
|
||||
X(CmdEndRenderPass) \
|
||||
X(CmdClearAttachments) \
|
||||
X(CreateFence) \
|
||||
X(DestroyFence) \
|
||||
X(ResetFences) \
|
||||
X(WaitForFences) \
|
||||
X(QueueSubmit)
|
||||
#define DECLARE(name) PFN_vk##name name = nullptr;
|
||||
MOCKTAIL_PREVIEW_VK_FUNCTIONS(DECLARE)
|
||||
#undef DECLARE
|
||||
|
||||
Vulkan() = default;
|
||||
Vulkan(const Vulkan&) = delete;
|
||||
Vulkan& operator=(const Vulkan&) = delete;
|
||||
~Vulkan() {
|
||||
if (device && DestroyDevice) DestroyDevice(device, nullptr);
|
||||
if (instance && DestroyInstance) DestroyInstance(instance, nullptr);
|
||||
if (library) dlclose(library);
|
||||
}
|
||||
|
||||
void OpenLoader() {
|
||||
library = dlopen("libvulkan.so.1", RTLD_NOW | RTLD_LOCAL);
|
||||
Require(library != nullptr,
|
||||
"Cannot load the host Vulkan loader libvulkan.so.1");
|
||||
get_instance_proc = reinterpret_cast<PFN_vkGetInstanceProcAddr>(
|
||||
dlsym(library, "vkGetInstanceProcAddr"));
|
||||
Require(get_instance_proc != nullptr,
|
||||
"Host vkGetInstanceProcAddr is missing");
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T InstanceFunction(const char* name) {
|
||||
const auto function =
|
||||
reinterpret_cast<T>(get_instance_proc(instance, name));
|
||||
Require(function != nullptr,
|
||||
std::string("Missing Vulkan function: ") + name);
|
||||
return function;
|
||||
}
|
||||
|
||||
void LoadDeviceFunctions() {
|
||||
#define LOAD(name) \
|
||||
name = reinterpret_cast<PFN_vk##name>(get_device_proc(device, "vk" #name)); \
|
||||
Require(name != nullptr, "Missing Vulkan function: vk" #name);
|
||||
MOCKTAIL_PREVIEW_VK_FUNCTIONS(LOAD)
|
||||
#undef LOAD
|
||||
GetDeviceQueue(device, queue_family, 0, &queue);
|
||||
}
|
||||
};
|
||||
#undef MOCKTAIL_PREVIEW_VK_FUNCTIONS
|
||||
|
||||
class Preview {
|
||||
public:
|
||||
Preview() = default;
|
||||
Preview(const Preview&) = delete;
|
||||
Preview& operator=(const Preview&) = delete;
|
||||
~Preview() {
|
||||
// The runtime still needs our VkDevice when destroying its
|
||||
// swapchain/session.
|
||||
if (vk_.device && vk_.DeviceWaitIdle) (void)vk_.DeviceWaitIdle(vk_.device);
|
||||
for (auto framebuffer : framebuffers_)
|
||||
vk_.DestroyFramebuffer(vk_.device, framebuffer, nullptr);
|
||||
for (auto view : image_views_)
|
||||
vk_.DestroyImageView(vk_.device, view, nullptr);
|
||||
if (render_pass_) vk_.DestroyRenderPass(vk_.device, render_pass_, nullptr);
|
||||
if (fence_) vk_.DestroyFence(vk_.device, fence_, nullptr);
|
||||
if (pool_) vk_.DestroyCommandPool(vk_.device, pool_, nullptr);
|
||||
if (swapchain_) (void)xrDestroySwapchain(swapchain_);
|
||||
if (view_space_) (void)xrDestroySpace(view_space_);
|
||||
if (local_space_) (void)xrDestroySpace(local_space_);
|
||||
if (session_) (void)xrDestroySession(session_);
|
||||
if (instance_) (void)xrDestroyInstance(instance_);
|
||||
}
|
||||
|
||||
void Initialize() {
|
||||
const char* extension = XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME;
|
||||
auto info = XrInfo<XrInstanceCreateInfo>(XR_TYPE_INSTANCE_CREATE_INFO);
|
||||
std::strcpy(info.applicationInfo.applicationName, "Mocktail VR preview");
|
||||
std::strcpy(info.applicationInfo.engineName, "Mocktail");
|
||||
info.applicationInfo.apiVersion = XR_MAKE_VERSION(1, 0, 0);
|
||||
info.enabledExtensionCount = 1;
|
||||
info.enabledExtensionNames = &extension;
|
||||
Check(xrCreateInstance(&info, &instance_), "xrCreateInstance");
|
||||
auto system = XrInfo<XrSystemGetInfo>(XR_TYPE_SYSTEM_GET_INFO);
|
||||
system.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
|
||||
Check(xrGetSystem(instance_, &system, &system_), "xrGetSystem");
|
||||
|
||||
const auto views = Enumerate<XrViewConfigurationView>(
|
||||
XR_TYPE_VIEW_CONFIGURATION_VIEW,
|
||||
[&](auto capacity, auto* count, auto* data) {
|
||||
return xrEnumerateViewConfigurationViews(
|
||||
instance_, system_, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
|
||||
capacity, count, data);
|
||||
});
|
||||
Require(views.size() == 2,
|
||||
"The VR preview requires PRIMARY_STEREO support");
|
||||
const auto blends = Enumerate<XrEnvironmentBlendMode>(
|
||||
XR_TYPE_UNKNOWN, [&](auto capacity, auto* count, auto* data) {
|
||||
return xrEnumerateEnvironmentBlendModes(
|
||||
instance_, system_, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
|
||||
capacity, count, data);
|
||||
});
|
||||
Require(!blends.empty(), "The runtime exposes no environment blend modes");
|
||||
blend_ = std::find(blends.begin(), blends.end(),
|
||||
XR_ENVIRONMENT_BLEND_MODE_OPAQUE) != blends.end()
|
||||
? XR_ENVIRONMENT_BLEND_MODE_OPAQUE
|
||||
: blends.front();
|
||||
CreateGraphics();
|
||||
auto binding = XrInfo<XrGraphicsBindingVulkan2KHR>(
|
||||
XR_TYPE_GRAPHICS_BINDING_VULKAN2_KHR);
|
||||
binding.instance = vk_.instance;
|
||||
binding.physicalDevice = vk_.physical_device;
|
||||
binding.device = vk_.device;
|
||||
binding.queueFamilyIndex = vk_.queue_family;
|
||||
auto session_info =
|
||||
XrInfo<XrSessionCreateInfo>(XR_TYPE_SESSION_CREATE_INFO);
|
||||
session_info.next = &binding;
|
||||
session_info.systemId = system_;
|
||||
Check(xrCreateSession(instance_, &session_info, &session_),
|
||||
"xrCreateSession");
|
||||
auto space_info =
|
||||
XrInfo<XrReferenceSpaceCreateInfo>(XR_TYPE_REFERENCE_SPACE_CREATE_INFO);
|
||||
space_info.poseInReferenceSpace.orientation.w = 1;
|
||||
space_info.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL;
|
||||
Check(xrCreateReferenceSpace(session_, &space_info, &local_space_),
|
||||
"xrCreateReferenceSpace LOCAL");
|
||||
space_info.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW;
|
||||
Check(xrCreateReferenceSpace(session_, &space_info, &view_space_),
|
||||
"xrCreateReferenceSpace VIEW");
|
||||
CreateImages();
|
||||
}
|
||||
|
||||
void Run(const PreviewOptions& options, PreviewResult* report) {
|
||||
const auto ready_deadline = Clock::now() + std::chrono::seconds(30);
|
||||
auto exit_deadline = Clock::time_point::max();
|
||||
bool running = false, exit_requested = false;
|
||||
while (true) {
|
||||
while (true) {
|
||||
auto event = XrInfo<XrEventDataBuffer>(XR_TYPE_EVENT_DATA_BUFFER);
|
||||
const auto result = xrPollEvent(instance_, &event);
|
||||
if (result == XR_EVENT_UNAVAILABLE) break;
|
||||
Check(result, "xrPollEvent");
|
||||
if (event.type == XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING)
|
||||
throw std::runtime_error("The OpenXR runtime was lost");
|
||||
if (event.type != XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) continue;
|
||||
const auto& changed =
|
||||
reinterpret_cast<const XrEventDataSessionStateChanged&>(event);
|
||||
if (changed.session != session_) continue;
|
||||
switch (changed.state) {
|
||||
case XR_SESSION_STATE_READY: {
|
||||
auto begin = XrInfo<XrSessionBeginInfo>(XR_TYPE_SESSION_BEGIN_INFO);
|
||||
begin.primaryViewConfigurationType =
|
||||
XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
||||
Check(xrBeginSession(session_, &begin), "xrBeginSession");
|
||||
running = true;
|
||||
std::cerr << "[VR] Session started. A test panel is fixed two "
|
||||
"metres ahead.\n";
|
||||
break;
|
||||
}
|
||||
case XR_SESSION_STATE_STOPPING:
|
||||
if (running) Check(xrEndSession(session_), "xrEndSession");
|
||||
return;
|
||||
case XR_SESSION_STATE_EXITING:
|
||||
return;
|
||||
case XR_SESSION_STATE_LOSS_PENDING:
|
||||
throw std::runtime_error(
|
||||
"The OpenXR session was lost; reconnect and launch again");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (interrupted ||
|
||||
(options.frame_limit && report->frames >= options.frame_limit)) {
|
||||
if (!running) return;
|
||||
if (!exit_requested) {
|
||||
Check(xrRequestExitSession(session_), "xrRequestExitSession");
|
||||
exit_requested = true;
|
||||
exit_deadline = Clock::now() + std::chrono::seconds(5);
|
||||
// Consume an immediate STOPPING event before starting another frame.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Require(Clock::now() < exit_deadline,
|
||||
"The runtime did not stop the VR session after an exit request");
|
||||
if (!running) {
|
||||
Require(Clock::now() < ready_deadline,
|
||||
"Timed out waiting for the VR session to become READY");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
continue;
|
||||
}
|
||||
Frame(report);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void Check(XrResult result, const char* operation) const {
|
||||
if (XR_SUCCEEDED(result)) return;
|
||||
char text[XR_MAX_RESULT_STRING_SIZE]{};
|
||||
if (instance_) (void)xrResultToString(instance_, result, text);
|
||||
if (!text[0] && result == XR_ERROR_RUNTIME_UNAVAILABLE)
|
||||
std::strcpy(text, "XR_ERROR_RUNTIME_UNAVAILABLE");
|
||||
std::string message =
|
||||
std::string(operation) +
|
||||
" failed: " + (text[0] ? std::string(text) : std::to_string(result));
|
||||
if (result == XR_ERROR_RUNTIME_UNAVAILABLE)
|
||||
message += ". No active OpenXR runtime was found. Start WiVRn for a real "
|
||||
"headset. Without a headset, run mocktail-vr-simulated from "
|
||||
"this build directory.";
|
||||
else if (result == XR_ERROR_FORM_FACTOR_UNAVAILABLE)
|
||||
message += ". Connect a headset or select a simulated runtime.";
|
||||
else if (result == XR_ERROR_EXTENSION_NOT_PRESENT)
|
||||
message += ". XR_KHR_vulkan_enable2 is required.";
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T Function(const char* name) {
|
||||
PFN_xrVoidFunction function = nullptr;
|
||||
Check(xrGetInstanceProcAddr(instance_, name, &function), name);
|
||||
Require(function != nullptr,
|
||||
std::string("Missing OpenXR function: ") + name);
|
||||
return reinterpret_cast<T>(function);
|
||||
}
|
||||
|
||||
template <typename T, typename F>
|
||||
std::vector<T> Enumerate(XrStructureType type, F function) {
|
||||
for (int retry = 0; retry < 3; ++retry) {
|
||||
std::uint32_t count = 0;
|
||||
Check(function(0, &count, static_cast<T*>(nullptr)),
|
||||
"OpenXR enumeration count");
|
||||
Require(count <= 4096, "OpenXR enumeration exceeds the supported size");
|
||||
std::vector<T> values(count);
|
||||
if constexpr (std::is_class_v<T>)
|
||||
for (auto& value : values) value.type = type;
|
||||
if (!count) return values;
|
||||
const auto result = function(count, &count, values.data());
|
||||
if (result == XR_ERROR_SIZE_INSUFFICIENT) continue;
|
||||
Check(result, "OpenXR enumeration data");
|
||||
Require(count <= values.size(),
|
||||
"OpenXR enumeration returned an invalid size");
|
||||
values.resize(count);
|
||||
return values;
|
||||
}
|
||||
throw std::runtime_error("OpenXR enumeration kept changing size");
|
||||
}
|
||||
|
||||
void CreateGraphics() {
|
||||
auto requirements = XrInfo<XrGraphicsRequirementsVulkan2KHR>(
|
||||
XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN2_KHR);
|
||||
Check(Function<PFN_xrGetVulkanGraphicsRequirements2KHR>(
|
||||
"xrGetVulkanGraphicsRequirements2KHR")(instance_, system_,
|
||||
&requirements),
|
||||
"xrGetVulkanGraphicsRequirements2KHR");
|
||||
vk_.OpenLoader();
|
||||
auto application =
|
||||
VkInfo<VkApplicationInfo>(VK_STRUCTURE_TYPE_APPLICATION_INFO);
|
||||
application.pApplicationName = "Mocktail VR preview";
|
||||
const auto version = std::max<XrVersion>(
|
||||
requirements.minApiVersionSupported, XR_MAKE_VERSION(1, 0, 0));
|
||||
application.apiVersion = VK_MAKE_API_VERSION(0, XR_VERSION_MAJOR(version),
|
||||
XR_VERSION_MINOR(version), 0);
|
||||
auto instance_info =
|
||||
VkInfo<VkInstanceCreateInfo>(VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO);
|
||||
instance_info.pApplicationInfo = &application;
|
||||
auto xr_instance_info = XrInfo<XrVulkanInstanceCreateInfoKHR>(
|
||||
XR_TYPE_VULKAN_INSTANCE_CREATE_INFO_KHR);
|
||||
xr_instance_info.systemId = system_;
|
||||
xr_instance_info.pfnGetInstanceProcAddr = vk_.get_instance_proc;
|
||||
xr_instance_info.vulkanCreateInfo = &instance_info;
|
||||
VkResult result = VK_ERROR_INITIALIZATION_FAILED;
|
||||
Check(Function<PFN_xrCreateVulkanInstanceKHR>("xrCreateVulkanInstanceKHR")(
|
||||
instance_, &xr_instance_info, &vk_.instance, &result),
|
||||
"xrCreateVulkanInstanceKHR");
|
||||
CheckVk(result, "vkCreateInstance");
|
||||
vk_.DestroyInstance =
|
||||
vk_.InstanceFunction<PFN_vkDestroyInstance>("vkDestroyInstance");
|
||||
vk_.get_device_proc =
|
||||
vk_.InstanceFunction<PFN_vkGetDeviceProcAddr>("vkGetDeviceProcAddr");
|
||||
auto device_info = XrInfo<XrVulkanGraphicsDeviceGetInfoKHR>(
|
||||
XR_TYPE_VULKAN_GRAPHICS_DEVICE_GET_INFO_KHR);
|
||||
device_info.systemId = system_;
|
||||
device_info.vulkanInstance = vk_.instance;
|
||||
Check(Function<PFN_xrGetVulkanGraphicsDevice2KHR>(
|
||||
"xrGetVulkanGraphicsDevice2KHR")(instance_, &device_info,
|
||||
&vk_.physical_device),
|
||||
"xrGetVulkanGraphicsDevice2KHR");
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
vk_.InstanceFunction<PFN_vkGetPhysicalDeviceProperties>(
|
||||
"vkGetPhysicalDeviceProperties")(vk_.physical_device, &properties);
|
||||
Require(properties.apiVersion >= application.apiVersion,
|
||||
"The runtime-selected GPU does not support the required Vulkan "
|
||||
"version");
|
||||
const auto get_families =
|
||||
vk_.InstanceFunction<PFN_vkGetPhysicalDeviceQueueFamilyProperties>(
|
||||
"vkGetPhysicalDeviceQueueFamilyProperties");
|
||||
std::uint32_t count = 0;
|
||||
get_families(vk_.physical_device, &count, nullptr);
|
||||
Require(count > 0 && count <= 256,
|
||||
"The runtime-selected GPU has no usable queue families");
|
||||
std::vector<VkQueueFamilyProperties> families(count);
|
||||
get_families(vk_.physical_device, &count, families.data());
|
||||
const auto family =
|
||||
std::find_if(families.begin(), families.end(), [](const auto& value) {
|
||||
return value.queueCount && (value.queueFlags & VK_QUEUE_GRAPHICS_BIT);
|
||||
});
|
||||
Require(family != families.end(),
|
||||
"The runtime-selected GPU has no graphics queue");
|
||||
vk_.queue_family = static_cast<std::uint32_t>(family - families.begin());
|
||||
const float priority = 1;
|
||||
auto queue = VkInfo<VkDeviceQueueCreateInfo>(
|
||||
VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO);
|
||||
queue.queueFamilyIndex = vk_.queue_family;
|
||||
queue.queueCount = 1;
|
||||
queue.pQueuePriorities = &priority;
|
||||
auto create =
|
||||
VkInfo<VkDeviceCreateInfo>(VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO);
|
||||
create.queueCreateInfoCount = 1;
|
||||
create.pQueueCreateInfos = &queue;
|
||||
auto xr_create = XrInfo<XrVulkanDeviceCreateInfoKHR>(
|
||||
XR_TYPE_VULKAN_DEVICE_CREATE_INFO_KHR);
|
||||
xr_create.systemId = system_;
|
||||
xr_create.pfnGetInstanceProcAddr = vk_.get_instance_proc;
|
||||
xr_create.vulkanPhysicalDevice = vk_.physical_device;
|
||||
xr_create.vulkanCreateInfo = &create;
|
||||
Check(Function<PFN_xrCreateVulkanDeviceKHR>("xrCreateVulkanDeviceKHR")(
|
||||
instance_, &xr_create, &vk_.device, &result),
|
||||
"xrCreateVulkanDeviceKHR");
|
||||
CheckVk(result, "vkCreateDevice");
|
||||
vk_.LoadDeviceFunctions();
|
||||
std::cerr << "[VR] Vulkan device: " << properties.deviceName << '\n';
|
||||
}
|
||||
|
||||
void CreateImages() {
|
||||
const auto formats = Enumerate<std::int64_t>(
|
||||
XR_TYPE_UNKNOWN, [&](auto capacity, auto* count, auto* data) {
|
||||
return xrEnumerateSwapchainFormats(session_, capacity, count, data);
|
||||
});
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
for (auto candidate :
|
||||
{VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_B8G8R8A8_SRGB,
|
||||
VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM}) {
|
||||
if (std::find(formats.begin(), formats.end(), candidate) !=
|
||||
formats.end()) {
|
||||
format = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Require(format != VK_FORMAT_UNDEFINED,
|
||||
"The runtime exposes no supported RGBA swapchain format");
|
||||
auto system_properties =
|
||||
XrInfo<XrSystemProperties>(XR_TYPE_SYSTEM_PROPERTIES);
|
||||
Check(xrGetSystemProperties(instance_, system_, &system_properties),
|
||||
"xrGetSystemProperties");
|
||||
width_ = std::min(
|
||||
512u, system_properties.graphicsProperties.maxSwapchainImageWidth);
|
||||
height_ = std::min(
|
||||
512u, system_properties.graphicsProperties.maxSwapchainImageHeight);
|
||||
Require(width_ >= 64 && height_ >= 64 &&
|
||||
system_properties.graphicsProperties.maxLayerCount > 0,
|
||||
"The runtime cannot display the preview panel");
|
||||
auto swapchain =
|
||||
XrInfo<XrSwapchainCreateInfo>(XR_TYPE_SWAPCHAIN_CREATE_INFO);
|
||||
swapchain.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;
|
||||
swapchain.format = format;
|
||||
swapchain.sampleCount = 1;
|
||||
swapchain.width = width_;
|
||||
swapchain.height = height_;
|
||||
swapchain.faceCount = swapchain.arraySize = swapchain.mipCount = 1;
|
||||
Check(xrCreateSwapchain(session_, &swapchain, &swapchain_),
|
||||
"xrCreateSwapchain");
|
||||
const auto images = Enumerate<XrSwapchainImageVulkanKHR>(
|
||||
XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR,
|
||||
[&](auto capacity, auto* count, auto* data) {
|
||||
return xrEnumerateSwapchainImages(
|
||||
swapchain_, capacity, count,
|
||||
reinterpret_cast<XrSwapchainImageBaseHeader*>(data));
|
||||
});
|
||||
Require(!images.empty(), "The runtime returned an empty swapchain");
|
||||
|
||||
VkAttachmentDescription attachment{};
|
||||
attachment.format = format;
|
||||
attachment.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||
attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
attachment.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
VkAttachmentReference reference{0,
|
||||
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.colorAttachmentCount = 1;
|
||||
subpass.pColorAttachments = &reference;
|
||||
VkSubpassDependency dependency{};
|
||||
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
|
||||
dependency.dstSubpass = 0;
|
||||
dependency.srcStageMask = dependency.dstStageMask =
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||
auto pass = VkInfo<VkRenderPassCreateInfo>(
|
||||
VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO);
|
||||
pass.attachmentCount = 1;
|
||||
pass.pAttachments = &attachment;
|
||||
pass.subpassCount = 1;
|
||||
pass.pSubpasses = &subpass;
|
||||
pass.dependencyCount = 1;
|
||||
pass.pDependencies = &dependency;
|
||||
CheckVk(vk_.CreateRenderPass(vk_.device, &pass, nullptr, &render_pass_),
|
||||
"vkCreateRenderPass");
|
||||
for (const auto& image : images) {
|
||||
auto view_info = VkInfo<VkImageViewCreateInfo>(
|
||||
VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO);
|
||||
view_info.image = image.image;
|
||||
view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
view_info.format = format;
|
||||
view_info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
CheckVk(vk_.CreateImageView(vk_.device, &view_info, nullptr, &view),
|
||||
"vkCreateImageView");
|
||||
image_views_.push_back(view);
|
||||
auto framebuffer_info = VkInfo<VkFramebufferCreateInfo>(
|
||||
VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO);
|
||||
framebuffer_info.renderPass = render_pass_;
|
||||
framebuffer_info.attachmentCount = 1;
|
||||
framebuffer_info.pAttachments = &image_views_.back();
|
||||
framebuffer_info.width = width_;
|
||||
framebuffer_info.height = height_;
|
||||
framebuffer_info.layers = 1;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
CheckVk(vk_.CreateFramebuffer(vk_.device, &framebuffer_info, nullptr,
|
||||
&framebuffer),
|
||||
"vkCreateFramebuffer");
|
||||
framebuffers_.push_back(framebuffer);
|
||||
}
|
||||
auto pool = VkInfo<VkCommandPoolCreateInfo>(
|
||||
VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO);
|
||||
pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
pool.queueFamilyIndex = vk_.queue_family;
|
||||
CheckVk(vk_.CreateCommandPool(vk_.device, &pool, nullptr, &pool_),
|
||||
"vkCreateCommandPool");
|
||||
auto allocate = VkInfo<VkCommandBufferAllocateInfo>(
|
||||
VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO);
|
||||
allocate.commandPool = pool_;
|
||||
allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocate.commandBufferCount = 1;
|
||||
CheckVk(vk_.AllocateCommandBuffers(vk_.device, &allocate, &command_),
|
||||
"vkAllocateCommandBuffers");
|
||||
auto fence = VkInfo<VkFenceCreateInfo>(VK_STRUCTURE_TYPE_FENCE_CREATE_INFO);
|
||||
CheckVk(vk_.CreateFence(vk_.device, &fence, nullptr, &fence_),
|
||||
"vkCreateFence");
|
||||
}
|
||||
|
||||
void Render(std::uint32_t image, std::uint64_t frame) {
|
||||
Require(image < framebuffers_.size(),
|
||||
"The runtime acquired an invalid swapchain image index");
|
||||
CheckVk(vk_.ResetCommandBuffer(command_, 0), "vkResetCommandBuffer");
|
||||
auto begin = VkInfo<VkCommandBufferBeginInfo>(
|
||||
VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO);
|
||||
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
CheckVk(vk_.BeginCommandBuffer(command_, &begin), "vkBeginCommandBuffer");
|
||||
VkClearValue background{};
|
||||
background.color = {{0.018f, 0.024f, 0.04f, 1.f}};
|
||||
auto pass =
|
||||
VkInfo<VkRenderPassBeginInfo>(VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO);
|
||||
pass.renderPass = render_pass_;
|
||||
pass.framebuffer = framebuffers_[image];
|
||||
pass.renderArea.extent = {width_, height_};
|
||||
pass.clearValueCount = 1;
|
||||
pass.pClearValues = &background;
|
||||
vk_.CmdBeginRenderPass(command_, &pass, VK_SUBPASS_CONTENTS_INLINE);
|
||||
auto rectangle = [&](std::uint32_t x, std::uint32_t y, std::uint32_t w,
|
||||
std::uint32_t h, VkClearColorValue color) {
|
||||
VkClearAttachment attachment{};
|
||||
attachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
attachment.clearValue.color = color;
|
||||
VkClearRect rect{
|
||||
{{static_cast<std::int32_t>(x), static_cast<std::int32_t>(y)},
|
||||
{w, h}},
|
||||
0,
|
||||
1};
|
||||
vk_.CmdClearAttachments(command_, 1, &attachment, 1, &rect);
|
||||
};
|
||||
for (std::uint32_t y = 0; y < 8; ++y)
|
||||
for (std::uint32_t x = 0; x < 8; ++x)
|
||||
if ((x + y) % 2 == 0)
|
||||
rectangle(x * width_ / 8, y * height_ / 8, width_ / 8, height_ / 8,
|
||||
x < 4 ? VkClearColorValue{{0.025f, 0.3f, 0.38f, 1.f}}
|
||||
: VkClearColorValue{{0.23f, 0.08f, 0.4f, 1.f}});
|
||||
rectangle(width_ / 2 - 1, height_ / 4, 2, height_ / 2,
|
||||
{{0.9f, 0.9f, 0.9f, 1.f}});
|
||||
rectangle(width_ / 4, height_ / 2 - 1, width_ / 2, 2,
|
||||
{{0.9f, 0.9f, 0.9f, 1.f}});
|
||||
rectangle(static_cast<std::uint32_t>(frame % (width_ - 16)), height_ - 12,
|
||||
16, 6, {{0.8f, 0.55f, 0.08f, 1.f}});
|
||||
vk_.CmdEndRenderPass(command_);
|
||||
CheckVk(vk_.EndCommandBuffer(command_), "vkEndCommandBuffer");
|
||||
CheckVk(vk_.ResetFences(vk_.device, 1, &fence_), "vkResetFences");
|
||||
auto submit = VkInfo<VkSubmitInfo>(VK_STRUCTURE_TYPE_SUBMIT_INFO);
|
||||
submit.commandBufferCount = 1;
|
||||
submit.pCommandBuffers = &command_;
|
||||
CheckVk(vk_.QueueSubmit(vk_.queue, 1, &submit, fence_), "vkQueueSubmit");
|
||||
// One frame in flight for the first prototype: all GPU writes complete
|
||||
// before release and command-buffer reuse. No runtime-owned image is freed.
|
||||
CheckVk(
|
||||
vk_.WaitForFences(vk_.device, 1, &fence_, VK_TRUE, 5'000'000'000ULL),
|
||||
"vkWaitForFences");
|
||||
}
|
||||
|
||||
void Frame(PreviewResult* report) {
|
||||
auto wait = XrInfo<XrFrameWaitInfo>(XR_TYPE_FRAME_WAIT_INFO);
|
||||
auto state = XrInfo<XrFrameState>(XR_TYPE_FRAME_STATE);
|
||||
Check(xrWaitFrame(session_, &wait, &state), "xrWaitFrame");
|
||||
auto begin = XrInfo<XrFrameBeginInfo>(XR_TYPE_FRAME_BEGIN_INFO);
|
||||
Check(xrBeginFrame(session_, &begin), "xrBeginFrame");
|
||||
auto end = XrInfo<XrFrameEndInfo>(XR_TYPE_FRAME_END_INFO);
|
||||
end.displayTime = state.predictedDisplayTime;
|
||||
end.environmentBlendMode = blend_;
|
||||
bool frame_open = true, image_waited = false;
|
||||
try {
|
||||
auto head = XrInfo<XrSpaceLocation>(XR_TYPE_SPACE_LOCATION);
|
||||
Check(xrLocateSpace(view_space_, local_space_, state.predictedDisplayTime,
|
||||
&head),
|
||||
"xrLocateSpace");
|
||||
constexpr auto valid = XR_SPACE_LOCATION_ORIENTATION_VALID_BIT |
|
||||
XR_SPACE_LOCATION_POSITION_VALID_BIT;
|
||||
const bool pose_valid = (head.locationFlags & valid) == valid;
|
||||
if (pose_valid) {
|
||||
++report->tracked_frames;
|
||||
report->head_position = {head.pose.position.x, head.pose.position.y,
|
||||
head.pose.position.z};
|
||||
report->head_orientation = {
|
||||
head.pose.orientation.x, head.pose.orientation.y,
|
||||
head.pose.orientation.z, head.pose.orientation.w};
|
||||
}
|
||||
auto layer =
|
||||
XrInfo<XrCompositionLayerQuad>(XR_TYPE_COMPOSITION_LAYER_QUAD);
|
||||
const XrCompositionLayerBaseHeader* layer_pointer =
|
||||
reinterpret_cast<const XrCompositionLayerBaseHeader*>(&layer);
|
||||
if (state.shouldRender && pose_valid) {
|
||||
auto acquire = XrInfo<XrSwapchainImageAcquireInfo>(
|
||||
XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO);
|
||||
std::uint32_t image = 0;
|
||||
Check(xrAcquireSwapchainImage(swapchain_, &acquire, &image),
|
||||
"xrAcquireSwapchainImage");
|
||||
auto image_wait =
|
||||
XrInfo<XrSwapchainImageWaitInfo>(XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO);
|
||||
image_wait.timeout = 50'000'000;
|
||||
const auto deadline = Clock::now() + std::chrono::seconds(5);
|
||||
while (true) {
|
||||
const auto result = xrWaitSwapchainImage(swapchain_, &image_wait);
|
||||
if (result != XR_TIMEOUT_EXPIRED) {
|
||||
Check(result, "xrWaitSwapchainImage");
|
||||
break;
|
||||
}
|
||||
Require(!interrupted && Clock::now() < deadline,
|
||||
"Timed out waiting for an OpenXR swapchain image");
|
||||
}
|
||||
image_waited = true;
|
||||
Render(image, report->frames);
|
||||
auto release = XrInfo<XrSwapchainImageReleaseInfo>(
|
||||
XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO);
|
||||
image_waited = false;
|
||||
Check(xrReleaseSwapchainImage(swapchain_, &release),
|
||||
"xrReleaseSwapchainImage");
|
||||
layer.space = local_space_;
|
||||
layer.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
layer.subImage.swapchain = swapchain_;
|
||||
layer.subImage.imageRect.extent = {static_cast<std::int32_t>(width_),
|
||||
static_cast<std::int32_t>(height_)};
|
||||
layer.pose.orientation.w = 1;
|
||||
layer.pose.position.z = -2;
|
||||
layer.size = {1.4f, 1.4f};
|
||||
end.layerCount = 1;
|
||||
end.layers = &layer_pointer;
|
||||
}
|
||||
frame_open = false;
|
||||
Check(xrEndFrame(session_, &end), "xrEndFrame");
|
||||
++report->frames;
|
||||
if (end.layerCount) ++report->rendered_frames;
|
||||
} catch (...) {
|
||||
if (image_waited) {
|
||||
if (vk_.DeviceWaitIdle) (void)vk_.DeviceWaitIdle(vk_.device);
|
||||
auto release = XrInfo<XrSwapchainImageReleaseInfo>(
|
||||
XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO);
|
||||
(void)xrReleaseSwapchainImage(swapchain_, &release);
|
||||
}
|
||||
if (frame_open) {
|
||||
end.layerCount = 0;
|
||||
end.layers = nullptr;
|
||||
(void)xrEndFrame(session_, &end);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
Vulkan vk_;
|
||||
XrInstance instance_ = XR_NULL_HANDLE;
|
||||
XrSystemId system_ = XR_NULL_SYSTEM_ID;
|
||||
XrSession session_ = XR_NULL_HANDLE;
|
||||
XrSpace local_space_ = XR_NULL_HANDLE, view_space_ = XR_NULL_HANDLE;
|
||||
XrSwapchain swapchain_ = XR_NULL_HANDLE;
|
||||
XrEnvironmentBlendMode blend_ = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
|
||||
std::uint32_t width_ = 512, height_ = 512;
|
||||
VkRenderPass render_pass_ = VK_NULL_HANDLE;
|
||||
VkCommandPool pool_ = VK_NULL_HANDLE;
|
||||
VkCommandBuffer command_ = VK_NULL_HANDLE;
|
||||
VkFence fence_ = VK_NULL_HANDLE;
|
||||
std::vector<VkImageView> image_views_;
|
||||
std::vector<VkFramebuffer> framebuffers_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
PreviewResult RunOpenXrPreview(const PreviewOptions& options) {
|
||||
PreviewResult result;
|
||||
SignalScope signals;
|
||||
try {
|
||||
Preview preview;
|
||||
preview.Initialize();
|
||||
std::cerr << "[VR] Experimental test scene. Roblox rendering is not "
|
||||
"connected. Ctrl+C exits.\n";
|
||||
preview.Run(options, &result);
|
||||
result.success = true;
|
||||
result.message = "VR preview closed";
|
||||
} catch (const std::exception& error) {
|
||||
result.message = error.what();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace mocktail::vr
|
||||
@@ -0,0 +1,209 @@
|
||||
#include "mocktail/vr/openxr_probe.h"
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_platform.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
namespace mocktail::vr {
|
||||
namespace {
|
||||
|
||||
std::string Version(XrVersion version) {
|
||||
return std::to_string(XR_VERSION_MAJOR(version)) + "." +
|
||||
std::to_string(XR_VERSION_MINOR(version)) + "." +
|
||||
std::to_string(XR_VERSION_PATCH(version));
|
||||
}
|
||||
|
||||
struct InstanceScope {
|
||||
XrInstance instance = XR_NULL_HANDLE;
|
||||
~InstanceScope() {
|
||||
if (instance != XR_NULL_HANDLE) (void)xrDestroyInstance(instance);
|
||||
}
|
||||
};
|
||||
|
||||
void Failure(ProbeReport* report, XrInstance instance, const char* operation,
|
||||
XrResult result) {
|
||||
report->status = ProbeStatus::kError;
|
||||
if (result == XR_ERROR_RUNTIME_UNAVAILABLE) {
|
||||
report->status = ProbeStatus::kRuntimeUnavailable;
|
||||
} else if (result == XR_ERROR_FORM_FACTOR_UNAVAILABLE) {
|
||||
report->status = ProbeStatus::kHeadsetUnavailable;
|
||||
} else if (result == XR_ERROR_FORM_FACTOR_UNSUPPORTED ||
|
||||
result == XR_ERROR_VIEW_CONFIGURATION_TYPE_UNSUPPORTED) {
|
||||
report->status = ProbeStatus::kIncompatibleRuntime;
|
||||
}
|
||||
char name[XR_MAX_RESULT_STRING_SIZE] = {};
|
||||
if (instance != XR_NULL_HANDLE)
|
||||
(void)xrResultToString(instance, result, name);
|
||||
report->message = std::string(operation) + " failed: " +
|
||||
(name[0] ? std::string(name) : std::to_string(result));
|
||||
if (report->status == ProbeStatus::kRuntimeUnavailable) {
|
||||
report->message += ". Start an OpenXR runtime such as WiVRn or Monado.";
|
||||
} else if (report->status == ProbeStatus::kHeadsetUnavailable) {
|
||||
report->message +=
|
||||
". Connect a headset, or use a simulated runtime for development.";
|
||||
}
|
||||
}
|
||||
|
||||
// Counts can change between the two enumeration calls. Retry a bounded number
|
||||
// of times and never consume an output array after XR_ERROR_SIZE_INSUFFICIENT.
|
||||
template <typename T, typename Enumerate>
|
||||
XrResult EnumerateProperties(XrStructureType type, Enumerate enumerate,
|
||||
std::vector<T>* values) {
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
std::uint32_t count = 0;
|
||||
XrResult result = enumerate(0, &count, nullptr);
|
||||
if (XR_FAILED(result)) return result;
|
||||
if (count > 4096) return XR_ERROR_LIMIT_REACHED;
|
||||
values->assign(count, T{});
|
||||
for (auto& value : *values) value.type = type;
|
||||
if (count == 0) return XR_SUCCESS;
|
||||
result = enumerate(count, &count, values->data());
|
||||
if (result == XR_ERROR_SIZE_INSUFFICIENT) continue;
|
||||
if (XR_FAILED(result)) return result;
|
||||
if (count > values->size()) return XR_ERROR_RUNTIME_FAILURE;
|
||||
values->resize(count);
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
return XR_ERROR_SIZE_INSUFFICIENT;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProbeReport ProbeOpenXr() {
|
||||
ProbeReport report;
|
||||
report.sdk_version = Version(XR_CURRENT_API_VERSION);
|
||||
std::vector<XrExtensionProperties> extensions;
|
||||
XrResult result = EnumerateProperties<XrExtensionProperties>(
|
||||
XR_TYPE_EXTENSION_PROPERTIES,
|
||||
[](std::uint32_t capacity, std::uint32_t* count,
|
||||
XrExtensionProperties* data) {
|
||||
return xrEnumerateInstanceExtensionProperties(nullptr, capacity, count,
|
||||
data);
|
||||
},
|
||||
&extensions);
|
||||
if (XR_FAILED(result)) {
|
||||
Failure(&report, XR_NULL_HANDLE, "xrEnumerateInstanceExtensionProperties",
|
||||
result);
|
||||
return report;
|
||||
}
|
||||
for (const auto& extension : extensions) {
|
||||
report.extensions.emplace_back(extension.extensionName);
|
||||
}
|
||||
const bool vulkan =
|
||||
std::find(report.extensions.begin(), report.extensions.end(),
|
||||
XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME) !=
|
||||
report.extensions.end();
|
||||
const char* enabled_extension = XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME;
|
||||
XrInstanceCreateInfo create_info{};
|
||||
create_info.type = XR_TYPE_INSTANCE_CREATE_INFO;
|
||||
std::strcpy(create_info.applicationInfo.applicationName, "Mocktail VR probe");
|
||||
std::strcpy(create_info.applicationInfo.engineName, "Mocktail");
|
||||
// Use the latest SDK while requiring only the core API this probe actually
|
||||
// uses, so runtimes exposing OpenXR 1.0 can also be diagnosed.
|
||||
create_info.applicationInfo.apiVersion = XR_MAKE_VERSION(1, 0, 0);
|
||||
create_info.enabledExtensionCount = vulkan ? 1 : 0;
|
||||
create_info.enabledExtensionNames = vulkan ? &enabled_extension : nullptr;
|
||||
InstanceScope scope;
|
||||
result = xrCreateInstance(&create_info, &scope.instance);
|
||||
if (XR_FAILED(result)) {
|
||||
Failure(&report, XR_NULL_HANDLE, "xrCreateInstance", result);
|
||||
return report;
|
||||
}
|
||||
XrInstanceProperties properties{};
|
||||
properties.type = XR_TYPE_INSTANCE_PROPERTIES;
|
||||
result = xrGetInstanceProperties(scope.instance, &properties);
|
||||
if (XR_FAILED(result)) {
|
||||
Failure(&report, scope.instance, "xrGetInstanceProperties", result);
|
||||
return report;
|
||||
}
|
||||
report.runtime_name = properties.runtimeName;
|
||||
report.runtime_version = Version(properties.runtimeVersion);
|
||||
|
||||
XrSystemGetInfo system_info{};
|
||||
system_info.type = XR_TYPE_SYSTEM_GET_INFO;
|
||||
system_info.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
|
||||
XrSystemId system = XR_NULL_SYSTEM_ID;
|
||||
result = xrGetSystem(scope.instance, &system_info, &system);
|
||||
if (XR_FAILED(result)) {
|
||||
Failure(&report, scope.instance, "xrGetSystem", result);
|
||||
return report;
|
||||
}
|
||||
XrSystemProperties system_properties{};
|
||||
system_properties.type = XR_TYPE_SYSTEM_PROPERTIES;
|
||||
result = xrGetSystemProperties(scope.instance, system, &system_properties);
|
||||
if (XR_FAILED(result)) {
|
||||
Failure(&report, scope.instance, "xrGetSystemProperties", result);
|
||||
return report;
|
||||
}
|
||||
report.system_name = system_properties.systemName;
|
||||
report.orientation_tracking =
|
||||
system_properties.trackingProperties.orientationTracking == XR_TRUE;
|
||||
report.position_tracking =
|
||||
system_properties.trackingProperties.positionTracking == XR_TRUE;
|
||||
if (!vulkan) {
|
||||
report.status = ProbeStatus::kIncompatibleRuntime;
|
||||
report.message = "The runtime does not expose XR_KHR_vulkan_enable2.";
|
||||
return report;
|
||||
}
|
||||
|
||||
std::vector<XrViewConfigurationView> views;
|
||||
result = EnumerateProperties<XrViewConfigurationView>(
|
||||
XR_TYPE_VIEW_CONFIGURATION_VIEW,
|
||||
[&](std::uint32_t capacity, std::uint32_t* count,
|
||||
XrViewConfigurationView* data) {
|
||||
return xrEnumerateViewConfigurationViews(
|
||||
scope.instance, system, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
|
||||
capacity, count, data);
|
||||
},
|
||||
&views);
|
||||
if (XR_FAILED(result)) {
|
||||
Failure(&report, scope.instance, "xrEnumerateViewConfigurationViews",
|
||||
result);
|
||||
return report;
|
||||
}
|
||||
for (const auto& view : views) {
|
||||
report.views.push_back({view.recommendedImageRectWidth,
|
||||
view.recommendedImageRectHeight,
|
||||
view.recommendedSwapchainSampleCount});
|
||||
}
|
||||
if (views.size() != 2 || std::any_of(report.views.begin(), report.views.end(),
|
||||
[](const ViewConfiguration& view) {
|
||||
return !view.width || !view.height ||
|
||||
!view.samples;
|
||||
})) {
|
||||
report.status = ProbeStatus::kIncompatibleRuntime;
|
||||
report.message = "The runtime did not provide two usable stereo views.";
|
||||
return report;
|
||||
}
|
||||
|
||||
PFN_xrVoidFunction function = nullptr;
|
||||
result = xrGetInstanceProcAddr(
|
||||
scope.instance, "xrGetVulkanGraphicsRequirements2KHR", &function);
|
||||
if (XR_FAILED(result) || function == nullptr) {
|
||||
Failure(&report, scope.instance,
|
||||
"xrGetVulkanGraphicsRequirements2KHR lookup",
|
||||
XR_FAILED(result) ? result : XR_ERROR_FUNCTION_UNSUPPORTED);
|
||||
return report;
|
||||
}
|
||||
XrGraphicsRequirementsVulkan2KHR requirements{};
|
||||
requirements.type = XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN2_KHR;
|
||||
result = reinterpret_cast<PFN_xrGetVulkanGraphicsRequirements2KHR>(function)(
|
||||
scope.instance, system, &requirements);
|
||||
if (XR_FAILED(result)) {
|
||||
Failure(&report, scope.instance, "xrGetVulkanGraphicsRequirements2KHR",
|
||||
result);
|
||||
return report;
|
||||
}
|
||||
report.minimum_vulkan_version = Version(requirements.minApiVersionSupported);
|
||||
report.maximum_vulkan_version = Version(requirements.maxApiVersionSupported);
|
||||
report.status = ProbeStatus::kReady;
|
||||
report.message =
|
||||
"OpenXR HMD and Vulkan requirements discovered successfully.";
|
||||
return report;
|
||||
}
|
||||
|
||||
} // namespace mocktail::vr
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "mocktail/vr/openxr_probe.h"
|
||||
#include "mocktail/vr/openxr_preview.h"
|
||||
|
||||
#include <charconv>
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (argc == 2 && (std::string_view(argv[1]) == "--help" ||
|
||||
std::string_view(argv[1]) == "-h")) {
|
||||
std::cout
|
||||
<< "Usage: mocktail-vr-probe [--scene [--frames N]]\n"
|
||||
"Inspect the host OpenXR runtime and HMD capabilities.\n"
|
||||
"Uses the active runtime, or XR_RUNTIME_JSON if set.\n"
|
||||
"Without arguments: discovery only, no XR session.\n"
|
||||
"--scene: render a tracked test panel in a real Vulkan/XR session.\n"
|
||||
"--frames N: request exit after N frames (positive integer).\n"
|
||||
"Ctrl+C or the runtime closes the scene. Does not load Roblox.\n"
|
||||
"Exit codes: 0=discovery ready, 1=API error, 2=build disabled,\n"
|
||||
"3=runtime unavailable, 4=HMD unavailable, 5=incompatible "
|
||||
"runtime.\n";
|
||||
return 0;
|
||||
}
|
||||
if (argc >= 2 && std::string_view(argv[1]) == "--scene") {
|
||||
mocktail::vr::PreviewOptions options;
|
||||
if (argc == 4 && std::string_view(argv[2]) == "--frames") {
|
||||
const std::string_view value(argv[3]);
|
||||
const auto parsed = std::from_chars(
|
||||
value.data(), value.data() + value.size(), options.frame_limit);
|
||||
if (parsed.ec != std::errc() ||
|
||||
parsed.ptr != value.data() + value.size() || !options.frame_limit) {
|
||||
std::cerr << "--frames requires a positive integer.\n";
|
||||
return 1;
|
||||
}
|
||||
} else if (argc != 2) {
|
||||
std::cerr << "Use --scene [--frames N].\n";
|
||||
return 1;
|
||||
}
|
||||
const auto result = mocktail::vr::RunOpenXrPreview(options);
|
||||
std::cout << mocktail::vr::FormatPreviewResult(result);
|
||||
return result.success ? 0 : 1;
|
||||
}
|
||||
if (argc != 1) {
|
||||
std::cerr << "Use mocktail-vr-probe --help for supported options.\n";
|
||||
return 1;
|
||||
}
|
||||
const auto report = mocktail::vr::ProbeOpenXr();
|
||||
std::cout << mocktail::vr::FormatProbeReport(report);
|
||||
return mocktail::vr::ProbeExitCode(report.status);
|
||||
}
|
||||
@@ -2,6 +2,34 @@
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License")
|
||||
|
||||
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
||||
add_test(NAME OpenXrSimulatorLauncher
|
||||
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/vr/simulator_launcher_test.py"
|
||||
"${CMAKE_BINARY_DIR}/mocktail-vr-simulated")
|
||||
if(MOCKTAIL_ENABLE_VR)
|
||||
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
|
||||
OpenXR::headers Vulkan::Headers)
|
||||
target_compile_definitions(mocktail_test_openxr_runtime PRIVATE
|
||||
XR_USE_GRAPHICS_API_VULKAN)
|
||||
set_target_properties(mocktail_test_openxr_runtime PROPERTIES
|
||||
CXX_VISIBILITY_PRESET hidden)
|
||||
mocktail_apply_compile_options(mocktail_test_openxr_runtime)
|
||||
add_test(NAME OpenXrProbeContract
|
||||
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/vr/probe_contract_test.py"
|
||||
"$<TARGET_FILE:mocktail_vr_probe>" "$<TARGET_FILE:mocktail_test_openxr_runtime>")
|
||||
add_test(NAME OpenXrPreviewContract
|
||||
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/vr/preview_contract_test.py"
|
||||
"$<TARGET_FILE:mocktail_vr_probe>" "$<TARGET_FILE:mocktail_test_openxr_runtime>"
|
||||
"$<TARGET_FILE:mocktail>")
|
||||
set_tests_properties(OpenXrPreviewContract PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 90)
|
||||
else()
|
||||
add_test(NAME OpenXrProbeDisabled
|
||||
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/vr/probe_contract_test.py"
|
||||
"$<TARGET_FILE:mocktail_vr_probe>" --disabled "$<TARGET_FILE:mocktail>")
|
||||
endif()
|
||||
|
||||
add_executable(jnivm_test
|
||||
jnivm_test.cc
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -11,6 +12,52 @@ namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
|
||||
TEST(CommandLineTest, VrOverrideSurvivesReexecAndCanDisableEnvironmentDefault) {
|
||||
const std::array<const char*, 4> arguments = {"mocktail", "--vr",
|
||||
"--headless", "--no-vr"};
|
||||
const auto parsed = ParseCommandLine(arguments.size(), arguments.data());
|
||||
ASSERT_TRUE(parsed) << parsed.error;
|
||||
ASSERT_TRUE(parsed.options.vr_enabled.has_value());
|
||||
EXPECT_FALSE(*parsed.options.vr_enabled);
|
||||
std::vector<std::string> reexec;
|
||||
std::string error;
|
||||
ASSERT_TRUE(BuildCommandLineReexecArguments(
|
||||
parsed.options, arguments.size(), arguments.data(), &reexec, &error))
|
||||
<< error;
|
||||
EXPECT_EQ(reexec,
|
||||
(std::vector<std::string>{"--vr", "--headless", "--no-vr"}));
|
||||
|
||||
const char* previous = std::getenv("MOCKTAIL_VR_ENABLED");
|
||||
const std::optional<std::string> saved =
|
||||
previous ? std::optional<std::string>(previous) : std::nullopt;
|
||||
const char* previous_headless = std::getenv("MOCKTAIL_HEADLESS");
|
||||
const std::optional<std::string> saved_headless =
|
||||
previous_headless ? std::optional<std::string>(previous_headless)
|
||||
: std::nullopt;
|
||||
setenv("MOCKTAIL_VR_ENABLED", "true", 1);
|
||||
EXPECT_TRUE(ApplyCommandLineEnvironment(parsed.options, &error)) << error;
|
||||
EXPECT_STREQ(std::getenv("MOCKTAIL_VR_ENABLED"), "0");
|
||||
if (saved)
|
||||
setenv("MOCKTAIL_VR_ENABLED", saved->c_str(), 1);
|
||||
else
|
||||
unsetenv("MOCKTAIL_VR_ENABLED");
|
||||
if (saved_headless)
|
||||
setenv("MOCKTAIL_HEADLESS", saved_headless->c_str(), 1);
|
||||
else
|
||||
unsetenv("MOCKTAIL_HEADLESS");
|
||||
}
|
||||
|
||||
TEST(CommandLineTest, ParsesExplicitVrAndKeepsUnspecifiedDistinctFromFalse) {
|
||||
const char* enabled[] = {"mocktail", "--vr"};
|
||||
const auto parsed = ParseCommandLine(2, enabled);
|
||||
ASSERT_TRUE(parsed) << parsed.error;
|
||||
EXPECT_EQ(parsed.options.vr_enabled, std::optional<bool>(true));
|
||||
const char* normal[] = {"mocktail"};
|
||||
EXPECT_FALSE(ParseCommandLine(1, normal).options.vr_enabled.has_value());
|
||||
const char* incompatible[] = {"mocktail", "--force-run-latest", "--vr"};
|
||||
EXPECT_FALSE(ParseCommandLine(3, incompatible));
|
||||
}
|
||||
|
||||
TEST(CommandLineTest, ParsesCompleteRunContractOnce) {
|
||||
const std::array<const char*, 11> arguments = {
|
||||
"mocktail",
|
||||
|
||||
@@ -99,6 +99,7 @@ TEST(RuntimeConfigBootstrapTest, CreatesCompletePrivateFirstRunFile) {
|
||||
LoadRuntimeConfig(MapEnvironment(), file);
|
||||
ASSERT_TRUE(loaded) << loaded.error;
|
||||
EXPECT_TRUE(loaded.file_loaded);
|
||||
EXPECT_FALSE(loaded.config.vr_enabled());
|
||||
EXPECT_EQ(loaded.config.graphics_backend(), GraphicsBackend::kVulkan);
|
||||
EXPECT_EQ(loaded.config.theme_mode(), "roblox");
|
||||
EXPECT_EQ(loaded.config.frame_rate().mode, FrameRateLimitMode::kUnmanaged);
|
||||
@@ -115,6 +116,58 @@ TEST(RuntimeConfigBootstrapTest, CreatesCompletePrivateFirstRunFile) {
|
||||
EXPECT_EQ(loaded.config.device_profile().name, "pc-windows-11");
|
||||
}
|
||||
|
||||
TEST(RuntimeConfigFileTest,
|
||||
VrEnvironmentOverridesYamlIncludingExplicitDisable) {
|
||||
TemporaryDirectory temporary;
|
||||
const auto file = temporary.Write("version: 1\nvr:\n enabled: true\n");
|
||||
const auto enabled = LoadRuntimeConfig(MapEnvironment(), file);
|
||||
ASSERT_TRUE(enabled) << enabled.error;
|
||||
EXPECT_TRUE(enabled.config.vr_enabled());
|
||||
const auto disabled =
|
||||
LoadRuntimeConfig(MapEnvironment({{"MOCKTAIL_VR_ENABLED", "0"}}), file);
|
||||
ASSERT_TRUE(disabled) << disabled.error;
|
||||
EXPECT_FALSE(disabled.config.vr_enabled());
|
||||
const auto invalid = LoadRuntimeConfig(
|
||||
MapEnvironment({{"MOCKTAIL_VR_ENABLED", "maybe"}}), file);
|
||||
EXPECT_FALSE(invalid);
|
||||
EXPECT_NE(invalid.error.find("VR configuration"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(RuntimeConfigFileTest, RejectsMalformedVrSectionAndUnsupportedKeys) {
|
||||
TemporaryDirectory temporary;
|
||||
for (const auto* yaml :
|
||||
{"vr: true\n", "vr:\n enabled: perhaps\n", "vr:\n enabled: [true]\n",
|
||||
"vr:\n enable: true\n", "vr:\n enabled: true\n enabled: false\n"}) {
|
||||
const auto result =
|
||||
LoadRuntimeConfig(MapEnvironment(), temporary.Write(yaml));
|
||||
EXPECT_FALSE(result) << yaml;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RuntimeConfigFileTest, ExportsResolvedVrStateAndRejectsInvalidState) {
|
||||
const char* previous = std::getenv("MOCKTAIL_VR_ENABLED");
|
||||
const std::optional<std::string> saved =
|
||||
previous ? std::optional<std::string>(previous) : std::nullopt;
|
||||
std::string error;
|
||||
const auto enabled = RuntimeConfig::FromEnvironment(
|
||||
MapEnvironment({{"MOCKTAIL_VR_ENABLED", "on"}}));
|
||||
EXPECT_TRUE(ExportRuntimeConfigEnvironment(enabled, &error)) << error;
|
||||
EXPECT_STREQ(std::getenv("MOCKTAIL_VR_ENABLED"), "1");
|
||||
EXPECT_TRUE(ExportRuntimeConfigEnvironment(
|
||||
RuntimeConfig::FromEnvironment(MapEnvironment()), &error))
|
||||
<< error;
|
||||
EXPECT_STREQ(std::getenv("MOCKTAIL_VR_ENABLED"), "0");
|
||||
EXPECT_FALSE(ExportRuntimeConfigEnvironment(
|
||||
RuntimeConfig::FromEnvironment(
|
||||
MapEnvironment({{"MOCKTAIL_VR_ENABLED", "invalid"}})),
|
||||
&error));
|
||||
EXPECT_STREQ(std::getenv("MOCKTAIL_VR_ENABLED"), "0");
|
||||
if (saved)
|
||||
setenv("MOCKTAIL_VR_ENABLED", saved->c_str(), 1);
|
||||
else
|
||||
unsetenv("MOCKTAIL_VR_ENABLED");
|
||||
}
|
||||
|
||||
TEST(RuntimeConfigBootstrapTest,
|
||||
MatchesShippedExampleAndDocumentsEveryDefault) {
|
||||
const std::filesystem::path example =
|
||||
|
||||
@@ -37,6 +37,8 @@ TEST(RuntimeConfigTest, UsesSupportedDefaults) {
|
||||
const RuntimeConfig config = RuntimeConfig::FromEnvironment(environment);
|
||||
|
||||
EXPECT_FALSE(config.headless());
|
||||
EXPECT_FALSE(config.vr_enabled());
|
||||
EXPECT_TRUE(config.vr_valid());
|
||||
EXPECT_EQ(config.roblox_library_path(), "rbx_bin/libroblox.so");
|
||||
EXPECT_EQ(config.graphics_backend(), GraphicsBackend::kVulkan);
|
||||
EXPECT_EQ(config.graphics_backend_name(), "direct-vulkan");
|
||||
@@ -89,9 +91,17 @@ TEST(RuntimeConfigTest, UsesSupportedDefaults) {
|
||||
EXPECT_TRUE(config.unsafe_detached_thread_overrides().empty());
|
||||
}
|
||||
|
||||
TEST(RuntimeConfigTest, RejectsMalformedVrEnvironmentInsteadOfEnablingIt) {
|
||||
const auto config = RuntimeConfig::FromEnvironment(
|
||||
MapEnvironment({{"MOCKTAIL_VR_ENABLED", "maybe"}}));
|
||||
EXPECT_FALSE(config.vr_valid());
|
||||
EXPECT_FALSE(config.vr_enabled());
|
||||
}
|
||||
|
||||
TEST(RuntimeConfigTest, ReadsTypedRuntimeValues) {
|
||||
const MapEnvironment environment({
|
||||
{"MOCKTAIL_HEADLESS", "1"},
|
||||
{"MOCKTAIL_VR_ENABLED", "true"},
|
||||
{"ROBLOX_LIB_PATH", "/tmp/libroblox.so"},
|
||||
{"MOCKTAIL_GRAPHICS_BACKEND", "vulkan"},
|
||||
{"MOCKTAIL_WIN_WIDTH", "1920"},
|
||||
@@ -122,6 +132,8 @@ TEST(RuntimeConfigTest, ReadsTypedRuntimeValues) {
|
||||
const RuntimeConfig config = RuntimeConfig::FromEnvironment(environment);
|
||||
|
||||
EXPECT_TRUE(config.headless());
|
||||
EXPECT_TRUE(config.vr_enabled());
|
||||
EXPECT_TRUE(config.vr_valid());
|
||||
EXPECT_EQ(config.roblox_library_path(), "/tmp/libroblox.so");
|
||||
EXPECT_EQ(config.graphics_backend(), GraphicsBackend::kVulkan);
|
||||
EXPECT_EQ(config.window().width, 1920);
|
||||
|
||||
@@ -0,0 +1,508 @@
|
||||
// Session contract fixture. Vulkan devices, images, rendering and readback are
|
||||
// real; OpenXR scheduling/tracking are scripted to exercise error paths.
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_platform.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
bool Scenario(const char* name) {
|
||||
const char* value = std::getenv("MOCKTAIL_TEST_XR_SCENARIO");
|
||||
return value && std::strcmp(value, name) == 0;
|
||||
}
|
||||
|
||||
void Trace(const char* event) {
|
||||
if (const char* path = std::getenv("MOCKTAIL_TEST_XR_TRACE")) {
|
||||
if (auto* file = std::fopen(path, "a")) {
|
||||
std::fprintf(file, "%s\n", event);
|
||||
std::fclose(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#define REQUIRE(value) \
|
||||
if (!(value)) { \
|
||||
Trace("contract-error: " #value); \
|
||||
return XR_ERROR_VALIDATION_FAILURE; \
|
||||
}
|
||||
|
||||
struct Fixture {
|
||||
PFN_vkGetInstanceProcAddr get_instance_proc = nullptr;
|
||||
PFN_vkGetDeviceProcAddr get_device_proc = nullptr;
|
||||
VkInstance instance = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice physical = VK_NULL_HANDLE;
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
VkQueue queue = VK_NULL_HANDLE;
|
||||
std::uint32_t queue_family = 0;
|
||||
std::array<VkImage, 3> images{};
|
||||
std::array<VkDeviceMemory, 3> memory{};
|
||||
std::uint32_t width = 0, height = 0, index = 0, frames = 0;
|
||||
bool running = false, begun = false, acquired = false, waited = false;
|
||||
bool timeout_returned = false, pixels_verified = false;
|
||||
int local = 0, view = 0, swapchain = 0;
|
||||
XrSessionState state = XR_SESSION_STATE_IDLE;
|
||||
std::deque<XrSessionState> events;
|
||||
} fixture;
|
||||
|
||||
XrSession Session() { return reinterpret_cast<XrSession>(&fixture); }
|
||||
XrSwapchain Swapchain() {
|
||||
return reinterpret_cast<XrSwapchain>(&fixture.swapchain);
|
||||
}
|
||||
XrSpace Local() { return reinterpret_cast<XrSpace>(&fixture.local); }
|
||||
XrSpace View() { return reinterpret_cast<XrSpace>(&fixture.view); }
|
||||
|
||||
template <typename T>
|
||||
T Device(const char* name) {
|
||||
return reinterpret_cast<T>(fixture.get_device_proc(fixture.device, name));
|
||||
}
|
||||
#define VK(name, ...) Device<PFN_vk##name>("vk" #name)(__VA_ARGS__)
|
||||
|
||||
std::uint32_t MemoryType(std::uint32_t bits, VkMemoryPropertyFlags flags) {
|
||||
VkPhysicalDeviceMemoryProperties memory{};
|
||||
reinterpret_cast<PFN_vkGetPhysicalDeviceMemoryProperties>(
|
||||
fixture.get_instance_proc(fixture.instance,
|
||||
"vkGetPhysicalDeviceMemoryProperties"))(
|
||||
fixture.physical, &memory);
|
||||
for (std::uint32_t i = 0; i < memory.memoryTypeCount; ++i)
|
||||
if ((bits & (1u << i)) &&
|
||||
(memory.memoryTypes[i].propertyFlags & flags) == flags)
|
||||
return i;
|
||||
return UINT32_MAX;
|
||||
}
|
||||
|
||||
// Independently inspect submitted pixels, so a successful xrEndFrame alone
|
||||
// cannot make an empty or unrendered texture pass the integration test.
|
||||
bool VerifyPixels() {
|
||||
struct Resources {
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
VkCommandPool pool = VK_NULL_HANDLE;
|
||||
~Resources() {
|
||||
VK(DeviceWaitIdle, fixture.device);
|
||||
if (pool) VK(DestroyCommandPool, fixture.device, pool, nullptr);
|
||||
if (buffer) VK(DestroyBuffer, fixture.device, buffer, nullptr);
|
||||
if (memory) VK(FreeMemory, fixture.device, memory, nullptr);
|
||||
}
|
||||
} resources;
|
||||
VkBufferCreateInfo buffer{};
|
||||
buffer.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
||||
buffer.size = fixture.width * fixture.height * 4;
|
||||
buffer.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
if (VK(CreateBuffer, fixture.device, &buffer, nullptr, &resources.buffer) !=
|
||||
VK_SUCCESS)
|
||||
return false;
|
||||
VkMemoryRequirements requirements{};
|
||||
VK(GetBufferMemoryRequirements, fixture.device, resources.buffer,
|
||||
&requirements);
|
||||
VkMemoryAllocateInfo allocate{};
|
||||
allocate.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
||||
allocate.allocationSize = requirements.size;
|
||||
allocate.memoryTypeIndex = MemoryType(
|
||||
requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
|
||||
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||||
if (allocate.memoryTypeIndex == UINT32_MAX ||
|
||||
VK(AllocateMemory, fixture.device, &allocate, nullptr,
|
||||
&resources.memory) != VK_SUCCESS ||
|
||||
VK(BindBufferMemory, fixture.device, resources.buffer, resources.memory,
|
||||
0) != VK_SUCCESS)
|
||||
return false;
|
||||
VkCommandPoolCreateInfo pool{};
|
||||
pool.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
|
||||
pool.queueFamilyIndex = fixture.queue_family;
|
||||
if (VK(CreateCommandPool, fixture.device, &pool, nullptr, &resources.pool) !=
|
||||
VK_SUCCESS)
|
||||
return false;
|
||||
VkCommandBufferAllocateInfo commands{};
|
||||
commands.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||
commands.commandPool = resources.pool;
|
||||
commands.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
commands.commandBufferCount = 1;
|
||||
VkCommandBuffer command = VK_NULL_HANDLE;
|
||||
if (VK(AllocateCommandBuffers, fixture.device, &commands, &command) !=
|
||||
VK_SUCCESS)
|
||||
return false;
|
||||
VkCommandBufferBeginInfo begin{};
|
||||
begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
if (VK(BeginCommandBuffer, command, &begin) != VK_SUCCESS) return false;
|
||||
VkImageMemoryBarrier barrier{};
|
||||
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||
barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex =
|
||||
VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.image = fixture.images[fixture.index];
|
||||
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
VK(CmdPipelineBarrier, command, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
VkBufferImageCopy copy{};
|
||||
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
copy.imageExtent = {fixture.width, fixture.height, 1};
|
||||
VK(CmdCopyImageToBuffer, command, barrier.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, resources.buffer, 1, ©);
|
||||
barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||
barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
VK(CmdPipelineBarrier, command, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr,
|
||||
1, &barrier);
|
||||
if (VK(EndCommandBuffer, command) != VK_SUCCESS) return false;
|
||||
VkSubmitInfo submit{};
|
||||
submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
||||
submit.commandBufferCount = 1;
|
||||
submit.pCommandBuffers = &command;
|
||||
if (VK(QueueSubmit, fixture.queue, 1, &submit, VK_NULL_HANDLE) !=
|
||||
VK_SUCCESS ||
|
||||
VK(QueueWaitIdle, fixture.queue) != VK_SUCCESS)
|
||||
return false;
|
||||
void* mapped = nullptr;
|
||||
if (VK(MapMemory, fixture.device, resources.memory, 0, VK_WHOLE_SIZE, 0,
|
||||
&mapped) != VK_SUCCESS)
|
||||
return false;
|
||||
const auto* bytes = static_cast<const unsigned char*>(mapped);
|
||||
const auto* cyan =
|
||||
bytes + (fixture.height / 16 * fixture.width + fixture.width / 16) * 4;
|
||||
const auto* purple = cyan + fixture.width / 2 * 4;
|
||||
const bool valid = cyan[1] > 60 && cyan[2] > 80 && cyan[0] < 15 &&
|
||||
purple[0] > 40 && purple[1] < 35 && purple[2] > 80;
|
||||
VK(UnmapMemory, fixture.device, resources.memory);
|
||||
if (valid) Trace("pixels-verified");
|
||||
return valid;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL
|
||||
CreateVulkanInstance(XrInstance, const XrVulkanInstanceCreateInfoKHR* info,
|
||||
VkInstance* instance, VkResult* result) {
|
||||
fixture.get_instance_proc = info->pfnGetInstanceProcAddr;
|
||||
*result = reinterpret_cast<PFN_vkCreateInstance>(
|
||||
fixture.get_instance_proc(VK_NULL_HANDLE, "vkCreateInstance"))(
|
||||
info->vulkanCreateInfo, info->vulkanAllocator, instance);
|
||||
if (*result == VK_SUCCESS) {
|
||||
fixture.instance = *instance;
|
||||
fixture.get_device_proc = reinterpret_cast<PFN_vkGetDeviceProcAddr>(
|
||||
fixture.get_instance_proc(*instance, "vkGetDeviceProcAddr"));
|
||||
}
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL
|
||||
GetVulkanDevice(XrInstance, const XrVulkanGraphicsDeviceGetInfoKHR* info,
|
||||
VkPhysicalDevice* device) {
|
||||
const auto enumerate = reinterpret_cast<PFN_vkEnumeratePhysicalDevices>(
|
||||
fixture.get_instance_proc(info->vulkanInstance,
|
||||
"vkEnumeratePhysicalDevices"));
|
||||
std::uint32_t count = 0;
|
||||
if (enumerate(info->vulkanInstance, &count, nullptr) != VK_SUCCESS || !count)
|
||||
return XR_ERROR_GRAPHICS_DEVICE_INVALID;
|
||||
std::vector<VkPhysicalDevice> devices(count);
|
||||
if (enumerate(info->vulkanInstance, &count, devices.data()) != VK_SUCCESS)
|
||||
return XR_ERROR_RUNTIME_FAILURE;
|
||||
*device = fixture.physical = devices.front();
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL CreateVulkanDevice(XrInstance,
|
||||
const XrVulkanDeviceCreateInfoKHR* info,
|
||||
VkDevice* device, VkResult* result) {
|
||||
if (Scenario("device_failure")) {
|
||||
*result = VK_ERROR_FEATURE_NOT_PRESENT;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
*result = reinterpret_cast<PFN_vkCreateDevice>(fixture.get_instance_proc(
|
||||
fixture.instance, "vkCreateDevice"))(info->vulkanPhysicalDevice,
|
||||
info->vulkanCreateInfo,
|
||||
info->vulkanAllocator, device);
|
||||
if (*result == VK_SUCCESS) fixture.device = *device;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL EnumerateBlends(XrInstance, XrSystemId,
|
||||
XrViewConfigurationType,
|
||||
std::uint32_t capacity,
|
||||
std::uint32_t* count,
|
||||
XrEnvironmentBlendMode* blends) {
|
||||
*count = 1;
|
||||
if (capacity) blends[0] = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL CreateSession(XrInstance, const XrSessionCreateInfo* info,
|
||||
XrSession* session) {
|
||||
if (Scenario("session_failure")) return XR_ERROR_INITIALIZATION_FAILED;
|
||||
const auto* binding =
|
||||
static_cast<const XrGraphicsBindingVulkan2KHR*>(info->next);
|
||||
REQUIRE(binding && binding->type == XR_TYPE_GRAPHICS_BINDING_VULKAN2_KHR);
|
||||
REQUIRE(binding->device == fixture.device &&
|
||||
binding->physicalDevice == fixture.physical);
|
||||
fixture.queue_family = binding->queueFamilyIndex;
|
||||
VK(GetDeviceQueue, fixture.device, binding->queueFamilyIndex,
|
||||
binding->queueIndex, &fixture.queue);
|
||||
*session = Session();
|
||||
fixture.events.push_back(XR_SESSION_STATE_READY);
|
||||
Trace("session-create");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL DestroySession(XrSession) {
|
||||
REQUIRE(fixture.images[0] == VK_NULL_HANDLE);
|
||||
Trace("session-destroy");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL PollEvent(XrInstance, XrEventDataBuffer* buffer) {
|
||||
if (fixture.events.empty()) return XR_EVENT_UNAVAILABLE;
|
||||
XrEventDataSessionStateChanged event{};
|
||||
event.type = XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED;
|
||||
event.session = Session();
|
||||
event.state = fixture.state = fixture.events.front();
|
||||
fixture.events.pop_front();
|
||||
std::memcpy(buffer, &event, sizeof(event));
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL BeginSession(XrSession, const XrSessionBeginInfo* begin) {
|
||||
REQUIRE(fixture.state == XR_SESSION_STATE_READY && !fixture.running);
|
||||
REQUIRE(begin->primaryViewConfigurationType ==
|
||||
XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO);
|
||||
fixture.running = true;
|
||||
fixture.events.push_back(XR_SESSION_STATE_FOCUSED);
|
||||
Trace("session-begin");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL RequestExit(XrSession) {
|
||||
REQUIRE(fixture.running);
|
||||
fixture.events.push_back(XR_SESSION_STATE_STOPPING);
|
||||
Trace("session-exit-request");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL EndSession(XrSession) {
|
||||
REQUIRE(fixture.state == XR_SESSION_STATE_STOPPING && fixture.running);
|
||||
fixture.running = false;
|
||||
Trace("session-end");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL CreateSpace(XrSession,
|
||||
const XrReferenceSpaceCreateInfo* info,
|
||||
XrSpace* space) {
|
||||
REQUIRE(info->poseInReferenceSpace.orientation.w == 1);
|
||||
REQUIRE(info->referenceSpaceType == XR_REFERENCE_SPACE_TYPE_LOCAL ||
|
||||
info->referenceSpaceType == XR_REFERENCE_SPACE_TYPE_VIEW);
|
||||
*space = info->referenceSpaceType == XR_REFERENCE_SPACE_TYPE_LOCAL ? Local()
|
||||
: View();
|
||||
Trace("space-create");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL DestroySpace(XrSpace) {
|
||||
Trace("space-destroy");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL LocateSpace(XrSpace space, XrSpace base, XrTime,
|
||||
XrSpaceLocation* location) {
|
||||
REQUIRE(space == View() && base == Local());
|
||||
location->locationFlags = Scenario("invalid_tracking") && fixture.frames == 0
|
||||
? 0
|
||||
: XR_SPACE_LOCATION_ORIENTATION_VALID_BIT |
|
||||
XR_SPACE_LOCATION_POSITION_VALID_BIT;
|
||||
location->pose.orientation.w = 1;
|
||||
location->pose.position.x = static_cast<float>(fixture.frames) / 10;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL EnumerateFormats(XrSession, std::uint32_t capacity,
|
||||
std::uint32_t* count,
|
||||
std::int64_t* formats) {
|
||||
*count = 1;
|
||||
if (capacity) formats[0] = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL CreateSwapchain(XrSession,
|
||||
const XrSwapchainCreateInfo* info,
|
||||
XrSwapchain* swapchain) {
|
||||
if (Scenario("swapchain_failure")) return XR_ERROR_RUNTIME_FAILURE;
|
||||
REQUIRE(info->sampleCount == 1 && info->arraySize == 1 &&
|
||||
info->faceCount == 1 && info->mipCount == 1);
|
||||
fixture.width = info->width;
|
||||
fixture.height = info->height;
|
||||
VkImageCreateInfo create{};
|
||||
create.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||
create.imageType = VK_IMAGE_TYPE_2D;
|
||||
create.format = static_cast<VkFormat>(info->format);
|
||||
create.extent = {info->width, info->height, 1};
|
||||
create.mipLevels = create.arrayLayers = 1;
|
||||
create.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
create.usage =
|
||||
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
for (std::size_t i = 0; i < fixture.images.size(); ++i) {
|
||||
REQUIRE(VK(CreateImage, fixture.device, &create, nullptr,
|
||||
&fixture.images[i]) == VK_SUCCESS);
|
||||
VkMemoryRequirements requirements{};
|
||||
VK(GetImageMemoryRequirements, fixture.device, fixture.images[i],
|
||||
&requirements);
|
||||
VkMemoryAllocateInfo memory{};
|
||||
memory.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
||||
memory.allocationSize = requirements.size;
|
||||
memory.memoryTypeIndex = MemoryType(requirements.memoryTypeBits, 0);
|
||||
REQUIRE(memory.memoryTypeIndex != UINT32_MAX);
|
||||
REQUIRE(VK(AllocateMemory, fixture.device, &memory, nullptr,
|
||||
&fixture.memory[i]) == VK_SUCCESS);
|
||||
REQUIRE(VK(BindImageMemory, fixture.device, fixture.images[i],
|
||||
fixture.memory[i], 0) == VK_SUCCESS);
|
||||
}
|
||||
*swapchain = Swapchain();
|
||||
Trace("swapchain-create");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL DestroySwapchain(XrSwapchain) {
|
||||
for (std::size_t i = 0; i < fixture.images.size(); ++i) {
|
||||
if (fixture.images[i])
|
||||
VK(DestroyImage, fixture.device, fixture.images[i], nullptr);
|
||||
if (fixture.memory[i])
|
||||
VK(FreeMemory, fixture.device, fixture.memory[i], nullptr);
|
||||
fixture.images[i] = VK_NULL_HANDLE;
|
||||
}
|
||||
Trace("swapchain-destroy");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL EnumerateImages(XrSwapchain, std::uint32_t capacity,
|
||||
std::uint32_t* count,
|
||||
XrSwapchainImageBaseHeader* base) {
|
||||
*count = fixture.images.size();
|
||||
if (!capacity) return XR_SUCCESS;
|
||||
if (capacity < *count) return XR_ERROR_SIZE_INSUFFICIENT;
|
||||
auto* images = reinterpret_cast<XrSwapchainImageVulkanKHR*>(base);
|
||||
for (std::uint32_t i = 0; i < *count; ++i) {
|
||||
REQUIRE(images[i].type == XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR);
|
||||
images[i].image = fixture.images[i];
|
||||
}
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL WaitFrame(XrSession, const XrFrameWaitInfo*,
|
||||
XrFrameState* state) {
|
||||
REQUIRE(fixture.running && !fixture.begun);
|
||||
state->predictedDisplayTime = (fixture.frames + 1) * 16'666'666;
|
||||
state->predictedDisplayPeriod = 16'666'666;
|
||||
state->shouldRender = !(Scenario("skip_render") && fixture.frames == 0);
|
||||
Trace("frame-wait");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL BeginFrame(XrSession, const XrFrameBeginInfo*) {
|
||||
REQUIRE(fixture.running && !fixture.begun);
|
||||
fixture.begun = true;
|
||||
Trace("frame-begin");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL AcquireImage(XrSwapchain,
|
||||
const XrSwapchainImageAcquireInfo*,
|
||||
std::uint32_t* index) {
|
||||
REQUIRE(fixture.begun && !fixture.acquired);
|
||||
fixture.acquired = true;
|
||||
fixture.index = fixture.frames % fixture.images.size();
|
||||
*index = Scenario("bad_image_index") ? 1000 : fixture.index;
|
||||
Trace("image-acquire");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL WaitImage(XrSwapchain, const XrSwapchainImageWaitInfo*) {
|
||||
REQUIRE(fixture.acquired && !fixture.waited);
|
||||
if (Scenario("image_timeout") && !fixture.timeout_returned) {
|
||||
fixture.timeout_returned = true;
|
||||
Trace("image-timeout");
|
||||
return XR_TIMEOUT_EXPIRED;
|
||||
}
|
||||
fixture.waited = true;
|
||||
Trace("image-wait");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL ReleaseImage(XrSwapchain,
|
||||
const XrSwapchainImageReleaseInfo*) {
|
||||
REQUIRE(fixture.acquired && fixture.waited);
|
||||
fixture.acquired = fixture.waited = false;
|
||||
Trace("image-release");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL EndFrame(XrSession, const XrFrameEndInfo* end) {
|
||||
REQUIRE(fixture.begun && !fixture.acquired);
|
||||
REQUIRE(end->displayTime == (fixture.frames + 1) * 16'666'666);
|
||||
REQUIRE(end->environmentBlendMode == XR_ENVIRONMENT_BLEND_MODE_OPAQUE);
|
||||
if (end->layerCount) {
|
||||
REQUIRE(end->layerCount == 1 &&
|
||||
end->layers[0]->type == XR_TYPE_COMPOSITION_LAYER_QUAD);
|
||||
const auto* layer =
|
||||
reinterpret_cast<const XrCompositionLayerQuad*>(end->layers[0]);
|
||||
REQUIRE(layer->space == Local() &&
|
||||
layer->eyeVisibility == XR_EYE_VISIBILITY_BOTH);
|
||||
REQUIRE(layer->pose.position.z == -2 && layer->pose.orientation.w == 1);
|
||||
REQUIRE(layer->subImage.swapchain == Swapchain());
|
||||
if (!fixture.pixels_verified) {
|
||||
REQUIRE(VerifyPixels());
|
||||
fixture.pixels_verified = true;
|
||||
}
|
||||
Trace("layer-submit");
|
||||
} else {
|
||||
Trace("empty-submit");
|
||||
}
|
||||
fixture.begun = false;
|
||||
++fixture.frames;
|
||||
if (Scenario("preview_auto_stop") && fixture.frames == 3)
|
||||
fixture.events.push_back(XR_SESSION_STATE_STOPPING);
|
||||
if (Scenario("session_loss"))
|
||||
fixture.events.push_back(XR_SESSION_STATE_LOSS_PENDING);
|
||||
Trace("frame-end");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PFN_xrVoidFunction MocktailTestPreviewFunction(const char* name) {
|
||||
#define MAP(xr_name, implementation) \
|
||||
if (std::strcmp(name, #xr_name) == 0) \
|
||||
return reinterpret_cast<PFN_xrVoidFunction>(implementation);
|
||||
MAP(xrCreateVulkanInstanceKHR, CreateVulkanInstance)
|
||||
MAP(xrGetVulkanGraphicsDevice2KHR, GetVulkanDevice)
|
||||
MAP(xrCreateVulkanDeviceKHR, CreateVulkanDevice)
|
||||
MAP(xrEnumerateEnvironmentBlendModes, EnumerateBlends)
|
||||
MAP(xrCreateSession, CreateSession)
|
||||
MAP(xrDestroySession, DestroySession)
|
||||
MAP(xrPollEvent, PollEvent)
|
||||
MAP(xrBeginSession, BeginSession)
|
||||
MAP(xrRequestExitSession, RequestExit)
|
||||
MAP(xrEndSession, EndSession)
|
||||
MAP(xrCreateReferenceSpace, CreateSpace)
|
||||
MAP(xrDestroySpace, DestroySpace)
|
||||
MAP(xrLocateSpace, LocateSpace)
|
||||
MAP(xrEnumerateSwapchainFormats, EnumerateFormats)
|
||||
MAP(xrCreateSwapchain, CreateSwapchain)
|
||||
MAP(xrDestroySwapchain, DestroySwapchain)
|
||||
MAP(xrEnumerateSwapchainImages, EnumerateImages)
|
||||
MAP(xrWaitFrame, WaitFrame)
|
||||
MAP(xrBeginFrame, BeginFrame)
|
||||
MAP(xrEndFrame, EndFrame)
|
||||
MAP(xrAcquireSwapchainImage, AcquireImage)
|
||||
MAP(xrWaitSwapchainImage, WaitImage)
|
||||
MAP(xrReleaseSwapchainImage, ReleaseImage)
|
||||
#undef MAP
|
||||
return nullptr;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// Test-only runtime for exercising the real Khronos loader without hardware.
|
||||
// Implements discovery only; it is neither a compositor nor an XR emulator.
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_loader_negotiation.h>
|
||||
#include <openxr/openxr_platform.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
PFN_xrVoidFunction MocktailTestPreviewFunction(const char* name);
|
||||
|
||||
namespace {
|
||||
|
||||
bool Scenario(const char* expected) {
|
||||
const char* value = std::getenv("MOCKTAIL_TEST_XR_SCENARIO");
|
||||
return value && std::strcmp(value, expected) == 0;
|
||||
}
|
||||
|
||||
void Trace(const char* event) {
|
||||
const char* path = std::getenv("MOCKTAIL_TEST_XR_TRACE");
|
||||
if (path) {
|
||||
if (FILE* file = std::fopen(path, "a")) {
|
||||
std::fprintf(file, "%s\n", event);
|
||||
std::fclose(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL EnumerateExtensions(const char*, std::uint32_t capacity,
|
||||
std::uint32_t* count,
|
||||
XrExtensionProperties* properties) {
|
||||
*count = Scenario("no_vulkan") ? 0 : 1;
|
||||
if (capacity == 0 || *count == 0) return XR_SUCCESS;
|
||||
if (properties[0].type != XR_TYPE_EXTENSION_PROPERTIES)
|
||||
return XR_ERROR_VALIDATION_FAILURE;
|
||||
std::strcpy(properties[0].extensionName,
|
||||
XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME);
|
||||
properties[0].extensionVersion = XR_KHR_vulkan_enable2_SPEC_VERSION;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL CreateInstance(const XrInstanceCreateInfo* info,
|
||||
XrInstance* instance) {
|
||||
if (Scenario("create_failure")) return XR_ERROR_INITIALIZATION_FAILED;
|
||||
if (info->type != XR_TYPE_INSTANCE_CREATE_INFO || info->next != nullptr)
|
||||
return XR_ERROR_VALIDATION_FAILURE;
|
||||
static int handle;
|
||||
*instance = reinterpret_cast<XrInstance>(&handle);
|
||||
Trace("create");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL DestroyInstance(XrInstance) {
|
||||
Trace("destroy");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL GetInstanceProperties(XrInstance,
|
||||
XrInstanceProperties* properties) {
|
||||
if (Scenario("api_error")) return XR_ERROR_RUNTIME_FAILURE;
|
||||
std::strcpy(properties->runtimeName, "Mocktail discovery fixture");
|
||||
properties->runtimeVersion = XR_MAKE_VERSION(1, 0, 42);
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL GetSystem(XrInstance, const XrSystemGetInfo* info,
|
||||
XrSystemId* system) {
|
||||
if (info->formFactor != XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY)
|
||||
return XR_ERROR_FORM_FACTOR_UNSUPPORTED;
|
||||
if (Scenario("no_headset")) return XR_ERROR_FORM_FACTOR_UNAVAILABLE;
|
||||
*system = 1;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL GetSystemProperties(XrInstance, XrSystemId,
|
||||
XrSystemProperties* properties) {
|
||||
properties->systemId = 1;
|
||||
std::strcpy(properties->systemName, "Simulated test HMD");
|
||||
properties->graphicsProperties = {4096, 4096, 16};
|
||||
properties->trackingProperties = {XR_TRUE, XR_TRUE};
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL EnumerateViews(XrInstance, XrSystemId,
|
||||
XrViewConfigurationType type,
|
||||
std::uint32_t capacity, std::uint32_t* count,
|
||||
XrViewConfigurationView* views) {
|
||||
if (type != XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO)
|
||||
return XR_ERROR_VIEW_CONFIGURATION_TYPE_UNSUPPORTED;
|
||||
*count = Scenario("bad_stereo") ? 1 : 2;
|
||||
if (capacity == 0) return XR_SUCCESS;
|
||||
static bool grew = false;
|
||||
if (Scenario("enumeration_changed") && !grew) {
|
||||
grew = true;
|
||||
return XR_ERROR_SIZE_INSUFFICIENT;
|
||||
}
|
||||
if (capacity < *count) return XR_ERROR_SIZE_INSUFFICIENT;
|
||||
for (std::uint32_t eye = 0; eye < *count; ++eye) {
|
||||
if (views[eye].type != XR_TYPE_VIEW_CONFIGURATION_VIEW || views[eye].next)
|
||||
return XR_ERROR_VALIDATION_FAILURE;
|
||||
views[eye].recommendedImageRectWidth = 1440;
|
||||
views[eye].recommendedImageRectHeight = 1600;
|
||||
views[eye].maxImageRectWidth = 4096;
|
||||
views[eye].maxImageRectHeight = 4096;
|
||||
views[eye].recommendedSwapchainSampleCount = 1;
|
||||
views[eye].maxSwapchainSampleCount = 4;
|
||||
}
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL GetGraphicsRequirements(
|
||||
XrInstance, XrSystemId, XrGraphicsRequirementsVulkan2KHR* requirements) {
|
||||
if (Scenario("graphics_error")) return XR_ERROR_RUNTIME_FAILURE;
|
||||
if (requirements->type != XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN2_KHR ||
|
||||
requirements->next)
|
||||
return XR_ERROR_VALIDATION_FAILURE;
|
||||
requirements->minApiVersionSupported = XR_MAKE_VERSION(1, 1, 0);
|
||||
requirements->maxApiVersionSupported = XR_MAKE_VERSION(1, 3, 0);
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL ResultToString(XrInstance, XrResult result, char* buffer) {
|
||||
std::snprintf(buffer, XR_MAX_RESULT_STRING_SIZE, "fixture_result_%d", result);
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL GetProcAddr(XrInstance, const char* name,
|
||||
PFN_xrVoidFunction* function) {
|
||||
*function = nullptr;
|
||||
#define MOCKTAIL_TEST_XR_PROC(xr_name, implementation) \
|
||||
if (std::strcmp(name, #xr_name) == 0) { \
|
||||
*function = reinterpret_cast<PFN_xrVoidFunction>(implementation); \
|
||||
return XR_SUCCESS; \
|
||||
}
|
||||
MOCKTAIL_TEST_XR_PROC(xrGetInstanceProcAddr, GetProcAddr)
|
||||
MOCKTAIL_TEST_XR_PROC(xrEnumerateInstanceExtensionProperties,
|
||||
EnumerateExtensions)
|
||||
MOCKTAIL_TEST_XR_PROC(xrCreateInstance, CreateInstance)
|
||||
MOCKTAIL_TEST_XR_PROC(xrDestroyInstance, DestroyInstance)
|
||||
MOCKTAIL_TEST_XR_PROC(xrGetInstanceProperties, GetInstanceProperties)
|
||||
MOCKTAIL_TEST_XR_PROC(xrGetSystem, GetSystem)
|
||||
MOCKTAIL_TEST_XR_PROC(xrGetSystemProperties, GetSystemProperties)
|
||||
MOCKTAIL_TEST_XR_PROC(xrEnumerateViewConfigurationViews, EnumerateViews)
|
||||
MOCKTAIL_TEST_XR_PROC(xrGetVulkanGraphicsRequirements2KHR,
|
||||
GetGraphicsRequirements)
|
||||
MOCKTAIL_TEST_XR_PROC(xrResultToString, ResultToString)
|
||||
#undef MOCKTAIL_TEST_XR_PROC
|
||||
*function = MocktailTestPreviewFunction(name);
|
||||
if (*function) return XR_SUCCESS;
|
||||
return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" __attribute__((visibility("default"))) XrResult XRAPI_CALL
|
||||
xrNegotiateLoaderRuntimeInterface(const XrNegotiateLoaderInfo* loader,
|
||||
XrNegotiateRuntimeRequest* request) {
|
||||
if (loader->structType != XR_LOADER_INTERFACE_STRUCT_LOADER_INFO ||
|
||||
request->structType != XR_LOADER_INTERFACE_STRUCT_RUNTIME_REQUEST ||
|
||||
loader->minInterfaceVersion > XR_CURRENT_LOADER_RUNTIME_VERSION ||
|
||||
loader->maxInterfaceVersion < XR_CURRENT_LOADER_RUNTIME_VERSION)
|
||||
return XR_ERROR_INITIALIZATION_FAILED;
|
||||
request->runtimeInterfaceVersion = XR_CURRENT_LOADER_RUNTIME_VERSION;
|
||||
request->runtimeApiVersion = XR_MAKE_VERSION(1, 0, 42);
|
||||
request->getInstanceProcAddr = GetProcAddr;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
"""Real Vulkan rendering/readback with a scripted OpenXR session lifecycle."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
|
||||
probe, runtime, mocktail = map(Path, sys.argv[1:4])
|
||||
with tempfile.TemporaryDirectory(prefix="mocktail-xr-preview-") as directory:
|
||||
root = Path(directory)
|
||||
manifest = root / "runtime.json"
|
||||
manifest.write_text(json.dumps({"file_format_version": "1.0.0",
|
||||
"runtime": {"library_path": str(runtime.resolve())}}))
|
||||
env = {k: v for k, v in os.environ.items()
|
||||
if not k.startswith(("XR_", "MOCKTAIL_", "VK_"))}
|
||||
env.update(XR_RUNTIME_JSON=str(manifest), XR_LOADER_DEBUG="none",
|
||||
XDG_CONFIG_HOME=str(root / "config"), XDG_DATA_HOME=str(root / "data"),
|
||||
VK_LOADER_LAYERS_DISABLE="~implicit~")
|
||||
lavapipe = Path("/usr/share/vulkan/icd.d/lvp_icd.json")
|
||||
if lavapipe.exists():
|
||||
env["VK_DRIVER_FILES"] = str(lavapipe)
|
||||
if Path("/usr/share/vulkan/explicit_layer.d/VkLayer_khronos_validation.json").exists():
|
||||
env["VK_INSTANCE_LAYERS"] = "VK_LAYER_KHRONOS_validation"
|
||||
trace = root / "trace"
|
||||
env["MOCKTAIL_TEST_XR_TRACE"] = str(trace)
|
||||
|
||||
def check(result, code):
|
||||
output = result.stdout + result.stderr
|
||||
assert result.returncode == code, (result.returncode, output)
|
||||
assert "Validation Error" not in output and "VUID-" not in output, output
|
||||
events = trace.read_text().splitlines() if trace.exists() else []
|
||||
assert not any(event.startswith("contract-error") for event in events), events
|
||||
assert events.count("create") == events.count("destroy"), events
|
||||
assert events.count("space-create") == events.count("space-destroy"), events
|
||||
assert events.count("session-create") == events.count("session-destroy"), events
|
||||
assert events.count("swapchain-create") == events.count("swapchain-destroy"), events
|
||||
assert events.count("frame-begin") == events.count("frame-end"), events
|
||||
assert events.count("image-acquire") == events.count("image-release"), events
|
||||
if "swapchain-destroy" in events:
|
||||
assert events.index("swapchain-destroy") < events.index("session-destroy") < events.index("destroy")
|
||||
return output, events
|
||||
|
||||
cases = [
|
||||
("preview_ready", 0, "rendered: 3, tracked: 3"),
|
||||
("skip_render", 0, "rendered: 2, tracked: 3"),
|
||||
("invalid_tracking", 0, "rendered: 2, tracked: 2"),
|
||||
("image_timeout", 0, "rendered: 3, tracked: 3"),
|
||||
("device_failure", 1, "vkCreateDevice failed"),
|
||||
("session_failure", 1, "xrCreateSession failed"),
|
||||
("swapchain_failure", 1, "xrCreateSwapchain failed"),
|
||||
("bad_image_index", 1, "invalid swapchain image index"),
|
||||
("session_loss", 1, "OpenXR session was lost"),
|
||||
]
|
||||
for scenario, code, message in cases:
|
||||
trace.unlink(missing_ok=True)
|
||||
env["MOCKTAIL_TEST_XR_SCENARIO"] = scenario
|
||||
result = subprocess.run([str(probe), "--scene", "--frames", "3"], env=env,
|
||||
capture_output=True, text=True, timeout=15)
|
||||
if scenario == "preview_ready" and result.returncode != 0:
|
||||
output = result.stdout + result.stderr
|
||||
if "Cannot load the host Vulkan loader" in output or "vkCreateInstance failed: -9" in output:
|
||||
print("SKIP: no Vulkan implementation available for GPU contract checks")
|
||||
sys.exit(77)
|
||||
output, events = check(result, code)
|
||||
assert message in output, (scenario, output)
|
||||
if code == 0:
|
||||
assert "pixels-verified" in events, (scenario, events)
|
||||
assert events.count("session-exit-request") == events.count("session-end") == 1
|
||||
assert "Head position (metres): 0.2, 0, 0" in output, output
|
||||
if scenario in ("skip_render", "invalid_tracking", "bad_image_index"):
|
||||
assert events.count("empty-submit") == 1, events
|
||||
if scenario == "image_timeout":
|
||||
assert events.count("image-timeout") == 1, events
|
||||
print(scenario + ": passed")
|
||||
|
||||
# VR must traverse the real Roblox startup, never fall back to this scene.
|
||||
# Exercise configuration/CLI precedence with a deliberately missing payload.
|
||||
for scenario, yaml_value, switches, env_value in [
|
||||
("yaml", "true", [], None),
|
||||
("cli", "false", ["--vr"], "0"),
|
||||
("environment", "false", [], "1"),
|
||||
]:
|
||||
case = root / scenario
|
||||
config = case / "config"
|
||||
config.mkdir(parents=True)
|
||||
contents = f"version: 1\nvr:\n enabled: {yaml_value}\n"
|
||||
(config / "config.yaml").write_text(contents)
|
||||
env.update(MOCKTAIL_CONFIG_ROOT=str(config), MOCKTAIL_DATA_ROOT=str(case / "data"),
|
||||
MOCKTAIL_STATE_ROOT=str(case / "state"), MOCKTAIL_TEST_XR_SCENARIO="preview_auto_stop")
|
||||
if env_value is None:
|
||||
env.pop("MOCKTAIL_VR_ENABLED", None)
|
||||
else:
|
||||
env["MOCKTAIL_VR_ENABLED"] = env_value
|
||||
trace.unlink(missing_ok=True)
|
||||
result = subprocess.run([str(mocktail), "--headless", "--roblox-lib",
|
||||
str(case / "missing-libroblox.so"), *switches],
|
||||
env=env, capture_output=True, text=True, timeout=15)
|
||||
output = result.stdout + result.stderr
|
||||
assert result.returncode != 0, (scenario, output)
|
||||
assert "missing-libroblox.so" in output, (scenario, output)
|
||||
assert not trace.exists(), (scenario, trace.read_text() if trace.exists() else "")
|
||||
assert (config / "config.yaml").read_text() == contents
|
||||
print("Mocktail VR rejects missing Roblox via " + scenario + ": passed")
|
||||
|
||||
env["MOCKTAIL_TEST_XR_SCENARIO"] = "preview_ready"
|
||||
trace.unlink(missing_ok=True)
|
||||
child = subprocess.Popen([str(probe), "--scene"], env=env,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
try:
|
||||
deadline = time.monotonic() + 10
|
||||
while not trace.exists() or "layer-submit" not in trace.read_text():
|
||||
assert child.poll() is None and time.monotonic() < deadline, "Scene failed to start for interrupt test"
|
||||
time.sleep(0.01)
|
||||
child.send_signal(signal.SIGINT)
|
||||
stdout, stderr = child.communicate(timeout=10)
|
||||
_, events = check(subprocess.CompletedProcess(child.args, child.returncode, stdout, stderr), 0)
|
||||
assert "session-exit-request" in events and "session-end" in events, events
|
||||
finally:
|
||||
if child.poll() is None:
|
||||
child.kill()
|
||||
child.wait()
|
||||
print("SIGINT session shutdown: passed")
|
||||
|
||||
for count in ("0", "-1", "abc", "4294967296", "1x"):
|
||||
result = subprocess.run([str(probe), "--scene", "--frames", count], env=env,
|
||||
capture_output=True, text=True, timeout=5)
|
||||
assert result.returncode == 1 and "positive integer" in result.stderr, (count, result)
|
||||
print("frame limit validation: passed")
|
||||
|
||||
env["XR_RUNTIME_JSON"] = str(root / "missing-runtime.json")
|
||||
result = subprocess.run([str(probe), "--scene", "--frames", "1"], env=env,
|
||||
capture_output=True, text=True, timeout=5)
|
||||
assert result.returncode == 1, result
|
||||
assert "XR_ERROR_RUNTIME_UNAVAILABLE" in result.stdout, result.stdout
|
||||
assert "mocktail-vr-simulated" in result.stdout, result.stdout
|
||||
print("missing runtime diagnostic offers simulator: passed")
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Run the actual OpenXR loader against a discovery-only test runtime."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
probe = Path(sys.argv[1])
|
||||
if sys.argv[2] == "--disabled":
|
||||
result = subprocess.run([str(probe)], text=True, capture_output=True, timeout=15)
|
||||
assert result.returncode == 2, (result.returncode, result.stdout, result.stderr)
|
||||
assert "does not include OpenXR support" in result.stdout, result.stdout
|
||||
result = subprocess.run([str(probe), "--scene", "--frames", "1"],
|
||||
text=True, capture_output=True, timeout=15)
|
||||
assert result.returncode == 1 and "does not include OpenXR support" in result.stdout, result
|
||||
with tempfile.TemporaryDirectory(prefix="mocktail-xr-disabled-") as directory:
|
||||
root = Path(directory)
|
||||
env = {k: v for k, v in os.environ.items() if not k.startswith(("XR_", "MOCKTAIL_"))}
|
||||
for kind in ("CONFIG", "DATA", "STATE"):
|
||||
env["MOCKTAIL_" + kind + "_ROOT"] = str(root / kind.lower())
|
||||
result = subprocess.run([sys.argv[3], "--headless", "--vr"], env=env,
|
||||
text=True, capture_output=True, timeout=15)
|
||||
assert result.returncode == 1 and "does not include OpenXR support" in result.stderr, result
|
||||
assert not (root / "data").exists() and not (root / "state").exists()
|
||||
print("OpenXR-disabled build: passed")
|
||||
sys.exit(0)
|
||||
|
||||
runtime = Path(sys.argv[2])
|
||||
with tempfile.TemporaryDirectory(prefix="mocktail-xr-probe-") as directory:
|
||||
root = Path(directory)
|
||||
manifest = root / "runtime.json"
|
||||
manifest.write_text(json.dumps({
|
||||
"file_format_version": "1.0.0",
|
||||
"runtime": {"library_path": str(runtime.resolve())},
|
||||
}))
|
||||
environment = {k: v for k, v in os.environ.items()
|
||||
if not k.startswith(("XR_", "MOCKTAIL_"))}
|
||||
environment["XR_RUNTIME_JSON"] = str(manifest)
|
||||
environment["XR_LOADER_DEBUG"] = "none"
|
||||
environment["XDG_CONFIG_HOME"] = str(root / "config")
|
||||
environment["XDG_DATA_HOME"] = str(root / "data")
|
||||
environment["XR_API_LAYER_PATH"] = str(root / "layers")
|
||||
(root / "layers").mkdir()
|
||||
trace = root / "trace"
|
||||
environment["MOCKTAIL_TEST_XR_TRACE"] = str(trace)
|
||||
|
||||
cases = [
|
||||
("ready", 0, "discovered successfully"),
|
||||
("no_headset", 4, "Connect a headset"),
|
||||
("no_vulkan", 5, "does not expose XR_KHR_vulkan_enable2"),
|
||||
("bad_stereo", 5, "two usable stereo views"),
|
||||
("api_error", 1, "xrGetInstanceProperties failed"),
|
||||
("create_failure", 1, "xrCreateInstance failed"),
|
||||
("enumeration_changed", 0, "discovered successfully"),
|
||||
("graphics_error", 1, "xrGetVulkanGraphicsRequirements2KHR failed"),
|
||||
]
|
||||
for scenario, code, message in cases:
|
||||
trace.unlink(missing_ok=True)
|
||||
environment["MOCKTAIL_TEST_XR_SCENARIO"] = scenario
|
||||
result = subprocess.run([str(probe)], env=environment,
|
||||
text=True, capture_output=True, timeout=15)
|
||||
assert result.returncode == code, (scenario, result.returncode, result.stdout, result.stderr)
|
||||
assert message in result.stdout, (scenario, result.stdout, result.stderr)
|
||||
assert "SDK: 1.1.63" in result.stdout, result.stdout
|
||||
assert "Roblox VR rendering: not implemented yet" in result.stdout
|
||||
events = trace.read_text().splitlines() if trace.exists() else []
|
||||
assert events == ([] if scenario == "create_failure" else ["create", "destroy"]), (scenario, events)
|
||||
if code == 0:
|
||||
assert "Eye 0: 1440x1600" in result.stdout
|
||||
assert "Eye 1: 1440x1600" in result.stdout
|
||||
assert "minimum=1.1.0" in result.stdout
|
||||
print(scenario + ": passed")
|
||||
|
||||
environment["XR_RUNTIME_JSON"] = str(root / "does-not-exist.json")
|
||||
result = subprocess.run([str(probe)], env=environment,
|
||||
text=True, capture_output=True, timeout=15)
|
||||
assert result.returncode == 3, (result.returncode, result.stdout, result.stderr)
|
||||
assert "runtime" in result.stdout.lower(), result.stdout
|
||||
print("missing runtime: passed")
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Check process-local runtime selection and preparation independently of VR hardware."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
launcher = Path(sys.argv[1]).resolve()
|
||||
with tempfile.TemporaryDirectory(prefix="mocktail-simulator-launcher-") as directory:
|
||||
root = Path(directory)
|
||||
build = root / "build with spaces"
|
||||
build.mkdir()
|
||||
source = root / "source"
|
||||
source.mkdir()
|
||||
library = source / "runtime.so"
|
||||
library.write_bytes(b"runtime fixture for preparation only")
|
||||
manifest = source / "manifest.json"
|
||||
manifest.write_text(json.dumps({"runtime": {"library_path": "./runtime.so"}}))
|
||||
command = [sys.executable, str(launcher), "--build-dir", str(build)]
|
||||
result = subprocess.run([*command, "--prepare", str(manifest)],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
assert result.returncode == 0, result.stderr
|
||||
prepared = build / "vr-simulator/runtime.json"
|
||||
document = json.loads(prepared.read_text())
|
||||
relative_library = Path(document["runtime"]["library_path"])
|
||||
assert not relative_library.is_absolute()
|
||||
assert (prepared.parent / relative_library).read_bytes() == library.read_bytes()
|
||||
|
||||
# Moving the prepared build must not depend on the original /tmp/source path.
|
||||
relocated = root / "relocated build"
|
||||
build.rename(relocated)
|
||||
build = relocated
|
||||
prepared = build / "vr-simulator/runtime.json"
|
||||
command = [sys.executable, str(launcher), "--build-dir", str(build)]
|
||||
probe = build / "mocktail-vr-probe"
|
||||
probe.write_text("""#!/usr/bin/env python3
|
||||
import json, os, sys
|
||||
names = ('XR_RUNTIME_JSON', 'SIMULATED_ENABLE', 'XRT_COMPOSITOR_NULL',
|
||||
'XRT_COMPOSITOR_FORCE_XCB', 'XRT_COMPOSITOR_FORCE_WAYLAND',
|
||||
'XDG_CONFIG_HOME', 'XDG_RUNTIME_DIR')
|
||||
print(json.dumps({'args': sys.argv[1:], 'env': {key: os.environ.get(key) for key in names}}))
|
||||
sys.exit(7)
|
||||
""")
|
||||
probe.chmod(0o755)
|
||||
env = os.environ.copy()
|
||||
env.update(DISPLAY=":123", WAYLAND_DISPLAY="wayland-test",
|
||||
XR_RUNTIME_JSON=str(root / "real-headset.json"),
|
||||
XRT_COMPOSITOR_NULL="1", XRT_COMPOSITOR_FORCE_WAYLAND="1",
|
||||
XDG_CONFIG_HOME=str(root / "user-config"), XDG_RUNTIME_DIR=str(root / "desktop-runtime"))
|
||||
original_selection = env["XR_RUNTIME_JSON"]
|
||||
original_config = Path(env["XDG_CONFIG_HOME"])
|
||||
original_config.mkdir()
|
||||
marker = original_config / "keep"
|
||||
marker.write_text("preserve user settings")
|
||||
|
||||
for graphical_backend in ("xcb", "wayland"):
|
||||
if graphical_backend == "wayland":
|
||||
env.pop("DISPLAY")
|
||||
result = subprocess.run([*command, "--frames", "4"], env=env,
|
||||
capture_output=True, text=True, timeout=10)
|
||||
assert result.returncode == 7, (result.stdout, result.stderr)
|
||||
child = json.loads(result.stdout.splitlines()[-1])
|
||||
assert child["args"] == ["--scene", "--frames", "4"]
|
||||
selected = child["env"]
|
||||
assert selected["XR_RUNTIME_JSON"] == str(prepared)
|
||||
assert selected["SIMULATED_ENABLE"] == "1" and selected["XRT_COMPOSITOR_NULL"] == "0"
|
||||
assert selected["XRT_COMPOSITOR_FORCE_XCB"] == ("1" if graphical_backend == "xcb" else "0")
|
||||
assert selected["XRT_COMPOSITOR_FORCE_WAYLAND"] == ("0" if graphical_backend == "xcb" else "1")
|
||||
assert selected["XDG_CONFIG_HOME"] != str(original_config)
|
||||
assert selected["XDG_RUNTIME_DIR"] == env["XDG_RUNTIME_DIR"]
|
||||
assert env["XR_RUNTIME_JSON"] == original_selection
|
||||
assert marker.read_text() == "preserve user settings"
|
||||
assert not (original_config / "openxr").exists()
|
||||
print(graphical_backend + " process-local selection: passed")
|
||||
|
||||
env.pop("WAYLAND_DISPLAY")
|
||||
result = subprocess.run(command, env=env, capture_output=True, text=True, timeout=10)
|
||||
assert result.returncode == 2 and "graphical desktop" in result.stderr, result
|
||||
result = subprocess.run([*command, "--runtime", str(root / "missing.json")], env=env,
|
||||
capture_output=True, text=True, timeout=10)
|
||||
assert result.returncode == 2 and "No simulated Monado runtime" in result.stderr, result
|
||||
for count in ("0", "-1", "4294967296"):
|
||||
result = subprocess.run([*command, "--frames", count], env=env,
|
||||
capture_output=True, text=True, timeout=10)
|
||||
assert result.returncode == 2 and "positive 32-bit integer" in result.stderr, result
|
||||
print("missing runtime/display and frame limit diagnostics: passed")
|
||||
+1
Submodule third_party/OpenXR-SDK added at f2448a8797.
Reference in new issue
Block a user