mirror of
https://github.com/daniel-lynch/ovrplugin-openxr-shim.git
synced 2026-10-06 06:00:08 +02:00
Initial public release: OVRPlugin→OpenXR interoperability shim
An independent reimplementation of Meta's libOVRPlugin ABI on top of OpenXR, so VrApi-era Meta Quest VR titles can run on non-Meta OpenXR runtimes (Monado, Steam Frame) instead of being locked to Meta hardware. Original code only — no Meta/Epic/Capcom binaries, headers, or assets. Includes a desktop harness that drives the shim against Monado headless. Scope/legal: interoperability; entitlement handling is out of scope. See README for the legal/scope section and docs/ for the research trail and design notes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6sFYGXZPsq3v7xtcDES6g
This commit is contained in:
commit
a72a79ad29
57 files changed
+9304
No files matched your search
@@ -0,0 +1,96 @@
|
||||
# Desktop harness — drive the shim against Monado on a PC
|
||||
|
||||
The shim is normally exercised only on a Quest (inside RE4's APK). This harness lets you
|
||||
run its **OpenXR path on a Linux desktop**, headless, against
|
||||
[Monado](https://monado.freedesktop.org/)'s *simulated HMD* — no headset, no game, no
|
||||
`libUE4`. It's the fast iteration loop for the Steam Frame / Monado / Lepton bring-up.
|
||||
|
||||
## What it is
|
||||
|
||||
`harness.c` stands in for the game (UE4 + `OculusHMD`). It creates a Vulkan
|
||||
instance/device the way UE's VulkanRHI does, then calls our `ovrp_*` exports **in UE's
|
||||
order**:
|
||||
|
||||
```
|
||||
PreInitialize3 -> Get{Instance,Device}ExtensionsVk -> Initialize5
|
||||
-> CalculateEyeLayerDesc2 -> SetupLayer (once)
|
||||
-> per frame: Update3, WaitToBeginFrame, BeginFrame4, GetLayerTexture2,
|
||||
(clear the eye image), EndFrame4
|
||||
-> Shutdown2
|
||||
```
|
||||
|
||||
The shim does the real OpenXR work underneath — `xrCreateInstance`, `xrGetSystem`,
|
||||
`xrCreateSession` (Vulkan binding), `xrCreateSwapchain`, the `xrWaitFrame/Begin/EndFrame`
|
||||
loop — against whatever runtime the OpenXR loader selects. Here that's Monado's simulated
|
||||
HMD with the **NULL compositor** (renders nowhere), so it runs over SSH / in CI.
|
||||
|
||||
It ships nothing from Capcom/Epic/Meta — it only calls our own public `ovrp_*` ABI.
|
||||
|
||||
## Prereqs (Debian/Ubuntu)
|
||||
|
||||
```sh
|
||||
sudo apt-get install monado-service libopenxr1-monado libopenxr-loader1 libopenxr-dev \
|
||||
libvulkan-dev
|
||||
```
|
||||
|
||||
## Build & run
|
||||
|
||||
```sh
|
||||
shim/build_host.sh # builds build/host/libOVRPlugin.so + build/host/harness
|
||||
tools/desktop-harness/run.sh # brings up monado-service headless, runs 300 frames
|
||||
tools/desktop-harness/run.sh 1000 # custom frame count
|
||||
```
|
||||
|
||||
Logs land in `build/host/monado.log` and `build/host/harness.log`.
|
||||
|
||||
### The scene (pose→view validation)
|
||||
|
||||
Each frame the harness queries the shim's per-eye pose (`ovrp_GetNodePoseState3` for
|
||||
`EyeLeft`/`EyeRight`, with true IPD separation) and FOV, builds per-pixel world rays, and
|
||||
renders a **world-locked procedural scene** — checkerboard floor 1.6 m below the eye, sky
|
||||
gradient, and an orbiting sun — into the acquired eye image. This exercises the shim's
|
||||
pose/FOV math: the two eyes show correct stereo parallax, and the world counter-moves as the
|
||||
head pose changes (Monado's simulated HMD sways, so there's real motion). The first few
|
||||
frames log per-eye pose + FOV. CPU-rendered (fine at the sim's 128×128; it's a test tool, not
|
||||
a fast path).
|
||||
|
||||
### Watch it (windowed)
|
||||
|
||||
`VISIBLE=1` uses Monado's main compositor (mirror window) + the imgui debug GUI instead of
|
||||
the NULL compositor, so you can watch the scene (floor grid, horizon, orbiting sun, stereo
|
||||
parallax) and inspect swapchains. Needs a display — run it from the physical desktop session,
|
||||
not over SSH, and give it a big frame count:
|
||||
|
||||
```sh
|
||||
VISIBLE=1 tools/desktop-harness/run.sh 3600
|
||||
```
|
||||
|
||||
## What "pass" looks like
|
||||
|
||||
`harness.log` should show the lifecycle succeed and frames present:
|
||||
|
||||
```
|
||||
[harness] PreInitialize3 OK (XrInstance + system up)
|
||||
[harness] VkInstance created
|
||||
[harness] VkDevice + graphics queue (family 0) created
|
||||
[harness] Initialize5 OK (XrSession created)
|
||||
[harness] SetupLayer OK layerId=0 swapchainStages=3
|
||||
[harness] ...
|
||||
[harness] loop done: NNN/NNN frames presented
|
||||
[harness] Shutdown2 OK — clean exit
|
||||
```
|
||||
|
||||
Exit code 0 = frames presented; 2 = ran but presented nothing (session never reached the
|
||||
running state — check `monado.log`); 1 = a hard failure.
|
||||
|
||||
## Limits / notes
|
||||
|
||||
- This validates the **OpenXR + Vulkan binding + frame loop + swapchain** path. It does
|
||||
*not* reproduce the game-thread/render-thread pacing or GPU load that drove the on-Quest
|
||||
"ghost"; those are device-side behaviours. It's for ABI/path correctness and porting to
|
||||
new runtimes, not perf tuning.
|
||||
- The Android session path (`XR_KHR_android_create_instance`, `xrInitializeLoaderKHR`, the
|
||||
JavaVM/Activity chain) is `#ifdef __ANDROID__`-guarded in `xr_runtime.c` /
|
||||
`android_init.c`, so the same sources serve both targets.
|
||||
- `passthru.c` (P4 native forwarding) is Android-only; on host its arm64 trampolines fall
|
||||
back to plain stubs and passthru stays inactive.
|
||||
@@ -0,0 +1,366 @@
|
||||
/* harness.c — desktop OpenXR harness for the OVRPlugin->OpenXR shim.
|
||||
*
|
||||
* Stands in for Resident Evil 4 VR (UE4 + OculusHMD) on a Linux desktop: it creates
|
||||
* a Vulkan instance/device the way UE's VulkanRHI does, then drives the shim's ovrp_*
|
||||
* entry points in the exact order UE calls them — PreInitialize3 -> Get*ExtensionsVk
|
||||
* -> Initialize5 -> (per frame) Update3/WaitToBeginFrame/BeginFrame4/EndFrame4 -> a
|
||||
* one-time CalculateEyeLayerDesc2/SetupLayer -> Shutdown2. The shim creates the real
|
||||
* XrInstance/session/swapchains against whatever OpenXR runtime the loader selects
|
||||
* (here: Monado's simulated HMD, headless via XRT_COMPOSITOR_NULL).
|
||||
*
|
||||
* This exercises the whole non-Android OpenXR path of the shim on a PC — no Quest, no
|
||||
* libUE4 — so the Steam Frame / Monado / Lepton bring-up can be iterated on a laptop.
|
||||
*
|
||||
* It is NOT the game and ships nothing from Capcom/Epic/Meta: it only calls our own
|
||||
* public ovrp_* ABI. Build: shim/build_host.sh. Run: tools/desktop-harness/run.sh. */
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
#include "ovrplugin_shim.h" /* shim public types/enums (ovrpLayerDesc, ovrpLayout, ...) */
|
||||
|
||||
/* ovrp_* the shim exports but doesn't declare in the public header — declare here so we
|
||||
* call them with the verified ABI without depending on header completeness. */
|
||||
extern ovrpResult ovrp_GetInstanceExtensionsVk(const char **outArray, int *inoutCount);
|
||||
extern ovrpResult ovrp_GetDeviceExtensionsVk(const char **outArray, int *inoutCount);
|
||||
extern ovrpResult ovrp_CalculateEyeLayerDesc2(ovrpLayout layout, float textureScale,
|
||||
int mipLevels, int sampleCount, ovrpTextureFormat colorFormat,
|
||||
ovrpTextureFormat depthFormat, int layerFlags, ovrpLayerDesc *out);
|
||||
extern ovrpResult ovrp_SetupLayer(void *device, ovrpLayerDesc *desc, int *outLayerId);
|
||||
extern ovrpResult ovrp_GetLayerTextureStageCount(int layerId, int *outCount);
|
||||
extern ovrpResult ovrp_GetLayerTexture2(int layerId, int stage, int eyeId,
|
||||
uint64_t *outColorTex, uint64_t *outDepthTex);
|
||||
|
||||
#define VKOK(call) do { VkResult _r = (call); if (_r != VK_SUCCESS) { \
|
||||
fprintf(stderr, "[harness] FAIL %s = %d\n", #call, _r); exit(1); } } while (0)
|
||||
#define LOG(...) do { fprintf(stderr, "[harness] " __VA_ARGS__); fputc('\n', stderr); } while (0)
|
||||
|
||||
static VkInstance g_inst;
|
||||
static VkPhysicalDevice g_phys;
|
||||
static VkDevice g_dev;
|
||||
static VkQueue g_queue;
|
||||
static uint32_t g_gfxFamily;
|
||||
static VkCommandPool g_cmdPool;
|
||||
|
||||
/* CPU-side staging for the scene renderer (host-visible; both eye layers, RGBA8). */
|
||||
static VkBuffer g_stageBuf;
|
||||
static VkDeviceMemory g_stageMem;
|
||||
static void *g_stagePtr;
|
||||
static uint32_t g_stageW, g_stageH;
|
||||
|
||||
/* The ovrp_*ExtensionsVk getters report a count then fill a caller array of char*. */
|
||||
static const char **query_exts(int forDevice, int *outCount) {
|
||||
int n = 0;
|
||||
ovrpResult r = forDevice ? ovrp_GetDeviceExtensionsVk(NULL, &n)
|
||||
: ovrp_GetInstanceExtensionsVk(NULL, &n);
|
||||
if (!OVRP_SUCCESS(r) || n <= 0) { *outCount = 0; return NULL; }
|
||||
const char **arr = calloc((size_t)n, sizeof(char *));
|
||||
int cap = n;
|
||||
r = forDevice ? ovrp_GetDeviceExtensionsVk(arr, &cap)
|
||||
: ovrp_GetInstanceExtensionsVk(arr, &cap);
|
||||
if (!OVRP_SUCCESS(r)) { free(arr); *outCount = 0; return NULL; }
|
||||
*outCount = n;
|
||||
LOG("%s extensions required by runtime (%d):", forDevice ? "device" : "instance", n);
|
||||
for (int i = 0; i < n; i++) LOG(" %s", arr[i]);
|
||||
return arr;
|
||||
}
|
||||
|
||||
static void make_vk_instance(void) {
|
||||
int n = 0;
|
||||
const char **exts = query_exts(0, &n); /* needs the XrInstance (PreInitialize3 done) */
|
||||
VkApplicationInfo ai = { VK_STRUCTURE_TYPE_APPLICATION_INFO };
|
||||
ai.pApplicationName = "re4vr-shim-harness";
|
||||
ai.apiVersion = VK_API_VERSION_1_1; /* UE/Quest Vulkan baseline */
|
||||
VkInstanceCreateInfo ci = { VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO };
|
||||
ci.pApplicationInfo = &ai;
|
||||
ci.enabledExtensionCount = (uint32_t)n;
|
||||
ci.ppEnabledExtensionNames = exts;
|
||||
VKOK(vkCreateInstance(&ci, NULL, &g_inst));
|
||||
free(exts);
|
||||
LOG("VkInstance created");
|
||||
}
|
||||
|
||||
static void pick_physical_and_device(void) {
|
||||
uint32_t pc = 0;
|
||||
VKOK(vkEnumeratePhysicalDevices(g_inst, &pc, NULL));
|
||||
if (!pc) { LOG("no Vulkan physical devices"); exit(1); }
|
||||
VkPhysicalDevice *pd = calloc(pc, sizeof(*pd));
|
||||
VKOK(vkEnumeratePhysicalDevices(g_inst, &pc, pd));
|
||||
g_phys = pd[0]; /* shim picks the runtime's preferred device internally; smoke test = [0] */
|
||||
VkPhysicalDeviceProperties props;
|
||||
vkGetPhysicalDeviceProperties(g_phys, &props);
|
||||
LOG("physical device: %s", props.deviceName);
|
||||
free(pd);
|
||||
|
||||
uint32_t qf = 0;
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(g_phys, &qf, NULL);
|
||||
VkQueueFamilyProperties *qp = calloc(qf, sizeof(*qp));
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(g_phys, &qf, qp);
|
||||
g_gfxFamily = UINT32_MAX;
|
||||
for (uint32_t i = 0; i < qf; i++)
|
||||
if (qp[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { g_gfxFamily = i; break; }
|
||||
free(qp);
|
||||
if (g_gfxFamily == UINT32_MAX) { LOG("no graphics queue family"); exit(1); }
|
||||
|
||||
int n = 0;
|
||||
const char **exts = query_exts(1, &n);
|
||||
float prio = 1.0f;
|
||||
VkDeviceQueueCreateInfo qci = { VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO };
|
||||
qci.queueFamilyIndex = g_gfxFamily;
|
||||
qci.queueCount = 1;
|
||||
qci.pQueuePriorities = &prio;
|
||||
VkDeviceCreateInfo dci = { VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO };
|
||||
dci.queueCreateInfoCount = 1;
|
||||
dci.pQueueCreateInfos = &qci;
|
||||
dci.enabledExtensionCount = (uint32_t)n;
|
||||
dci.ppEnabledExtensionNames = exts;
|
||||
VKOK(vkCreateDevice(g_phys, &dci, NULL, &g_dev));
|
||||
free(exts);
|
||||
vkGetDeviceQueue(g_dev, g_gfxFamily, 0, &g_queue);
|
||||
|
||||
VkCommandPoolCreateInfo pci = { VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO };
|
||||
pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
pci.queueFamilyIndex = g_gfxFamily;
|
||||
VKOK(vkCreateCommandPool(g_dev, &pci, NULL, &g_cmdPool));
|
||||
LOG("VkDevice + graphics queue (family %u) created", g_gfxFamily);
|
||||
}
|
||||
|
||||
static uint32_t find_mem(uint32_t typeBits, VkMemoryPropertyFlags want) {
|
||||
VkPhysicalDeviceMemoryProperties mp;
|
||||
vkGetPhysicalDeviceMemoryProperties(g_phys, &mp);
|
||||
for (uint32_t i = 0; i < mp.memoryTypeCount; i++)
|
||||
if ((typeBits & (1u << i)) && (mp.memoryTypes[i].propertyFlags & want) == want) return i;
|
||||
return UINT32_MAX;
|
||||
}
|
||||
|
||||
/* Host-visible staging buffer big enough for both eye layers (RGBA8). Persistently mapped. */
|
||||
static int make_staging(uint32_t w, uint32_t h) {
|
||||
VkDeviceSize sz = (VkDeviceSize)w * h * 4u * 2u; /* 2 array layers */
|
||||
VkBufferCreateInfo bci = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
|
||||
bci.size = sz; bci.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
||||
bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
if (vkCreateBuffer(g_dev, &bci, NULL, &g_stageBuf) != VK_SUCCESS) return 0;
|
||||
VkMemoryRequirements mr; vkGetBufferMemoryRequirements(g_dev, g_stageBuf, &mr);
|
||||
uint32_t mt = find_mem(mr.memoryTypeBits,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||||
if (mt == UINT32_MAX) return 0;
|
||||
VkMemoryAllocateInfo mai = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
|
||||
mai.allocationSize = mr.size; mai.memoryTypeIndex = mt;
|
||||
if (vkAllocateMemory(g_dev, &mai, NULL, &g_stageMem) != VK_SUCCESS) return 0;
|
||||
vkBindBufferMemory(g_dev, g_stageBuf, g_stageMem, 0);
|
||||
if (vkMapMemory(g_dev, g_stageMem, 0, sz, 0, &g_stagePtr) != VK_SUCCESS) return 0;
|
||||
g_stageW = w; g_stageH = h;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* rotate vector v by quaternion q (x,y,z,w): v + 2*qw*(qv x v) + 2*(qv x (qv x v)) */
|
||||
static void qrot(float qx, float qy, float qz, float qw,
|
||||
float vx, float vy, float vz, float *ox, float *oy, float *oz) {
|
||||
float tx = 2.0f * (qy * vz - qz * vy);
|
||||
float ty = 2.0f * (qz * vx - qx * vz);
|
||||
float tz = 2.0f * (qx * vy - qy * vx);
|
||||
*ox = vx + qw * tx + (qy * tz - qz * ty);
|
||||
*oy = vy + qw * ty + (qz * tx - qx * tz);
|
||||
*oz = vz + qw * tz + (qx * ty - qy * tx);
|
||||
}
|
||||
|
||||
/* Procedural world-locked scene along a world-space ray: checkerboard floor 1.6m below the
|
||||
* eye, sky gradient, and an orbiting sun (the motion). Writes linear RGB into r/g/b. */
|
||||
static void shade(float ox, float oy, float oz, float dx, float dy, float dz, float t,
|
||||
float *r, float *g, float *b) {
|
||||
if (dy < -1e-3f) {
|
||||
float floorY = oy - 1.6f;
|
||||
float tt = (floorY - oy) / dy; /* = 1.6 / -dy > 0 */
|
||||
if (tt > 0.0f) {
|
||||
float hx = ox + dx * tt, hz = oz + dz * tt;
|
||||
int chk = (((int)floorf(hx)) + ((int)floorf(hz))) & 1;
|
||||
float base = chk ? 0.85f : 0.25f;
|
||||
float fog = 1.0f / (1.0f + tt * 0.04f); /* fade distant floor into sky */
|
||||
*r = base * fog + 0.55f * (1.0f - fog);
|
||||
*g = base * fog + 0.65f * (1.0f - fog);
|
||||
*b = base * fog + 0.85f * (1.0f - fog);
|
||||
return;
|
||||
}
|
||||
}
|
||||
float up = dy * 0.5f + 0.5f; /* sky gradient */
|
||||
*r = 0.30f + 0.20f * up; *g = 0.50f + 0.30f * up; *b = 0.70f + 0.30f * up;
|
||||
float sx = cosf(t), sy = 0.40f, sz = sinf(t); /* orbiting sun */
|
||||
float sl = 1.0f / sqrtf(sx * sx + sy * sy + sz * sz); sx *= sl; sy *= sl; sz *= sl;
|
||||
if (dx * sx + dy * sy + dz * sz > 0.995f) { *r = 1.0f; *g = 0.95f; *b = 0.70f; }
|
||||
}
|
||||
|
||||
/* Best-effort pose-driven render: for each eye, build per-pixel world rays from the shim's
|
||||
* located eye pose + FOV, shade the procedural scene, and copy into that array layer. This
|
||||
* exercises the shim's pose/FOV math (stereo parallax between eyes; world-locked content
|
||||
* counter-moves as the head pose changes). Failures here don't fail the harness. */
|
||||
static void render_scene(uint64_t image, uint32_t arrayLayers, const ovrpLayerDesc *desc,
|
||||
const ovrpPoseStatef pose[2], float t) {
|
||||
if (!image || !g_stagePtr) return;
|
||||
uint32_t W = g_stageW, H = g_stageH;
|
||||
for (uint32_t eye = 0; eye < arrayLayers; eye++) {
|
||||
const ovrpPosef *p = &pose[eye].Pose;
|
||||
float ox = p->Position.x, oy = p->Position.y, oz = p->Position.z;
|
||||
float lt = desc->Fov[eye].LeftTan, rt = desc->Fov[eye].RightTan;
|
||||
float ut = desc->Fov[eye].UpTan, dt = desc->Fov[eye].DownTan;
|
||||
uint8_t *px = (uint8_t *)g_stagePtr + (size_t)eye * W * H * 4u;
|
||||
/* ovrpFovf tangents are positive magnitudes: horizontal spans -LeftTan..+RightTan,
|
||||
* vertical spans +UpTan (top) ..-DownTan (bottom). */
|
||||
for (uint32_t y = 0; y < H; y++) {
|
||||
float v = ut - (ut + dt) * ((y + 0.5f) / H);
|
||||
for (uint32_t x = 0; x < W; x++) {
|
||||
float u = -lt + (rt + lt) * ((x + 0.5f) / W);
|
||||
float il = 1.0f / sqrtf(u * u + v * v + 1.0f);
|
||||
float ex = u * il, ey = v * il, ez = -1.0f * il; /* OpenXR: -Z forward */
|
||||
float dx, dy, dz;
|
||||
qrot(p->Orientation.x, p->Orientation.y, p->Orientation.z, p->Orientation.w,
|
||||
ex, ey, ez, &dx, &dy, &dz);
|
||||
float r, g, b; shade(ox, oy, oz, dx, dy, dz, t, &r, &g, &b);
|
||||
uint8_t *o = px + ((size_t)y * W + x) * 4u;
|
||||
o[0] = (uint8_t)(r * 255.0f); o[1] = (uint8_t)(g * 255.0f);
|
||||
o[2] = (uint8_t)(b * 255.0f); o[3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VkCommandBufferAllocateInfo ai = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO };
|
||||
ai.commandPool = g_cmdPool; ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; ai.commandBufferCount = 1;
|
||||
VkCommandBuffer cb;
|
||||
if (vkAllocateCommandBuffers(g_dev, &ai, &cb) != VK_SUCCESS) return;
|
||||
VkCommandBufferBeginInfo bi = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
|
||||
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
vkBeginCommandBuffer(cb, &bi);
|
||||
|
||||
VkImageSubresourceRange range = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, arrayLayers };
|
||||
VkImageMemoryBarrier toDst = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
|
||||
toDst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
toDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
toDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
toDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
toDst.image = (VkImage)image; toDst.subresourceRange = range;
|
||||
toDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0, 0, NULL, 0, NULL, 1, &toDst);
|
||||
|
||||
VkBufferImageCopy region[2]; uint32_t nr = 0;
|
||||
for (uint32_t eye = 0; eye < arrayLayers; eye++) {
|
||||
VkBufferImageCopy c; memset(&c, 0, sizeof c);
|
||||
c.bufferOffset = (VkDeviceSize)eye * W * H * 4u;
|
||||
c.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
c.imageSubresource.mipLevel = 0; c.imageSubresource.baseArrayLayer = eye;
|
||||
c.imageSubresource.layerCount = 1;
|
||||
c.imageExtent.width = W; c.imageExtent.height = H; c.imageExtent.depth = 1;
|
||||
region[nr++] = c;
|
||||
}
|
||||
vkCmdCopyBufferToImage(cb, g_stageBuf, (VkImage)image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, nr, region);
|
||||
|
||||
VkImageMemoryBarrier toRead = toDst;
|
||||
toRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
toRead.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; /* what the compositor reads */
|
||||
toRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
toRead.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
0, 0, NULL, 0, NULL, 1, &toRead);
|
||||
|
||||
vkEndCommandBuffer(cb);
|
||||
VkSubmitInfo si = { VK_STRUCTURE_TYPE_SUBMIT_INFO };
|
||||
si.commandBufferCount = 1; si.pCommandBuffers = &cb;
|
||||
vkQueueSubmit(g_queue, 1, &si, VK_NULL_HANDLE);
|
||||
vkQueueWaitIdle(g_queue);
|
||||
vkFreeCommandBuffers(g_dev, g_cmdPool, 1, &cb);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int frames = (argc > 1) ? atoi(argv[1]) : 300;
|
||||
if (frames < 1) frames = 1;
|
||||
LOG("starting; %d frames. Runtime via OpenXR loader (XR_RUNTIME_JSON / active_runtime.json).", frames);
|
||||
|
||||
/* 1. lifecycle: PreInitialize3 creates the XrInstance + picks the system */
|
||||
if (!OVRP_SUCCESS(ovrp_PreInitialize3(NULL))) { LOG("PreInitialize3 failed"); return 1; }
|
||||
LOG("PreInitialize3 OK (XrInstance + system up)");
|
||||
|
||||
/* 2. Vulkan, created with the runtime-required extensions (UE's VulkanRHI order) */
|
||||
make_vk_instance();
|
||||
pick_physical_and_device();
|
||||
|
||||
/* 3. Initialize5 hands the shim our Vulkan handles -> it creates the XrSession */
|
||||
long versionStub[4] = {0}; /* arg9 = const ovrpVersion& — shim ignores the contents */
|
||||
ovrpResult ir = ovrp_Initialize5(ovrpRenderAPI_Vulkan, NULL, NULL,
|
||||
(void *)g_inst, (void *)g_phys, (void *)g_dev,
|
||||
(void *)g_queue, 0, versionStub);
|
||||
if (!OVRP_SUCCESS(ir)) { LOG("Initialize5 failed (%d)", ir); return 1; }
|
||||
LOG("Initialize5 OK (XrSession created)");
|
||||
|
||||
/* 4. eye-fov layer (UE: CalculateEyeLayerDesc2 -> SetupLayer once) */
|
||||
ovrpLayerDesc desc;
|
||||
ovrpResult dr = ovrp_CalculateEyeLayerDesc2(ovrpLayout_Array, 1.0f, 1, 1,
|
||||
ovrpTextureFormat_R8G8B8A8_sRGB, (ovrpTextureFormat)0, 0, &desc);
|
||||
if (!OVRP_SUCCESS(dr)) { LOG("CalculateEyeLayerDesc2 failed (%d)", dr); return 1; }
|
||||
LOG("EyeLayerDesc %dx%d arraylayout, fmt=%d", desc.TextureSize.w, desc.TextureSize.h, desc.Format);
|
||||
int layerId = -1;
|
||||
ovrpResult sr = ovrp_SetupLayer((void *)g_dev, &desc, &layerId);
|
||||
if (!OVRP_SUCCESS(sr) || layerId < 0) { LOG("SetupLayer failed (%d)", sr); return 1; }
|
||||
int stageCount = 0; ovrp_GetLayerTextureStageCount(layerId, &stageCount);
|
||||
LOG("SetupLayer OK layerId=%d swapchainStages=%d", layerId, stageCount);
|
||||
uint32_t arrayLayers = (desc.Layout == ovrpLayout_Array) ? 2u : 1u;
|
||||
if (make_staging((uint32_t)desc.TextureSize.w, (uint32_t)desc.TextureSize.h))
|
||||
LOG("scene renderer ready (%dx%d, %u eye layers)", desc.TextureSize.w, desc.TextureSize.h, arrayLayers);
|
||||
else
|
||||
LOG("WARN: staging buffer alloc failed — frames will be submitted blank");
|
||||
|
||||
/* 5. frame loop. Update3 advances the session state machine (IDLE->READY->FOCUSED);
|
||||
* Wait/Begin/EndFrame no-op until the session is running, so early frames are fine. */
|
||||
if (stageCount < 1) stageCount = 1;
|
||||
int presented = 0, renderStage = 0; /* stage advances per presented frame, in lockstep
|
||||
* with the shim's one-acquire-per-running-frame */
|
||||
for (int f = 0; f < frames; f++) {
|
||||
ovrp_Update3(ovrpStep_Render, f, 0.0);
|
||||
ovrp_WaitToBeginFrame(f);
|
||||
ovrp_BeginFrame4(f, NULL);
|
||||
int stage = renderStage % stageCount; /* = the image begin_frame just acquired */
|
||||
|
||||
/* per-eye pose from the shim's located views (true IPD separation) + the eye FOV
|
||||
* from the layer desc -> render a world-locked scene into the acquired eye image. */
|
||||
ovrpPoseStatef eyePose[2]; memset(eyePose, 0, sizeof eyePose);
|
||||
eyePose[0].Pose.Orientation.w = eyePose[1].Pose.Orientation.w = 1.0f;
|
||||
ovrp_GetNodePoseState3(ovrpStep_Render, f, ovrpNode_EyeLeft, &eyePose[0]);
|
||||
ovrp_GetNodePoseState3(ovrpStep_Render, f, ovrpNode_EyeRight, &eyePose[1]);
|
||||
if (f < 3)
|
||||
LOG("frame %d eyeL pos=(%.3f %.3f %.3f) eyeR pos=(%.3f %.3f %.3f) fovL(R%.3f L%.3f)",
|
||||
f, eyePose[0].Pose.Position.x, eyePose[0].Pose.Position.y, eyePose[0].Pose.Position.z,
|
||||
eyePose[1].Pose.Position.x, eyePose[1].Pose.Position.y, eyePose[1].Pose.Position.z,
|
||||
desc.Fov[0].RightTan, desc.Fov[0].LeftTan);
|
||||
|
||||
uint64_t color = 0, depthTex = 0;
|
||||
if (OVRP_SUCCESS(ovrp_GetLayerTexture2(layerId, stage, 0, &color, &depthTex)) && color)
|
||||
render_scene(color, arrayLayers, &desc, eyePose, (float)f * 0.03f);
|
||||
|
||||
ovrpLayerSubmit submit;
|
||||
memset(&submit, 0, sizeof submit);
|
||||
submit.LayerId = layerId;
|
||||
submit.TextureStage = stage;
|
||||
submit.Pose.Orientation.w = 1.0f; /* pose/FOV come from the shim's located views */
|
||||
const ovrpLayerSubmit *ptrs[1] = { &submit };
|
||||
ovrpResult er = ovrp_EndFrame4(f, ptrs, 1, NULL);
|
||||
if (OVRP_SUCCESS(er)) {
|
||||
if (presented == 0) /* views are located now — refresh FOV (setup value was the fallback) */
|
||||
ovrp_CalculateEyeLayerDesc2(ovrpLayout_Array, 1.0f, 1, 1,
|
||||
ovrpTextureFormat_R8G8B8A8_sRGB, (ovrpTextureFormat)0, 0, &desc);
|
||||
presented++; renderStage++;
|
||||
}
|
||||
|
||||
if (f < 5 || (f % 60) == 0)
|
||||
LOG("frame %d: end=%d (presented=%d)", f, er, presented);
|
||||
}
|
||||
|
||||
LOG("loop done: %d/%d frames presented", presented, frames);
|
||||
ovrp_Shutdown2();
|
||||
LOG("Shutdown2 OK — clean exit");
|
||||
return presented > 0 ? 0 : 2;
|
||||
}
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# run.sh — launch the desktop harness against Monado's simulated HMD, headless.
|
||||
#
|
||||
# Brings up monado-service with the NULL compositor (no display needed -> works over SSH /
|
||||
# in CI), points the OpenXR loader at Monado, then runs the harness which drives the shim's
|
||||
# ovrp_* -> OpenXR path. Output: build/host/monado.log + build/host/harness.log.
|
||||
#
|
||||
# Prereqs (Debian/Ubuntu): sudo apt-get install monado-service libopenxr1-monado \
|
||||
# libopenxr-loader1 libopenxr-dev
|
||||
# Build first: shim/build_host.sh
|
||||
set -uo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
OUT="$ROOT/build/host"
|
||||
FRAMES="${1:-300}"
|
||||
|
||||
[ -x "$OUT/harness" ] || { echo "no $OUT/harness — run shim/build_host.sh first"; exit 1; }
|
||||
|
||||
# Point the loader at Monado explicitly (in case another runtime is also registered).
|
||||
MONADO_JSON="$(ls /usr/share/openxr/1/openxr_monado*.json 2>/dev/null | head -1 || true)"
|
||||
[ -n "$MONADO_JSON" ] && export XR_RUNTIME_JSON="$MONADO_JSON"
|
||||
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/tmp/re4vr-monado-rt}"
|
||||
mkdir -p "$XDG_RUNTIME_DIR"
|
||||
|
||||
# Simulated HMD driver is auto-selected when no real hardware is present.
|
||||
# Default = headless: NULL compositor renders to nothing, so it needs no X/Wayland (SSH/CI).
|
||||
# VISIBLE=1 = windowed: the main compositor opens a mirror window + Monado's imgui debug GUI
|
||||
# (swapchain preview), so you can WATCH the harness's animated eye fill. Needs a display, so
|
||||
# run it from the physical desktop session, not over SSH, and give it a big frame count to
|
||||
# watch, e.g. VISIBLE=1 tools/desktop-harness/run.sh 3600
|
||||
if [ "${VISIBLE:-0}" = 1 ]; then
|
||||
export XRT_DEBUG_GUI=1
|
||||
echo "VISIBLE mode: main compositor + debug GUI (needs DISPLAY/WAYLAND — run locally)"
|
||||
else
|
||||
export XRT_COMPOSITOR_NULL=1
|
||||
fi
|
||||
export QWERTY_ENABLE=0
|
||||
export U_PACING_APP_USE_MIN_FRAME_PERIOD=1
|
||||
|
||||
SOCK="$XDG_RUNTIME_DIR/monado_comp_ipc"
|
||||
rm -f "$SOCK"
|
||||
# monado-service adds stdin to its epoll loop (for its "press a key to quit" handler).
|
||||
# A redirected /dev/null or regular file isn't epoll-able -> epoll_ctl fails -> the IPC
|
||||
# loop dies. Feed it a real FIFO held open by a writer that never sends data.
|
||||
FIFO="$XDG_RUNTIME_DIR/monado_stdin"
|
||||
rm -f "$FIFO"; mkfifo "$FIFO"
|
||||
sleep 100000 >"$FIFO" &
|
||||
HOLD=$!
|
||||
echo "== starting monado-service (headless, NULL compositor) =="
|
||||
monado-service <"$FIFO" >"$OUT/monado.log" 2>&1 &
|
||||
SVC=$!
|
||||
trap 'kill $SVC $HOLD 2>/dev/null; wait $SVC 2>/dev/null; rm -f "$FIFO"' EXIT
|
||||
|
||||
# wait up to ~10s for the IPC socket
|
||||
for _ in $(seq 1 100); do [ -S "$SOCK" ] && break; kill -0 $SVC 2>/dev/null || { echo "monado-service died:"; cat "$OUT/monado.log"; exit 1; }; sleep 0.1; done
|
||||
[ -S "$SOCK" ] || { echo "monado-service IPC socket never appeared:"; tail -20 "$OUT/monado.log"; exit 1; }
|
||||
echo "monado-service up (pid $SVC)"
|
||||
|
||||
echo "== running harness ($FRAMES frames) =="
|
||||
XRRLOG_STDERR=1 "$OUT/harness" "$FRAMES" 2>&1 | tee "$OUT/harness.log"
|
||||
rc=${PIPESTATUS[0]}
|
||||
echo "== harness exit: $rc =="
|
||||
exit $rc
|
||||
@@ -0,0 +1,31 @@
|
||||
/* xrexts.c — list the OpenXR instance extensions the active runtime advertises.
|
||||
* Recon for the foveation bring-up: tells us which foveation / FDM / eye-tracking
|
||||
* extensions THIS runtime (Monado/Lepton) exposes, so we know what to wire into the
|
||||
* shim's apply_foveation() extension point. No Vulkan, no session — just enumerate.
|
||||
* cc xrexts.c -lopenxr_loader -o xrexts && XR_RUNTIME_JSON=.../openxr_monado.json ./xrexts */
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <openxr/openxr.h>
|
||||
|
||||
int main(void) {
|
||||
uint32_t n = 0;
|
||||
if (xrEnumerateInstanceExtensionProperties(NULL, 0, &n, NULL) != XR_SUCCESS || !n) {
|
||||
fprintf(stderr, "xrEnumerateInstanceExtensionProperties failed (runtime selected? service up?)\n");
|
||||
return 1;
|
||||
}
|
||||
XrExtensionProperties *p = calloc(n, sizeof *p);
|
||||
for (uint32_t i = 0; i < n; i++) p[i].type = XR_TYPE_EXTENSION_PROPERTIES;
|
||||
if (xrEnumerateInstanceExtensionProperties(NULL, n, &n, p) != XR_SUCCESS) return 1;
|
||||
|
||||
printf("runtime advertises %u instance extensions:\n", n);
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
const char *e = p[i].extensionName;
|
||||
int hot = strcasestr(e, "fov") || strcasestr(e, "foveat") || strcasestr(e, "density")
|
||||
|| strcasestr(e, "fdm") || strcasestr(e, "eye") || strcasestr(e, "gaze")
|
||||
|| strcasestr(e, "vrs") || strcasestr(e, "shading_rate") || strcasestr(e, "quad");
|
||||
printf(" %s %s (v%u)\n", hot ? "**" : " ", e, p[i].extensionVersion);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user