mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-27 08:07:12 +01:00
add roblox 3092 support
Also fix openxr shit
This commit is contained in:
1 parent
ecb80b3ee1
commit
9a1ccbb5ce
34 files changed
+4407
-766
No files matched your search
@@ -1,131 +1,35 @@
|
||||
# Mocktail VR (experimental)
|
||||
# Mocktail VR
|
||||
|
||||
Build this checkout, start WiVRn or SteamVR/ALVR, then connect your headset before launching:
|
||||
Experimental VR support for [Mocktail](https://github.com/komaruworld/mocktail).
|
||||
Linux x86_64, WiVRn or SteamVR/ALVR.
|
||||
|
||||
```sh
|
||||
git submodule update --init --recursive
|
||||
## Build
|
||||
|
||||
```bash
|
||||
git clone --branch vr --recurse-submodules https://github.com/komaruworld/mocktail.git
|
||||
cd mocktail
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DMOCKTAIL_ENABLE_VR=ON
|
||||
cmake --build build -j4
|
||||
```
|
||||
|
||||
Build dependencies are listed in the [main README](https://github.com/komaruworld/mocktail#building).
|
||||
|
||||
## Run
|
||||
|
||||
Start WiVRn or SteamVR/ALVR and connect your headset first.
|
||||
|
||||
```bash
|
||||
./build/mocktail --vr
|
||||
```
|
||||
|
||||
## Automatic runtime selection
|
||||
The runtime is detected automatically. To pick one manually:
|
||||
|
||||
Normally just run `./build/mocktail --vr`. The default `auto` mode detects a
|
||||
running SteamVR server (also used by ALVR) or a listening WiVRn IPC socket for
|
||||
the current desktop user. A stale socket file does not count as a running
|
||||
server. If exactly one server is detected, its runtime takes priority over an
|
||||
old registration for the other server. If both or neither are detected, the
|
||||
registered OpenXR runtime has priority.
|
||||
|
||||
SteamVR is also found through `openvr/openvrpaths.vrpath`, standard Steam
|
||||
locations, and the running server's executable path. WiVRn development builds
|
||||
are discovered near the running server executable; a matching registered
|
||||
WiVRn manifest is preferred, including a Flatpak registration. The selected
|
||||
path and the reason for selecting it are printed at startup.
|
||||
|
||||
Explicit `XR_RUNTIME_JSON` and `MOCKTAIL_VR_RUNTIME` overrides remain respected.
|
||||
To return to automatic selection after manually choosing a runtime:
|
||||
|
||||
```sh
|
||||
unset XR_RUNTIME_JSON MOCKTAIL_VR_RUNTIME
|
||||
./build/mocktail --vr
|
||||
```
|
||||
|
||||
Detection does not start servers, connect a headset, or modify system runtime
|
||||
registration. It observes the current process/user namespace; container and
|
||||
host runtimes still need correctly shared libraries and IPC access.
|
||||
|
||||
## WiVRn
|
||||
|
||||
Start WiVRn and connect its headset client first. Run WiVRn and Mocktail as the
|
||||
same desktop user, with the same runtime environment; do not start Mocktail
|
||||
from a root shell while WiVRn runs in your user session. For containers/chroots,
|
||||
the host runtime library and IPC socket must both be accessible inside.
|
||||
|
||||
```sh
|
||||
```bash
|
||||
MOCKTAIL_VR_RUNTIME=wivrn ./build/mocktail --vr
|
||||
# or
|
||||
MOCKTAIL_VR_RUNTIME=alvr MOCKTAIL_GRAPHICS_BACKEND=direct-vulkan ./build/mocktail --vr
|
||||
```
|
||||
|
||||
If WiVRn disconnects and exits, reconnect/restart it before retrying Mocktail.
|
||||
`Connection refused` on `wivrn/comp_ipc` means the selected server is not
|
||||
accepting the connection. Installing `dbus-launch` alone does not fix that.
|
||||
If you previously set `XR_RUNTIME_JSON`, unset it before switching runtimes.
|
||||
Use `--no-vr` for desktop mode.
|
||||
|
||||
## ALVR through SteamVR
|
||||
|
||||
Install SteamVR and ALVR, register the ALVR driver using its dashboard, then
|
||||
connect the headset and confirm SteamVR sees it. Follow the upstream
|
||||
[ALVR Linux instructions](https://github.com/alvr-org/ALVR/wiki/Linux-Troubleshooting),
|
||||
including the SteamVR launch-options workaround when required.
|
||||
|
||||
```sh
|
||||
env -u XR_RUNTIME_JSON MOCKTAIL_VR_RUNTIME=alvr \
|
||||
MOCKTAIL_GRAPHICS_BACKEND=direct-vulkan ./build/mocktail --vr
|
||||
```
|
||||
|
||||
The command clears an inherited WiVRn manifest override. `steamvr` is an alias
|
||||
for `alvr`. The selection uses SteamVR's OpenXR runtime;
|
||||
ALVR handles the streaming. If automatic discovery cannot find a custom
|
||||
installation, specify its actual manifest:
|
||||
|
||||
```sh
|
||||
XR_RUNTIME_JSON="/path/to/SteamVR/steamxr_linux64.json" ./build/mocktail --vr
|
||||
```
|
||||
|
||||
`XR_RUNTIME_JSON` always wins. `MOCKTAIL_VR_RUNTIME=system` delegates selection
|
||||
to the OpenXR loader. The default `auto` uses the server detection described above, then registered
|
||||
and installed runtimes. Selection affects only this process.
|
||||
|
||||
Vulkan VR mirrors the left eye into the desktop Roblox window using a GPU
|
||||
blit, preserving its aspect ratio. DebugDeviceVR otherwise leaves that window
|
||||
black even when the headset receives eye images. Set
|
||||
`MOCKTAIL_VR_DESKTOP_MIRROR=0` to disable this copy. This is a view of the eye,
|
||||
not a separate desktop camera or a replacement for VR menu input.
|
||||
|
||||
## OpenGL ES
|
||||
|
||||
The `opengl`/`system` renderer uses the guest's real EGL/OpenGL ES context:
|
||||
|
||||
```sh
|
||||
MOCKTAIL_GRAPHICS_BACKEND=opengl ./build/mocktail --vr
|
||||
```
|
||||
|
||||
The runtime must provide `XR_KHR_opengl_es_enable` and `XR_MNDX_egl_enable`, and
|
||||
support sharing images with the guest EGL device. Both eye copies stay on the
|
||||
GPU; no Python viewer or CPU readback is required. Camera tracking uses the
|
||||
same OpenXR frame/pose pipeline as Vulkan. If the runtime cannot support this
|
||||
EGL binding, use `MOCKTAIL_GRAPHICS_BACKEND=direct-vulkan`; the ALVR command
|
||||
above deliberately selects Vulkan.
|
||||
|
||||
## Current validation and limitations
|
||||
|
||||
- Native VR camera/eye hooks are verified for Roblox build **2998**, Build ID
|
||||
`ade08266c67aee88ec9c1d00902150e1684dad3a`. Approval of a newer payload for
|
||||
ordinary rendering does **not** establish VR compatibility. Do not bypass
|
||||
the Build-ID checks to run the old VR hooks on build 3092.
|
||||
- GLES framebuffer copying, distinct eyes, state restoration, resource
|
||||
invalidation, and an EGL OpenXR session with session-loss recovery have been
|
||||
tested. A local unauthenticated Roblox run on hardware GLES submitted over
|
||||
240 projection frames through a simulated Monado headset.
|
||||
- This host's software GLES/Vulkan combination could create XR texture names
|
||||
but not usable framebuffer attachments. The copy failure is reported and
|
||||
XR output stops; this is not counted as a successful rendering test.
|
||||
- The local Monado test runtime reports `u_graphics_buffer_unref: Bad file
|
||||
descriptor (fd: 0)` during GLES teardown, including successful hardware
|
||||
runs. This remains an integration caveat; clean headset teardown is not
|
||||
claimed from those simulator runs. During auto-selection validation, one
|
||||
run also failed a later EGL session recreation with
|
||||
`vkCreateFence: VK_ERROR_OUT_OF_HOST_MEMORY`; a fresh repeat passed.
|
||||
Runtime selection and initial frame submission succeeded in both runs.
|
||||
- Real headset acceptance through WiVRn and ALVR/SteamVR is still pending.
|
||||
Controller input and canted eye orientations are not implemented.
|
||||
- Use `./build/mocktail --no-vr` for ordinary rendering. No helper script is
|
||||
needed for normal VR startup.
|
||||
|
||||
The GLES OpenXR integration test is opt-in and needs an already connected
|
||||
runtime (or a separately prepared simulator). Run from a graphical desktop:
|
||||
|
||||
```sh
|
||||
MOCKTAIL_TEST_XR_GLES=1 ./build/tests/gles_vr_transport_test \
|
||||
--gtest_filter='*OpenXrEglSession*'
|
||||
```
|
||||
@@ -51,12 +51,12 @@ graphics:
|
||||
|
||||
# vr:
|
||||
# Boolean (default: enabled in VR builds, disabled in non-VR builds).
|
||||
# Experimental VR for the exact Build ID 2998
|
||||
# payload. Mocktail initializes the guest DebugDeviceVR eye resources (two
|
||||
# Experimental VR for verified build 3092. Mocktail initializes
|
||||
# the guest DebugDeviceVR eye resources (two
|
||||
# distinct 500x500 eye framebuffers), the Roblox renderer draws both eyes
|
||||
# every frame, and the frames are submitted to the active OpenXR runtime as
|
||||
# a stereo projection layer with head pose (runtime-tracked, or scripted for
|
||||
# desktop testing). Controllers are NOT connected yet. Standalone OpenXR
|
||||
# desktop testing). Controllers use the native VR bridge. Standalone OpenXR
|
||||
# test scene: mocktail-vr-probe --scene. Use --no-vr to launch normally.
|
||||
# enabled: false
|
||||
|
||||
|
||||
@@ -91,6 +91,36 @@
|
||||
"device_create_framebuffer_vtable_offset": "0x180"
|
||||
},
|
||||
"reason": "Verified 2026-09-06: exact Build-ID profile passed two isolated no-recovery Tier C Vulkan canaries and real game sessions, including fullscreen state synchronization and host FMOD output-device interposition; legacy executable patches and signal recovery remain disabled. Experimental vr_debug_device_bridge metadata (DebugDeviceVR constructor/state/eye/initializer RVAs) is machine-validated at activation and gated behind --vr; native stereo output is not yet verified end-to-end."
|
||||
},
|
||||
{
|
||||
"allow_host_abi_bridges": true,
|
||||
"allow_host_constructor_replay": true,
|
||||
"allow_legacy_binary_patches": false,
|
||||
"default_allowed": true,
|
||||
"elf_build_id": "5f0704edd9064f566ee3d6df2bd2fabbcc709f03",
|
||||
"fmod_output_device_bridge": {
|
||||
"count_method_rva": "0x320c676",
|
||||
"current_method_rva": "0x320c6c6",
|
||||
"info_method_rva": "0x320c716",
|
||||
"select_method_rva": "0x320bf78",
|
||||
"string_constructor_rva": "0x1d7a038",
|
||||
"vtable_layout_version": 2,
|
||||
"vtable_rva": "0x6cd3ce0"
|
||||
},
|
||||
"reason": "Exact-Build-ID host ABI promoted from the validated probation profile; native stereo, VR state/pointer/haptic contracts and FMOD output-device layout v2 were re-derived for 3092. Physical headset acceptance remains pending.",
|
||||
"status": "supported",
|
||||
"user_game_settings_fullscreen_setter_rva": "0x45c5f42",
|
||||
"version_code": 3092,
|
||||
"version_name": "2.738.1397",
|
||||
"vr_debug_device_bridge": {
|
||||
"vtable_rva": "0x6d08108",
|
||||
"constructor_rva": "0x37f9fe4",
|
||||
"state_getter_rva": "0x37fa3c6",
|
||||
"eye_getter_rva": "0x37fa3b6",
|
||||
"eye_initializer_rva": "0x37fa3e8",
|
||||
"emulator_flag_storage_rva": "0x73f4690",
|
||||
"device_create_framebuffer_vtable_offset": "0x188"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -22,13 +22,12 @@ struct FmodOutputDeviceBridgeProfile {
|
||||
std::uintptr_t info_method_rva = 0;
|
||||
std::uintptr_t current_method_rva = 0;
|
||||
std::uintptr_t select_method_rva = 0;
|
||||
std::uint32_t vtable_layout_version = 1;
|
||||
};
|
||||
|
||||
// Exact-build metadata for the experimental native VR bridge. The bridge
|
||||
// interposes only DebugDeviceVR virtual slots (state getter and eye
|
||||
// framebuffer getter) and invokes the guest eye-resource initializer through
|
||||
// its own vtable; executable guest code is never modified. All RVAs are
|
||||
// validated against machine-code contracts before the first use.
|
||||
// interposes DebugDeviceVR slots and a checked world-pointer trampoline.
|
||||
// All RVAs are validated against machine-code contracts before first use.
|
||||
struct VrDebugDeviceBridgeProfile {
|
||||
std::uintptr_t vtable_rva = 0;
|
||||
std::uintptr_t constructor_rva = 0;
|
||||
|
||||
@@ -15,9 +15,12 @@
|
||||
namespace mocktail::audio {
|
||||
|
||||
class NativeInputCapture;
|
||||
namespace internal {
|
||||
struct FmodInputCaptureProfile;
|
||||
}
|
||||
|
||||
// Replaces the output-device query/select slots of the exact Build-ID-scoped
|
||||
// FmodAudioDevice vtable, and on Build 2998 its microphone capture slots.
|
||||
// FmodAudioDevice vtable, including microphone capture on verified builds.
|
||||
// Roblox keeps owning its FMOD engine, while the existing settings UI sees
|
||||
// and selects the SDL host routes that consume Android AudioTrack PCM.
|
||||
class RobloxOutputDeviceBridge final {
|
||||
@@ -59,7 +62,7 @@ class RobloxOutputDeviceBridge final {
|
||||
void ConstructGuestString(void* destination, std::string_view value) const;
|
||||
|
||||
std::unique_ptr<NativeInputCapture> capture_;
|
||||
bool capture_profile_supported_ = false;
|
||||
const internal::FmodInputCaptureProfile* capture_profile_ = nullptr;
|
||||
std::array<std::uintptr_t, 6> original_capture_methods_{};
|
||||
mutable std::mutex mutex_;
|
||||
compat::FmodOutputDeviceBridgeProfile profile_;
|
||||
|
||||
@@ -17,6 +17,16 @@
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
namespace internal {
|
||||
// Consume binary present dependencies exactly once. On success a later present
|
||||
// must omit them; on failure the caller must propagate the error, not retry.
|
||||
VkResult WaitForPresentDependencies(VkDevice device, VkQueue queue,
|
||||
const VkPresentInfoKHR& present,
|
||||
VkFence fence, PFN_vkResetFences reset,
|
||||
PFN_vkQueueSubmit submit,
|
||||
PFN_vkWaitForFences wait);
|
||||
} // namespace internal
|
||||
|
||||
// Resolved --vr output mode. kXrOutput drives the real Roblox stereo images
|
||||
// into an OpenXR projection layer; kNativeOnly keeps the diagnostic native
|
||||
// stereo prototype without any runtime requirement. Selected through
|
||||
@@ -56,7 +66,10 @@ struct ScriptedPoseSample {
|
||||
float eye_fov[2][4] = {}; // left, right, up, down angles
|
||||
// Index 0 = left, 1 = right (Roblox UserCFrame LeftHand/RightHand).
|
||||
VrHandPose hands[2];
|
||||
// Exact 2998 DebugDeviceVR input channels, published with the same pose.
|
||||
float floor_position[3] = {};
|
||||
float floor_orientation[4] = {0.f, 0.f, 0.f, 1.f};
|
||||
bool floor_valid = false;
|
||||
// Verified DebugDeviceVR input channels, published with the same pose.
|
||||
float controller_channels[28] = {};
|
||||
bool controllers_connected = false;
|
||||
std::uint64_t frame = 0;
|
||||
@@ -126,6 +139,24 @@ class OpenXrBackend final {
|
||||
void NoteInstanceDestroyed(VkInstance instance);
|
||||
void NoteQueue(VkDevice device, VkQueue queue, std::uint32_t family,
|
||||
std::uint32_t index);
|
||||
std::vector<std::uint32_t> DesktopQueueFamilies(VkDevice device) const;
|
||||
void RecordCommandPool(VkDevice device, VkCommandPool pool,
|
||||
std::uint32_t family);
|
||||
void RecordCommandBuffers(VkDevice device, VkCommandPool pool,
|
||||
std::uint32_t count,
|
||||
const VkCommandBuffer* buffers);
|
||||
void ForgetCommandBuffers(std::uint32_t count,
|
||||
const VkCommandBuffer* buffers);
|
||||
void ForgetCommandPool(VkCommandPool pool);
|
||||
void ResetCommandPool(VkCommandPool pool);
|
||||
void ResetCommandBuffer(VkCommandBuffer buffer);
|
||||
void NoteCommandBuffersSubmitted(VkQueue queue, std::uint32_t count,
|
||||
const VkCommandBuffer* buffers);
|
||||
VkImageLayout NormalizeDesktopBarrier(VkCommandBuffer command, VkImage image,
|
||||
std::uint32_t* source,
|
||||
std::uint32_t* destination,
|
||||
VkImageLayout* old_layout,
|
||||
VkImageLayout new_layout);
|
||||
|
||||
// Resource provenance recording (adapter wrappers).
|
||||
bool WantsResourceRecords() const {
|
||||
@@ -149,8 +180,14 @@ class OpenXrBackend final {
|
||||
|
||||
// Render-thread frame cycle at host present begin.
|
||||
void NoteHostPresent(VkQueue queue, VkDevice device);
|
||||
// VK_NOT_READY leaves the present untouched until eye submission provenance
|
||||
// is known. VK_SUCCESS means its waits were consumed on the graphics queue.
|
||||
VkResult PrepareHostPresent(VkQueue queue, VkDevice device,
|
||||
const VkPresentInfoKHR* info);
|
||||
void RecordDesktopSwapchain(VkDevice device, VkSwapchainKHR swapchain,
|
||||
const VkSwapchainCreateInfoKHR* info, const VkImage* images, unsigned count);
|
||||
const VkSwapchainCreateInfoKHR* info,
|
||||
const VkImage* images, unsigned count,
|
||||
bool converted_to_concurrent = false);
|
||||
// On success the original present semaphores have been consumed and the
|
||||
// copy has completed. The caller must present with no wait semaphores.
|
||||
VkResult MirrorDesktop(VkQueue queue, const VkPresentInfoKHR* info);
|
||||
@@ -197,6 +234,7 @@ class OpenXrBackend final {
|
||||
std::uint32_t height = 0;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageUsageFlags usage = 0;
|
||||
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
void* owner = nullptr;
|
||||
std::uint64_t order = 0;
|
||||
};
|
||||
@@ -251,6 +289,9 @@ class OpenXrBackend final {
|
||||
void HandleReferenceSpaceChange(std::int64_t change_time);
|
||||
bool RecoverSessionLocked(std::uint64_t now_ns);
|
||||
void PollSessionEvents();
|
||||
bool SupportsPresentQueueLocked(VkQueue queue) const;
|
||||
VkQueue GraphicsQueueForPresentLocked(VkQueue present) const;
|
||||
bool unsupported_present_queue_logged_ = false;
|
||||
// Drops the pose/images of the frame currently in flight without touching
|
||||
// guest ownership, so the next xrEndFrame submits no layer instead of an
|
||||
// image paired with a pose that no longer describes the same origin.
|
||||
@@ -290,6 +331,7 @@ class OpenXrBackend final {
|
||||
void* xr_session_ = nullptr;
|
||||
void* xr_local_space_ = nullptr;
|
||||
void* xr_view_space_ = nullptr;
|
||||
void* xr_stage_space_ = nullptr;
|
||||
std::uint32_t xr_eye_width_[2] = {};
|
||||
std::uint32_t xr_eye_height_[2] = {};
|
||||
EyeSwapchain eye_swapchains_[2];
|
||||
@@ -302,6 +344,8 @@ class OpenXrBackend final {
|
||||
VkDevice vk_device_ = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice vk_physical_device_ = VK_NULL_HANDLE;
|
||||
std::uint32_t vk_queue_family_ = 0;
|
||||
VkQueue vk_graphics_queue_ = VK_NULL_HANDLE;
|
||||
std::vector<std::uint32_t> device_queue_families_;
|
||||
void* vk_loader_ = nullptr;
|
||||
struct VkProcs;
|
||||
VkProcs* vk_ = nullptr;
|
||||
@@ -319,6 +363,9 @@ class OpenXrBackend final {
|
||||
VkExtent2D extent;
|
||||
VkFormat format;
|
||||
std::vector<VkImage> images;
|
||||
VkSharingMode sharing_mode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
std::vector<std::uint32_t> families;
|
||||
bool converted_to_concurrent = false;
|
||||
};
|
||||
std::unordered_map<VkSwapchainKHR, DesktopSwapchain> desktop_swapchains_;
|
||||
std::unordered_map<VkImageView, VkImage> image_views_;
|
||||
@@ -326,6 +373,15 @@ class OpenXrBackend final {
|
||||
std::unordered_map<VkFramebuffer, VkRenderPass> framebuffer_render_pass_;
|
||||
std::unordered_map<VkRenderPass, std::vector<VkImageLayout>> render_pass_final_layouts_;
|
||||
std::unordered_map<VkQueue, std::uint32_t> queue_families_;
|
||||
struct CommandRecord {
|
||||
VkCommandPool pool = VK_NULL_HANDLE;
|
||||
std::uint32_t family = UINT32_MAX;
|
||||
std::vector<std::pair<VkImage, std::uint64_t>> eye_images;
|
||||
};
|
||||
std::unordered_map<VkCommandPool, std::uint32_t> command_pool_families_;
|
||||
std::unordered_map<VkCommandBuffer, CommandRecord> command_records_;
|
||||
std::unordered_map<VkImage, VkQueue> eye_render_queues_;
|
||||
std::unordered_map<VkImage, std::uint64_t> eye_render_frames_;
|
||||
std::uint64_t image_order_ = 0;
|
||||
EyeBinding eyes_[2];
|
||||
|
||||
@@ -353,7 +409,7 @@ class OpenXrBackend final {
|
||||
bool frame_views_valid_ = false;
|
||||
// One-shot guard so a canted-view runtime reports the rejection reason once
|
||||
// instead of once per frame per eye.
|
||||
bool canted_rejection_logged_ = false;
|
||||
bool projection_rejection_logged_ = false;
|
||||
mutable ScriptedPoseSample published_pose_;
|
||||
std::uint64_t xr_frame_counter_ = 0;
|
||||
std::unordered_map<void*, std::uint64_t> applied_poses_;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string_view>
|
||||
|
||||
#include "compat/build_profile.h"
|
||||
#include "mocktail/status.h"
|
||||
@@ -11,8 +12,12 @@
|
||||
namespace mocktail::vr {
|
||||
namespace internal {
|
||||
struct VrBridgeTestAccess;
|
||||
struct VrBuildContract;
|
||||
}
|
||||
|
||||
const compat::VrDebugDeviceBridgeProfile* FindRobloxVrDeviceProfile(
|
||||
std::string_view build_id);
|
||||
|
||||
// Experimental native stereo bring-up for the exact Build-ID-scoped
|
||||
// RBX::Graphics::DebugDeviceVR object of the guest client.
|
||||
//
|
||||
@@ -21,7 +26,7 @@ struct VrBridgeTestAccess;
|
||||
// initialization never dereferences cached guest object/device pointers. The
|
||||
// guest owns resource destruction. Recreated objects and reused addresses
|
||||
// therefore need no cache invalidation. Initialization happens only on a thread
|
||||
// observed presenting. OpenXR image submission and poses are not connected yet.
|
||||
// observed presenting.
|
||||
class RobloxVrDeviceBridge final {
|
||||
public:
|
||||
using StateGetterFn = void *(*)(void *result_buffer, void *device_object);
|
||||
@@ -54,6 +59,7 @@ public:
|
||||
|
||||
// Guest-side hooks invoked by the interposed vtable stubs.
|
||||
void OnHapticsCall(void* device_object, int hand, float amplitude);
|
||||
void OnPointerFrameCall(void* result, void* service);
|
||||
void OnStateGetterCall(void *result_buffer, void *device_object);
|
||||
void OnEyeGetterCall(void *device_object, int eye_index, void *framebuffer);
|
||||
|
||||
@@ -85,6 +91,7 @@ private:
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
compat::VrDebugDeviceBridgeProfile profile_{};
|
||||
const internal::VrBuildContract* contract_ = nullptr;
|
||||
bool installed_ = false;
|
||||
std::atomic<bool> active_{false};
|
||||
std::atomic<std::uintptr_t> image_base_{0};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef MOCKTAIL_VR_POSE_MATH_H_
|
||||
#define MOCKTAIL_VR_POSE_MATH_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
@@ -147,16 +148,61 @@ inline ProjectionTangents FovToTangents(const EyeFov& fov) {
|
||||
std::tan(-fov.angle_left), std::tan(fov.angle_right)};
|
||||
}
|
||||
|
||||
// The pinned guest state can only represent two parallel eye orientations.
|
||||
// A canted (rotated) relative eye pose needs a different camera ABI, so it is
|
||||
// rejected explicitly rather than silently submitted with a projection that
|
||||
// does not match how the image was rendered.
|
||||
inline constexpr float kCantedTolerance = 0.001f;
|
||||
inline constexpr float kMaxEyeFovAngle = 1.56f;
|
||||
|
||||
inline bool EyeOrientationIsParallel(const Quat& relative_orientation) {
|
||||
return std::abs(relative_orientation.x) <= kCantedTolerance &&
|
||||
std::abs(relative_orientation.y) <= kCantedTolerance &&
|
||||
std::abs(relative_orientation.z) <= kCantedTolerance;
|
||||
// Enclose the display frustum in a head-parallel camera for the guest renderer.
|
||||
// The compositor receives this rendered FOV and pose, not the canted display's.
|
||||
inline bool ParallelEyeFov(const Quat& eye_orientation, const EyeFov& display,
|
||||
EyeFov* rendered) {
|
||||
Quat orientation;
|
||||
if (!rendered || !QuatNormalize(eye_orientation, &orientation) ||
|
||||
!(display.angle_left < display.angle_right) ||
|
||||
!(display.angle_down < display.angle_up)) {
|
||||
return false;
|
||||
}
|
||||
const float angles[] = {display.angle_left, display.angle_right,
|
||||
display.angle_up, display.angle_down};
|
||||
for (float angle : angles) {
|
||||
if (!std::isfinite(angle) || std::abs(angle) >= kMaxEyeFovAngle)
|
||||
return false;
|
||||
}
|
||||
if (orientation.x == 0.f && orientation.y == 0.f && orientation.z == 0.f) {
|
||||
*rendered = display;
|
||||
return true;
|
||||
}
|
||||
const float xs[] = {std::tan(display.angle_left),
|
||||
std::tan(display.angle_right)};
|
||||
const float ys[] = {std::tan(display.angle_down), std::tan(display.angle_up)};
|
||||
float left = 0.f, right = 0.f, down = 0.f, up = 0.f;
|
||||
bool first = true;
|
||||
for (float x : xs) {
|
||||
for (float y : ys) {
|
||||
const Vec3 ray = QuatRotate(orientation, {x, y, -1.f});
|
||||
if (!std::isfinite(ray.z) || ray.z >= -1e-6f) return false;
|
||||
const float horizontal = ray.x / -ray.z;
|
||||
const float vertical = ray.y / -ray.z;
|
||||
if (!std::isfinite(horizontal) || !std::isfinite(vertical)) return false;
|
||||
if (first) {
|
||||
left = right = horizontal;
|
||||
down = up = vertical;
|
||||
first = false;
|
||||
} else {
|
||||
left = std::min(left, horizontal);
|
||||
right = std::max(right, horizontal);
|
||||
down = std::min(down, vertical);
|
||||
up = std::max(up, vertical);
|
||||
}
|
||||
}
|
||||
}
|
||||
const EyeFov result{std::atan(left), std::atan(right), std::atan(up),
|
||||
std::atan(down)};
|
||||
const float bounds[] = {result.angle_left, result.angle_right,
|
||||
result.angle_up, result.angle_down};
|
||||
for (float angle : bounds) {
|
||||
if (std::abs(angle) >= kMaxEyeFovAngle) return false;
|
||||
}
|
||||
*rendered = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Metres-to-studs factor the exact-build native helper applies itself. Exposed
|
||||
|
||||
Executable
+1510
File diff suppressed because it is too large.
Load diff
@@ -15,12 +15,7 @@
|
||||
|
||||
#include "mocktail/audio/sdl_audio_capture.h"
|
||||
|
||||
// Port of main's native input capture (537828a "Fix vc") to Build 2998.
|
||||
// Main pins the voice sink vtable, its PCM entry point and the shared_ptr
|
||||
// destructor to 2.738 RVAs. Here the sink is dispatched through its own
|
||||
// vtable, which is validated against the guest image first, and the
|
||||
// shared_ptr is released through the libc++ control-block ABI, so no
|
||||
// further 2998 RVAs are needed.
|
||||
// Sink calls and shared ownership use the validated guest vtables.
|
||||
namespace mocktail::audio {
|
||||
class NativeInputCapture;
|
||||
|
||||
|
||||
@@ -30,8 +30,6 @@ namespace {
|
||||
|
||||
constexpr std::size_t kOutputCountVtableIndex = 5;
|
||||
constexpr std::size_t kOutputInfoVtableIndex = 6;
|
||||
constexpr std::size_t kCurrentOutputVtableIndex = 7;
|
||||
constexpr std::size_t kSelectOutputVtableIndex = 17;
|
||||
constexpr std::size_t kGuestStringSize = 24;
|
||||
constexpr std::size_t kGuestDeviceInfoSize = kGuestStringSize * 2 + 1;
|
||||
constexpr std::size_t kStringConstructorContractSize = 24;
|
||||
@@ -39,22 +37,15 @@ constexpr std::size_t kMaximumMenuDevices = 128;
|
||||
constexpr std::size_t kMaximumDeviceNameBytes = 512;
|
||||
constexpr std::size_t kFirstPatchedVtableIndex = 4;
|
||||
|
||||
// Microphone capture on Build 2998 (Roblox 2.736.1408 x86_64). Roblox 2.738
|
||||
// inserted device-list methods at slots 7 and 11; main verified its capture
|
||||
// methods at {4, 13..17}, between the input current (12) and select (18)
|
||||
// slots, so 2998 keeps them at {4, 11..15}, between input current (10) and
|
||||
// select (16). The input slots, with main's input RVAs for this image, anchor
|
||||
// that layout before anything is patched.
|
||||
constexpr std::string_view kCaptureBuildId =
|
||||
"ade08266c67aee88ec9c1d00902150e1684dad3a";
|
||||
constexpr std::uintptr_t kCaptureVtableRva = 0x6c58040;
|
||||
// Format, latency, start, stop, poll, recording.
|
||||
constexpr std::array<std::size_t, 6> kCaptureSlots{4, 11, 12, 13, 14, 15};
|
||||
constexpr std::array<std::size_t, 4> kInputAnchorSlots{8, 9, 10, 16};
|
||||
constexpr std::array<std::uintptr_t, 4> kInputAnchorRvas{
|
||||
0x32d12dc, 0x32d12ea, 0x32d13a6, 0x32d0a56};
|
||||
// On 2.738 the capture methods lie within 0x1400 bytes of the input methods.
|
||||
constexpr std::uintptr_t kCaptureCodeWindow = 0x4000;
|
||||
std::size_t CurrentOutputVtableIndex(
|
||||
const compat::FmodOutputDeviceBridgeProfile& profile) {
|
||||
return profile.vtable_layout_version == 2 ? 8 : 7;
|
||||
}
|
||||
|
||||
std::size_t SelectOutputVtableIndex(
|
||||
const compat::FmodOutputDeviceBridgeProfile& profile) {
|
||||
return profile.vtable_layout_version == 2 ? 19 : 17;
|
||||
}
|
||||
|
||||
std::atomic<RobloxOutputDeviceBridge*> g_active_bridge{nullptr};
|
||||
|
||||
@@ -114,7 +105,8 @@ bool IsRelroImageRange(std::uintptr_t image_base, std::uintptr_t rva,
|
||||
return IsProgramHeaderRange(image_base, rva, size, PT_GNU_RELRO, 0);
|
||||
}
|
||||
|
||||
bool SetVtableWritable(std::uintptr_t* vtable, bool writable) {
|
||||
bool SetVtableWritable(std::uintptr_t* vtable, std::size_t last_slot,
|
||||
bool writable) {
|
||||
if (vtable == nullptr) {
|
||||
return false;
|
||||
}
|
||||
@@ -126,7 +118,7 @@ bool SetVtableWritable(std::uintptr_t* vtable, bool writable) {
|
||||
const std::uintptr_t first =
|
||||
reinterpret_cast<std::uintptr_t>(vtable + kFirstPatchedVtableIndex);
|
||||
const std::uintptr_t last =
|
||||
reinterpret_cast<std::uintptr_t>(vtable + kSelectOutputVtableIndex + 1);
|
||||
reinterpret_cast<std::uintptr_t>(vtable + last_slot + 1);
|
||||
if (last <= first) {
|
||||
return false;
|
||||
}
|
||||
@@ -150,6 +142,48 @@ std::uintptr_t FunctionAddress(Function function) {
|
||||
|
||||
namespace internal {
|
||||
|
||||
const FmodInputCaptureProfile* FindFmodInputCaptureProfile(
|
||||
const compat::BuildProfile& profile) {
|
||||
static constexpr FmodInputCaptureProfile kProfiles[] = {
|
||||
{"ade08266c67aee88ec9c1d00902150e1684dad3a",
|
||||
0x6c58040,
|
||||
1,
|
||||
{4, 11, 12, 13, 14, 15},
|
||||
{0x32d0e9a, 0x32d0a34, 0x32d16a8, 0x32d1f84, 0x32d056c, 0x32d26ac}},
|
||||
{"5f0704edd9064f566ee3d6df2bd2fabbcc709f03",
|
||||
0x6cd3ce0,
|
||||
2,
|
||||
{4, 13, 14, 15, 16, 17},
|
||||
{0x320c630, 0x320bcf6, 0x320d4b4, 0x320dd94, 0x320b844, 0x320e49a}},
|
||||
};
|
||||
if (!profile.allow_host_abi_bridges || !profile.fmod_output_device_bridge) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto& output = *profile.fmod_output_device_bridge;
|
||||
for (const auto& capture : kProfiles) {
|
||||
if (capture.build_id == profile.elf_build_id &&
|
||||
capture.vtable_rva == output.vtable_rva &&
|
||||
capture.vtable_layout_version == output.vtable_layout_version) {
|
||||
return &capture;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool HasExpectedFmodInputCaptureVtable(const std::uintptr_t* vtable,
|
||||
std::uintptr_t image_base,
|
||||
const FmodInputCaptureProfile& profile) {
|
||||
if (vtable == nullptr || image_base == 0) return false;
|
||||
for (std::size_t i = 0; i < profile.slots.size(); ++i) {
|
||||
if (profile.method_rvas[i] >
|
||||
std::numeric_limits<std::uintptr_t>::max() - image_base ||
|
||||
vtable[profile.slots[i]] != image_base + profile.method_rvas[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HasExpectedFmodStringConstructorContract(const std::uint8_t* code,
|
||||
std::size_t size) {
|
||||
constexpr std::array<std::uint8_t, kStringConstructorContractSize>
|
||||
@@ -165,14 +199,16 @@ bool HasExpectedFmodStringConstructorContract(const std::uint8_t* code,
|
||||
bool HasExpectedFmodOutputDeviceVtable(
|
||||
const std::uintptr_t* vtable, std::uintptr_t image_base,
|
||||
const compat::FmodOutputDeviceBridgeProfile& profile) {
|
||||
return vtable != nullptr && image_base != 0 &&
|
||||
return (profile.vtable_layout_version == 1 ||
|
||||
profile.vtable_layout_version == 2) &&
|
||||
vtable != nullptr && image_base != 0 &&
|
||||
vtable[kOutputCountVtableIndex] ==
|
||||
image_base + profile.count_method_rva &&
|
||||
vtable[kOutputInfoVtableIndex] ==
|
||||
image_base + profile.info_method_rva &&
|
||||
vtable[kCurrentOutputVtableIndex] ==
|
||||
vtable[CurrentOutputVtableIndex(profile)] ==
|
||||
image_base + profile.current_method_rva &&
|
||||
vtable[kSelectOutputVtableIndex] ==
|
||||
vtable[SelectOutputVtableIndex(profile)] ==
|
||||
image_base + profile.select_method_rva;
|
||||
}
|
||||
|
||||
@@ -209,6 +245,10 @@ Status RobloxOutputDeviceBridge::Install(const compat::BuildProfile& profile) {
|
||||
if (!profile.fmod_output_device_bridge.has_value()) {
|
||||
return Status::Ok();
|
||||
}
|
||||
if (profile.fmod_output_device_bridge->vtable_layout_version != 1 &&
|
||||
profile.fmod_output_device_bridge->vtable_layout_version != 2) {
|
||||
return FailedPrecondition("unsupported FMOD output-device vtable layout");
|
||||
}
|
||||
if (!profile.allow_host_abi_bridges) {
|
||||
return FailedPrecondition(
|
||||
"FMOD output-device profile requires host ABI bridges");
|
||||
@@ -223,10 +263,10 @@ Status RobloxOutputDeviceBridge::Install(const compat::BuildProfile& profile) {
|
||||
}
|
||||
profile_ = *profile.fmod_output_device_bridge;
|
||||
const char* native_capture = std::getenv("MOCKTAIL_NATIVE_INPUT_CAPTURE");
|
||||
capture_profile_supported_ =
|
||||
profile.elf_build_id == kCaptureBuildId &&
|
||||
profile_.vtable_rva == kCaptureVtableRva &&
|
||||
!(native_capture != nullptr && std::strcmp(native_capture, "0") == 0);
|
||||
capture_profile_ =
|
||||
native_capture != nullptr && std::strcmp(native_capture, "0") == 0
|
||||
? nullptr
|
||||
: internal::FindFmodInputCaptureProfile(profile);
|
||||
if (!linker::SetAndroidLibraryLoadObserver(&ObserveAndroidLibrary, this)) {
|
||||
g_active_bridge.store(nullptr, std::memory_order_release);
|
||||
profile_ = {};
|
||||
@@ -264,6 +304,7 @@ void RobloxOutputDeviceBridge::Shutdown() {
|
||||
(void)g_active_bridge.compare_exchange_strong(
|
||||
expected, nullptr, std::memory_order_acq_rel, std::memory_order_acquire);
|
||||
profile_ = {};
|
||||
capture_profile_ = nullptr;
|
||||
installed_ = false;
|
||||
}
|
||||
|
||||
@@ -372,13 +413,13 @@ Status RobloxOutputDeviceBridge::Activate(std::uintptr_t image_base) {
|
||||
Status RobloxOutputDeviceBridge::PatchVtableLocked() {
|
||||
if (profile_.vtable_rva >
|
||||
std::numeric_limits<std::uintptr_t>::max() -
|
||||
(kSelectOutputVtableIndex + 1) * sizeof(std::uintptr_t)) {
|
||||
(SelectOutputVtableIndex(profile_) + 1) * sizeof(std::uintptr_t)) {
|
||||
return FailedPrecondition("FMOD output-device vtable range overflows");
|
||||
}
|
||||
const std::uintptr_t first_slot_rva =
|
||||
profile_.vtable_rva + kFirstPatchedVtableIndex * sizeof(std::uintptr_t);
|
||||
const std::size_t slot_range_size =
|
||||
(kSelectOutputVtableIndex - kFirstPatchedVtableIndex + 1) *
|
||||
(SelectOutputVtableIndex(profile_) - kFirstPatchedVtableIndex + 1) *
|
||||
sizeof(std::uintptr_t);
|
||||
if (!IsRelroImageRange(library_base_, first_slot_rva, slot_range_size) ||
|
||||
!IsExecutableImageRange(library_base_, profile_.string_constructor_rva,
|
||||
@@ -403,7 +444,7 @@ Status RobloxOutputDeviceBridge::PatchVtableLocked() {
|
||||
"FmodAudioDevice string ABI contract validation failed");
|
||||
}
|
||||
|
||||
if (capture_profile_supported_) {
|
||||
if (capture_profile_ != nullptr) {
|
||||
// The output routes do not depend on capture; keep them if it is refused.
|
||||
const Status capture_status = PrepareCaptureLocked();
|
||||
if (!capture_status.ok()) {
|
||||
@@ -415,10 +456,10 @@ Status RobloxOutputDeviceBridge::PatchVtableLocked() {
|
||||
original_methods_ = {
|
||||
vtable_[kOutputCountVtableIndex],
|
||||
vtable_[kOutputInfoVtableIndex],
|
||||
vtable_[kCurrentOutputVtableIndex],
|
||||
vtable_[kSelectOutputVtableIndex],
|
||||
vtable_[CurrentOutputVtableIndex(profile_)],
|
||||
vtable_[SelectOutputVtableIndex(profile_)],
|
||||
};
|
||||
if (!SetVtableWritable(vtable_, true)) {
|
||||
if (!SetVtableWritable(vtable_, SelectOutputVtableIndex(profile_), true)) {
|
||||
capture_.reset();
|
||||
return Status::Error(StatusCode::kPlatformError,
|
||||
"cannot make FmodAudioDevice vtable writable");
|
||||
@@ -428,9 +469,9 @@ Status RobloxOutputDeviceBridge::PatchVtableLocked() {
|
||||
FunctionAddress(&GetOutputDeviceCount), __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[kOutputInfoVtableIndex],
|
||||
FunctionAddress(&GetOutputDeviceInfo), __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[kCurrentOutputVtableIndex],
|
||||
__atomic_store_n(&vtable_[CurrentOutputVtableIndex(profile_)],
|
||||
FunctionAddress(&GetCurrentOutputDevice), __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[kSelectOutputVtableIndex],
|
||||
__atomic_store_n(&vtable_[SelectOutputVtableIndex(profile_)],
|
||||
FunctionAddress(&SetCurrentOutputDevice), __ATOMIC_RELEASE);
|
||||
if (capture_) {
|
||||
g_native_capture.store(capture_.get(), std::memory_order_release);
|
||||
@@ -441,74 +482,55 @@ Status RobloxOutputDeviceBridge::PatchVtableLocked() {
|
||||
FunctionAddress(&NativeInputCapture::Stop),
|
||||
FunctionAddress(&NativeInputCapture::Poll),
|
||||
FunctionAddress(&NativeInputCapture::Recording)};
|
||||
for (std::size_t i = 0; i < kCaptureSlots.size(); ++i)
|
||||
__atomic_store_n(&vtable_[kCaptureSlots[i]], methods[i], __ATOMIC_RELEASE);
|
||||
for (std::size_t i = 0; i < capture_profile_->slots.size(); ++i)
|
||||
__atomic_store_n(&vtable_[capture_profile_->slots[i]], methods[i],
|
||||
__ATOMIC_RELEASE);
|
||||
}
|
||||
if (!SetVtableWritable(vtable_, false)) {
|
||||
if (!SetVtableWritable(vtable_, SelectOutputVtableIndex(profile_), false)) {
|
||||
__atomic_store_n(&vtable_[kOutputCountVtableIndex], original_methods_[0],
|
||||
__ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[kOutputInfoVtableIndex], original_methods_[1],
|
||||
__ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[kCurrentOutputVtableIndex], original_methods_[2],
|
||||
__ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[kSelectOutputVtableIndex], original_methods_[3],
|
||||
__ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[CurrentOutputVtableIndex(profile_)],
|
||||
original_methods_[2], __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[SelectOutputVtableIndex(profile_)],
|
||||
original_methods_[3], __ATOMIC_RELEASE);
|
||||
if (capture_) {
|
||||
for (std::size_t i = 0; i < kCaptureSlots.size(); ++i)
|
||||
__atomic_store_n(&vtable_[kCaptureSlots[i]],
|
||||
for (std::size_t i = 0; i < capture_profile_->slots.size(); ++i)
|
||||
__atomic_store_n(&vtable_[capture_profile_->slots[i]],
|
||||
original_capture_methods_[i], __ATOMIC_RELEASE);
|
||||
NativeInputCapture::Retire(std::move(capture_));
|
||||
}
|
||||
(void)SetVtableWritable(vtable_, false);
|
||||
(void)SetVtableWritable(vtable_, SelectOutputVtableIndex(profile_), false);
|
||||
return Status::Error(StatusCode::kPlatformError,
|
||||
"cannot restore FmodAudioDevice RELRO protection");
|
||||
}
|
||||
if (capture_) {
|
||||
std::fprintf(stderr,
|
||||
" [audio-input] native capture armed for Build ID %s "
|
||||
"(slots 4/11-15 = %#" PRIxPTR "/%#" PRIxPTR "/%#" PRIxPTR
|
||||
"/%#" PRIxPTR "/%#" PRIxPTR "/%#" PRIxPTR ")\n",
|
||||
kCaptureBuildId.data(),
|
||||
original_capture_methods_[0] - library_base_,
|
||||
original_capture_methods_[1] - library_base_,
|
||||
original_capture_methods_[2] - library_base_,
|
||||
original_capture_methods_[3] - library_base_,
|
||||
original_capture_methods_[4] - library_base_,
|
||||
original_capture_methods_[5] - library_base_);
|
||||
"(format/latency/start/stop/poll/recording slots "
|
||||
"%zu/%zu/%zu/%zu/%zu/%zu)\n",
|
||||
capture_profile_->build_id.data(), capture_profile_->slots[0],
|
||||
capture_profile_->slots[1], capture_profile_->slots[2],
|
||||
capture_profile_->slots[3], capture_profile_->slots[4],
|
||||
capture_profile_->slots[5]);
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status RobloxOutputDeviceBridge::PrepareCaptureLocked() {
|
||||
for (std::size_t i = 0; i < kInputAnchorSlots.size(); ++i) {
|
||||
if (vtable_[kInputAnchorSlots[i]] != library_base_ + kInputAnchorRvas[i]) {
|
||||
return FailedPrecondition(
|
||||
"FmodAudioDevice input slots do not match Build 2998");
|
||||
}
|
||||
if (!capture_profile_ || !internal::HasExpectedFmodInputCaptureVtable(
|
||||
vtable_, library_base_, *capture_profile_)) {
|
||||
return FailedPrecondition(
|
||||
"FmodAudioDevice capture vtable does not match the Build ID");
|
||||
}
|
||||
const auto [lowest_input, highest_input] =
|
||||
std::minmax_element(kInputAnchorRvas.begin(), kInputAnchorRvas.end());
|
||||
for (std::size_t i = 0; i < kCaptureSlots.size(); ++i) {
|
||||
const std::uintptr_t method = vtable_[kCaptureSlots[i]];
|
||||
const std::uintptr_t rva = method - library_base_;
|
||||
if (method < library_base_ ||
|
||||
!IsExecutableImageRange(library_base_, rva, 1) ||
|
||||
rva + kCaptureCodeWindow < *lowest_input ||
|
||||
rva > *highest_input + kCaptureCodeWindow) {
|
||||
for (std::size_t i = 0; i < capture_profile_->slots.size(); ++i) {
|
||||
if (!IsExecutableImageRange(library_base_, capture_profile_->method_rvas[i],
|
||||
1)) {
|
||||
return FailedPrecondition(
|
||||
"FmodAudioDevice capture slot is outside the device code");
|
||||
"FmodAudioDevice capture method is outside executable code");
|
||||
}
|
||||
const bool repeated =
|
||||
std::find(original_capture_methods_.begin(),
|
||||
original_capture_methods_.begin() + i,
|
||||
method) != original_capture_methods_.begin() + i ||
|
||||
std::find(kInputAnchorRvas.begin(), kInputAnchorRvas.end(), rva) !=
|
||||
kInputAnchorRvas.end();
|
||||
if (repeated) {
|
||||
return FailedPrecondition(
|
||||
"FmodAudioDevice capture slots are not distinct methods");
|
||||
}
|
||||
original_capture_methods_[i] = method;
|
||||
original_capture_methods_[i] = vtable_[capture_profile_->slots[i]];
|
||||
}
|
||||
const char* input_device = std::getenv("MOCKTAIL_AUDIO_INPUT_DEVICE");
|
||||
const bool enabled =
|
||||
@@ -523,23 +545,24 @@ Status RobloxOutputDeviceBridge::PrepareCaptureLocked() {
|
||||
}
|
||||
|
||||
bool RobloxOutputDeviceBridge::RestoreVtableLocked() {
|
||||
if (vtable_ == nullptr || !SetVtableWritable(vtable_, true)) {
|
||||
if (vtable_ == nullptr ||
|
||||
!SetVtableWritable(vtable_, SelectOutputVtableIndex(profile_), true)) {
|
||||
return false;
|
||||
}
|
||||
__atomic_store_n(&vtable_[kOutputCountVtableIndex], original_methods_[0],
|
||||
__ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[kOutputInfoVtableIndex], original_methods_[1],
|
||||
__ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[kCurrentOutputVtableIndex], original_methods_[2],
|
||||
__ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[kSelectOutputVtableIndex], original_methods_[3],
|
||||
__ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[CurrentOutputVtableIndex(profile_)],
|
||||
original_methods_[2], __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[SelectOutputVtableIndex(profile_)],
|
||||
original_methods_[3], __ATOMIC_RELEASE);
|
||||
if (capture_) {
|
||||
for (std::size_t i = 0; i < kCaptureSlots.size(); ++i)
|
||||
__atomic_store_n(&vtable_[kCaptureSlots[i]], original_capture_methods_[i],
|
||||
__ATOMIC_RELEASE);
|
||||
for (std::size_t i = 0; i < capture_profile_->slots.size(); ++i)
|
||||
__atomic_store_n(&vtable_[capture_profile_->slots[i]],
|
||||
original_capture_methods_[i], __ATOMIC_RELEASE);
|
||||
}
|
||||
return SetVtableWritable(vtable_, false);
|
||||
return SetVtableWritable(vtable_, SelectOutputVtableIndex(profile_), false);
|
||||
}
|
||||
|
||||
int RobloxOutputDeviceBridge::GetOutputDeviceCount(void* self) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef MOCKTAIL_AUDIO_ROBLOX_OUTPUT_DEVICE_BRIDGE_INTERNAL_H_
|
||||
#define MOCKTAIL_AUDIO_ROBLOX_OUTPUT_DEVICE_BRIDGE_INTERNAL_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
@@ -10,6 +11,22 @@
|
||||
|
||||
namespace mocktail::audio::internal {
|
||||
|
||||
struct FmodInputCaptureProfile {
|
||||
std::string_view build_id;
|
||||
std::uintptr_t vtable_rva;
|
||||
std::uint32_t vtable_layout_version;
|
||||
// Format, latency, start, stop, poll, recording.
|
||||
std::array<std::size_t, 6> slots;
|
||||
std::array<std::uintptr_t, 6> method_rvas;
|
||||
};
|
||||
|
||||
const FmodInputCaptureProfile* FindFmodInputCaptureProfile(
|
||||
const compat::BuildProfile& profile);
|
||||
|
||||
bool HasExpectedFmodInputCaptureVtable(const std::uintptr_t* vtable,
|
||||
std::uintptr_t image_base,
|
||||
const FmodInputCaptureProfile& profile);
|
||||
|
||||
bool HasExpectedFmodStringConstructorContract(const std::uint8_t* code,
|
||||
std::size_t size);
|
||||
|
||||
|
||||
@@ -119,9 +119,18 @@ bool ParseOptionalFmodOutputDeviceBridge(
|
||||
return false;
|
||||
}
|
||||
|
||||
std::uint32_t layout_version = 1;
|
||||
const auto layout = field->find("vtable_layout_version");
|
||||
if (layout != field->end()) {
|
||||
if (!layout->is_number_integer() || (*layout != 1 && *layout != 2)) {
|
||||
return false;
|
||||
}
|
||||
layout_version = layout->get<std::uint32_t>();
|
||||
}
|
||||
*bridge_profile = FmodOutputDeviceBridgeProfile{
|
||||
*vtable_rva, *string_constructor_rva, *count_method_rva,
|
||||
*info_method_rva, *current_method_rva, *select_method_rva,
|
||||
layout_version,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,27 @@ namespace mocktail::compat {
|
||||
namespace {
|
||||
|
||||
constexpr HostAbiProfile kProfiles[] = {
|
||||
// Roblox 2.738.1397; promoted from the exact-build probation profile.
|
||||
{"5f0704edd9064f566ee3d6df2bd2fabbcc709f03",
|
||||
{{{0x1d73422, HostBridgeKind::kAllocate, "small-allocate"},
|
||||
{0x1d7667a, HostBridgeKind::kUsableSize, "usable-size"},
|
||||
{0x222f4c0, HostBridgeKind::kReallocate, "reallocate"},
|
||||
{0x1d731c2, HostBridgeKind::kAllocate, "allocate"},
|
||||
{0x1d8e7f9, HostBridgeKind::kAlignedAllocate, "aligned-allocate-direct"},
|
||||
{0x1d76c19, HostBridgeKind::kFree, "free"}}},
|
||||
6,
|
||||
{0x76e2da0, 0x77efb98, 0x7c4b780, 0x400, 0x1d73838, 0x1dffad7, 0x7b87830,
|
||||
0x7b878c0, 0x400000},
|
||||
{0x1d731c2, 0x1d76c19},
|
||||
0x70e1618,
|
||||
3599,
|
||||
{{{2, 3}, {5, 3599}}},
|
||||
2,
|
||||
{{{2, 3599}}},
|
||||
1,
|
||||
2,
|
||||
{0x22308d3, 0x7b8b998},
|
||||
HostAllocatorStrategy::kNativeMimalloc},
|
||||
// Roblox 2.736.1408. Derived from the verified 2908 profile and validated
|
||||
// by two isolated no-recovery Vulkan canaries and real game sessions.
|
||||
{"ade08266c67aee88ec9c1d00902150e1684dad3a",
|
||||
@@ -13,12 +34,11 @@ constexpr HostAbiProfile kProfiles[] = {
|
||||
{0x1d2f43a, HostBridgeKind::kUsableSize, "usable-size"},
|
||||
{0x21e68bf, HostBridgeKind::kReallocate, "reallocate"},
|
||||
{0x1d2bf82, HostBridgeKind::kAllocate, "allocate"},
|
||||
{0x1d47a63, HostBridgeKind::kAlignedAllocate,
|
||||
"aligned-allocate-direct"},
|
||||
{0x1d47a63, HostBridgeKind::kAlignedAllocate, "aligned-allocate-direct"},
|
||||
{0x1d2f9d9, HostBridgeKind::kFree, "free"}}},
|
||||
6,
|
||||
{0x7652e70, 0x775fd28, 0x7bb4340, 0x400, 0x1d2c5f8, 0x1db7b57,
|
||||
0x7af0430, 0x7af04c0, 0x400000},
|
||||
{0x7652e70, 0x775fd28, 0x7bb4340, 0x400, 0x1d2c5f8, 0x1db7b57, 0x7af0430,
|
||||
0x7af04c0, 0x400000},
|
||||
{0x1d2bf82, 0x1d2f9d9},
|
||||
0x7057818,
|
||||
3570,
|
||||
@@ -38,12 +58,11 @@ constexpr HostAbiProfile kProfiles[] = {
|
||||
{0x1cf9c3a, HostBridgeKind::kUsableSize, "usable-size"},
|
||||
{0x21af33f, HostBridgeKind::kReallocate, "reallocate"},
|
||||
{0x1cf677e, HostBridgeKind::kAllocate, "allocate"},
|
||||
{0x1d12197, HostBridgeKind::kAlignedAllocate,
|
||||
"aligned-allocate-direct"},
|
||||
{0x1d12197, HostBridgeKind::kAlignedAllocate, "aligned-allocate-direct"},
|
||||
{0x1cfa1d9, HostBridgeKind::kFree, "free"}}},
|
||||
6,
|
||||
{0x7571570, 0x767e428, 0x7ad47e8, 0x400, 0x1cf6df8, 0x1d82be7,
|
||||
0x7a10730, 0x7a107c0, 0x400000},
|
||||
{0x7571570, 0x767e428, 0x7ad47e8, 0x400, 0x1cf6df8, 0x1d82be7, 0x7a10730,
|
||||
0x7a107c0, 0x400000},
|
||||
{0x1cf677e, 0x1cfa1d9},
|
||||
0x6f78a18,
|
||||
3556,
|
||||
@@ -64,12 +83,11 @@ constexpr HostAbiProfile kProfiles[] = {
|
||||
{0x1fe4c84, HostBridgeKind::kUsableSize, "usable-size"},
|
||||
{0x203daae, HostBridgeKind::kReallocate, "reallocate"},
|
||||
{0x1c39c3a, HostBridgeKind::kAllocate, "allocate"},
|
||||
{0x1c8dbf8, HostBridgeKind::kAlignedAllocate,
|
||||
"aligned-allocate-direct"},
|
||||
{0x1c8dbf8, HostBridgeKind::kAlignedAllocate, "aligned-allocate-direct"},
|
||||
{0x1c3eb72, HostBridgeKind::kFree, "free"}}},
|
||||
6,
|
||||
{0x7030ae0, 0x703a7e8, 0x72a16d0, 0x400, 0x1c39f1d, 0x2040817,
|
||||
0x71e92f0, 0x71e9380, 0x400000},
|
||||
{0x7030ae0, 0x703a7e8, 0x72a16d0, 0x400, 0x1c39f1d, 0x2040817, 0x71e92f0,
|
||||
0x71e9380, 0x400000},
|
||||
{0x1c39c3a, 0x1c3eb72},
|
||||
0x6c2c9b0,
|
||||
0x6cc8 / 8,
|
||||
|
||||
@@ -10,8 +10,11 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -22,8 +25,14 @@
|
||||
|
||||
extern "C" {
|
||||
VKAPI_ATTR VkResult VKAPI_CALL
|
||||
vkCreateInstance(const VkInstanceCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator, VkInstance* instance);
|
||||
mocktail_vulkan_idle_synchronized(PFN_vkDeviceWaitIdle raw, VkDevice device);
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mocktail_vulkan_submit_synchronized(
|
||||
PFN_vkQueueSubmit raw, VkDevice device, VkQueue queue, uint32_t count,
|
||||
const VkSubmitInfo *submits, VkFence fence);
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL
|
||||
vkCreateInstance(const VkInstanceCreateInfo *create_info,
|
||||
const VkAllocationCallbacks *allocator, VkInstance *instance);
|
||||
VKAPI_ATTR void VKAPI_CALL
|
||||
vkDestroyInstance(VkInstance instance, const VkAllocationCallbacks* allocator);
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkCreateAndroidSurfaceKHR(
|
||||
@@ -62,13 +71,29 @@ VKAPI_ATTR VkResult VKAPI_CALL vkAcquireNextImageKHR(
|
||||
VkDevice device, VkSwapchainKHR swapchain, std::uint64_t timeout,
|
||||
VkSemaphore semaphore, VkFence fence, std::uint32_t* image_index);
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkAcquireNextImage2KHR(
|
||||
VkDevice device, const VkAcquireNextImageInfoKHR* acquire_info,
|
||||
std::uint32_t* image_index);
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkWaitForFences(
|
||||
VkDevice device, std::uint32_t fence_count, const VkFence* fences,
|
||||
VkBool32 wait_all, std::uint64_t timeout);
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkResetFences(
|
||||
VkDevice device, std::uint32_t fence_count, const VkFence* fences);
|
||||
VkDevice device, const VkAcquireNextImageInfoKHR *acquire_info,
|
||||
std::uint32_t *image_index);
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkWaitForFences(VkDevice device,
|
||||
std::uint32_t fence_count,
|
||||
const VkFence *fences,
|
||||
VkBool32 wait_all,
|
||||
std::uint64_t timeout);
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkResetFences(VkDevice device,
|
||||
std::uint32_t fence_count,
|
||||
const VkFence *fences);
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkCreateCommandPool(
|
||||
VkDevice device, const VkCommandPoolCreateInfo *info,
|
||||
const VkAllocationCallbacks *allocator, VkCommandPool *pool);
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdPipelineBarrier(
|
||||
VkCommandBuffer command, VkPipelineStageFlags src, VkPipelineStageFlags dst,
|
||||
VkDependencyFlags flags, uint32_t memory_count,
|
||||
const VkMemoryBarrier *memory, uint32_t buffer_count,
|
||||
const VkBufferMemoryBarrier *buffers, uint32_t image_count,
|
||||
const VkImageMemoryBarrier *images);
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdPipelineBarrier2(VkCommandBuffer command,
|
||||
const VkDependencyInfo *info);
|
||||
VKAPI_ATTR void VKAPI_CALL
|
||||
vkCmdPipelineBarrier2KHR(VkCommandBuffer command, const VkDependencyInfo *info);
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandPool(
|
||||
VkDevice device, VkCommandPool command_pool,
|
||||
VkCommandPoolResetFlags flags);
|
||||
@@ -286,6 +311,46 @@ Function ResolveProcessFunction(const char* name) {
|
||||
return reinterpret_cast<Function>(dlsym(RTLD_DEFAULT, name));
|
||||
}
|
||||
|
||||
// Queue calls share the gate; device idle is exclusive.
|
||||
// Release it before fence waits and backend callbacks.
|
||||
std::shared_ptr<std::shared_mutex> DeviceIdleGate(VkDevice device) {
|
||||
static std::mutex registry_mutex;
|
||||
static std::unordered_map<VkDevice, std::shared_ptr<std::shared_mutex>>
|
||||
registry;
|
||||
std::lock_guard<std::mutex> lock(registry_mutex);
|
||||
auto &gate = registry[device];
|
||||
if (!gate)
|
||||
gate = std::make_shared<std::shared_mutex>();
|
||||
return gate;
|
||||
}
|
||||
std::shared_ptr<std::mutex> QueueCallMutex(VkQueue queue) {
|
||||
static std::mutex registry_mutex;
|
||||
static std::unordered_map<VkQueue, std::shared_ptr<std::mutex>> registry;
|
||||
std::lock_guard<std::mutex> lock(registry_mutex);
|
||||
auto &mutex = registry[queue];
|
||||
if (!mutex)
|
||||
mutex = std::make_shared<std::mutex>();
|
||||
return mutex;
|
||||
}
|
||||
|
||||
void NoteVrSubmitted(VkQueue queue, uint32_t count,
|
||||
const VkSubmitInfo *submits) {
|
||||
static const auto note = ResolveProcessFunction<void (*)(
|
||||
VkQueue, unsigned, const VkCommandBuffer *)>("mocktail_vr_xr_submitted");
|
||||
if (note && submits)
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
note(queue, submits[i].commandBufferCount, submits[i].pCommandBuffers);
|
||||
}
|
||||
void NoteVrSubmitted2(VkQueue queue, uint32_t count,
|
||||
const VkSubmitInfo2 *submits) {
|
||||
static const auto note = ResolveProcessFunction<void (*)(
|
||||
VkQueue, unsigned, const VkCommandBuffer *)>("mocktail_vr_xr_submitted");
|
||||
if (note && submits)
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
for (uint32_t j = 0; j < submits[i].commandBufferInfoCount; ++j)
|
||||
note(queue, 1, &submits[i].pCommandBufferInfos[j].commandBuffer);
|
||||
}
|
||||
|
||||
bool EnsureInitialized() {
|
||||
AdapterState& state = State();
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
@@ -293,6 +358,18 @@ bool EnsureInitialized() {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto register_submit =
|
||||
ResolveProcessFunction<void (*)(VkResult(VKAPI_PTR *)(
|
||||
PFN_vkQueueSubmit, VkDevice, VkQueue, uint32_t, const VkSubmitInfo *,
|
||||
VkFence))>("mocktail_vr_xr_queue_submit_adapter");
|
||||
if (register_submit)
|
||||
register_submit(mocktail_vulkan_submit_synchronized);
|
||||
const auto register_idle = ResolveProcessFunction<void (*)(
|
||||
VkResult(VKAPI_PTR *)(PFN_vkDeviceWaitIdle, VkDevice))>(
|
||||
"mocktail_vr_xr_device_idle_adapter");
|
||||
if (register_idle)
|
||||
register_idle(mocktail_vulkan_idle_synchronized);
|
||||
|
||||
const BackendWindowFn backend_window =
|
||||
ResolveProcessFunction<BackendWindowFn>("mocktail_window_backend_window");
|
||||
const UsesDirectVulkanFn uses_direct_vulkan =
|
||||
@@ -564,6 +641,57 @@ const AdapterState::DeviceDispatch& HostDispatchForCommandBuffer(
|
||||
return kEmptyDeviceDispatch;
|
||||
}
|
||||
|
||||
VkResult VKAPI_CALL LockedHostSubmit(VkQueue queue, uint32_t count,
|
||||
const VkSubmitInfo *submits,
|
||||
VkFence fence) {
|
||||
return mocktail_vulkan_submit_synchronized(
|
||||
HostDispatchForQueue(queue).queue_submit,
|
||||
HostDispatchForQueue(queue).device, queue, count, submits, fence);
|
||||
}
|
||||
VkResult VKAPI_CALL LockedHostSubmit2(VkQueue queue, uint32_t count,
|
||||
const VkSubmitInfo2 *submits,
|
||||
VkFence fence) {
|
||||
const auto raw = HostDispatchForQueue(queue).queue_submit2;
|
||||
const auto gate = DeviceIdleGate(HostDispatchForQueue(queue).device);
|
||||
std::shared_lock<std::shared_mutex> device_gate(*gate);
|
||||
const auto mutex = QueueCallMutex(queue);
|
||||
std::lock_guard<std::mutex> lock(*mutex);
|
||||
return raw(queue, count, submits, fence);
|
||||
}
|
||||
VkResult VKAPI_CALL LockedHostSubmit2KHR(VkQueue queue, uint32_t count,
|
||||
const VkSubmitInfo2 *submits,
|
||||
VkFence fence) {
|
||||
const auto raw = HostDispatchForQueue(queue).queue_submit2_khr;
|
||||
const auto gate = DeviceIdleGate(HostDispatchForQueue(queue).device);
|
||||
std::shared_lock<std::shared_mutex> device_gate(*gate);
|
||||
const auto mutex = QueueCallMutex(queue);
|
||||
std::lock_guard<std::mutex> lock(*mutex);
|
||||
return raw(queue, count, submits, fence);
|
||||
}
|
||||
VkResult VKAPI_CALL LockedHostBindSparse(VkQueue queue, uint32_t count,
|
||||
const VkBindSparseInfo *binds,
|
||||
VkFence fence) {
|
||||
const auto raw = HostDispatchForQueue(queue).queue_bind_sparse;
|
||||
const auto gate = DeviceIdleGate(HostDispatchForQueue(queue).device);
|
||||
std::shared_lock<std::shared_mutex> device_gate(*gate);
|
||||
const auto mutex = QueueCallMutex(queue);
|
||||
std::lock_guard<std::mutex> lock(*mutex);
|
||||
return raw(queue, count, binds, fence);
|
||||
}
|
||||
VkResult VKAPI_CALL LockedHostQueueWaitIdle(VkQueue queue) {
|
||||
const auto raw = HostDispatchForQueue(queue).queue_wait_idle;
|
||||
const auto gate = DeviceIdleGate(HostDispatchForQueue(queue).device);
|
||||
std::shared_lock<std::shared_mutex> device_gate(*gate);
|
||||
const auto mutex = QueueCallMutex(queue);
|
||||
std::lock_guard<std::mutex> lock(*mutex);
|
||||
return raw(queue);
|
||||
}
|
||||
|
||||
VkResult VKAPI_CALL LockedHostDeviceWaitIdle(VkDevice device) {
|
||||
return mocktail_vulkan_idle_synchronized(
|
||||
HostDispatchForDevice(device).device_wait_idle, device);
|
||||
}
|
||||
|
||||
void RegisterHostDeviceDispatch(VkDevice device,
|
||||
VkPhysicalDevice physical_device,
|
||||
PFN_vkGetDeviceProcAddr get_device_proc_addr) {
|
||||
@@ -834,9 +962,9 @@ PFN_vkVoidFunction AdapterProc(const char* name) {
|
||||
if (name == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
#define MOCKTAIL_VK_PROC(function) \
|
||||
if (std::strcmp(name, #function) == 0) { \
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(function); \
|
||||
#define MOCKTAIL_VK_PROC(function) \
|
||||
if (std::strcmp(name, #function) == 0) { \
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(function); \
|
||||
}
|
||||
MOCKTAIL_VK_PROC(vkCreateInstance)
|
||||
MOCKTAIL_VK_PROC(vkDestroyInstance)
|
||||
@@ -856,6 +984,10 @@ PFN_vkVoidFunction AdapterProc(const char* name) {
|
||||
MOCKTAIL_VK_PROC(vkAcquireNextImage2KHR)
|
||||
MOCKTAIL_VK_PROC(vkWaitForFences)
|
||||
MOCKTAIL_VK_PROC(vkResetFences)
|
||||
MOCKTAIL_VK_PROC(vkCreateCommandPool)
|
||||
MOCKTAIL_VK_PROC(vkCmdPipelineBarrier)
|
||||
MOCKTAIL_VK_PROC(vkCmdPipelineBarrier2)
|
||||
MOCKTAIL_VK_PROC(vkCmdPipelineBarrier2KHR)
|
||||
MOCKTAIL_VK_PROC(vkResetCommandPool)
|
||||
MOCKTAIL_VK_PROC(vkGetQueryPoolResults)
|
||||
MOCKTAIL_VK_PROC(vkAllocateCommandBuffers)
|
||||
@@ -886,43 +1018,47 @@ PFN_vkVoidFunction AdapterProc(const char* name) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsDeviceAdapterProc(const char* name) {
|
||||
return name != nullptr && (std::strcmp(name, "vkDestroyDevice") == 0 ||
|
||||
std::strcmp(name, "vkGetDeviceQueue") == 0 ||
|
||||
std::strcmp(name, "vkGetDeviceQueue2") == 0 ||
|
||||
std::strcmp(name, "vkCreateSwapchainKHR") == 0 ||
|
||||
std::strcmp(name, "vkDestroySwapchainKHR") == 0 ||
|
||||
std::strcmp(name, "vkAcquireNextImageKHR") == 0 ||
|
||||
std::strcmp(name, "vkAcquireNextImage2KHR") == 0 ||
|
||||
std::strcmp(name, "vkWaitForFences") == 0 ||
|
||||
std::strcmp(name, "vkResetFences") == 0 ||
|
||||
std::strcmp(name, "vkResetCommandPool") == 0 ||
|
||||
std::strcmp(name, "vkGetQueryPoolResults") == 0 ||
|
||||
std::strcmp(name, "vkAllocateCommandBuffers") ==
|
||||
0 ||
|
||||
std::strcmp(name, "vkFreeCommandBuffers") == 0 ||
|
||||
std::strcmp(name, "vkDestroyCommandPool") == 0 ||
|
||||
std::strcmp(name, "vkBeginCommandBuffer") == 0 ||
|
||||
std::strcmp(name, "vkEndCommandBuffer") == 0 ||
|
||||
std::strcmp(name, "vkResetCommandBuffer") == 0 ||
|
||||
std::strcmp(name, "vkWaitSemaphores") == 0 ||
|
||||
std::strcmp(name, "vkWaitSemaphoresKHR") == 0 ||
|
||||
std::strcmp(name, "vkQueueSubmit") == 0 ||
|
||||
std::strcmp(name, "vkQueueSubmit2") == 0 ||
|
||||
std::strcmp(name, "vkQueueSubmit2KHR") == 0 ||
|
||||
std::strcmp(name, "vkQueueBindSparse") == 0 ||
|
||||
std::strcmp(name, "vkQueueWaitIdle") == 0 ||
|
||||
std::strcmp(name, "vkDeviceWaitIdle") == 0 ||
|
||||
std::strcmp(name, "vkQueuePresentKHR") == 0 ||
|
||||
std::strcmp(name, "vkCreateImage") == 0 ||
|
||||
std::strcmp(name, "vkDestroyImage") == 0 ||
|
||||
std::strcmp(name, "vkCreateImageView") == 0 ||
|
||||
std::strcmp(name, "vkDestroyImageView") == 0 ||
|
||||
std::strcmp(name, "vkCreateFramebuffer") == 0 ||
|
||||
std::strcmp(name, "vkDestroyFramebuffer") == 0 ||
|
||||
std::strcmp(name, "vkCreateRenderPass") == 0 ||
|
||||
std::strcmp(name, "vkDestroyRenderPass") == 0 ||
|
||||
std::strcmp(name, "vkCmdBeginRenderPass") == 0);
|
||||
bool IsDeviceAdapterProc(const char *name) {
|
||||
return name != nullptr &&
|
||||
(std::strcmp(name, "vkDestroyDevice") == 0 ||
|
||||
std::strcmp(name, "vkGetDeviceQueue") == 0 ||
|
||||
std::strcmp(name, "vkGetDeviceQueue2") == 0 ||
|
||||
std::strcmp(name, "vkCreateSwapchainKHR") == 0 ||
|
||||
std::strcmp(name, "vkDestroySwapchainKHR") == 0 ||
|
||||
std::strcmp(name, "vkAcquireNextImageKHR") == 0 ||
|
||||
std::strcmp(name, "vkAcquireNextImage2KHR") == 0 ||
|
||||
std::strcmp(name, "vkWaitForFences") == 0 ||
|
||||
std::strcmp(name, "vkResetFences") == 0 ||
|
||||
std::strcmp(name, "vkCreateCommandPool") == 0 ||
|
||||
std::strcmp(name, "vkCmdPipelineBarrier") == 0 ||
|
||||
std::strcmp(name, "vkCmdPipelineBarrier2") == 0 ||
|
||||
std::strcmp(name, "vkCmdPipelineBarrier2KHR") == 0 ||
|
||||
std::strcmp(name, "vkResetCommandPool") == 0 ||
|
||||
std::strcmp(name, "vkGetQueryPoolResults") == 0 ||
|
||||
std::strcmp(name, "vkAllocateCommandBuffers") == 0 ||
|
||||
std::strcmp(name, "vkFreeCommandBuffers") == 0 ||
|
||||
std::strcmp(name, "vkDestroyCommandPool") == 0 ||
|
||||
std::strcmp(name, "vkBeginCommandBuffer") == 0 ||
|
||||
std::strcmp(name, "vkEndCommandBuffer") == 0 ||
|
||||
std::strcmp(name, "vkResetCommandBuffer") == 0 ||
|
||||
std::strcmp(name, "vkWaitSemaphores") == 0 ||
|
||||
std::strcmp(name, "vkWaitSemaphoresKHR") == 0 ||
|
||||
std::strcmp(name, "vkQueueSubmit") == 0 ||
|
||||
std::strcmp(name, "vkQueueSubmit2") == 0 ||
|
||||
std::strcmp(name, "vkQueueSubmit2KHR") == 0 ||
|
||||
std::strcmp(name, "vkQueueBindSparse") == 0 ||
|
||||
std::strcmp(name, "vkQueueWaitIdle") == 0 ||
|
||||
std::strcmp(name, "vkDeviceWaitIdle") == 0 ||
|
||||
std::strcmp(name, "vkQueuePresentKHR") == 0 ||
|
||||
std::strcmp(name, "vkCreateImage") == 0 ||
|
||||
std::strcmp(name, "vkDestroyImage") == 0 ||
|
||||
std::strcmp(name, "vkCreateImageView") == 0 ||
|
||||
std::strcmp(name, "vkDestroyImageView") == 0 ||
|
||||
std::strcmp(name, "vkCreateFramebuffer") == 0 ||
|
||||
std::strcmp(name, "vkDestroyFramebuffer") == 0 ||
|
||||
std::strcmp(name, "vkCreateRenderPass") == 0 ||
|
||||
std::strcmp(name, "vkDestroyRenderPass") == 0 ||
|
||||
std::strcmp(name, "vkCmdBeginRenderPass") == 0);
|
||||
}
|
||||
|
||||
bool IsGlobalAdapterProc(const char* name) {
|
||||
@@ -1201,12 +1337,35 @@ ObservedHostQueuePresent(VkQueue queue, const VkPresentInfoKHR* present_info) {
|
||||
}
|
||||
const VrXrNotePresentFn vr_note_present =
|
||||
state.vr_note_present.load(std::memory_order_acquire);
|
||||
VkPresentInfoKHR synchronized{};
|
||||
if (vr_note_present != nullptr) {
|
||||
vr_note_present(queue, HostDispatchForQueue(queue).device);
|
||||
static const auto prepare = ResolveProcessFunction<VkResult (*)(
|
||||
VkQueue, VkDevice, const VkPresentInfoKHR *)>(
|
||||
"mocktail_vr_xr_prepare_present");
|
||||
// Mirror/overlay may have consumed waits; use the final dependency list.
|
||||
const VkResult prepared =
|
||||
prepare
|
||||
? prepare(queue, HostDispatchForQueue(queue).device, present_info)
|
||||
: VK_NOT_READY;
|
||||
if (prepared < 0)
|
||||
return prepared;
|
||||
if (prepared == VK_SUCCESS) {
|
||||
vr_note_present(queue, HostDispatchForQueue(queue).device);
|
||||
synchronized = *present_info;
|
||||
synchronized.waitSemaphoreCount = 0;
|
||||
synchronized.pWaitSemaphores = nullptr;
|
||||
present_info = &synchronized;
|
||||
}
|
||||
}
|
||||
const bool fps_trace = FpsTraceEnabled();
|
||||
const std::uint64_t present_start_ns = fps_trace ? MonotonicNanos() : 0;
|
||||
const VkResult result = host_present(queue, present_info);
|
||||
const VkResult result = [&] {
|
||||
const auto gate = DeviceIdleGate(HostDispatchForQueue(queue).device);
|
||||
std::shared_lock<std::shared_mutex> device_gate(*gate);
|
||||
const auto mutex = QueueCallMutex(queue);
|
||||
std::lock_guard<std::mutex> lock(*mutex);
|
||||
return host_present(queue, present_info);
|
||||
}();
|
||||
if (fps_trace) {
|
||||
PresentWaitTrace().Record(present_start_ns);
|
||||
}
|
||||
@@ -1689,7 +1848,23 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateSwapchainKHR(
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
VkSwapchainCreateInfoKHR host_info = *create_info;
|
||||
static const auto vr_desktop_enabled = ResolveProcessFunction<bool (*)()>("mocktail_vr_desktop_enabled");
|
||||
unsigned vr_families[256]{};
|
||||
bool converted_to_concurrent = false;
|
||||
static const auto shared_families =
|
||||
ResolveProcessFunction<unsigned (*)(VkDevice, unsigned, unsigned *)>(
|
||||
"mocktail_vr_desktop_queue_families");
|
||||
if (shared_families &&
|
||||
host_info.imageSharingMode == VK_SHARING_MODE_EXCLUSIVE) {
|
||||
const unsigned count = shared_families(device, 256, vr_families);
|
||||
if (count > 1) {
|
||||
host_info.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
|
||||
host_info.queueFamilyIndexCount = count;
|
||||
host_info.pQueueFamilyIndices = vr_families;
|
||||
converted_to_concurrent = true;
|
||||
}
|
||||
}
|
||||
static const auto vr_desktop_enabled =
|
||||
ResolveProcessFunction<bool (*)()>("mocktail_vr_desktop_enabled");
|
||||
if (vr_desktop_enabled && vr_desktop_enabled()) {
|
||||
PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR caps;
|
||||
{ std::lock_guard<std::mutex> lock(State().mutex); caps = State().host_surface_capabilities; }
|
||||
@@ -1783,11 +1958,15 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateSwapchainKHR(
|
||||
host_get_images(device, *swapchain, &image_count, images.data());
|
||||
if (images_result == VK_SUCCESS || images_result == VK_INCOMPLETE) {
|
||||
images.resize(image_count);
|
||||
static const auto record = ResolveProcessFunction<void (*)(VkDevice, VkSwapchainKHR,
|
||||
const VkSwapchainCreateInfoKHR*, const VkImage*, unsigned)>("mocktail_vr_desktop_swapchain");
|
||||
if (record) record(device, *swapchain, &host_info, images.data(), image_count);
|
||||
(void)State().text_overlay.RegisterSwapchain(
|
||||
device, *swapchain, *create_info, images.data(), image_count);
|
||||
static const auto record = ResolveProcessFunction<void (*)(
|
||||
VkDevice, VkSwapchainKHR, const VkSwapchainCreateInfoKHR *,
|
||||
const VkImage *, unsigned, bool)>(
|
||||
"mocktail_vr_desktop_swapchain_shared");
|
||||
if (record)
|
||||
record(device, *swapchain, &host_info, images.data(), image_count,
|
||||
converted_to_concurrent);
|
||||
(void)State().text_overlay.RegisterSwapchain(device, *swapchain, host_info,
|
||||
images.data(), image_count);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1905,6 +2084,10 @@ VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandPool(
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
const VkResult result = host_reset(device, command_pool, flags);
|
||||
static const auto note = ResolveProcessFunction<void (*)(VkCommandPool)>(
|
||||
"mocktail_vr_xr_reset_pool");
|
||||
if (result == VK_SUCCESS && note)
|
||||
note(command_pool);
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
}
|
||||
@@ -1944,6 +2127,12 @@ VKAPI_ATTR VkResult VKAPI_CALL vkAllocateCommandBuffers(
|
||||
RegisterHostCommandBuffers(device, allocate_info->commandPool,
|
||||
allocate_info->commandBufferCount,
|
||||
command_buffers);
|
||||
static const auto note = ResolveProcessFunction<void (*)(
|
||||
VkDevice, VkCommandPool, unsigned, const VkCommandBuffer *)>(
|
||||
"mocktail_vr_xr_command_buffers");
|
||||
if (note)
|
||||
note(device, allocate_info->commandPool,
|
||||
allocate_info->commandBufferCount, command_buffers);
|
||||
}
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
@@ -1960,6 +2149,11 @@ VKAPI_ATTR void VKAPI_CALL vkFreeCommandBuffers(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return;
|
||||
}
|
||||
static const auto note = ResolveProcessFunction<void (*)(
|
||||
VkDevice, VkCommandPool, unsigned, const VkCommandBuffer *)>(
|
||||
"mocktail_vr_xr_command_buffers");
|
||||
if (note)
|
||||
note(device, VK_NULL_HANDLE, command_buffer_count, command_buffers);
|
||||
host_free(device, command_pool, command_buffer_count, command_buffers);
|
||||
RemoveHostCommandBuffers(device, command_pool, command_buffer_count,
|
||||
command_buffers);
|
||||
@@ -1976,6 +2170,11 @@ VKAPI_ATTR void VKAPI_CALL vkDestroyCommandPool(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return;
|
||||
}
|
||||
static const auto note =
|
||||
ResolveProcessFunction<void (*)(VkDevice, VkCommandPool, unsigned)>(
|
||||
"mocktail_vr_xr_command_pool");
|
||||
if (note)
|
||||
note(device, command_pool, UINT32_MAX);
|
||||
host_destroy(device, command_pool, allocator);
|
||||
RemoveHostCommandPoolBindings(device, command_pool);
|
||||
observation.SetResult(VK_SUCCESS);
|
||||
@@ -1992,6 +2191,10 @@ VKAPI_ATTR VkResult VKAPI_CALL vkBeginCommandBuffer(
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
const VkResult result = host_begin(command_buffer, begin_info);
|
||||
static const auto note = ResolveProcessFunction<void (*)(VkCommandBuffer)>(
|
||||
"mocktail_vr_xr_reset_command");
|
||||
if (result == VK_SUCCESS && note)
|
||||
note(command_buffer);
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
}
|
||||
@@ -2020,6 +2223,10 @@ VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandBuffer(
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
const VkResult result = host_reset(command_buffer, flags);
|
||||
static const auto note = ResolveProcessFunction<void (*)(VkCommandBuffer)>(
|
||||
"mocktail_vr_xr_reset_command");
|
||||
if (result == VK_SUCCESS && note)
|
||||
note(command_buffer);
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
}
|
||||
@@ -2056,7 +2263,9 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit(
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
const VkResult result = State().text_overlay.QueueSubmit(
|
||||
queue, submit_count, submits, fence, host_submit);
|
||||
queue, submit_count, submits, fence, LockedHostSubmit);
|
||||
if (result == VK_SUCCESS)
|
||||
NoteVrSubmitted(queue, submit_count, submits);
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
}
|
||||
@@ -2072,7 +2281,9 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2(
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
const VkResult result = State().text_overlay.QueueSubmit2(
|
||||
queue, submit_count, submits, fence, host_submit);
|
||||
queue, submit_count, submits, fence, LockedHostSubmit2);
|
||||
if (result == VK_SUCCESS)
|
||||
NoteVrSubmitted2(queue, submit_count, submits);
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
}
|
||||
@@ -2088,8 +2299,9 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2KHR(
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
const VkResult result = State().text_overlay.QueueSubmit2(
|
||||
queue, submit_count, submits, fence,
|
||||
reinterpret_cast<PFN_vkQueueSubmit2>(host_submit));
|
||||
queue, submit_count, submits, fence, LockedHostSubmit2KHR);
|
||||
if (result == VK_SUCCESS)
|
||||
NoteVrSubmitted2(queue, submit_count, submits);
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
}
|
||||
@@ -2105,7 +2317,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueBindSparse(
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
const VkResult result = State().text_overlay.QueueBindSparse(
|
||||
queue, bind_info_count, bind_info, fence, host_bind);
|
||||
queue, bind_info_count, bind_info, fence, LockedHostBindSparse);
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
}
|
||||
@@ -2121,7 +2333,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueWaitIdle(VkQueue queue) {
|
||||
const bool fps_trace = FpsTraceEnabled();
|
||||
const std::uint64_t idle_start_ns = fps_trace ? MonotonicNanos() : 0;
|
||||
const VkResult result =
|
||||
State().text_overlay.QueueWaitIdle(queue, host_wait);
|
||||
State().text_overlay.QueueWaitIdle(queue, LockedHostQueueWaitIdle);
|
||||
if (fps_trace) {
|
||||
QueueIdleWaitTrace().Record(idle_start_ns);
|
||||
}
|
||||
@@ -2138,7 +2350,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkDeviceWaitIdle(VkDevice device) {
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
const VkResult result =
|
||||
State().text_overlay.DeviceWaitIdle(device, host_wait);
|
||||
State().text_overlay.DeviceWaitIdle(device, LockedHostDeviceWaitIdle);
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
}
|
||||
@@ -2434,4 +2646,100 @@ vkCmdBeginRenderPass(VkCommandBuffer command_buffer,
|
||||
}
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkCreateCommandPool(
|
||||
VkDevice device, const VkCommandPoolCreateInfo *info,
|
||||
const VkAllocationCallbacks *allocator, VkCommandPool *pool) {
|
||||
const auto create = reinterpret_cast<PFN_vkCreateCommandPool>(
|
||||
HostDeviceProc(device, "vkCreateCommandPool"));
|
||||
if (!create)
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
const VkResult result = create(device, info, allocator, pool);
|
||||
static const auto note =
|
||||
ResolveProcessFunction<void (*)(VkDevice, VkCommandPool, unsigned)>(
|
||||
"mocktail_vr_xr_command_pool");
|
||||
if (result == VK_SUCCESS && note && info && pool)
|
||||
note(device, *pool, info->queueFamilyIndex);
|
||||
return result;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdPipelineBarrier(
|
||||
VkCommandBuffer command, VkPipelineStageFlags src, VkPipelineStageFlags dst,
|
||||
VkDependencyFlags flags, uint32_t memory_count,
|
||||
const VkMemoryBarrier *memory, uint32_t buffer_count,
|
||||
const VkBufferMemoryBarrier *buffers, uint32_t image_count,
|
||||
const VkImageMemoryBarrier *images) {
|
||||
const auto barrier = reinterpret_cast<PFN_vkCmdPipelineBarrier>(
|
||||
HostDeviceProc(HostDispatchForCommandBuffer(command).device,
|
||||
"vkCmdPipelineBarrier"));
|
||||
if (!barrier)
|
||||
return;
|
||||
static const auto normalize = ResolveProcessFunction<VkImageLayout (*)(
|
||||
VkCommandBuffer, VkImage, unsigned *, unsigned *, VkImageLayout *,
|
||||
VkImageLayout)>("mocktail_vr_xr_desktop_barrier");
|
||||
std::vector<VkImageMemoryBarrier> adjusted;
|
||||
if (normalize && images && image_count) {
|
||||
adjusted.assign(images, images + image_count);
|
||||
for (auto &image : adjusted)
|
||||
image.newLayout = normalize(
|
||||
command, image.image, &image.srcQueueFamilyIndex,
|
||||
&image.dstQueueFamilyIndex, &image.oldLayout, image.newLayout);
|
||||
images = adjusted.data();
|
||||
}
|
||||
barrier(command, src, dst, flags, memory_count, memory, buffer_count, buffers,
|
||||
image_count, images);
|
||||
}
|
||||
|
||||
static void ForwardVrBarrier2(VkCommandBuffer command,
|
||||
const VkDependencyInfo *info, const char *name) {
|
||||
const auto barrier = reinterpret_cast<PFN_vkCmdPipelineBarrier2>(
|
||||
HostDeviceProc(HostDispatchForCommandBuffer(command).device, name));
|
||||
if (!barrier || !info)
|
||||
return;
|
||||
static const auto normalize = ResolveProcessFunction<VkImageLayout (*)(
|
||||
VkCommandBuffer, VkImage, unsigned *, unsigned *, VkImageLayout *,
|
||||
VkImageLayout)>("mocktail_vr_xr_desktop_barrier");
|
||||
VkDependencyInfo adjusted_info = *info;
|
||||
std::vector<VkImageMemoryBarrier2> adjusted;
|
||||
if (normalize && info->pImageMemoryBarriers &&
|
||||
info->imageMemoryBarrierCount) {
|
||||
adjusted.assign(info->pImageMemoryBarriers,
|
||||
info->pImageMemoryBarriers + info->imageMemoryBarrierCount);
|
||||
for (auto &image : adjusted)
|
||||
image.newLayout = normalize(
|
||||
command, image.image, &image.srcQueueFamilyIndex,
|
||||
&image.dstQueueFamilyIndex, &image.oldLayout, image.newLayout);
|
||||
adjusted_info.pImageMemoryBarriers = adjusted.data();
|
||||
}
|
||||
barrier(command, &adjusted_info);
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdPipelineBarrier2(VkCommandBuffer command,
|
||||
const VkDependencyInfo *info) {
|
||||
ForwardVrBarrier2(command, info, "vkCmdPipelineBarrier2");
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdPipelineBarrier2KHR(
|
||||
VkCommandBuffer command, const VkDependencyInfo *info) {
|
||||
ForwardVrBarrier2(command, info, "vkCmdPipelineBarrier2KHR");
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mocktail_vulkan_submit_synchronized(
|
||||
PFN_vkQueueSubmit raw, VkDevice device, VkQueue queue, uint32_t count,
|
||||
const VkSubmitInfo *submits, VkFence fence) {
|
||||
if (!raw)
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
const auto gate = DeviceIdleGate(device);
|
||||
std::shared_lock<std::shared_mutex> device_gate(*gate);
|
||||
const auto mutex = QueueCallMutex(queue);
|
||||
std::lock_guard<std::mutex> lock(*mutex);
|
||||
return raw(queue, count, submits, fence);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL
|
||||
mocktail_vulkan_idle_synchronized(PFN_vkDeviceWaitIdle raw, VkDevice device) {
|
||||
if (!raw)
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
const auto gate = DeviceIdleGate(device);
|
||||
std::unique_lock<std::shared_mutex> device_gate(*gate);
|
||||
return raw(device);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -4507,17 +4507,11 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
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.
|
||||
const mocktail::compat::BuildProfile& build_profile = *profile_result.profile;
|
||||
if (IsEnabled("MOCKTAIL_VR_ENABLED") &&
|
||||
build_profile.elf_build_id !=
|
||||
"ade08266c67aee88ec9c1d00902150e1684dad3a") {
|
||||
std::cerr << "[FATAL] Experimental Roblox VR requires exact Build ID "
|
||||
"ade08266c67aee88ec9c1d00902150e1684dad3a; loaded "
|
||||
mocktail::vr::FindRobloxVrDeviceProfile(build_profile.elf_build_id) ==
|
||||
nullptr) {
|
||||
std::cerr << "[FATAL] Experimental Roblox VR has no verified profile for "
|
||||
<< build_profile.elf_build_id << "\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
+8
-1
@@ -742,6 +742,12 @@ int main(int argc, char* argv[]) {
|
||||
std::cerr << "[FATAL] " << compatibility.error << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (!compatibility.profile.vr_debug_device_bridge.has_value()) {
|
||||
if (const auto* vr_profile =
|
||||
mocktail::vr::FindRobloxVrDeviceProfile(compatibility.build_id)) {
|
||||
compatibility.profile.vr_debug_device_bridge = *vr_profile;
|
||||
}
|
||||
}
|
||||
vr_active = runtime_config.config.vr_enabled() &&
|
||||
(vr_requested ||
|
||||
compatibility.profile.vr_debug_device_bridge.has_value());
|
||||
@@ -759,7 +765,8 @@ int main(int argc, char* argv[]) {
|
||||
std::cerr << " [vr] experimental backend="
|
||||
<< mocktail::vr::VrBackendModeName(vr_backend_mode)
|
||||
<< ": Roblox renders both eyes through its DebugDeviceVR "
|
||||
"debug device (exact Build ID 2998 only)";
|
||||
"debug device (Build ID "
|
||||
<< compatibility.build_id << ")";
|
||||
if (vr_backend_mode == mocktail::vr::VrBackendMode::kXrOutput) {
|
||||
std::cerr << "; eye images are submitted to the active OpenXR runtime "
|
||||
"as a projection layer. Start WiVRn or SteamVR/ALVR and "
|
||||
|
||||
@@ -69,12 +69,12 @@ graphics:
|
||||
|
||||
# vr:
|
||||
# Boolean (default: enabled in VR builds, disabled in non-VR builds).
|
||||
# Experimental VR for the exact Build ID 2998
|
||||
# payload. Mocktail initializes the guest DebugDeviceVR eye resources (two
|
||||
# Experimental VR for verified builds 2998 and 3092. Mocktail initializes
|
||||
# the guest DebugDeviceVR eye resources (two
|
||||
# distinct 500x500 eye framebuffers), the Roblox renderer draws both eyes
|
||||
# every frame, and the frames are submitted to the active OpenXR runtime as
|
||||
# a stereo projection layer with head pose (runtime-tracked, or scripted for
|
||||
# desktop testing). Controllers are NOT connected yet. Standalone OpenXR
|
||||
# desktop testing). Controllers use the native VR bridge. Standalone OpenXR
|
||||
# test scene: mocktail-vr-probe --scene. Use --no-vr to launch normally.
|
||||
# enabled: false
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ std::string FormatProbeReport(const ProbeReport& report) {
|
||||
for (const auto& extension : report.extensions)
|
||||
output << " " << extension << '\n';
|
||||
output << "Discovery: " << report.message << '\n'
|
||||
<< "Roblox VR rendering: not implemented yet.\n"
|
||||
<< "Roblox VR rendering: run mocktail --vr with a supported client.\n"
|
||||
<< "This probe does not create an XR session or render frames.\n";
|
||||
return output.str();
|
||||
}
|
||||
|
||||
+810
-301
File diff suppressed because it is too large.
Load diff
@@ -43,6 +43,14 @@ using internal::kStateGetterVtableSlot;
|
||||
using internal::kTextureSlotOffset;
|
||||
using internal::kWidthOffset;
|
||||
|
||||
std::uintptr_t RelativeTarget(const std::uint8_t* displacement,
|
||||
std::uintptr_t next_instruction_rva) {
|
||||
std::int32_t relative;
|
||||
std::memcpy(&relative, displacement, sizeof(relative));
|
||||
return static_cast<std::uintptr_t>(
|
||||
static_cast<std::int64_t>(next_instruction_rva) + relative);
|
||||
}
|
||||
|
||||
std::atomic<RobloxVrDeviceBridge *> g_active_bridge{nullptr};
|
||||
// Evidence queries expose only the current native call's provenance. These
|
||||
// pointers are identities for the layer, never retained/dereferenced by it.
|
||||
@@ -55,8 +63,7 @@ std::mutex g_native_pose_mutex;
|
||||
ScriptedPoseSample g_native_pose;
|
||||
using PointerFrameFn = void *(*)(void *, void *);
|
||||
std::atomic<PointerFrameFn> g_pointer_original{nullptr};
|
||||
constexpr std::uintptr_t kPointerFrameRva = 0x4d573ee;
|
||||
// Whole instructions, no RIP-relative operands. 2998's sret prologue.
|
||||
// Whole instructions, no RIP-relative operands; shared sret prologue.
|
||||
constexpr std::array<unsigned char, 15> kPointerPrologue = {
|
||||
0x55, 0x48, 0x89, 0xe5, 0x41, 0x57, 0x41, 0x56,
|
||||
0x41, 0x54, 0x53, 0x48, 0x83, 0xec, 0x70};
|
||||
@@ -75,50 +82,7 @@ extern "C" void *mocktail_vr_pointer_frame(void *result, void *service) {
|
||||
original(result, service);
|
||||
auto *bridge = g_active_bridge.load(std::memory_order_acquire);
|
||||
if (!bridge || !bridge->active()) return result;
|
||||
ScriptedPoseSample native_pose;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_native_pose_mutex);
|
||||
native_pose = g_native_pose;
|
||||
}
|
||||
if (!native_pose.valid) return result;
|
||||
auto *backend = ActiveVrBackend();
|
||||
if (!backend)
|
||||
return result;
|
||||
const auto current = backend->PublishedHeadPose();
|
||||
if (!current.valid)
|
||||
return result;
|
||||
int index = 0;
|
||||
std::memcpy(&index, static_cast<unsigned char *>(service) + 0x118, 4);
|
||||
if (index != 1 && index != 2)
|
||||
return result;
|
||||
if (!current.hands[index - 1].aim_valid)
|
||||
return result;
|
||||
const auto &hand = native_pose.hands[index - 1];
|
||||
if (!hand.grip_valid || !hand.aim_valid)
|
||||
return result;
|
||||
const auto base = bridge->image_base();
|
||||
// Same camera lookup as the original getter (see pointer-world-2998.asm).
|
||||
auto get_workspace = reinterpret_cast<void *(*)(void *)>(base + 0x263778c);
|
||||
void *workspace = get_workspace(service);
|
||||
if (!workspace)
|
||||
return result;
|
||||
auto *vtable = *static_cast<std::uintptr_t **>(workspace);
|
||||
void *camera =
|
||||
reinterpret_cast<void *(*)(void *)>(vtable[0x3a8 / 8])(workspace);
|
||||
if (!camera)
|
||||
return result;
|
||||
float scale = 0;
|
||||
int degrees = 0;
|
||||
std::memcpy(&scale, static_cast<unsigned char *>(camera) + 0x140, 4);
|
||||
std::memcpy(°rees, reinterpret_cast<void *>(base + 0x73677a8), 4);
|
||||
if (internal::ApplyAimToWorldFrame(static_cast<float *>(result), hand, scale,
|
||||
-static_cast<float>(degrees))) {
|
||||
static std::atomic<bool> logged[2]{};
|
||||
if (!logged[index - 1].exchange(true))
|
||||
std::fprintf(
|
||||
stderr, " [vr-input] native menu pointer uses OpenXR aim: hand=%d\n",
|
||||
index - 1);
|
||||
}
|
||||
bridge->OnPointerFrameCall(result, service);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -133,10 +97,10 @@ bool SetCodeWritable(std::uintptr_t target, bool writable) {
|
||||
PROT_READ | PROT_EXEC | (writable ? PROT_WRITE : 0)) == 0;
|
||||
}
|
||||
|
||||
bool InstallPointerHook(std::uintptr_t base) {
|
||||
bool InstallPointerHook(std::uintptr_t base, std::uintptr_t pointer_rva) {
|
||||
if (g_pointer_target)
|
||||
return false;
|
||||
auto *target = reinterpret_cast<unsigned char *>(base + kPointerFrameRva);
|
||||
auto* target = reinterpret_cast<unsigned char*>(base + pointer_rva);
|
||||
if (std::memcmp(target, kPointerPrologue.data(), kPointerPrologue.size()) !=
|
||||
0)
|
||||
return false;
|
||||
@@ -148,9 +112,9 @@ bool InstallPointerHook(std::uintptr_t base) {
|
||||
return false;
|
||||
std::memcpy(code, target, kPointerPrologue.size());
|
||||
AbsoluteJump(code + kPointerPrologue.size(),
|
||||
base + kPointerFrameRva + kPointerPrologue.size());
|
||||
base + pointer_rva + kPointerPrologue.size());
|
||||
if (mprotect(code, page_size, PROT_READ | PROT_EXEC) != 0 ||
|
||||
!SetCodeWritable(base + kPointerFrameRva, true)) {
|
||||
!SetCodeWritable(base + pointer_rva, true)) {
|
||||
munmap(code, page_size);
|
||||
return false;
|
||||
}
|
||||
@@ -159,14 +123,14 @@ bool InstallPointerHook(std::uintptr_t base) {
|
||||
AbsoluteJump(target,
|
||||
reinterpret_cast<std::uintptr_t>(&mocktail_vr_pointer_frame));
|
||||
target[14] = 0x90;
|
||||
if (!SetCodeWritable(base + kPointerFrameRva, false)) {
|
||||
if (!SetCodeWritable(base + pointer_rva, false)) {
|
||||
std::memcpy(target, kPointerPrologue.data(), kPointerPrologue.size());
|
||||
(void)SetCodeWritable(base + kPointerFrameRva, false);
|
||||
(void)SetCodeWritable(base + pointer_rva, false);
|
||||
return false;
|
||||
}
|
||||
// Retain this single RX page for process lifetime: an in-flight guest call
|
||||
// may still be returning through it when the entrypoint is restored.
|
||||
g_pointer_target = base + kPointerFrameRva;
|
||||
g_pointer_target = base + pointer_rva;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -324,6 +288,48 @@ void Log(const char *format, ...) {
|
||||
|
||||
namespace internal {
|
||||
|
||||
const VrBuildContract* FindVrBuildContract(std::string_view build_id) {
|
||||
static constexpr VrBuildContract kContracts[] = {
|
||||
{"ade08266c67aee88ec9c1d00902150e1684dad3a",
|
||||
{0x6c87b38, 0x3810614, 0x38109f6, 0x38109e6, 0x3810a18, 0x7369920,
|
||||
0x180},
|
||||
0x6b7bfe0,
|
||||
0x4d573ee,
|
||||
0x263778c,
|
||||
0x3a8,
|
||||
0x73677a8,
|
||||
0x1d48b70},
|
||||
{"5f0704edd9064f566ee3d6df2bd2fabbcc709f03",
|
||||
{0x6d08108, 0x37f9fe4, 0x37fa3c6, 0x37fa3b6, 0x37fa3e8, 0x73f4690,
|
||||
0x188},
|
||||
0x6bfb180,
|
||||
0x4d97b98,
|
||||
0x265cebc,
|
||||
0x3a0,
|
||||
0x73f24c8,
|
||||
0x1d8f8c0},
|
||||
};
|
||||
for (const auto& contract : kContracts) {
|
||||
if (contract.build_id == build_id) return &contract;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool HasExpectedVrPointerContract(const std::uint8_t* code, std::size_t size,
|
||||
const VrBuildContract& contract) {
|
||||
if (code == nullptr || size < 0x1b2 ||
|
||||
std::memcmp(code, kPointerPrologue.data(), kPointerPrologue.size()) !=
|
||||
0 ||
|
||||
code[0x59] != 0xe8 || code[0x6d] != 0xff || code[0x6e] != 0x91) {
|
||||
return false;
|
||||
}
|
||||
std::uint32_t camera_slot;
|
||||
std::memcpy(&camera_slot, code + 0x6f, sizeof(camera_slot));
|
||||
return RelativeTarget(code + 0x5a, contract.pointer_frame_rva + 0x5e) ==
|
||||
contract.workspace_getter_rva &&
|
||||
camera_slot == contract.camera_vtable_offset;
|
||||
}
|
||||
|
||||
EyeResourceState InspectEyeResources(const void *object) {
|
||||
if (object == nullptr)
|
||||
return EyeResourceState::kInvalid;
|
||||
@@ -348,8 +354,9 @@ EyeResourceState InspectEyeResources(const void *object) {
|
||||
return EyeResourceState::kInvalid;
|
||||
}
|
||||
|
||||
bool HasExpectedVrStateGetterContract(const std::uint8_t *code,
|
||||
std::size_t size) {
|
||||
bool HasExpectedVrStateGetterContract(const std::uint8_t* code,
|
||||
std::size_t size,
|
||||
const VrBuildContract& contract) {
|
||||
// push rbp; mov rbp,rsp; push rbx; push rax; mov rbx,rdi; add rsi,0x14;
|
||||
// mov edx,0x138; call memcpy; mov rax,rbx; add rsp,8; pop rbx; pop rbp; ret
|
||||
static constexpr std::array<std::uint8_t, kStateGetterContractSize>
|
||||
@@ -358,7 +365,10 @@ bool HasExpectedVrStateGetterContract(const std::uint8_t *code,
|
||||
0xe8, 0xd3, 0xb5, 0x36, 0x03, 0x48, 0x89, 0xd8, 0x48,
|
||||
0x83, 0xc4, 0x08, 0x5b, 0x5d, 0xc3};
|
||||
return code != nullptr && size >= kExpected.size() &&
|
||||
std::memcmp(code, kExpected.data(), kExpected.size()) == 0;
|
||||
std::memcmp(code, kExpected.data(), 19) == 0 &&
|
||||
std::memcmp(code + 23, kExpected.data() + 23, 10) == 0 &&
|
||||
RelativeTarget(code + 19, contract.device.state_getter_rva + 23) ==
|
||||
contract.state_memcpy_rva;
|
||||
}
|
||||
|
||||
bool HasExpectedVrEyeGetterContract(const std::uint8_t *code,
|
||||
@@ -371,15 +381,16 @@ bool HasExpectedVrEyeGetterContract(const std::uint8_t *code,
|
||||
std::memcmp(code, kExpected.data(), kExpected.size()) == 0;
|
||||
}
|
||||
|
||||
bool HasExpectedVrConstructorContract(const std::uint8_t *code,
|
||||
std::size_t size) {
|
||||
bool HasExpectedVrConstructorContract(
|
||||
const std::uint8_t* code, std::size_t size,
|
||||
const compat::VrDebugDeviceBridgeProfile& profile) {
|
||||
static constexpr std::array<std::uint8_t, 29> kPrologue = {
|
||||
0x55, 0x48, 0x89, 0xe5, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55,
|
||||
0x41, 0x54, 0x53, 0x48, 0x83, 0xec, 0x28, 0x49, 0x89, 0xcf,
|
||||
0x41, 0x89, 0xd4, 0x41, 0x89, 0xf5, 0x48, 0x89, 0xfb};
|
||||
// lea rax,[rip -> DebugDeviceVR vtable]; mov [rdi],rax
|
||||
static constexpr std::array<std::uint8_t, 7> kVtableStore = {
|
||||
0x48, 0x8d, 0x05, 0xf2, 0x74, 0x47, 0x03};
|
||||
static constexpr std::array<std::uint8_t, 3> kVtableStore = {0x48, 0x8d,
|
||||
0x05};
|
||||
static constexpr std::array<std::uint8_t, 3> kVptrWrite = {0x48, 0x89, 0x07};
|
||||
// mov dword ptr [rdi+0x10], 6
|
||||
static constexpr std::array<std::uint8_t, 7> kTypeStore = {
|
||||
@@ -398,7 +409,10 @@ bool HasExpectedVrConstructorContract(const std::uint8_t *code,
|
||||
return false;
|
||||
}
|
||||
return std::memcmp(code, kPrologue.data(), kPrologue.size()) == 0 &&
|
||||
ContainsBytes(code, size, kVtableStore.data(), kVtableStore.size()) &&
|
||||
std::memcmp(code + 43, kVtableStore.data(), kVtableStore.size()) ==
|
||||
0 &&
|
||||
RelativeTarget(code + 46, profile.constructor_rva + 50) ==
|
||||
profile.vtable_rva &&
|
||||
ContainsBytes(code, size, kVptrWrite.data(), kVptrWrite.size()) &&
|
||||
ContainsBytes(code, size, kTypeStore.data(), kTypeStore.size()) &&
|
||||
ContainsBytes(code, size, kStateMemset.data(), kStateMemset.size()) &&
|
||||
@@ -529,6 +543,12 @@ extern "C" void mocktail_vr_note_host_present_begin() {
|
||||
}
|
||||
}
|
||||
|
||||
const compat::VrDebugDeviceBridgeProfile* FindRobloxVrDeviceProfile(
|
||||
std::string_view build_id) {
|
||||
const auto* contract = internal::FindVrBuildContract(build_id);
|
||||
return contract == nullptr ? nullptr : &contract->device;
|
||||
}
|
||||
|
||||
RobloxVrDeviceBridge::~RobloxVrDeviceBridge() {
|
||||
Shutdown();
|
||||
RobloxVrDeviceBridge *expected = this;
|
||||
@@ -536,6 +556,50 @@ RobloxVrDeviceBridge::~RobloxVrDeviceBridge() {
|
||||
expected, nullptr, std::memory_order_acq_rel, std::memory_order_acquire);
|
||||
}
|
||||
|
||||
void RobloxVrDeviceBridge::OnPointerFrameCall(void* result, void* service) {
|
||||
if (!active() || contract_ == nullptr) return;
|
||||
ScriptedPoseSample native_pose;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_native_pose_mutex);
|
||||
native_pose = g_native_pose;
|
||||
}
|
||||
if (!native_pose.valid) return;
|
||||
auto* backend = ActiveVrBackend();
|
||||
if (!backend) return;
|
||||
const auto current = backend->PublishedHeadPose();
|
||||
if (!current.valid) return;
|
||||
int index = 0;
|
||||
std::memcpy(&index, static_cast<unsigned char*>(service) + 0x118, 4);
|
||||
if (index != 1 && index != 2) return;
|
||||
if (!current.hands[index - 1].aim_valid) return;
|
||||
const auto& hand = native_pose.hands[index - 1];
|
||||
if (!hand.grip_valid || !hand.aim_valid) return;
|
||||
const auto base = image_base();
|
||||
// Follow the same workspace/camera path as the guest pointer getter.
|
||||
auto get_workspace = reinterpret_cast<void* (*)(void*)>(
|
||||
base + contract_->workspace_getter_rva);
|
||||
void* workspace = get_workspace(service);
|
||||
if (!workspace) return;
|
||||
auto* vtable = *static_cast<std::uintptr_t**>(workspace);
|
||||
void* camera = reinterpret_cast<void* (*)(void*)>(
|
||||
vtable[contract_->camera_vtable_offset / sizeof(void*)])(workspace);
|
||||
if (!camera) return;
|
||||
float scale = 0;
|
||||
int degrees = 0;
|
||||
std::memcpy(&scale, static_cast<unsigned char*>(camera) + 0x140, 4);
|
||||
std::memcpy(°rees,
|
||||
reinterpret_cast<void*>(base + contract_->hand_pitch_storage_rva),
|
||||
4);
|
||||
if (internal::ApplyAimToWorldFrame(static_cast<float*>(result), hand, scale,
|
||||
-static_cast<float>(degrees))) {
|
||||
static std::atomic<bool> logged[2]{};
|
||||
if (!logged[index - 1].exchange(true))
|
||||
std::fprintf(
|
||||
stderr, " [vr-input] native menu pointer uses OpenXR aim: hand=%d\n",
|
||||
index - 1);
|
||||
}
|
||||
}
|
||||
|
||||
Status RobloxVrDeviceBridge::Install(const compat::BuildProfile &profile) {
|
||||
if (installed_) {
|
||||
return FailedPrecondition("Roblox VR device bridge is already armed");
|
||||
@@ -545,8 +609,25 @@ Status RobloxVrDeviceBridge::Install(const compat::BuildProfile &profile) {
|
||||
"experimental Roblox VR requires vr_debug_device_bridge metadata in "
|
||||
"the exact-build compatibility profile");
|
||||
}
|
||||
if (profile.elf_build_id != "ade08266c67aee88ec9c1d00902150e1684dad3a")
|
||||
return FailedPrecondition("VR controller/pointer/haptic contracts are verified only for Build 2998");
|
||||
const auto* contract = internal::FindVrBuildContract(profile.elf_build_id);
|
||||
if (contract == nullptr) {
|
||||
return FailedPrecondition(
|
||||
"VR contracts are not verified for this Build ID");
|
||||
}
|
||||
const auto& actual = *profile.vr_debug_device_bridge;
|
||||
const auto& expected_profile = contract->device;
|
||||
if (actual.vtable_rva != expected_profile.vtable_rva ||
|
||||
actual.constructor_rva != expected_profile.constructor_rva ||
|
||||
actual.state_getter_rva != expected_profile.state_getter_rva ||
|
||||
actual.eye_getter_rva != expected_profile.eye_getter_rva ||
|
||||
actual.eye_initializer_rva != expected_profile.eye_initializer_rva ||
|
||||
actual.emulator_flag_storage_rva !=
|
||||
expected_profile.emulator_flag_storage_rva ||
|
||||
actual.device_create_framebuffer_vtable_offset !=
|
||||
expected_profile.device_create_framebuffer_vtable_offset) {
|
||||
return FailedPrecondition(
|
||||
"VR metadata does not match the verified Build ID");
|
||||
}
|
||||
if (!profile.allow_host_abi_bridges) {
|
||||
return FailedPrecondition(
|
||||
"Roblox VR device bridge profile requires host ABI bridges");
|
||||
@@ -560,6 +641,7 @@ Status RobloxVrDeviceBridge::Install(const compat::BuildProfile &profile) {
|
||||
"process");
|
||||
}
|
||||
profile_ = *profile.vr_debug_device_bridge;
|
||||
contract_ = contract;
|
||||
installed_ = true;
|
||||
std::fprintf(stderr,
|
||||
" [vr-device] native stereo bridge armed for Build ID %s "
|
||||
@@ -588,7 +670,11 @@ Status RobloxVrDeviceBridge::Activate(std::uintptr_t image_base) {
|
||||
profile_.state_getter_rva + kStateGetterContractSize,
|
||||
profile_.eye_getter_rva + kEyeGetterContractSize,
|
||||
profile_.eye_initializer_rva + kInitializerContractSize,
|
||||
profile_.emulator_flag_storage_rva + 1});
|
||||
profile_.emulator_flag_storage_rva + 1, contract_->state_memcpy_rva + 1,
|
||||
contract_->pointer_frame_rva + 0x1b2,
|
||||
contract_->workspace_getter_rva + 1,
|
||||
contract_->hand_pitch_storage_rva + sizeof(int),
|
||||
contract_->haptic_sink_rva + 1});
|
||||
if (image_base > std::numeric_limits<std::uintptr_t>::max() - largest_rva) {
|
||||
return InvalidArgument("VR device profile address overflows");
|
||||
}
|
||||
@@ -606,12 +692,16 @@ Status RobloxVrDeviceBridge::Activate(std::uintptr_t image_base) {
|
||||
!IsExecutableImageRange(image_base, profile_.eye_getter_rva,
|
||||
kEyeGetterContractSize) ||
|
||||
!IsExecutableImageRange(image_base, profile_.eye_initializer_rva,
|
||||
kInitializerContractSize)) {
|
||||
kInitializerContractSize) ||
|
||||
!IsExecutableImageRange(image_base, contract_->state_memcpy_rva, 1) ||
|
||||
!IsExecutableImageRange(image_base, contract_->workspace_getter_rva, 1)) {
|
||||
return FailedPrecondition(
|
||||
"VR device profile functions are not in executable image ranges");
|
||||
}
|
||||
if (!IsWritableDataImageRange(image_base, profile_.emulator_flag_storage_rva,
|
||||
1)) {
|
||||
1) ||
|
||||
!IsWritableDataImageRange(image_base, contract_->hand_pitch_storage_rva,
|
||||
sizeof(int))) {
|
||||
return FailedPrecondition(
|
||||
"VR emulator flag storage is not in a writable data range");
|
||||
}
|
||||
@@ -630,15 +720,15 @@ Status RobloxVrDeviceBridge::Activate(std::uintptr_t image_base) {
|
||||
image_base + profile_.constructor_rva);
|
||||
const auto *initializer_code = reinterpret_cast<const std::uint8_t *>(
|
||||
image_base + profile_.eye_initializer_rva);
|
||||
if (!internal::HasExpectedVrStateGetterContract(state_getter_code,
|
||||
kStateGetterContractSize) ||
|
||||
if (!internal::HasExpectedVrStateGetterContract(
|
||||
state_getter_code, kStateGetterContractSize, *contract_) ||
|
||||
!internal::HasExpectedVrEyeGetterContract(eye_getter_code,
|
||||
kEyeGetterContractSize)) {
|
||||
return FailedPrecondition(
|
||||
"DebugDeviceVR getter machine contracts do not match the profile");
|
||||
}
|
||||
if (!internal::HasExpectedVrConstructorContract(constructor_code,
|
||||
kConstructorContractSize) ||
|
||||
if (!internal::HasExpectedVrConstructorContract(
|
||||
constructor_code, kConstructorContractSize, profile_) ||
|
||||
!internal::HasExpectedVrEyeInitializerContract(
|
||||
initializer_code, kInitializerContractSize, profile_)) {
|
||||
return FailedPrecondition(
|
||||
@@ -646,20 +736,23 @@ Status RobloxVrDeviceBridge::Activate(std::uintptr_t image_base) {
|
||||
"match the profile");
|
||||
}
|
||||
|
||||
if (!IsExecutableImageRange(image_base, kPointerFrameRva, 0x1b2) ||
|
||||
std::memcmp(reinterpret_cast<void*>(image_base + kPointerFrameRva),
|
||||
kPointerPrologue.data(), kPointerPrologue.size()) != 0)
|
||||
return FailedPrecondition("2998 VR pointer function contract mismatch");
|
||||
if (!IsExecutableImageRange(image_base, contract_->pointer_frame_rva,
|
||||
0x1b2) ||
|
||||
!internal::HasExpectedVrPointerContract(
|
||||
reinterpret_cast<const std::uint8_t*>(image_base +
|
||||
contract_->pointer_frame_rva),
|
||||
0x1b2, *contract_)) {
|
||||
return FailedPrecondition("VR pointer function contract mismatch");
|
||||
}
|
||||
if (vtable[3] != image_base + contract_->haptic_sink_rva ||
|
||||
!IsExecutableImageRange(image_base, contract_->haptic_sink_rva, 1) ||
|
||||
*reinterpret_cast<const unsigned char*>(
|
||||
image_base + contract_->haptic_sink_rva) != 0xc3) {
|
||||
return FailedPrecondition("VR haptic vtable contract mismatch");
|
||||
}
|
||||
image_base_.store(image_base, std::memory_order_relaxed);
|
||||
image_size_.store(ImageSize(image_base), std::memory_order_relaxed);
|
||||
vtable_ =
|
||||
reinterpret_cast<std::uintptr_t *>(image_base + profile_.vtable_rva);
|
||||
// 2998's haptic sink is a verified no-op (ret at 0x1d48b70).
|
||||
// The caller at 0x28a767e passes self, hand 0/1 and float amplitude.
|
||||
if (vtable_[3] != image_base + 0x1d48b70 ||
|
||||
!IsExecutableImageRange(image_base, 0x1d48b70, 1) ||
|
||||
*reinterpret_cast<const unsigned char*>(image_base + 0x1d48b70) != 0xc3)
|
||||
{ vtable_ = nullptr; return FailedPrecondition("2998 VR haptic vtable contract mismatch"); }
|
||||
vtable_ = reinterpret_cast<std::uintptr_t*>(image_base + profile_.vtable_rva);
|
||||
original_haptic_slot_ = vtable_[3];
|
||||
original_state_slot_ = vtable_[kStateGetterVtableSlot / sizeof(void *)];
|
||||
original_eye_slot_ = vtable_[kEyeGetterVtableSlot / sizeof(void *)];
|
||||
@@ -699,7 +792,7 @@ Status RobloxVrDeviceBridge::Activate(std::uintptr_t image_base) {
|
||||
"protection");
|
||||
}
|
||||
|
||||
if (!InstallPointerHook(image_base)) {
|
||||
if (!InstallPointerHook(image_base, contract_->pointer_frame_rva)) {
|
||||
(void)RestoreVtableLocked();
|
||||
return FailedPrecondition("could not install verified VR aim pointer hook");
|
||||
}
|
||||
@@ -891,8 +984,7 @@ bool internal::ApplyXrPose(void *state, const ScriptedPoseSample &pose) {
|
||||
std::memset(bytes + 0x88, 0, 28 * sizeof(float));
|
||||
// Hand records are cleared unless a tracked grip pose is supplied below.
|
||||
// Clearing the valid byte (never the whole record) makes the guest report
|
||||
// the hand as untracked instead of holding a stale pose. The extra index-3
|
||||
// record is unused by this backend and stays invalid.
|
||||
// the hand as untracked instead of holding a stale pose. Index 3 is Floor.
|
||||
bytes[kExtraRecordOffset + kRecordValidOffset] = 0;
|
||||
bytes[kLeftHandRecordOffset + kRecordValidOffset] = 0;
|
||||
bytes[kRightHandRecordOffset + kRecordValidOffset] = 0;
|
||||
@@ -914,7 +1006,7 @@ bool internal::ApplyXrPose(void *state, const ScriptedPoseSample &pose) {
|
||||
if (!std::isfinite(v))
|
||||
return false;
|
||||
for (float v : pose.eye_fov[eye])
|
||||
if (!std::isfinite(v) || std::abs(v) >= 1.56f)
|
||||
if (!std::isfinite(v) || std::abs(v) >= pose::kMaxEyeFovAngle)
|
||||
return false;
|
||||
}
|
||||
bytes[0x80] = pose.valid && pose.controllers_connected;
|
||||
@@ -928,9 +1020,25 @@ bool internal::ApplyXrPose(void *state, const ScriptedPoseSample &pose) {
|
||||
}
|
||||
bytes[0x132] = pose.valid && pose.controllers_connected;
|
||||
|
||||
// 2998's 0x37eacb8 converts metres to studs itself (factor 10/3).
|
||||
// The verified guest pose helper applies metres-to-studs conversion.
|
||||
std::memcpy(bytes + 4, pose.position, sizeof(pose.position));
|
||||
std::memcpy(bytes + 0x10, pose.orientation, sizeof(pose.orientation));
|
||||
if (pose.floor_valid) {
|
||||
bool finite = true;
|
||||
float floor_norm = 0.f;
|
||||
for (float value : pose.floor_position) finite &= std::isfinite(value);
|
||||
for (float value : pose.floor_orientation) {
|
||||
finite &= std::isfinite(value);
|
||||
floor_norm += value * value;
|
||||
}
|
||||
if (finite && floor_norm >= .99f && floor_norm <= 1.01f) {
|
||||
std::memcpy(bytes + kExtraRecordOffset + kRecordPositionOffset,
|
||||
pose.floor_position, sizeof(pose.floor_position));
|
||||
std::memcpy(bytes + kExtraRecordOffset + kRecordOrientationOffset,
|
||||
pose.floor_orientation, sizeof(pose.floor_orientation));
|
||||
bytes[kExtraRecordOffset + kRecordValidOffset] = 1;
|
||||
}
|
||||
}
|
||||
// Tracked hands go into the same state copy the head uses, so the guest's
|
||||
// own conversion (10/3 studs) and -20 deg hand pitch offset apply once,
|
||||
// exactly as they do for the head. Grip is the held-object pose; a hand
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
#include "compat/build_profile.h"
|
||||
#include "mocktail/vr/xr_controller.h"
|
||||
@@ -10,6 +11,19 @@
|
||||
namespace mocktail::vr { struct ScriptedPoseSample; }
|
||||
namespace mocktail::vr::internal {
|
||||
|
||||
struct VrBuildContract {
|
||||
std::string_view build_id;
|
||||
compat::VrDebugDeviceBridgeProfile device;
|
||||
std::uintptr_t state_memcpy_rva;
|
||||
std::uintptr_t pointer_frame_rva;
|
||||
std::uintptr_t workspace_getter_rva;
|
||||
std::uintptr_t camera_vtable_offset;
|
||||
std::uintptr_t hand_pitch_storage_rva;
|
||||
std::uintptr_t haptic_sink_rva;
|
||||
};
|
||||
|
||||
const VrBuildContract* FindVrBuildContract(std::string_view build_id);
|
||||
|
||||
// Writes only a caller-owned state copy; preserves all resource readiness.
|
||||
bool ApplyXrPose(void* state, const ScriptedPoseSample& pose);
|
||||
// Adjust a world-space grip CoordinateFrame to OpenXR aim without changing
|
||||
@@ -78,16 +92,21 @@ EyeResourceState InspectEyeResources(const void *object);
|
||||
|
||||
// Exact 33-byte state getter: sret ABI (rdi = hidden 0x138-byte result
|
||||
// buffer, rsi = device), copies state from self+0x14 and returns the buffer.
|
||||
bool HasExpectedVrStateGetterContract(const std::uint8_t *code,
|
||||
std::size_t size);
|
||||
bool HasExpectedVrStateGetterContract(const std::uint8_t* code,
|
||||
std::size_t size,
|
||||
const VrBuildContract& contract);
|
||||
|
||||
// Exact 16-byte eye framebuffer getter: self + 0x150 + eyeIndex * 16.
|
||||
bool HasExpectedVrEyeGetterContract(const std::uint8_t *code, std::size_t size);
|
||||
|
||||
// Constructor: captures rcx (graphics device) into self+0x198, width/height
|
||||
// into +0x190/+0x194, type dword 6 into +0x10, and clears the state block.
|
||||
bool HasExpectedVrConstructorContract(const std::uint8_t *code,
|
||||
std::size_t size);
|
||||
bool HasExpectedVrConstructorContract(
|
||||
const std::uint8_t* code, std::size_t size,
|
||||
const compat::VrDebugDeviceBridgeProfile& profile);
|
||||
|
||||
bool HasExpectedVrPointerContract(const std::uint8_t* code, std::size_t size,
|
||||
const VrBuildContract& contract);
|
||||
|
||||
// Eye-resource initializer: raises ready byte self+0x147 plus +0x94/+0x144
|
||||
// and creates the per-eye resources through the graphics-device vtable slot
|
||||
|
||||
+15
-5
@@ -54,6 +54,14 @@ constexpr const char *kProfileTouch =
|
||||
"/interaction_profiles/oculus/touch_controller";
|
||||
constexpr const char *kProfileQuest2 =
|
||||
"/interaction_profiles/meta/touch_controller_quest_2";
|
||||
constexpr const char* kTouchProfiles[] = {
|
||||
kProfileTouch,
|
||||
kProfileQuest2,
|
||||
"/interaction_profiles/facebook/touch_controller_pro",
|
||||
"/interaction_profiles/meta/touch_controller_plus",
|
||||
"/interaction_profiles/meta/touch_pro_controller",
|
||||
"/interaction_profiles/meta/touch_plus_controller",
|
||||
};
|
||||
constexpr const char *kProfileIndex =
|
||||
"/interaction_profiles/valve/index_controller";
|
||||
constexpr const char *kProfileVive =
|
||||
@@ -191,8 +199,9 @@ Status XrActions::Create(void *xr_instance) {
|
||||
binds.emplace_back(impl_->touches[static_cast<std::size_t>(kind)],
|
||||
path(component));
|
||||
};
|
||||
const bool is_touch = std::strcmp(profile, kProfileTouch) == 0 ||
|
||||
std::strcmp(profile, kProfileQuest2) == 0;
|
||||
const bool is_touch = std::any_of(
|
||||
std::begin(kTouchProfiles), std::end(kTouchProfiles),
|
||||
[&](const char* name) { return std::strcmp(profile, name) == 0; });
|
||||
const bool is_index = std::strcmp(profile, kProfileIndex) == 0;
|
||||
if (is_touch || is_index) {
|
||||
touch(ControllerTouch::kTrigger, "/user/hand/left/input/trigger/touch");
|
||||
@@ -209,7 +218,7 @@ Status XrActions::Create(void *xr_instance) {
|
||||
touch(ControllerTouch::kY, is_touch ? "/user/hand/left/input/y/touch"
|
||||
: "/user/hand/left/input/b/touch");
|
||||
}
|
||||
if (is_touch) {
|
||||
if (is_touch && std::strcmp(profile, kProfileQuest2) != 0) {
|
||||
touch(ControllerTouch::kThumbrest,
|
||||
"/user/hand/left/input/thumbrest/touch");
|
||||
touch(ControllerTouch::kThumbrest,
|
||||
@@ -244,7 +253,7 @@ Status XrActions::Create(void *xr_instance) {
|
||||
const XrPath l_haptic = path("/user/hand/left/output/haptic");
|
||||
const XrPath r_haptic = path("/user/hand/right/output/haptic");
|
||||
|
||||
for (const char *profile : {kProfileTouch, kProfileQuest2}) {
|
||||
for (const char* profile : kTouchProfiles) {
|
||||
suggest(
|
||||
profile,
|
||||
{
|
||||
@@ -337,7 +346,8 @@ Status XrActions::Create(void *xr_instance) {
|
||||
created_ = true;
|
||||
Log(" [vr-input] action set created: grip+aim poses, trigger/squeeze "
|
||||
"analog, thumbstick, face/menu buttons, haptic output; bindings "
|
||||
"suggested for Touch/Quest2/Index/Vive/Simple (system button never "
|
||||
"suggested for Touch/Quest2/Touch Pro/Touch Plus/Index/Vive/Simple "
|
||||
"(system button never "
|
||||
"bound)\n");
|
||||
return Status::Ok();
|
||||
} catch (const std::exception &error) {
|
||||
|
||||
@@ -60,6 +60,10 @@ if(MOCKTAIL_ENABLE_VR)
|
||||
target_include_directories(openxr_backend_test PRIVATE "${PROJECT_SOURCE_DIR}/third_party/OpenXR-SDK/include")
|
||||
target_link_libraries(openxr_backend_test PRIVATE Mocktail::VR GTest::gtest_main)
|
||||
gtest_discover_tests(openxr_backend_test)
|
||||
add_executable(vr_queue_submission_test vr/queue_submission_test.cc)
|
||||
target_link_libraries(vr_queue_submission_test PRIVATE
|
||||
Mocktail::VR stub_vulkan GTest::gtest_main)
|
||||
gtest_discover_tests(vr_queue_submission_test)
|
||||
add_executable(gles_vr_transport_test vr/gles_transport_test.cc)
|
||||
target_include_directories(gles_vr_transport_test PRIVATE "${PROJECT_SOURCE_DIR}/src" "${PROJECT_SOURCE_DIR}/third_party/OpenXR-SDK/include" ${MOCKTAIL_GLES3_INCLUDE_DIR})
|
||||
target_link_libraries(gles_vr_transport_test PRIVATE Mocktail::VR SDL3::SDL3 GTest::gtest_main)
|
||||
@@ -640,6 +644,14 @@ target_include_directories(roblox_output_device_bridge_test PRIVATE
|
||||
)
|
||||
mocktail_apply_compile_options(roblox_output_device_bridge_test)
|
||||
|
||||
add_executable(native_input_capture_test native_input_capture_test.cc)
|
||||
target_include_directories(native_input_capture_test PRIVATE
|
||||
"${PROJECT_SOURCE_DIR}/src")
|
||||
target_link_libraries(native_input_capture_test PRIVATE
|
||||
mocktail_audio_sdl GTest::gtest_main)
|
||||
mocktail_apply_compile_options(native_input_capture_test)
|
||||
gtest_discover_tests(native_input_capture_test)
|
||||
|
||||
add_executable(window_text_input_owner_test
|
||||
window_text_input_owner_test.cc
|
||||
)
|
||||
|
||||
@@ -14,6 +14,23 @@ namespace {
|
||||
const std::string kManifestPath =
|
||||
std::string(MOCKTAIL_TEST_SOURCE_DIR) + "/config/roblox_compatibility.json";
|
||||
|
||||
TEST(BuildProfileTest, Payload3092IncludesVrAndSecondAudioLayout) {
|
||||
const auto result = FindBuildProfile(
|
||||
kManifestPath, "5f0704edd9064f566ee3d6df2bd2fabbcc709f03");
|
||||
ASSERT_TRUE(result) << result.error;
|
||||
ASSERT_TRUE(result.profile.has_value());
|
||||
EXPECT_EQ(result.profile->version_name, "2.738.1397");
|
||||
EXPECT_EQ(result.profile->version_code, 3092);
|
||||
EXPECT_TRUE(result.profile->default_allowed);
|
||||
EXPECT_TRUE(result.profile->allow_host_abi_bridges);
|
||||
EXPECT_FALSE(result.profile->allow_legacy_binary_patches);
|
||||
ASSERT_TRUE(result.profile->vr_debug_device_bridge.has_value());
|
||||
EXPECT_EQ(result.profile->vr_debug_device_bridge->vtable_rva, 0x6d08108u);
|
||||
ASSERT_TRUE(result.profile->fmod_output_device_bridge.has_value());
|
||||
EXPECT_EQ(result.profile->fmod_output_device_bridge->vtable_layout_version,
|
||||
2u);
|
||||
}
|
||||
|
||||
TEST(BuildProfileTest, FindsCurrentPayloadAsSupported) {
|
||||
const ProfileLookupResult result = FindBuildProfile(
|
||||
kManifestPath, "d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21");
|
||||
|
||||
@@ -28,6 +28,28 @@
|
||||
namespace mocktail::compat {
|
||||
namespace {
|
||||
|
||||
TEST(HostAbiProfileTest, Payload3092KeepsVerifiedConstructorBoundaries) {
|
||||
const auto* profile =
|
||||
FindHostAbiProfile("5f0704edd9064f566ee3d6df2bd2fabbcc709f03");
|
||||
ASSERT_NE(profile, nullptr);
|
||||
EXPECT_EQ(profile->bridge_entry_count, 6u);
|
||||
EXPECT_EQ(profile->init_array_offset, 0x70e1618u);
|
||||
EXPECT_EQ(profile->init_array_count, 3599u);
|
||||
EXPECT_TRUE(profile->HasValidConstructorRanges());
|
||||
EXPECT_TRUE(profile->AllowsConstructor(2));
|
||||
EXPECT_FALSE(profile->AllowsConstructor(3));
|
||||
EXPECT_FALSE(profile->AllowsConstructor(4));
|
||||
EXPECT_TRUE(profile->AllowsConstructor(5));
|
||||
EXPECT_TRUE(profile->AllowsConstructor(3598));
|
||||
EXPECT_FALSE(profile->AllowsConstructor(3599));
|
||||
EXPECT_TRUE(profile->HasValidNativeMimallocConstructorRanges());
|
||||
EXPECT_FALSE(profile->AllowsNativeMimallocConstructor(1));
|
||||
EXPECT_TRUE(profile->AllowsNativeMimallocConstructor(2));
|
||||
EXPECT_TRUE(profile->AllowsNativeMimallocConstructor(3598));
|
||||
EXPECT_FALSE(profile->AllowsNativeMimallocConstructor(3599));
|
||||
EXPECT_TRUE(profile->HasValidNativePreJniBootstrap());
|
||||
}
|
||||
|
||||
TEST(HostAbiProfileTest, Supported2908HasExactDerivedBoundaries) {
|
||||
const HostAbiProfile* profile =
|
||||
FindHostAbiProfile("63c5109637b7d7b2bdb8ed8f858023ff5ef49326");
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
#include "audio/native_input_capture.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include "mocktail/audio/sdl_audio_sink.h"
|
||||
|
||||
namespace mocktail::audio {
|
||||
namespace {
|
||||
|
||||
struct TestSink {
|
||||
const std::uintptr_t* vtable;
|
||||
std::thread::id poll_thread = std::this_thread::get_id();
|
||||
int frames = 0;
|
||||
bool nonzero = false;
|
||||
bool invalid = false;
|
||||
};
|
||||
|
||||
struct TestOwner {
|
||||
const std::uintptr_t* vtable;
|
||||
long shared_owners = 0;
|
||||
long weak_owners = 0;
|
||||
int destroyed = 0;
|
||||
int released = 0;
|
||||
};
|
||||
|
||||
std::size_t ReceivePcm(void* self, const void* pcm, std::size_t samples,
|
||||
std::uint64_t delay, const NativePcmFormat* format) {
|
||||
auto& sink = *static_cast<TestSink*>(self);
|
||||
sink.invalid |= samples != 480 || format->rate != 48000 ||
|
||||
format->channels != 1 || format->bytes_per_sample != 2 ||
|
||||
delay < 10000000 || delay > 200000000 ||
|
||||
std::this_thread::get_id() != sink.poll_thread;
|
||||
const auto* data = static_cast<const std::int16_t*>(pcm);
|
||||
sink.nonzero |= std::any_of(data, data + samples,
|
||||
[](auto sample) { return sample != 0; });
|
||||
++sink.frames;
|
||||
return samples;
|
||||
}
|
||||
|
||||
void DestroySink(void* self) { ++static_cast<TestOwner*>(self)->destroyed; }
|
||||
void ReleaseOwner(void* self) { ++static_cast<TestOwner*>(self)->released; }
|
||||
void OriginalLatency(void*, std::uint32_t* playback, std::uint32_t* recording) {
|
||||
if (playback) *playback = 42;
|
||||
if (recording) *recording = 99;
|
||||
}
|
||||
|
||||
const std::array<std::uintptr_t, 3> kSinkVtable{
|
||||
0, 0, reinterpret_cast<std::uintptr_t>(&ReceivePcm)};
|
||||
const std::array<std::uintptr_t, 5> kOwnerVtable{
|
||||
0, 0, reinterpret_cast<std::uintptr_t>(&DestroySink), 0,
|
||||
reinterpret_cast<std::uintptr_t>(&ReleaseOwner)};
|
||||
|
||||
bool TestRelro(std::uintptr_t base, std::uintptr_t rva, std::size_t size) {
|
||||
return (base + rva == reinterpret_cast<std::uintptr_t>(kSinkVtable.data()) &&
|
||||
size <= sizeof(kSinkVtable)) ||
|
||||
(base + rva == reinterpret_cast<std::uintptr_t>(kOwnerVtable.data()) &&
|
||||
size <= sizeof(kOwnerVtable));
|
||||
}
|
||||
|
||||
bool TestCode(std::uintptr_t base, std::uintptr_t rva, std::size_t) {
|
||||
return base + rva == kSinkVtable[2] || base + rva == kOwnerVtable[2] ||
|
||||
base + rva == kOwnerVtable[4];
|
||||
}
|
||||
|
||||
class NativeInputCaptureTest : public testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
const int fd = mkstemp(pcm_path_);
|
||||
ASSERT_GE(fd, 0);
|
||||
FILE* file = fdopen(fd, "wb");
|
||||
if (!file) close(fd);
|
||||
ASSERT_NE(file, nullptr);
|
||||
const std::vector<unsigned char> samples(1024 * 1024, 0x3f);
|
||||
const auto written = std::fwrite(samples.data(), 1, samples.size(), file);
|
||||
const int closed = std::fclose(file);
|
||||
ASSERT_EQ(written, samples.size());
|
||||
ASSERT_EQ(closed, 0);
|
||||
ASSERT_TRUE(SDL_SetHintWithPriority(SDL_HINT_AUDIO_DRIVER, "disk",
|
||||
SDL_HINT_OVERRIDE));
|
||||
ASSERT_TRUE(SDL_SetHintWithPriority(SDL_HINT_AUDIO_DISK_INPUT_FILE,
|
||||
pcm_path_, SDL_HINT_OVERRIDE));
|
||||
ASSERT_TRUE(SDL_SetHintWithPriority(SDL_HINT_AUDIO_DISK_OUTPUT_FILE,
|
||||
"/dev/null", SDL_HINT_OVERRIDE));
|
||||
ASSERT_TRUE(InitializeSdlAudioSubsystem().ok());
|
||||
ASSERT_STREQ(SDL_GetCurrentAudioDriver(), "disk");
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
g_native_capture.store(nullptr);
|
||||
if (capture_) capture_->Shutdown();
|
||||
EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok());
|
||||
SDL_ResetHint(SDL_HINT_AUDIO_DRIVER);
|
||||
SDL_ResetHint(SDL_HINT_AUDIO_DISK_INPUT_FILE);
|
||||
SDL_ResetHint(SDL_HINT_AUDIO_DISK_OUTPUT_FILE);
|
||||
std::remove(pcm_path_);
|
||||
}
|
||||
|
||||
void Publish(bool enabled) {
|
||||
capture_ = std::make_unique<NativeInputCapture>(
|
||||
NativeInputCapture::SinkAbi{1, &TestRelro, &TestCode, &OriginalLatency},
|
||||
enabled);
|
||||
g_native_capture.store(capture_.get());
|
||||
}
|
||||
|
||||
char pcm_path_[64] = "/tmp/mocktail-native-capture-XXXXXX";
|
||||
std::unique_ptr<NativeInputCapture> capture_;
|
||||
TestSink sink_{kSinkVtable.data()};
|
||||
TestOwner owner_{kOwnerVtable.data()};
|
||||
int device_ = 0;
|
||||
};
|
||||
|
||||
TEST_F(NativeInputCaptureTest, DeliversPcmAndReopensAfterStop) {
|
||||
Publish(true);
|
||||
const auto format = NativeInputCapture::Format(&device_);
|
||||
EXPECT_EQ(format.present, 1u);
|
||||
EXPECT_EQ(format.format.rate, 48000u);
|
||||
EXPECT_EQ(format.format.channels, 1u);
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
owner_ = {kOwnerVtable.data()};
|
||||
sink_ = {kSinkVtable.data()};
|
||||
NativeSharedSink parameter{&sink_, &owner_};
|
||||
ASSERT_TRUE(NativeInputCapture::Start(&device_, ¶meter));
|
||||
EXPECT_EQ(parameter.object, nullptr);
|
||||
EXPECT_EQ(parameter.owner, nullptr);
|
||||
EXPECT_TRUE(NativeInputCapture::Recording(&device_));
|
||||
const auto deadline =
|
||||
std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
||||
while (sink_.frames < 3 && std::chrono::steady_clock::now() < deadline) {
|
||||
NativeInputCapture::Poll(&device_);
|
||||
SDL_Delay(10);
|
||||
}
|
||||
EXPECT_GE(sink_.frames, 3);
|
||||
EXPECT_TRUE(sink_.nonzero);
|
||||
EXPECT_FALSE(sink_.invalid);
|
||||
std::uint32_t playback = 0, recording = 0;
|
||||
NativeInputCapture::Latency(&device_, &playback, &recording);
|
||||
EXPECT_EQ(playback, 42u);
|
||||
EXPECT_GE(recording, 10u);
|
||||
NativeSharedSink stop{&sink_, &owner_};
|
||||
NativeInputCapture::Stop(&device_, &stop);
|
||||
EXPECT_FALSE(NativeInputCapture::Recording(&device_));
|
||||
EXPECT_EQ(owner_.destroyed, 1);
|
||||
EXPECT_EQ(owner_.released, 1);
|
||||
const int frames = sink_.frames;
|
||||
NativeInputCapture::Poll(&device_);
|
||||
NativeInputCapture::Stop(&device_, &stop);
|
||||
EXPECT_EQ(sink_.frames, frames);
|
||||
EXPECT_EQ(owner_.released, 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(NativeInputCaptureTest, RejectsDisabledAndClosedCapture) {
|
||||
Publish(false);
|
||||
NativeSharedSink parameter{&sink_, &owner_};
|
||||
EXPECT_EQ(NativeInputCapture::Format(&device_).present, 0u);
|
||||
EXPECT_FALSE(NativeInputCapture::Start(&device_, ¶meter));
|
||||
EXPECT_EQ(parameter.object, &sink_);
|
||||
Publish(true);
|
||||
ASSERT_TRUE(NativeInputCapture::Start(&device_, ¶meter));
|
||||
capture_->Shutdown();
|
||||
EXPECT_EQ(NativeInputCapture::Format(&device_).present, 0u);
|
||||
EXPECT_FALSE(NativeInputCapture::Recording(&device_));
|
||||
parameter = {&sink_, &owner_};
|
||||
EXPECT_FALSE(NativeInputCapture::Start(&device_, ¶meter));
|
||||
EXPECT_EQ(owner_.released, 1);
|
||||
}
|
||||
|
||||
TEST_F(NativeInputCaptureTest, RejectsUnverifiedSinkWithoutTakingOwnership) {
|
||||
Publish(true);
|
||||
const std::array<std::uintptr_t, 3> unknown_vtable{};
|
||||
TestSink unknown{unknown_vtable.data()};
|
||||
NativeSharedSink parameter{&unknown, &owner_};
|
||||
EXPECT_FALSE(NativeInputCapture::Start(&device_, ¶meter));
|
||||
EXPECT_EQ(parameter.object, &unknown);
|
||||
EXPECT_EQ(parameter.owner, &owner_);
|
||||
EXPECT_FALSE(NativeInputCapture::Recording(&device_));
|
||||
EXPECT_EQ(owner_.released, 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mocktail::audio
|
||||
@@ -48,6 +48,86 @@ TEST(RobloxOutputDeviceBridgeTest, ValidatesEveryInterposedVtableSlot) {
|
||||
vtable.data(), kImageBase, profile));
|
||||
}
|
||||
|
||||
TEST(RobloxOutputDeviceBridgeTest, Validates3092LayoutWithoutUsingListSlots) {
|
||||
constexpr std::uintptr_t kBase = 0x10000000;
|
||||
auto profile = TestProfile();
|
||||
profile.vtable_layout_version = 2;
|
||||
std::array<std::uintptr_t, 20> vtable{};
|
||||
vtable[5] = kBase + profile.count_method_rva;
|
||||
vtable[6] = kBase + profile.info_method_rva;
|
||||
vtable[7] = kBase + 0x7000;
|
||||
vtable[8] = kBase + profile.current_method_rva;
|
||||
vtable[17] = kBase + 0x8000;
|
||||
vtable[19] = kBase + profile.select_method_rva;
|
||||
EXPECT_TRUE(internal::HasExpectedFmodOutputDeviceVtable(vtable.data(), kBase,
|
||||
profile));
|
||||
profile.vtable_layout_version = 1;
|
||||
EXPECT_FALSE(internal::HasExpectedFmodOutputDeviceVtable(vtable.data(), kBase,
|
||||
profile));
|
||||
profile.vtable_layout_version = 3;
|
||||
EXPECT_FALSE(internal::HasExpectedFmodOutputDeviceVtable(vtable.data(), kBase,
|
||||
profile));
|
||||
}
|
||||
|
||||
TEST(RobloxOutputDeviceBridgeTest, PinsCaptureToBuildIdVtableAndLayout) {
|
||||
compat::BuildProfile profile;
|
||||
profile.allow_host_abi_bridges = true;
|
||||
profile.elf_build_id = "5f0704edd9064f566ee3d6df2bd2fabbcc709f03";
|
||||
EXPECT_EQ(internal::FindFmodInputCaptureProfile(profile), nullptr);
|
||||
profile.fmod_output_device_bridge = TestProfile();
|
||||
EXPECT_EQ(internal::FindFmodInputCaptureProfile(profile), nullptr);
|
||||
profile.fmod_output_device_bridge->vtable_rva = 0x6cd3ce0;
|
||||
EXPECT_EQ(internal::FindFmodInputCaptureProfile(profile), nullptr);
|
||||
profile.fmod_output_device_bridge->vtable_layout_version = 2;
|
||||
ASSERT_NE(internal::FindFmodInputCaptureProfile(profile), nullptr);
|
||||
profile.allow_host_abi_bridges = false;
|
||||
EXPECT_EQ(internal::FindFmodInputCaptureProfile(profile), nullptr);
|
||||
profile.allow_host_abi_bridges = true;
|
||||
profile.elf_build_id = "unknown";
|
||||
EXPECT_EQ(internal::FindFmodInputCaptureProfile(profile), nullptr);
|
||||
}
|
||||
|
||||
TEST(RobloxOutputDeviceBridgeTest, ValidatesEveryCaptureSlotOnBothBuilds) {
|
||||
constexpr std::uintptr_t kBase = 0x10000000;
|
||||
for (int version : {2998, 3092}) {
|
||||
compat::BuildProfile profile;
|
||||
profile.allow_host_abi_bridges = true;
|
||||
profile.elf_build_id = version == 2998
|
||||
? "ade08266c67aee88ec9c1d00902150e1684dad3a"
|
||||
: "5f0704edd9064f566ee3d6df2bd2fabbcc709f03";
|
||||
profile.fmod_output_device_bridge = TestProfile();
|
||||
profile.fmod_output_device_bridge->vtable_rva =
|
||||
version == 2998 ? 0x6c58040 : 0x6cd3ce0;
|
||||
profile.fmod_output_device_bridge->vtable_layout_version =
|
||||
version == 2998 ? 1 : 2;
|
||||
const auto* capture = internal::FindFmodInputCaptureProfile(profile);
|
||||
ASSERT_NE(capture, nullptr);
|
||||
const std::array<std::size_t, 6> expected_slots =
|
||||
version == 2998 ? std::array<std::size_t, 6>{4, 11, 12, 13, 14, 15}
|
||||
: std::array<std::size_t, 6>{4, 13, 14, 15, 16, 17};
|
||||
EXPECT_EQ(capture->slots, expected_slots);
|
||||
std::array<std::uintptr_t, 20> vtable{};
|
||||
for (std::size_t i = 0; i < capture->slots.size(); ++i) {
|
||||
vtable[capture->slots[i]] = kBase + capture->method_rvas[i];
|
||||
}
|
||||
EXPECT_TRUE(internal::HasExpectedFmodInputCaptureVtable(vtable.data(),
|
||||
kBase, *capture));
|
||||
for (const auto slot : capture->slots) {
|
||||
++vtable[slot];
|
||||
EXPECT_FALSE(internal::HasExpectedFmodInputCaptureVtable(vtable.data(),
|
||||
kBase, *capture))
|
||||
<< slot;
|
||||
--vtable[slot];
|
||||
}
|
||||
EXPECT_FALSE(
|
||||
internal::HasExpectedFmodInputCaptureVtable(nullptr, kBase, *capture));
|
||||
EXPECT_FALSE(internal::HasExpectedFmodInputCaptureVtable(vtable.data(), 0,
|
||||
*capture));
|
||||
EXPECT_FALSE(internal::HasExpectedFmodInputCaptureVtable(
|
||||
vtable.data(), UINTPTR_MAX, *capture));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RobloxOutputDeviceBridgeTest, BuildsStableDistinctHostGuids) {
|
||||
const std::string first = internal::MakeOutputDeviceGuid(17, "USB Headset");
|
||||
EXPECT_EQ(first, internal::MakeOutputDeviceGuid(17, "USB Headset"));
|
||||
|
||||
@@ -20,6 +20,18 @@ struct OpenXrBackendTestAccess {
|
||||
backend.published_pose_.valid = true;
|
||||
}
|
||||
static VkInstance Instance(const OpenXrBackend& b) { return b.vk_instance_; }
|
||||
static void GraphicsFamily(OpenXrBackend& b, unsigned family) {
|
||||
b.vk_queue_family_ = family;
|
||||
b.vk_graphics_queue_ = Handle<VkQueue>(30);
|
||||
b.device_queue_families_ = {family, family + 1};
|
||||
}
|
||||
static VkQueue CopyQueue(const OpenXrBackend& b, VkQueue present) {
|
||||
return b.GraphicsQueueForPresentLocked(present);
|
||||
}
|
||||
static void NextFrame(OpenXrBackend& b) { ++b.published_pose_.frame; }
|
||||
static bool SupportsQueue(const OpenXrBackend& b, VkQueue queue) {
|
||||
return b.SupportsPresentQueueLocked(queue);
|
||||
}
|
||||
static void Attachment(OpenXrBackend& b, int id, void* owner, VkDevice device) {
|
||||
auto image = Handle<VkImage>(id);
|
||||
auto& r = b.images_[image];
|
||||
@@ -29,16 +41,36 @@ struct OpenXrBackendTestAccess {
|
||||
b.image_views_[Handle<VkImageView>(id)] = image;
|
||||
b.framebuffer_views_[Handle<VkFramebuffer>(id)] = {Handle<VkImageView>(id)};
|
||||
}
|
||||
static void Pass(OpenXrBackend& b, RobloxVrDeviceBridge& bridge, int eye, int id, void* owner,
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
|
||||
static void Pass(
|
||||
OpenXrBackend& b, RobloxVrDeviceBridge& bridge, int eye, int id,
|
||||
void* owner,
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
VkCommandBuffer command = VK_NULL_HANDLE) {
|
||||
bridge.OnEyeGetterCall(owner, eye, Handle<void*>(id));
|
||||
VkRenderPassBeginInfo info{};
|
||||
info.framebuffer = Handle<VkFramebuffer>(id);
|
||||
info.renderPass = Handle<VkRenderPass>(99);
|
||||
b.render_pass_final_layouts_[info.renderPass] = {layout};
|
||||
b.NoteRenderPassBegin(VK_NULL_HANDLE, &info);
|
||||
b.NoteRenderPassBegin(command, &info);
|
||||
}
|
||||
static VkImage Image(const OpenXrBackend& b, int eye) { return b.eyes_[eye].image; }
|
||||
static void AddMsaaAttachment(OpenXrBackend& b, int framebuffer, int msaa) {
|
||||
b.images_[Handle<VkImage>(msaa)].samples = VK_SAMPLE_COUNT_4_BIT;
|
||||
b.framebuffer_views_[Handle<VkFramebuffer>(framebuffer)].insert(
|
||||
b.framebuffer_views_[Handle<VkFramebuffer>(framebuffer)].begin(),
|
||||
Handle<VkImageView>(msaa));
|
||||
}
|
||||
static void ResolvedPass(OpenXrBackend& b, RobloxVrDeviceBridge& bridge,
|
||||
void* owner, int framebuffer) {
|
||||
bridge.OnEyeGetterCall(owner, 0, Handle<void*>(framebuffer));
|
||||
VkRenderPassBeginInfo info{};
|
||||
info.framebuffer = Handle<VkFramebuffer>(framebuffer);
|
||||
info.renderPass = Handle<VkRenderPass>(99);
|
||||
b.render_pass_final_layouts_[info.renderPass] = {
|
||||
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
|
||||
b.NoteRenderPassBegin(VK_NULL_HANDLE, &info);
|
||||
}
|
||||
static VkImageLayout Layout(const OpenXrBackend& b, int eye) { return b.eyes_[eye].final_layout; }
|
||||
static void Invalidate(OpenXrBackend& b) {
|
||||
b.InvalidateInFlightPose("test");
|
||||
@@ -62,8 +94,12 @@ struct OpenXrBackendTestAccess {
|
||||
static VkDevice Device(OpenXrBackend& b) { return b.vk_device_; }
|
||||
static std::size_t Resources(OpenXrBackend& b) { return b.images_.size(); }
|
||||
static void SeedResource(OpenXrBackend& b) { b.images_[Handle<VkImage>(99)] = {}; }
|
||||
static void LatchCanted(OpenXrBackend& b) { b.canted_rejection_logged_ = true; }
|
||||
static bool CantedLogged(const OpenXrBackend& b) { return b.canted_rejection_logged_; }
|
||||
static void LatchProjectionRejection(OpenXrBackend& b) {
|
||||
b.projection_rejection_logged_ = true;
|
||||
}
|
||||
static bool ProjectionRejectionLogged(const OpenXrBackend& b) {
|
||||
return b.projection_rejection_logged_;
|
||||
}
|
||||
};
|
||||
using Access = OpenXrBackendTestAccess;
|
||||
TEST(OpenXrBackendLifecycle, DeviceDestructionPreservesLiveInstance) {
|
||||
@@ -181,13 +217,13 @@ TEST(OpenXrBackendLifecycle, VisibilityLostStateSurvivesUntilSessionReset) {
|
||||
EXPECT_FALSE(Access::VisibilityLost(b));
|
||||
}
|
||||
|
||||
TEST(OpenXrBackendLifecycle, TeardownClearsCantedRejectionLatch) {
|
||||
TEST(OpenXrBackendLifecycle, TeardownClearsProjectionRejectionLatch) {
|
||||
OpenXrBackend b;
|
||||
Access::Configure(b);
|
||||
Access::LatchCanted(b);
|
||||
ASSERT_TRUE(Access::CantedLogged(b));
|
||||
Access::LatchProjectionRejection(b);
|
||||
ASSERT_TRUE(Access::ProjectionRejectionLogged(b));
|
||||
b.NoteDeviceDestroyed(Access::Handle<VkDevice>(2));
|
||||
EXPECT_FALSE(Access::CantedLogged(b));
|
||||
EXPECT_FALSE(Access::ProjectionRejectionLogged(b));
|
||||
}
|
||||
} // namespace mocktail::vr
|
||||
|
||||
@@ -214,3 +250,311 @@ TEST(OpenXrDesktopMirror, RequiresTransferUsageAndDropsDestroyedSwapchain) {
|
||||
EXPECT_EQ(b.MirrorDesktop(VK_NULL_HANDLE, nullptr), VK_NOT_READY);
|
||||
}
|
||||
}
|
||||
|
||||
namespace mocktail::vr {
|
||||
namespace {
|
||||
VkResult reset_result, submit_result, wait_result;
|
||||
int resets, submits, waits;
|
||||
VkQueue expected_queue;
|
||||
VkSemaphore expected_semaphores[2];
|
||||
VKAPI_ATTR VkResult VKAPI_CALL ResetPresentFence(VkDevice, uint32_t count,
|
||||
const VkFence*) {
|
||||
EXPECT_EQ(count, 1u);
|
||||
++resets;
|
||||
return reset_result;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL SubmitPresentWait(VkQueue queue, uint32_t count,
|
||||
const VkSubmitInfo* info,
|
||||
VkFence fence) {
|
||||
++submits;
|
||||
EXPECT_EQ(queue, expected_queue);
|
||||
EXPECT_EQ(count, 1u);
|
||||
EXPECT_NE(fence, VK_NULL_HANDLE);
|
||||
EXPECT_EQ(info->commandBufferCount, 0u);
|
||||
EXPECT_EQ(info->waitSemaphoreCount, 2u);
|
||||
EXPECT_EQ(info->signalSemaphoreCount, 0u);
|
||||
for (unsigned i = 0; i < 2; ++i) {
|
||||
EXPECT_EQ(info->pWaitSemaphores[i], expected_semaphores[i]);
|
||||
EXPECT_EQ(info->pWaitDstStageMask[i], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
|
||||
}
|
||||
return submit_result;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL WaitPresentFence(VkDevice, uint32_t count,
|
||||
const VkFence*, VkBool32 all,
|
||||
uint64_t timeout) {
|
||||
++waits;
|
||||
EXPECT_EQ(count, 1u);
|
||||
EXPECT_EQ(all, VK_TRUE);
|
||||
EXPECT_EQ(timeout, UINT64_MAX);
|
||||
return wait_result;
|
||||
}
|
||||
class OpenXrPresentDependencies : public testing::Test {
|
||||
protected:
|
||||
VkPresentInfoKHR info{};
|
||||
void SetUp() override {
|
||||
reset_result = submit_result = wait_result = VK_SUCCESS;
|
||||
resets = submits = waits = 0;
|
||||
expected_queue = Access::Handle<VkQueue>(30);
|
||||
expected_semaphores[0] = Access::Handle<VkSemaphore>(31);
|
||||
expected_semaphores[1] = Access::Handle<VkSemaphore>(32);
|
||||
info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
|
||||
info.waitSemaphoreCount = 2;
|
||||
info.pWaitSemaphores = expected_semaphores;
|
||||
}
|
||||
VkResult Run() {
|
||||
return internal::WaitForPresentDependencies(
|
||||
Access::Handle<VkDevice>(2), expected_queue, info,
|
||||
Access::Handle<VkFence>(33), ResetPresentFence, SubmitPresentWait,
|
||||
WaitPresentFence);
|
||||
}
|
||||
};
|
||||
TEST_F(OpenXrPresentDependencies, ConsumesDependenciesBeforePermittingCopies) {
|
||||
EXPECT_EQ(Run(), VK_SUCCESS);
|
||||
EXPECT_EQ(resets, 1);
|
||||
EXPECT_EQ(submits, 1);
|
||||
EXPECT_EQ(waits, 1);
|
||||
EXPECT_EQ(info.waitSemaphoreCount, 2u);
|
||||
}
|
||||
TEST_F(OpenXrPresentDependencies, MirrorConsumedWaitsNeedNoSecondSubmission) {
|
||||
info.waitSemaphoreCount = 0;
|
||||
info.pWaitSemaphores = nullptr;
|
||||
EXPECT_EQ(Run(), VK_SUCCESS);
|
||||
EXPECT_EQ(resets + submits + waits, 0);
|
||||
}
|
||||
TEST_F(OpenXrPresentDependencies, SubmitFailureDoesNotWaitForUnsignalledFence) {
|
||||
submit_result = VK_ERROR_DEVICE_LOST;
|
||||
EXPECT_EQ(Run(), VK_ERROR_DEVICE_LOST);
|
||||
EXPECT_EQ(submits, 1);
|
||||
EXPECT_EQ(waits, 0);
|
||||
}
|
||||
TEST_F(OpenXrPresentDependencies,
|
||||
ConsumedWaitsNeverAuthorizeFallbackOnTimeout) {
|
||||
wait_result = VK_TIMEOUT;
|
||||
EXPECT_LT(Run(), VK_SUCCESS);
|
||||
EXPECT_EQ(submits, 1);
|
||||
EXPECT_EQ(waits, 1);
|
||||
}
|
||||
TEST_F(OpenXrPresentDependencies, InvalidDependenciesDoNotSubmit) {
|
||||
info.pWaitSemaphores = nullptr;
|
||||
EXPECT_LT(Run(), VK_SUCCESS);
|
||||
EXPECT_EQ(resets + submits + waits, 0);
|
||||
}
|
||||
TEST(OpenXrPresentQueue, RoutesSameAndSplitFamilyPresentationToGraphicsQueue) {
|
||||
OpenXrBackend backend;
|
||||
Access::Configure(backend);
|
||||
Access::GraphicsFamily(backend, 3);
|
||||
const auto device = Access::Handle<VkDevice>(2);
|
||||
const auto graphics = Access::Handle<VkQueue>(30);
|
||||
const auto same_family = Access::Handle<VkQueue>(31);
|
||||
const auto split_family = Access::Handle<VkQueue>(32);
|
||||
backend.NoteQueue(device, graphics, 3, 0);
|
||||
backend.NoteQueue(device, same_family, 3, 1);
|
||||
backend.NoteQueue(device, split_family, 4, 0);
|
||||
EXPECT_TRUE(Access::SupportsQueue(backend, graphics));
|
||||
EXPECT_TRUE(Access::SupportsQueue(backend, same_family));
|
||||
EXPECT_TRUE(Access::SupportsQueue(backend, split_family));
|
||||
EXPECT_EQ(Access::CopyQueue(backend, split_family), graphics);
|
||||
EXPECT_FALSE(Access::SupportsQueue(backend, Access::Handle<VkQueue>(99)));
|
||||
}
|
||||
} // namespace
|
||||
} // namespace mocktail::vr
|
||||
|
||||
namespace mocktail::vr {
|
||||
TEST(OpenXrBackendBinding,
|
||||
SelectsSingleSampleResolveAlongsideMultisampleColor) {
|
||||
OpenXrBackend backend;
|
||||
RobloxVrDeviceBridge bridge;
|
||||
internal::VrBridgeTestAccess::Enable(bridge);
|
||||
Access::Configure(backend);
|
||||
auto* owner = Access::Handle<void*>(100);
|
||||
const auto device = Access::Handle<VkDevice>(2);
|
||||
Access::Attachment(backend, 10, owner, device);
|
||||
Access::Attachment(backend, 11, owner, device);
|
||||
Access::AddMsaaAttachment(backend, 10, 11);
|
||||
Access::ResolvedPass(backend, bridge, owner, 10);
|
||||
EXPECT_EQ(Access::Image(backend, 0), Access::Handle<VkImage>(10));
|
||||
EXPECT_EQ(Access::Layout(backend, 0),
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
}
|
||||
TEST(OpenXrBackendBinding, RejectsMultisampleImageWithoutResolve) {
|
||||
OpenXrBackend backend;
|
||||
RobloxVrDeviceBridge bridge;
|
||||
internal::VrBridgeTestAccess::Enable(bridge);
|
||||
Access::Configure(backend);
|
||||
auto* owner = Access::Handle<void*>(100);
|
||||
Access::Attachment(backend, 10, owner, Access::Handle<VkDevice>(2));
|
||||
Access::AddMsaaAttachment(backend, 10, 10);
|
||||
Access::ResolvedPass(backend, bridge, owner, 10);
|
||||
EXPECT_EQ(Access::Image(backend, 0), VK_NULL_HANDLE);
|
||||
}
|
||||
} // namespace mocktail::vr
|
||||
|
||||
namespace mocktail::vr {
|
||||
namespace {
|
||||
class OpenXrSplitPresentation : public testing::Test {
|
||||
protected:
|
||||
OpenXrBackend backend;
|
||||
RobloxVrDeviceBridge bridge;
|
||||
const VkDevice device = Access::Handle<VkDevice>(2);
|
||||
const VkQueue graphics = Access::Handle<VkQueue>(30);
|
||||
const VkQueue present = Access::Handle<VkQueue>(31);
|
||||
const VkQueue graphics_second = Access::Handle<VkQueue>(32);
|
||||
const VkCommandPool graphics_pool = Access::Handle<VkCommandPool>(40);
|
||||
const VkCommandPool present_pool = Access::Handle<VkCommandPool>(41);
|
||||
const VkCommandBuffer draw = Access::Handle<VkCommandBuffer>(50);
|
||||
const VkCommandBuffer acquire = Access::Handle<VkCommandBuffer>(51);
|
||||
const VkSwapchainKHR chain = Access::Handle<VkSwapchainKHR>(60);
|
||||
const VkImage desktop = Access::Handle<VkImage>(61);
|
||||
void* const owner = Access::Handle<void*>(100);
|
||||
void SetUp() override {
|
||||
Access::Configure(backend);
|
||||
Access::GraphicsFamily(backend, 3);
|
||||
internal::VrBridgeTestAccess::Enable(bridge);
|
||||
backend.NoteQueue(device, graphics, 3, 0);
|
||||
backend.NoteQueue(device, present, 4, 0);
|
||||
backend.NoteQueue(device, graphics_second, 3, 1);
|
||||
backend.RecordCommandPool(device, graphics_pool, 3);
|
||||
backend.RecordCommandPool(device, present_pool, 4);
|
||||
backend.RecordCommandBuffers(device, graphics_pool, 1, &draw);
|
||||
backend.RecordCommandBuffers(device, present_pool, 1, &acquire);
|
||||
RegisterDesktop(true);
|
||||
}
|
||||
void RegisterDesktop(bool converted) {
|
||||
const unsigned families[] = {3, 4};
|
||||
VkSwapchainCreateInfoKHR info{};
|
||||
info.imageArrayLayers = 1;
|
||||
info.imageExtent = {800, 600};
|
||||
info.imageFormat = VK_FORMAT_B8G8R8A8_UNORM;
|
||||
info.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
info.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
|
||||
info.queueFamilyIndexCount = 2;
|
||||
info.pQueueFamilyIndices = families;
|
||||
backend.RecordDesktopSwapchain(device, chain, &info, &desktop, 1,
|
||||
converted);
|
||||
}
|
||||
void Draw() {
|
||||
Access::Attachment(backend, 10, owner, device);
|
||||
Access::Pass(backend, bridge, 0, 10, owner,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, draw);
|
||||
}
|
||||
};
|
||||
TEST_F(OpenXrSplitPresentation, ActualSubmittedGraphicsQueueOwnsTheEyeCopy) {
|
||||
Draw();
|
||||
EXPECT_EQ(Access::CopyQueue(backend, present), VK_NULL_HANDLE);
|
||||
backend.NoteCommandBuffersSubmitted(present, 1, &draw); // Wrong pool family.
|
||||
EXPECT_EQ(Access::CopyQueue(backend, present), VK_NULL_HANDLE);
|
||||
backend.NoteCommandBuffersSubmitted(graphics_second, 1, &draw);
|
||||
EXPECT_EQ(Access::CopyQueue(backend, present), graphics_second);
|
||||
Access::NextFrame(backend);
|
||||
backend.ResetCommandBuffer(draw);
|
||||
Draw();
|
||||
EXPECT_EQ(Access::CopyQueue(backend, present), VK_NULL_HANDLE);
|
||||
backend.NoteCommandBuffersSubmitted(graphics, 1, &draw);
|
||||
EXPECT_EQ(Access::CopyQueue(backend, present), graphics);
|
||||
}
|
||||
TEST_F(OpenXrSplitPresentation,
|
||||
ResetAndFreeDoNotResurrectUnsubmittedEyeCommands) {
|
||||
Draw();
|
||||
backend.ResetCommandBuffer(draw);
|
||||
backend.NoteCommandBuffersSubmitted(graphics, 1, &draw);
|
||||
EXPECT_EQ(Access::CopyQueue(backend, present), VK_NULL_HANDLE);
|
||||
Draw();
|
||||
backend.ForgetCommandPool(graphics_pool);
|
||||
backend.NoteCommandBuffersSubmitted(graphics, 1, &draw);
|
||||
EXPECT_EQ(Access::CopyQueue(backend, present), VK_NULL_HANDLE);
|
||||
}
|
||||
TEST_F(OpenXrSplitPresentation, ConcurrentSwapchainTracksBothFamilies) {
|
||||
EXPECT_EQ(Access::DesktopCount(backend), 1u);
|
||||
EXPECT_EQ(backend.DesktopQueueFamilies(device),
|
||||
(std::vector<unsigned>{3, 4}));
|
||||
EXPECT_TRUE(
|
||||
backend.DesktopQueueFamilies(Access::Handle<VkDevice>(9)).empty());
|
||||
}
|
||||
TEST_F(OpenXrSplitPresentation, PoolResetAndReusedImageRequireNewSubmission) {
|
||||
Draw();
|
||||
backend.NoteCommandBuffersSubmitted(graphics, 1, &draw);
|
||||
ASSERT_EQ(Access::CopyQueue(backend, present), graphics);
|
||||
backend.ResetCommandPool(graphics_pool);
|
||||
EXPECT_EQ(Access::CopyQueue(backend, present), VK_NULL_HANDLE);
|
||||
Draw();
|
||||
backend.NoteCommandBuffersSubmitted(graphics, 1, &draw);
|
||||
ASSERT_EQ(Access::CopyQueue(backend, present), graphics);
|
||||
backend.NoteDestroyImage(Access::Handle<VkImage>(10));
|
||||
Draw();
|
||||
EXPECT_EQ(Access::CopyQueue(backend, present), VK_NULL_HANDLE);
|
||||
backend.NoteCommandBuffersSubmitted(graphics_second, 1, &draw);
|
||||
EXPECT_EQ(Access::CopyQueue(backend, present), graphics_second);
|
||||
}
|
||||
TEST_F(OpenXrSplitPresentation,
|
||||
ReleaseTransitionsOnceAndMatchingAcquireDoesNotRepeat) {
|
||||
unsigned source = 3, destination = 4;
|
||||
VkImageLayout old_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
backend.NormalizeDesktopBarrier(draw, desktop, &source, &destination,
|
||||
&old_layout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
|
||||
EXPECT_EQ(source, VK_QUEUE_FAMILY_IGNORED);
|
||||
EXPECT_EQ(destination, VK_QUEUE_FAMILY_IGNORED);
|
||||
EXPECT_EQ(old_layout, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
source = 3;
|
||||
destination = 4;
|
||||
backend.NormalizeDesktopBarrier(acquire, desktop, &source, &destination,
|
||||
&old_layout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
|
||||
EXPECT_EQ(source, VK_QUEUE_FAMILY_IGNORED);
|
||||
EXPECT_EQ(destination, VK_QUEUE_FAMILY_IGNORED);
|
||||
EXPECT_EQ(old_layout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
|
||||
}
|
||||
TEST_F(OpenXrSplitPresentation, WsiAcquirePreservesPresentToColorTransition) {
|
||||
unsigned source = 4, destination = 3;
|
||||
VkImageLayout old_layout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||
backend.NormalizeDesktopBarrier(draw, desktop, &source, &destination,
|
||||
&old_layout,
|
||||
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
EXPECT_EQ(source, VK_QUEUE_FAMILY_IGNORED);
|
||||
EXPECT_EQ(destination, VK_QUEUE_FAMILY_IGNORED);
|
||||
EXPECT_EQ(old_layout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
|
||||
source = 4;
|
||||
destination = 3;
|
||||
old_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
backend.NormalizeDesktopBarrier(draw, desktop, &source, &destination,
|
||||
&old_layout,
|
||||
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
EXPECT_EQ(old_layout, VK_IMAGE_LAYOUT_UNDEFINED);
|
||||
}
|
||||
TEST_F(OpenXrSplitPresentation,
|
||||
ExplicitPresentReleaseDefersLayoutTransitionToAcquire) {
|
||||
unsigned source = 4, destination = 3;
|
||||
VkImageLayout old_layout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||
const auto released_layout = backend.NormalizeDesktopBarrier(
|
||||
acquire, desktop, &source, &destination, &old_layout,
|
||||
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
EXPECT_EQ(old_layout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
|
||||
EXPECT_EQ(released_layout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
|
||||
EXPECT_EQ(source, VK_QUEUE_FAMILY_IGNORED);
|
||||
source = 4;
|
||||
destination = 3;
|
||||
const auto acquired_layout = backend.NormalizeDesktopBarrier(
|
||||
draw, desktop, &source, &destination, &old_layout,
|
||||
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
EXPECT_EQ(old_layout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
|
||||
EXPECT_EQ(acquired_layout, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
}
|
||||
TEST_F(OpenXrSplitPresentation,
|
||||
OriginalConcurrentAndUnrelatedImagesRemainUnchanged) {
|
||||
RegisterDesktop(false);
|
||||
unsigned source = 3, destination = 4;
|
||||
VkImageLayout old_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
backend.NormalizeDesktopBarrier(acquire, desktop, &source, &destination,
|
||||
&old_layout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
|
||||
EXPECT_EQ(source, 3u);
|
||||
EXPECT_EQ(destination, 4u);
|
||||
RegisterDesktop(true);
|
||||
backend.NormalizeDesktopBarrier(acquire, Access::Handle<VkImage>(99), &source,
|
||||
&destination, &old_layout,
|
||||
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
|
||||
EXPECT_EQ(source, 3u);
|
||||
backend.RecordDesktopSwapchain(device, chain, nullptr, nullptr, 0);
|
||||
backend.NormalizeDesktopBarrier(acquire, desktop, &source, &destination,
|
||||
&old_layout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
|
||||
EXPECT_EQ(source, 3u);
|
||||
}
|
||||
} // namespace
|
||||
} // namespace mocktail::vr
|
||||
@@ -65,7 +65,7 @@ with tempfile.TemporaryDirectory(prefix="mocktail-xr-probe-") as directory:
|
||||
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
|
||||
assert "Roblox VR rendering: run mocktail --vr" 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:
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include "mocktail/vr/openxr_backend.h"
|
||||
|
||||
using SubmitAdapter = VkResult(VKAPI_PTR *)(PFN_vkQueueSubmit, VkDevice,
|
||||
VkQueue, uint32_t,
|
||||
const VkSubmitInfo *, VkFence);
|
||||
extern "C" VKAPI_ATTR VkResult VKAPI_CALL mocktail_vulkan_submit_synchronized(
|
||||
PFN_vkQueueSubmit raw, VkDevice device, VkQueue queue, uint32_t count,
|
||||
const VkSubmitInfo *submits, VkFence fence);
|
||||
extern "C" void mocktail_vr_xr_queue_submit_adapter(SubmitAdapter);
|
||||
extern "C" VKAPI_ATTR VkResult VKAPI_CALL
|
||||
mocktail_vulkan_idle_synchronized(PFN_vkDeviceWaitIdle raw, VkDevice device);
|
||||
|
||||
namespace {
|
||||
std::mutex state_mutex;
|
||||
std::condition_variable state_changed;
|
||||
bool waiting, signal_submitted;
|
||||
bool idle_release;
|
||||
std::atomic<int> inside_driver{0}, max_inside{0};
|
||||
VKAPI_ATTR VkResult VKAPI_CALL Reset(VkDevice, uint32_t, const VkFence *) {
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL Submit(VkQueue, uint32_t,
|
||||
const VkSubmitInfo *info, VkFence) {
|
||||
if (info && info->signalSemaphoreCount) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
signal_submitted = true;
|
||||
state_changed.notify_all();
|
||||
}
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL Wait(VkDevice, uint32_t, const VkFence *,
|
||||
VkBool32, uint64_t) {
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
waiting = true;
|
||||
state_changed.notify_all();
|
||||
return state_changed.wait_for(lock, std::chrono::seconds(2),
|
||||
[] { return signal_submitted; })
|
||||
? VK_SUCCESS
|
||||
: VK_TIMEOUT;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL SlowSubmit(VkQueue, uint32_t,
|
||||
const VkSubmitInfo *, VkFence) {
|
||||
const int active = ++inside_driver;
|
||||
int previous = max_inside.load();
|
||||
while (active > previous &&
|
||||
!max_inside.compare_exchange_weak(previous, active)) {
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
--inside_driver;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL BlockingIdle(VkDevice) {
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
waiting = true;
|
||||
state_changed.notify_all();
|
||||
return state_changed.wait_for(lock, std::chrono::seconds(2),
|
||||
[] { return idle_release; })
|
||||
? VK_SUCCESS
|
||||
: VK_TIMEOUT;
|
||||
}
|
||||
TEST(VrQueueSubmission, DeviceIdleExcludesPrivateSubmitsUntilDriverReturns) {
|
||||
waiting = signal_submitted = idle_release = false;
|
||||
VkResult idle_result = VK_NOT_READY;
|
||||
std::thread idler([&] {
|
||||
idle_result = mocktail_vulkan_idle_synchronized(
|
||||
BlockingIdle, reinterpret_cast<VkDevice>(2));
|
||||
});
|
||||
bool entered = false;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
entered = state_changed.wait_for(lock, std::chrono::seconds(1),
|
||||
[] { return waiting; });
|
||||
}
|
||||
const auto semaphore = reinterpret_cast<VkSemaphore>(32);
|
||||
VkSubmitInfo signal{};
|
||||
signal.signalSemaphoreCount = 1;
|
||||
signal.pSignalSemaphores = &semaphore;
|
||||
std::thread submitter([&] {
|
||||
EXPECT_EQ(mocktail_vulkan_submit_synchronized(
|
||||
Submit, reinterpret_cast<VkDevice>(2),
|
||||
reinterpret_cast<VkQueue>(30), 1, &signal, VK_NULL_HANDLE),
|
||||
VK_SUCCESS);
|
||||
});
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
EXPECT_FALSE(state_changed.wait_for(lock, std::chrono::milliseconds(50),
|
||||
[] { return signal_submitted; }));
|
||||
idle_release = true;
|
||||
state_changed.notify_all();
|
||||
}
|
||||
idler.join();
|
||||
submitter.join();
|
||||
EXPECT_TRUE(entered);
|
||||
EXPECT_EQ(idle_result, VK_SUCCESS);
|
||||
EXPECT_TRUE(signal_submitted);
|
||||
}
|
||||
TEST(VrQueueSubmission, OtherDeviceCanSignalWhileDeviceIdleIsPending) {
|
||||
waiting = signal_submitted = idle_release = false;
|
||||
VkResult idle_result = VK_NOT_READY;
|
||||
// Wait emulates device A waiting for an imported semaphore signal from B.
|
||||
auto idle = [](VkDevice) -> VkResult {
|
||||
return Wait(VK_NULL_HANDLE, 0, nullptr, VK_TRUE, UINT64_MAX);
|
||||
};
|
||||
std::thread idler([&] {
|
||||
idle_result =
|
||||
mocktail_vulkan_idle_synchronized(idle, reinterpret_cast<VkDevice>(2));
|
||||
});
|
||||
bool entered = false;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
entered = state_changed.wait_for(lock, std::chrono::seconds(1),
|
||||
[] { return waiting; });
|
||||
}
|
||||
const auto semaphore = reinterpret_cast<VkSemaphore>(32);
|
||||
VkSubmitInfo signal{};
|
||||
signal.signalSemaphoreCount = 1;
|
||||
signal.pSignalSemaphores = &semaphore;
|
||||
EXPECT_EQ(mocktail_vulkan_submit_synchronized(
|
||||
Submit, reinterpret_cast<VkDevice>(3),
|
||||
reinterpret_cast<VkQueue>(31), 1, &signal, VK_NULL_HANDLE),
|
||||
VK_SUCCESS);
|
||||
idler.join();
|
||||
EXPECT_TRUE(entered);
|
||||
EXPECT_EQ(idle_result, VK_SUCCESS);
|
||||
}
|
||||
TEST(VrQueueSubmission, AnotherQueueCanSignalWhilePresentWaitsForFence) {
|
||||
waiting = signal_submitted = false;
|
||||
mocktail_vr_xr_queue_submit_adapter(mocktail_vulkan_submit_synchronized);
|
||||
const auto graphics = reinterpret_cast<VkQueue>(30);
|
||||
const auto signal_queue = reinterpret_cast<VkQueue>(31);
|
||||
const auto semaphore = reinterpret_cast<VkSemaphore>(32);
|
||||
VkPresentInfoKHR present{};
|
||||
present.waitSemaphoreCount = 1;
|
||||
present.pWaitSemaphores = &semaphore;
|
||||
VkResult prepared = VK_NOT_READY;
|
||||
std::thread presenter([&] {
|
||||
prepared = mocktail::vr::internal::WaitForPresentDependencies(
|
||||
reinterpret_cast<VkDevice>(2), graphics, present,
|
||||
reinterpret_cast<VkFence>(33), Reset, Submit, Wait);
|
||||
});
|
||||
bool entered = false;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
entered = state_changed.wait_for(lock, std::chrono::seconds(1),
|
||||
[] { return waiting; });
|
||||
}
|
||||
VkSubmitInfo signal{};
|
||||
signal.signalSemaphoreCount = 1;
|
||||
signal.pSignalSemaphores = &semaphore;
|
||||
EXPECT_EQ(mocktail_vulkan_submit_synchronized(
|
||||
Submit, reinterpret_cast<VkDevice>(2), signal_queue, 1, &signal,
|
||||
VK_NULL_HANDLE),
|
||||
VK_SUCCESS);
|
||||
presenter.join();
|
||||
mocktail_vr_xr_queue_submit_adapter(nullptr);
|
||||
EXPECT_TRUE(entered);
|
||||
EXPECT_EQ(prepared, VK_SUCCESS);
|
||||
}
|
||||
TEST(VrQueueSubmission, PrivateAndGuestCallsCannotEnterOneQueueConcurrently) {
|
||||
inside_driver = max_inside = 0;
|
||||
const auto queue = reinterpret_cast<VkQueue>(30);
|
||||
auto call = [&] {
|
||||
EXPECT_EQ(mocktail_vulkan_submit_synchronized(
|
||||
SlowSubmit, reinterpret_cast<VkDevice>(2), queue, 0, nullptr,
|
||||
VK_NULL_HANDLE),
|
||||
VK_SUCCESS);
|
||||
};
|
||||
std::thread first(call), second(call);
|
||||
first.join();
|
||||
second.join();
|
||||
EXPECT_EQ(max_inside, 1);
|
||||
}
|
||||
TEST(VrQueueSubmission, FenceWaitDoesNotKeepItsOwnQueueHostLocked) {
|
||||
waiting = signal_submitted = false;
|
||||
mocktail_vr_xr_queue_submit_adapter(mocktail_vulkan_submit_synchronized);
|
||||
const auto queue = reinterpret_cast<VkQueue>(30);
|
||||
const auto semaphore = reinterpret_cast<VkSemaphore>(32);
|
||||
VkPresentInfoKHR present{};
|
||||
present.waitSemaphoreCount = 1;
|
||||
present.pWaitSemaphores = &semaphore;
|
||||
VkResult result = VK_NOT_READY;
|
||||
std::thread presenter([&] {
|
||||
result = mocktail::vr::internal::WaitForPresentDependencies(
|
||||
reinterpret_cast<VkDevice>(2), queue, present,
|
||||
reinterpret_cast<VkFence>(33), Reset, Submit, Wait);
|
||||
});
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
state_changed.wait_for(lock, std::chrono::seconds(1),
|
||||
[] { return waiting; });
|
||||
}
|
||||
// An unrelated already-ready batch can still be enqueued to this queue.
|
||||
EXPECT_EQ(
|
||||
mocktail_vulkan_submit_synchronized(Submit, reinterpret_cast<VkDevice>(2),
|
||||
queue, 0, nullptr, VK_NULL_HANDLE),
|
||||
VK_SUCCESS);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
signal_submitted = true;
|
||||
state_changed.notify_all();
|
||||
}
|
||||
presenter.join();
|
||||
mocktail_vr_xr_queue_submit_adapter(nullptr);
|
||||
EXPECT_EQ(result, VK_SUCCESS);
|
||||
}
|
||||
} // namespace
|
||||
@@ -54,6 +54,11 @@ const std::vector<uint8_t> kEyeGetter = {0x48, 0x63, 0xc6, 0x48, 0xc1, 0xe0,
|
||||
0x04, 0x48, 0x8b, 0x84, 0x07, 0x50,
|
||||
0x01, 0x00, 0x00, 0xc3};
|
||||
|
||||
const internal::VrBuildContract& TestContract() {
|
||||
return *internal::FindVrBuildContract(
|
||||
"ade08266c67aee88ec9c1d00902150e1684dad3a");
|
||||
}
|
||||
|
||||
std::vector<uint8_t> ValidConstructor() {
|
||||
std::vector<uint8_t> code(kConstructorContractSize, 0x90);
|
||||
const std::vector<uint8_t> prologue = {
|
||||
@@ -70,6 +75,7 @@ std::vector<uint8_t> ValidConstructor() {
|
||||
0x00, 0x00},
|
||||
{0x4c, 0x89, 0xbb, 0x98, 0x01, 0x00, 0x00},
|
||||
};
|
||||
std::memcpy(code.data() + 43, markers.front().data(), markers.front().size());
|
||||
size_t offset = 64;
|
||||
for (const auto &marker : markers) {
|
||||
std::memcpy(code.data() + offset, marker.data(), marker.size());
|
||||
@@ -240,19 +246,100 @@ TEST(VrResourceReadiness,
|
||||
|
||||
TEST(VrStateGetterContract, AcceptsExact2998Bytes) {
|
||||
ASSERT_EQ(kStateGetter.size(), kStateGetterContractSize);
|
||||
EXPECT_TRUE(internal::HasExpectedVrStateGetterContract(kStateGetter.data(),
|
||||
kStateGetter.size()));
|
||||
EXPECT_TRUE(internal::HasExpectedVrStateGetterContract(
|
||||
kStateGetter.data(), kStateGetter.size(), TestContract()));
|
||||
}
|
||||
|
||||
TEST(VrStateGetterContract, RejectsCorruptionAndShortInput) {
|
||||
EXPECT_FALSE(internal::HasExpectedVrStateGetterContract(nullptr, 0));
|
||||
EXPECT_FALSE(
|
||||
internal::HasExpectedVrStateGetterContract(nullptr, 0, TestContract()));
|
||||
EXPECT_FALSE(internal::HasExpectedVrStateGetterContract(
|
||||
kStateGetter.data(), kStateGetter.size() - 1));
|
||||
kStateGetter.data(), kStateGetter.size() - 1, TestContract()));
|
||||
std::vector<uint8_t> corrupted = kStateGetter;
|
||||
// The sret copy size (0x138) must be pinned; corrupt one byte of it.
|
||||
corrupted[13] ^= 0xff;
|
||||
EXPECT_FALSE(internal::HasExpectedVrStateGetterContract(corrupted.data(),
|
||||
corrupted.size()));
|
||||
EXPECT_FALSE(internal::HasExpectedVrStateGetterContract(
|
||||
corrupted.data(), corrupted.size(), TestContract()));
|
||||
}
|
||||
|
||||
TEST(VrBuildContracts, RejectsUnknownBuildsAndMismatchedMetadata) {
|
||||
EXPECT_EQ(FindRobloxVrDeviceProfile("unknown"), nullptr);
|
||||
auto profile = MakeBuildProfile(true, true);
|
||||
profile.elf_build_id = "5f0704edd9064f566ee3d6df2bd2fabbcc709f03";
|
||||
RobloxVrDeviceBridge bridge;
|
||||
EXPECT_FALSE(bridge.Install(profile).ok());
|
||||
profile.vr_debug_device_bridge =
|
||||
*FindRobloxVrDeviceProfile(profile.elf_build_id);
|
||||
EXPECT_TRUE(bridge.Install(profile).ok());
|
||||
EXPECT_FALSE(bridge.active());
|
||||
}
|
||||
|
||||
TEST(VrBuildContracts, RestoresVrForApprovedProfileWithoutVrMetadata) {
|
||||
auto profile = MakeBuildProfile(false, true);
|
||||
profile.elf_build_id = "5f0704edd9064f566ee3d6df2bd2fabbcc709f03";
|
||||
const auto* vr = FindRobloxVrDeviceProfile(profile.elf_build_id);
|
||||
ASSERT_NE(vr, nullptr);
|
||||
EXPECT_EQ(vr->device_create_framebuffer_vtable_offset, 0x188u);
|
||||
profile.vr_debug_device_bridge = *vr;
|
||||
RobloxVrDeviceBridge bridge;
|
||||
EXPECT_TRUE(bridge.Install(profile).ok());
|
||||
}
|
||||
|
||||
TEST(VrStateGetterContract, Verifies3092CopyTargetAndRejectsWrongBuild) {
|
||||
const auto& latest = *internal::FindVrBuildContract(
|
||||
"5f0704edd9064f566ee3d6df2bd2fabbcc709f03");
|
||||
auto code = kStateGetter;
|
||||
const std::uint8_t displacement[] = {0xa3, 0x0d, 0x40, 0x03};
|
||||
std::memcpy(code.data() + 19, displacement, sizeof(displacement));
|
||||
EXPECT_TRUE(internal::HasExpectedVrStateGetterContract(code.data(),
|
||||
code.size(), latest));
|
||||
EXPECT_FALSE(internal::HasExpectedVrStateGetterContract(
|
||||
code.data(), code.size(), TestContract()));
|
||||
code[19] ^= 1;
|
||||
EXPECT_FALSE(internal::HasExpectedVrStateGetterContract(code.data(),
|
||||
code.size(), latest));
|
||||
}
|
||||
|
||||
TEST(VrConstructorContract, VerifiesRelocatedVtableFor3092) {
|
||||
const auto& latest =
|
||||
*FindRobloxVrDeviceProfile("5f0704edd9064f566ee3d6df2bd2fabbcc709f03");
|
||||
auto code = ValidConstructor();
|
||||
const std::uint8_t displacement[] = {0xf2, 0xe0, 0x50, 0x03};
|
||||
std::memcpy(code.data() + 46, displacement, sizeof(displacement));
|
||||
EXPECT_TRUE(internal::HasExpectedVrConstructorContract(code.data(),
|
||||
code.size(), latest));
|
||||
EXPECT_FALSE(internal::HasExpectedVrConstructorContract(
|
||||
code.data(), code.size(), TestProfile()));
|
||||
}
|
||||
|
||||
TEST(VrPointerContract, PinsWorkspaceAndCameraSlotForEachBuild) {
|
||||
for (const auto* id : {"ade08266c67aee88ec9c1d00902150e1684dad3a",
|
||||
"5f0704edd9064f566ee3d6df2bd2fabbcc709f03"}) {
|
||||
const auto& contract = *internal::FindVrBuildContract(id);
|
||||
std::array<std::uint8_t, 0x1b2> code{};
|
||||
const std::uint8_t prologue[] = {0x55, 0x48, 0x89, 0xe5, 0x41,
|
||||
0x57, 0x41, 0x56, 0x41, 0x54,
|
||||
0x53, 0x48, 0x83, 0xec, 0x70};
|
||||
std::memcpy(code.data(), prologue, sizeof(prologue));
|
||||
code[0x59] = 0xe8;
|
||||
const std::int32_t displacement =
|
||||
static_cast<std::int64_t>(contract.workspace_getter_rva) -
|
||||
(contract.pointer_frame_rva + 0x5e);
|
||||
std::memcpy(code.data() + 0x5a, &displacement, sizeof(displacement));
|
||||
code[0x6d] = 0xff;
|
||||
code[0x6e] = 0x91;
|
||||
const std::uint32_t slot = contract.camera_vtable_offset;
|
||||
std::memcpy(code.data() + 0x6f, &slot, sizeof(slot));
|
||||
EXPECT_TRUE(internal::HasExpectedVrPointerContract(code.data(), code.size(),
|
||||
contract));
|
||||
code[0x6f] ^= 8;
|
||||
EXPECT_FALSE(internal::HasExpectedVrPointerContract(code.data(),
|
||||
code.size(), contract));
|
||||
code[0x6f] ^= 8;
|
||||
code[0x5a] ^= 1;
|
||||
EXPECT_FALSE(internal::HasExpectedVrPointerContract(code.data(),
|
||||
code.size(), contract));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(VrEyeGetterContract, AcceptsExact2998Bytes) {
|
||||
@@ -271,10 +358,10 @@ TEST(VrEyeGetterContract, RejectsDifferentEyeSlotOffset) {
|
||||
|
||||
TEST(VrConstructorContract, AcceptsMarkersAndRejectsEachMissingOne) {
|
||||
const std::vector<uint8_t> valid = ValidConstructor();
|
||||
EXPECT_TRUE(
|
||||
internal::HasExpectedVrConstructorContract(valid.data(), valid.size()));
|
||||
EXPECT_FALSE(internal::HasExpectedVrConstructorContract(valid.data(),
|
||||
valid.size() - 1));
|
||||
EXPECT_TRUE(internal::HasExpectedVrConstructorContract(
|
||||
valid.data(), valid.size(), TestProfile()));
|
||||
EXPECT_FALSE(internal::HasExpectedVrConstructorContract(
|
||||
valid.data(), valid.size() - 1, TestProfile()));
|
||||
// Corrupting the graphics-device capture (+0x198 store) must fail closed.
|
||||
std::vector<uint8_t> no_device_store = valid;
|
||||
for (size_t index = 0; index + 7 <= no_device_store.size(); ++index) {
|
||||
@@ -285,7 +372,7 @@ TEST(VrConstructorContract, AcceptsMarkersAndRejectsEachMissingOne) {
|
||||
}
|
||||
}
|
||||
EXPECT_FALSE(internal::HasExpectedVrConstructorContract(
|
||||
no_device_store.data(), no_device_store.size()));
|
||||
no_device_store.data(), no_device_store.size(), TestProfile()));
|
||||
}
|
||||
|
||||
TEST(VrInitializerContract, PinsDeviceVtableSlotFromProfile) {
|
||||
@@ -484,7 +571,8 @@ TEST(VrProfileParsing, Issue153VrBridgeFailsWithoutMetadataForArmedProfile) {
|
||||
"count_method_rva": "0x32d1000",
|
||||
"info_method_rva": "0x32d10a0",
|
||||
"current_method_rva": "0x32d1050",
|
||||
"select_method_rva": "0x32d0d04"
|
||||
"select_method_rva": "0x32d0d04",
|
||||
"vtable_layout_version": 2
|
||||
},
|
||||
"reason": "issue 153 reproduction profile: armed without vr metadata"
|
||||
}]
|
||||
@@ -500,6 +588,8 @@ TEST(VrProfileParsing, Issue153VrBridgeFailsWithoutMetadataForArmedProfile) {
|
||||
ASSERT_TRUE(result);
|
||||
ASSERT_TRUE(result.profile.has_value());
|
||||
ASSERT_TRUE(result.profile->fmod_output_device_bridge.has_value());
|
||||
EXPECT_EQ(result.profile->fmod_output_device_bridge->vtable_layout_version,
|
||||
2u);
|
||||
ASSERT_TRUE(result.profile->user_game_settings_fullscreen_setter_rva
|
||||
.has_value());
|
||||
// But it carries no VR debug-device bridge metadata.
|
||||
@@ -607,6 +697,30 @@ TEST(VrHandPoseBridge, ValidGripPosesFillBothHandRecordsInMetres) {
|
||||
EXPECT_FLOAT_EQ(right_pos[0], 0.25f);
|
||||
}
|
||||
|
||||
TEST(VrFloorPoseBridge, PublishesStagePoseInMetresAndClearsTrackingLoss) {
|
||||
std::array<std::uint8_t, internal::kStateCopySize> state{};
|
||||
state[internal::kStateReadyOffsetInCopy] = 1;
|
||||
ScriptedPoseSample pose;
|
||||
pose.valid = pose.floor_valid = true;
|
||||
pose.floor_position[1] = -1.7f;
|
||||
pose.floor_orientation[1] = std::sin(.2f);
|
||||
pose.floor_orientation[3] = std::cos(.2f);
|
||||
ASSERT_TRUE(internal::ApplyXrPose(state.data(), pose));
|
||||
EXPECT_EQ(state[internal::kExtraRecordOffset], 1);
|
||||
float height = 0;
|
||||
std::memcpy(&height, state.data() + internal::kExtraRecordOffset + 8, 4);
|
||||
EXPECT_FLOAT_EQ(height, -1.7f);
|
||||
EXPECT_EQ(state[internal::kStateReadyOffsetInCopy], 1);
|
||||
pose.floor_valid = false;
|
||||
ASSERT_TRUE(internal::ApplyXrPose(state.data(), pose));
|
||||
EXPECT_EQ(state[internal::kExtraRecordOffset], 0);
|
||||
EXPECT_EQ(state[internal::kHeadRecordOffset], 1);
|
||||
pose.floor_valid = true;
|
||||
pose.floor_position[1] = std::numeric_limits<float>::quiet_NaN();
|
||||
ASSERT_TRUE(internal::ApplyXrPose(state.data(), pose));
|
||||
EXPECT_EQ(state[internal::kExtraRecordOffset], 0);
|
||||
}
|
||||
|
||||
TEST(VrHandPoseBridge, UntrackedOrNonFiniteHandsStayInvalidWithoutTouchingHead) {
|
||||
std::array<std::uint8_t, internal::kStateCopySize> state;
|
||||
state.fill(0);
|
||||
|
||||
@@ -151,21 +151,64 @@ TEST(VrPoseMath, FovToTangentsPinsUpDownLeftRightOrderAndSigns) {
|
||||
EXPECT_NE(tangents.up, tangents.down);
|
||||
}
|
||||
|
||||
// ---- canted-view rejection ----
|
||||
TEST(VrPoseMath, ParallelProjectionKeepsUnrotatedFovExact) {
|
||||
const EyeFov display{-.7f, .8f, .9f, -.6f};
|
||||
for (float sign : {1.f, -1.f}) {
|
||||
EyeFov rendered;
|
||||
ASSERT_TRUE(ParallelEyeFov({0, 0, 0, sign}, display, &rendered));
|
||||
EXPECT_EQ(rendered.angle_left, display.angle_left);
|
||||
EXPECT_EQ(rendered.angle_right, display.angle_right);
|
||||
EXPECT_EQ(rendered.angle_up, display.angle_up);
|
||||
EXPECT_EQ(rendered.angle_down, display.angle_down);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(VrPoseMath, ParallelEyeOrientationsAcceptedCantedRejected) {
|
||||
EXPECT_TRUE(EyeOrientationIsParallel(Quat{0.f, 0.f, 0.f, 1.f}));
|
||||
// Sign flip only (q == -q) is still parallel.
|
||||
EXPECT_TRUE(EyeOrientationIsParallel(Quat{0.f, 0.f, 0.f, -1.f}));
|
||||
// Exactly at tolerance.
|
||||
EXPECT_TRUE(EyeOrientationIsParallel(
|
||||
Quat{kCantedTolerance, 0.f, 0.f, 1.f}));
|
||||
// A canted runtime rotates the eye about its view axis (+Z): must reject.
|
||||
const Quat canted = QuatFromAxisAngle(Vec3{0.f, 0.f, 1.f}, 0.1f);
|
||||
EXPECT_FALSE(EyeOrientationIsParallel(canted));
|
||||
// Any axis above tolerance rejects.
|
||||
EXPECT_FALSE(EyeOrientationIsParallel(
|
||||
Quat{0.f, 2.f * kCantedTolerance, 0.f, 1.f}));
|
||||
TEST(VrPoseMath, ParallelProjectionContainsEveryCantedDisplayRay) {
|
||||
const EyeFov display{-.7f, .8f, .9f, -.6f};
|
||||
for (const Vec3 axis : {Vec3{1, 0, 0}, Vec3{0, 1, 0}, Vec3{0, 0, 1}}) {
|
||||
for (float angle : {-.2f, .2f}) {
|
||||
const Quat rotation = QuatFromAxisAngle(axis, angle);
|
||||
EyeFov rendered;
|
||||
ASSERT_TRUE(ParallelEyeFov(rotation, display, &rendered));
|
||||
for (int x = 0; x <= 4; ++x) {
|
||||
for (int y = 0; y <= 4; ++y) {
|
||||
const Vec3 ray =
|
||||
QuatRotate(rotation, {std::tan(display.angle_left) +
|
||||
x * .25f *
|
||||
(std::tan(display.angle_right) -
|
||||
std::tan(display.angle_left)),
|
||||
std::tan(display.angle_down) +
|
||||
y * .25f *
|
||||
(std::tan(display.angle_up) -
|
||||
std::tan(display.angle_down)),
|
||||
-1});
|
||||
const float horizontal = std::atan2(ray.x, -ray.z);
|
||||
const float vertical = std::atan2(ray.y, -ray.z);
|
||||
EXPECT_GE(horizontal, rendered.angle_left - 1e-6f);
|
||||
EXPECT_LE(horizontal, rendered.angle_right + 1e-6f);
|
||||
EXPECT_GE(vertical, rendered.angle_down - 1e-6f);
|
||||
EXPECT_LE(vertical, rendered.angle_up + 1e-6f);
|
||||
}
|
||||
}
|
||||
const Pose head{{1, 2, 3}, QuatFromYawPitch(.4f, -.1f)};
|
||||
const Pose parallel_eye{{-.03f, .01f, .02f}, {}};
|
||||
const Pose submitted = PoseMultiply(head, parallel_eye);
|
||||
EXPECT_TRUE(
|
||||
QuatSameRotation(submitted.orientation, head.orientation, 1e-6f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(VrPoseMath, UnrepresentableProjectionDoesNotModifyOutput) {
|
||||
const EyeFov display{-.7f, .8f, .9f, -.6f};
|
||||
EyeFov rendered{1, 2, 3, 4};
|
||||
EXPECT_FALSE(ParallelEyeFov({0, 0, 0, 0}, display, &rendered));
|
||||
EXPECT_FALSE(ParallelEyeFov({}, {.8f, -.7f, .9f, -.6f}, &rendered));
|
||||
EXPECT_FALSE(
|
||||
ParallelEyeFov(QuatFromAxisAngle({0, 1, 0}, 1.5f), display, &rendered));
|
||||
EXPECT_FALSE(ParallelEyeFov({}, display, nullptr));
|
||||
EXPECT_EQ(rendered.angle_left, 1);
|
||||
EXPECT_EQ(rendered.angle_down, 4);
|
||||
}
|
||||
|
||||
// ---- units: the 0.10 m translation check through the native conversion ----
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
#include "mocktail/vr/xr_actions.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <openxr/openxr.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "runtime/roblox_input_router.h"
|
||||
#include "runtime/roblox_xr_controller_input.h"
|
||||
#include <cstring>
|
||||
#include <gtest/gtest.h>
|
||||
#include <map>
|
||||
#include <openxr/openxr.h>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
using namespace mocktail::vr;
|
||||
@@ -23,6 +27,7 @@ float last_amplitude;
|
||||
XrDuration last_duration;
|
||||
XrPath last_haptic_hand;
|
||||
int simple_haptic_bindings;
|
||||
std::map<XrPath, std::vector<XrPath>> suggested_paths;
|
||||
} // namespace
|
||||
extern "C" {
|
||||
XRAPI_ATTR XrResult XRAPI_CALL
|
||||
@@ -69,6 +74,10 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateAction(XrActionSet,
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrDestroyAction(XrAction) { return XR_SUCCESS; }
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrSuggestInteractionProfileBindings(
|
||||
XrInstance, const XrInteractionProfileSuggestedBinding *info) {
|
||||
for (std::uint32_t i = 0; i < info->countSuggestedBindings; ++i) {
|
||||
suggested_paths[info->interactionProfile].push_back(
|
||||
info->suggestedBindings[i].binding);
|
||||
}
|
||||
if (info->interactionProfile ==
|
||||
paths["/interaction_profiles/khr/simple_controller"])
|
||||
for (std::uint32_t i = 0; i < info->countSuggestedBindings; ++i)
|
||||
@@ -165,6 +174,7 @@ protected:
|
||||
void SetUp() override {
|
||||
names.clear();
|
||||
paths.clear();
|
||||
suggested_paths.clear();
|
||||
next_handle = 100;
|
||||
focused = input_active = true;
|
||||
simple = click = touching = false;
|
||||
@@ -188,6 +198,33 @@ protected:
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(XrActionsProduction, BindsTouchProAndPlusAndOmitsQuest2Thumbrest) {
|
||||
for (const char* name :
|
||||
{"/interaction_profiles/facebook/touch_controller_pro",
|
||||
"/interaction_profiles/meta/touch_controller_plus",
|
||||
"/interaction_profiles/meta/touch_pro_controller",
|
||||
"/interaction_profiles/meta/touch_plus_controller"}) {
|
||||
const auto& bindings = suggested_paths.at(paths.at(name));
|
||||
for (const char* component :
|
||||
{"/user/hand/left/input/grip/pose", "/user/hand/right/input/aim/pose",
|
||||
"/user/hand/left/input/trigger/value",
|
||||
"/user/hand/right/input/squeeze/value",
|
||||
"/user/hand/left/output/haptic", "/user/hand/right/output/haptic"}) {
|
||||
EXPECT_NE(
|
||||
std::find(bindings.begin(), bindings.end(), paths.at(component)),
|
||||
bindings.end())
|
||||
<< name << " " << component;
|
||||
}
|
||||
}
|
||||
const auto& quest = suggested_paths.at(
|
||||
paths.at("/interaction_profiles/meta/touch_controller_quest_2"));
|
||||
for (const char* component : {"/user/hand/left/input/thumbrest/touch",
|
||||
"/user/hand/right/input/thumbrest/touch"}) {
|
||||
EXPECT_EQ(std::find(quest.begin(), quest.end(), paths.at(component)),
|
||||
quest.end());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(XrActionsProduction, HapticSamplesKeepHandLeaseAndClampAmplitude) {
|
||||
EXPECT_EQ(simple_haptic_bindings, 2);
|
||||
actions.RequestHaptics(1, 1.5f, 50000000, 0);
|
||||
|
||||
Reference in new issue
Block a user