This commit is contained in:
komaruworld committed 2026-09-12 21:45:48 +03:00
1 parent c48db09f13
commit dd23f06a8b
31 files changed
+2650 -214

No files matched your search

+3
View File
@@ -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
+3
View File
@@ -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
+3 -190
View File
@@ -1,192 +1,5 @@
# Mocktail
This is a dev branch for vr support in Mocktail.
[![CI](https://github.com/komaruworld/mocktail/actions/workflows/ci.yml/badge.svg)](https://github.com/komaruworld/mocktail/actions/workflows/ci.yml)
[![Stars](https://img.shields.io/github/stars/komaruworld/mocktail?style=flat&logo=github)](https://github.com/komaruworld/mocktail/stargazers)
[![Downloads](https://img.shields.io/github/downloads/komaruworld/mocktail/total?logo=github)](https://github.com/komaruworld/mocktail/releases/latest)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
[![Discord](https://img.shields.io/discord/1543955607869063269?label=discord)](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>
![Roblox home in Mocktail](assets/screenshots/flatpak-home.png)
![Roblox gameplay in Mocktail](assets/screenshots/flatpak-gameplay-tower.png)
![Roblox experience in Mocktail](assets/screenshots/flatpak-gameplay-lobby.png)
</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 :)
+60
View File
@@ -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)
+7
View File
@@ -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.
+34
View File
@@ -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_
+48
View File
@@ -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_
+2
View File
@@ -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;
+4
View File
@@ -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";
+187
View File
@@ -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)
+14 -1
View File
@@ -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
View File
@@ -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();
+10
View File
@@ -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 "
+2
View File
@@ -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(
+7
View File
@@ -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.
+19 -1
View File
@@ -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",
+75
View File
@@ -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
+23
View File
@@ -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
+729
View File
@@ -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
+209
View File
@@ -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
+50
View File
@@ -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);
}
+28
View File
@@ -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
)
+47
View File
@@ -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",
+53
View File
@@ -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 =
+12
View File
@@ -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);
+508
View File
@@ -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, &copy);
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;
}
+170
View File
@@ -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;
}
+141
View File
@@ -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")
+82
View File
@@ -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")
+89
View File
@@ -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")
Vendored Submodule
+1
Submodule third_party/OpenXR-SDK added at f2448a8797.