Files
DramaticShapeVoxelMod/lib/VRXR.lua
T
2026-08-02 21:01:11 -04:00

1074 lines
44 KiB
Lua

-- VR: the OpenXR binding -- instance, session, swapchains and the frame
-- loop, spoken to the runtime directly over LuaJIT's FFI so the parent
-- app never has to know VR exists.
--
-- The loader (assets/vr/openxr_loader.dll, the Khronos-shipped x64 build)
-- finds whatever runtime the player has -- SteamVR, the Oculus runtime,
-- WMR -- through the OS's normal OpenXR plumbing. This module drives the
-- exact ceremony the spec requires and nothing else:
--
-- instance (with XR_KHR_opengl_enable) -> system (an HMD) ->
-- graphics-requirements call (mandatory before the session) ->
-- session bound to LOVE's own GL context (the HDC/HGLRC the window is
-- already rendering with, via VRGL) -> LOCAL reference space ->
-- one swapchain per eye at the runtime's recommended size, plus one
-- more for the UI quad -> per frame: poll events, wait/begin, locate
-- views, hand textures out to be blitted into, end with layers.
--
-- Everything FFI-shaped lives HERE: cdata never leaks to callers. Poses
-- and fovs come out as plain Lua tables (VRRig's shape); textures come
-- out as GL ids for VRGL. Every entry point is pcall-guarded and failure
-- is a status string, never an error -- a machine with no runtime, no
-- headset or no GL interop reports why and the mod plays flat.
--
-- What this deliberately does NOT do (v1): XR input (play with the pad,
-- keyboard or mouse -- they all still work with a headset on), depth
-- layers, and Linux/Android graphics bindings (the cdef binds Win32 GL,
-- the one platform LOVE + this loader serve together).
-- the mod namespace (see main.lua): V.require loads a sibling module
local V = ...
local VRGL = V.require("VRGL")
local VRXR = {}
local ffi = nil
local xr = nil -- the loader library
local C = nil -- ffi namespace shortcut
-- ------- state
local status = "not started"
local instance, systemId, session, space = nil, nil, nil, nil
local views = nil -- XrViewConfigurationView[2] (sizes)
local eyes = {} -- per eye: { swapchain, images={texids}, w, h }
local quad = nil -- { swapchain, images, w, h }
local sessionState = 0
local running = false -- xrBeginSession happened
local viewsBuf = nil -- XrView[2], refreshed per locate
local lastViews = nil -- lua copies of the located views
-- ------- constants (openxr.h, core 1.0 + XR_KHR_opengl_enable)
local XR = {
TYPE_INSTANCE_CREATE_INFO = 3,
TYPE_SYSTEM_GET_INFO = 4,
TYPE_VIEW_LOCATE_INFO = 6,
TYPE_VIEW = 7,
TYPE_SESSION_CREATE_INFO = 8,
TYPE_SWAPCHAIN_CREATE_INFO = 9,
TYPE_SESSION_BEGIN_INFO = 10,
TYPE_VIEW_STATE = 11,
TYPE_FRAME_END_INFO = 12,
TYPE_EVENT_DATA_BUFFER = 16,
TYPE_EVENT_DATA_SESSION_STATE_CHANGED = 18,
TYPE_FRAME_WAIT_INFO = 33,
TYPE_COMPOSITION_LAYER_PROJECTION = 35,
TYPE_COMPOSITION_LAYER_QUAD = 36,
TYPE_REFERENCE_SPACE_CREATE_INFO = 37,
TYPE_VIEW_CONFIGURATION_VIEW = 41,
TYPE_FRAME_STATE = 44,
TYPE_FRAME_BEGIN_INFO = 46,
TYPE_COMPOSITION_LAYER_PROJECTION_VIEW = 48,
TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO = 55,
TYPE_SWAPCHAIN_IMAGE_WAIT_INFO = 56,
TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO = 57,
TYPE_ACTION_STATE_BOOLEAN = 23,
TYPE_ACTION_STATE_FLOAT = 24,
TYPE_ACTION_STATE_VECTOR2F = 25,
TYPE_ACTION_SET_CREATE_INFO = 28,
TYPE_ACTION_CREATE_INFO = 29,
TYPE_ACTION_SPACE_CREATE_INFO = 38,
TYPE_SPACE_LOCATION = 42,
TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING = 51,
TYPE_ACTION_STATE_GET_INFO = 58,
TYPE_SESSION_ACTION_SETS_ATTACH_INFO = 60,
TYPE_ACTIONS_SYNC_INFO = 61,
TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR = 1000023000,
TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR = 1000023004,
TYPE_GRAPHICS_REQUIREMENTS_OPENGL_KHR = 1000023005,
ACTION_TYPE_BOOLEAN = 1,
ACTION_TYPE_FLOAT = 2,
ACTION_TYPE_VECTOR2F = 3,
ACTION_TYPE_POSE = 4,
SPACE_LOCATION_POSITION_VALID_BIT = 0x2,
FORM_FACTOR_HEAD_MOUNTED_DISPLAY = 1,
VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO = 2,
REFERENCE_SPACE_TYPE_LOCAL = 2,
ENVIRONMENT_BLEND_MODE_OPAQUE = 1,
EYE_VISIBILITY_BOTH = 0,
SESSION_STATE_READY = 2,
SESSION_STATE_STOPPING = 6,
SESSION_STATE_EXITING = 8,
SESSION_STATE_LOSS_PENDING = 7,
SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT = 0x1,
SWAPCHAIN_USAGE_TRANSFER_DST_BIT = 0x10,
SWAPCHAIN_USAGE_SAMPLED_BIT = 0x20,
INFINITE_DURATION = 0x7fffffffffffffffLL,
API_VERSION_1_0 = 0x0001000000000000ULL, -- XR_MAKE_VERSION(1, 0, 0)
GL_RGBA8 = 0x8058,
GL_SRGB8_ALPHA8 = 0x8C43,
}
VRXR.XR = XR
-- the failures a player can actually act on, by name (values verified
-- against openxr.h -- the probe once printed a bare number because two of
-- these were guessed wrong)
local RESULT_NAMES = {
[0] = "XR_SUCCESS",
[-2] = "XR_ERROR_RUNTIME_FAILURE",
[-4] = "XR_ERROR_API_VERSION_UNSUPPORTED",
[-6] = "XR_ERROR_INITIALIZATION_FAILED",
[-9] = "XR_ERROR_EXTENSION_NOT_PRESENT (runtime has no OpenGL support)",
[-26] = "XR_ERROR_SWAPCHAIN_FORMAT_UNSUPPORTED",
[-34] = "XR_ERROR_FORM_FACTOR_UNSUPPORTED",
[-35] = "XR_ERROR_FORM_FACTOR_UNAVAILABLE"
.. " (headset not connected or not ready)",
[-38] = "XR_ERROR_GRAPHICS_DEVICE_INVALID",
[-50] = "XR_ERROR_GRAPHICS_REQUIREMENTS_CALL_MISSING",
[-51] = "XR_ERROR_RUNTIME_UNAVAILABLE (no OpenXR runtime installed)",
}
local function resultName(r)
return RESULT_NAMES[tonumber(r)] or ("XrResult " .. tonumber(r))
end
local CDEF = [[
typedef uint64_t XrVersion; typedef uint64_t XrFlags64;
typedef uint64_t XrSystemId; typedef int64_t XrTime;
typedef int64_t XrDuration; typedef uint32_t XrBool32;
typedef int32_t XrResult; typedef int32_t XrStructureType;
typedef struct XrInstance_T* XrInstance;
typedef struct XrSession_T* XrSession;
typedef struct XrSpace_T* XrSpace;
typedef struct XrSwapchain_T* XrSwapchain;
typedef struct XrApplicationInfo {
char applicationName[128]; uint32_t applicationVersion;
char engineName[128]; uint32_t engineVersion; XrVersion apiVersion;
} XrApplicationInfo;
typedef struct XrInstanceCreateInfo {
XrStructureType type; const void* next; XrFlags64 createFlags;
XrApplicationInfo applicationInfo;
uint32_t enabledApiLayerCount; const char* const* enabledApiLayerNames;
uint32_t enabledExtensionCount; const char* const* enabledExtensionNames;
} XrInstanceCreateInfo;
typedef struct XrSystemGetInfo {
XrStructureType type; const void* next; int32_t formFactor;
} XrSystemGetInfo;
typedef struct XrGraphicsRequirementsOpenGLKHR {
XrStructureType type; void* next;
XrVersion minApiVersionSupported; XrVersion maxApiVersionSupported;
} XrGraphicsRequirementsOpenGLKHR;
typedef struct XrGraphicsBindingOpenGLWin32KHR {
XrStructureType type; const void* next; void* hDC; void* hGLRC;
} XrGraphicsBindingOpenGLWin32KHR;
typedef struct XrSessionCreateInfo {
XrStructureType type; const void* next; XrFlags64 createFlags;
XrSystemId systemId;
} XrSessionCreateInfo;
typedef struct XrVector3f { float x, y, z; } XrVector3f;
typedef struct XrQuaternionf { float x, y, z, w; } XrQuaternionf;
typedef struct XrPosef { XrQuaternionf orientation; XrVector3f position; } XrPosef;
typedef struct XrReferenceSpaceCreateInfo {
XrStructureType type; const void* next; int32_t referenceSpaceType;
XrPosef poseInReferenceSpace;
} XrReferenceSpaceCreateInfo;
typedef struct XrViewConfigurationView {
XrStructureType type; void* next;
uint32_t recommendedImageRectWidth; uint32_t maxImageRectWidth;
uint32_t recommendedImageRectHeight; uint32_t maxImageRectHeight;
uint32_t recommendedSwapchainSampleCount; uint32_t maxSwapchainSampleCount;
} XrViewConfigurationView;
typedef struct XrSwapchainCreateInfo {
XrStructureType type; const void* next; XrFlags64 createFlags;
XrFlags64 usageFlags; int64_t format; uint32_t sampleCount;
uint32_t width; uint32_t height; uint32_t faceCount; uint32_t arraySize;
uint32_t mipCount;
} XrSwapchainCreateInfo;
typedef struct XrSwapchainImageOpenGLKHR {
XrStructureType type; void* next; uint32_t image;
} XrSwapchainImageOpenGLKHR;
typedef struct XrEventDataBuffer {
XrStructureType type; const void* next; uint8_t varying[4000];
} XrEventDataBuffer;
typedef struct XrEventDataSessionStateChanged {
XrStructureType type; const void* next; XrSession session;
int32_t state; XrTime time;
} XrEventDataSessionStateChanged;
typedef struct XrFrameWaitInfo { XrStructureType type; const void* next; } XrFrameWaitInfo;
typedef struct XrFrameState {
XrStructureType type; void* next; XrTime predictedDisplayTime;
XrDuration predictedDisplayPeriod; XrBool32 shouldRender;
} XrFrameState;
typedef struct XrFrameBeginInfo { XrStructureType type; const void* next; } XrFrameBeginInfo;
typedef struct XrViewLocateInfo {
XrStructureType type; const void* next; int32_t viewConfigurationType;
XrTime displayTime; XrSpace space;
} XrViewLocateInfo;
typedef struct XrViewState { XrStructureType type; void* next; XrFlags64 viewStateFlags; } XrViewState;
typedef struct XrFovf { float angleLeft, angleRight, angleUp, angleDown; } XrFovf;
typedef struct XrView { XrStructureType type; void* next; XrPosef pose; XrFovf fov; } XrView;
typedef struct XrSwapchainImageAcquireInfo { XrStructureType type; const void* next; } XrSwapchainImageAcquireInfo;
typedef struct XrSwapchainImageWaitInfo {
XrStructureType type; const void* next; XrDuration timeout;
} XrSwapchainImageWaitInfo;
typedef struct XrSwapchainImageReleaseInfo { XrStructureType type; const void* next; } XrSwapchainImageReleaseInfo;
typedef struct XrOffset2Di { int32_t x, y; } XrOffset2Di;
typedef struct XrExtent2Di { int32_t width, height; } XrExtent2Di;
typedef struct XrRect2Di { XrOffset2Di offset; XrExtent2Di extent; } XrRect2Di;
typedef struct XrSwapchainSubImage {
XrSwapchain swapchain; XrRect2Di imageRect; uint32_t imageArrayIndex;
} XrSwapchainSubImage;
typedef struct XrCompositionLayerProjectionView {
XrStructureType type; const void* next; XrPosef pose; XrFovf fov;
XrSwapchainSubImage subImage;
} XrCompositionLayerProjectionView;
typedef struct XrCompositionLayerProjection {
XrStructureType type; const void* next; XrFlags64 layerFlags;
XrSpace space; uint32_t viewCount;
const XrCompositionLayerProjectionView* views;
} XrCompositionLayerProjection;
typedef struct XrExtent2Df { float width, height; } XrExtent2Df;
typedef struct XrCompositionLayerQuad {
XrStructureType type; const void* next; XrFlags64 layerFlags;
XrSpace space; int32_t eyeVisibility; XrSwapchainSubImage subImage;
XrPosef pose; XrExtent2Df size;
} XrCompositionLayerQuad;
typedef struct XrSessionBeginInfo {
XrStructureType type; const void* next; int32_t primaryViewConfigurationType;
} XrSessionBeginInfo;
typedef struct XrFrameEndInfo {
XrStructureType type; const void* next; XrTime displayTime;
int32_t environmentBlendMode; uint32_t layerCount;
const void* const* layers;
} XrFrameEndInfo;
XrResult xrCreateInstance(const XrInstanceCreateInfo*, XrInstance*);
XrResult xrDestroyInstance(XrInstance);
XrResult xrGetSystem(XrInstance, const XrSystemGetInfo*, XrSystemId*);
XrResult xrGetInstanceProcAddr(XrInstance, const char*, void**);
XrResult xrCreateSession(XrInstance, const XrSessionCreateInfo*, XrSession*);
XrResult xrDestroySession(XrSession);
XrResult xrCreateReferenceSpace(XrSession, const XrReferenceSpaceCreateInfo*, XrSpace*);
XrResult xrEnumerateViewConfigurationViews(XrInstance, XrSystemId, int32_t,
uint32_t, uint32_t*, XrViewConfigurationView*);
XrResult xrEnumerateSwapchainFormats(XrSession, uint32_t, uint32_t*, int64_t*);
XrResult xrCreateSwapchain(XrSession, const XrSwapchainCreateInfo*, XrSwapchain*);
XrResult xrEnumerateSwapchainImages(XrSwapchain, uint32_t, uint32_t*, void*);
XrResult xrAcquireSwapchainImage(XrSwapchain, const XrSwapchainImageAcquireInfo*, uint32_t*);
XrResult xrWaitSwapchainImage(XrSwapchain, const XrSwapchainImageWaitInfo*);
XrResult xrReleaseSwapchainImage(XrSwapchain, const XrSwapchainImageReleaseInfo*);
XrResult xrBeginSession(XrSession, const XrSessionBeginInfo*);
XrResult xrEndSession(XrSession);
XrResult xrPollEvent(XrInstance, XrEventDataBuffer*);
XrResult xrWaitFrame(XrSession, const XrFrameWaitInfo*, XrFrameState*);
XrResult xrBeginFrame(XrSession, const XrFrameBeginInfo*);
XrResult xrLocateViews(XrSession, const XrViewLocateInfo*, XrViewState*,
uint32_t, uint32_t*, XrView*);
XrResult xrEndFrame(XrSession, const XrFrameEndInfo*);
typedef XrResult (__stdcall *PFN_xrGetOpenGLGraphicsRequirementsKHR)(
XrInstance, XrSystemId, XrGraphicsRequirementsOpenGLKHR*);
typedef struct XrActionSet_T* XrActionSet;
typedef struct XrAction_T* XrAction;
typedef uint64_t XrPath;
typedef struct XrActionSetCreateInfo {
XrStructureType type; const void* next; char actionSetName[64];
char localizedActionSetName[128]; uint32_t priority;
} XrActionSetCreateInfo;
typedef struct XrActionCreateInfo {
XrStructureType type; const void* next; char actionName[64];
int32_t actionType; uint32_t countSubactionPaths;
const XrPath* subactionPaths; char localizedActionName[128];
} XrActionCreateInfo;
typedef struct XrActionSuggestedBinding {
XrAction action; XrPath binding;
} XrActionSuggestedBinding;
typedef struct XrInteractionProfileSuggestedBinding {
XrStructureType type; const void* next; XrPath interactionProfile;
uint32_t countSuggestedBindings;
const XrActionSuggestedBinding* suggestedBindings;
} XrInteractionProfileSuggestedBinding;
typedef struct XrSessionActionSetsAttachInfo {
XrStructureType type; const void* next; uint32_t countActionSets;
const XrActionSet* actionSets;
} XrSessionActionSetsAttachInfo;
typedef struct XrActiveActionSet {
XrActionSet actionSet; XrPath subactionPath;
} XrActiveActionSet;
typedef struct XrActionsSyncInfo {
XrStructureType type; const void* next; uint32_t countActiveActionSets;
const XrActiveActionSet* activeActionSets;
} XrActionsSyncInfo;
typedef struct XrActionStateGetInfo {
XrStructureType type; const void* next; XrAction action;
XrPath subactionPath;
} XrActionStateGetInfo;
typedef struct XrActionStateBoolean {
XrStructureType type; void* next; XrBool32 currentState;
XrBool32 changedSinceLastSync; XrTime lastChangeTime; XrBool32 isActive;
} XrActionStateBoolean;
typedef struct XrActionStateFloat {
XrStructureType type; void* next; float currentState;
XrBool32 changedSinceLastSync; XrTime lastChangeTime; XrBool32 isActive;
} XrActionStateFloat;
typedef struct XrVector2f { float x, y; } XrVector2f;
typedef struct XrActionStateVector2f {
XrStructureType type; void* next; XrVector2f currentState;
XrBool32 changedSinceLastSync; XrTime lastChangeTime; XrBool32 isActive;
} XrActionStateVector2f;
typedef struct XrActionSpaceCreateInfo {
XrStructureType type; const void* next; XrAction action;
XrPath subactionPath; XrPosef poseInActionSpace;
} XrActionSpaceCreateInfo;
typedef struct XrSpaceLocation {
XrStructureType type; void* next; XrFlags64 locationFlags; XrPosef pose;
} XrSpaceLocation;
XrResult xrStringToPath(XrInstance, const char*, XrPath*);
XrResult xrCreateActionSet(XrInstance, const XrActionSetCreateInfo*, XrActionSet*);
XrResult xrCreateAction(XrActionSet, const XrActionCreateInfo*, XrAction*);
XrResult xrSuggestInteractionProfileBindings(XrInstance,
const XrInteractionProfileSuggestedBinding*);
XrResult xrAttachSessionActionSets(XrSession, const XrSessionActionSetsAttachInfo*);
XrResult xrSyncActions(XrSession, const XrActionsSyncInfo*);
XrResult xrGetActionStateBoolean(XrSession, const XrActionStateGetInfo*,
XrActionStateBoolean*);
XrResult xrGetActionStateFloat(XrSession, const XrActionStateGetInfo*,
XrActionStateFloat*);
XrResult xrGetActionStateVector2f(XrSession, const XrActionStateGetInfo*,
XrActionStateVector2f*);
XrResult xrCreateActionSpace(XrSession, const XrActionSpaceCreateInfo*, XrSpace*);
XrResult xrLocateSpace(XrSpace, XrSpace, XrTime, XrSpaceLocation*);
]]
-- ------- plumbing
local function fail(what, r)
status = what .. ": " .. (r and resultName(r) or "error")
return nil
end
local function check(r, what)
if tonumber(r) ~= 0 then
error(what .. " failed: " .. resultName(r), 0)
end
end
-- Where the loader DLL might really be on disk. mod.path is
-- PHYSFS-relative; ffi.load needs an OS path. The dev tree answers to
-- the working directory or the source directory -- but an INSTALLED
-- release is neither: importing a mod there lands it in the game's SAVE
-- DIRECTORY, so the mount that actually holds the mod
-- (getRealDirectory) and the save directory itself are both tried too.
local DLL_REL = "assets/vr/openxr_loader.dll"
local function loaderCandidates()
local rel = (V.path or "mods/DramaticShapeVoxelMod") .. "/" .. DLL_REL
local list = { rel }
local okR, real = pcall(function()
return love.filesystem.getRealDirectory(rel)
end)
if okR and type(real) == "string" then
list[#list + 1] = real .. "/" .. rel
end
local ok, src = pcall(function() return love.filesystem.getSource() end)
if ok and type(src) == "string" then list[#list + 1] = src .. "/" .. rel end
local okS, save = pcall(function()
return love.filesystem.getSaveDirectory()
end)
if okS and type(save) == "string" then list[#list + 1] = save .. "/" .. rel end
list[#list + 1] = "openxr_loader" -- last resort: the system search path
return list
end
VRXR._loaderCandidates = loaderCandidates -- named for the suite
-- The escape hatch for a mod imported as an ARCHIVE (or mounted anywhere
-- else ffi.load cannot see): copy the DLL out of the mount into the save
-- directory -- a real folder on every platform -- and load it from
-- there. Written once and reused; rewritten only if the size changed (a
-- mod update shipping a new loader).
local EXTRACTED = "dramatic_shape_openxr_loader.dll"
local function extractLoader()
local okB, bytes = pcall(function()
return V.mod and V.mod.read and V.mod:read(DLL_REL)
end)
if not (okB and type(bytes) == "string" and #bytes > 0) then return nil end
local ok, path = pcall(function()
local have = love.filesystem.getInfo and love.filesystem.getInfo(EXTRACTED)
if not (have and have.size == #bytes) then
if not love.filesystem.write(EXTRACTED, bytes) then return nil end
end
local save = love.filesystem.getSaveDirectory()
if type(save) ~= "string" then return nil end
return save .. "/" .. EXTRACTED
end)
return ok and path or nil
end
local function loadLoader()
ffi = require("ffi")
pcall(ffi.cdef, CDEF)
C = ffi
local errs = {}
for _, path in ipairs(loaderCandidates()) do
local ok, lib = pcall(ffi.load, path)
if ok then return lib end
errs[#errs + 1] = tostring(lib)
end
-- nothing loadable in place: extract a copy to the save directory
local extracted = extractLoader()
if extracted then
local ok, lib = pcall(ffi.load, extracted)
if ok then return lib end
errs[#errs + 1] = tostring(lib)
end
error("openxr_loader.dll not loadable from the mod, the save directory "
.. "or the system: " .. table.concat(errs, " | "), 0)
end
local function makeSwapchain(w, h, format)
local info = ffi.new("XrSwapchainCreateInfo")
info.type = XR.TYPE_SWAPCHAIN_CREATE_INFO
info.usageFlags = XR.SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT
+ XR.SWAPCHAIN_USAGE_TRANSFER_DST_BIT
+ XR.SWAPCHAIN_USAGE_SAMPLED_BIT
info.format = format
info.sampleCount = 1
info.width, info.height = w, h
info.faceCount, info.arraySize, info.mipCount = 1, 1, 1
local out = ffi.new("XrSwapchain[1]")
check(xr.xrCreateSwapchain(session, info, out), "xrCreateSwapchain")
local chain = out[0]
local count = ffi.new("uint32_t[1]")
check(xr.xrEnumerateSwapchainImages(chain, 0, count, nil),
"xrEnumerateSwapchainImages(count)")
local imgs = ffi.new("XrSwapchainImageOpenGLKHR[?]", count[0])
for i = 0, count[0] - 1 do
imgs[i].type = XR.TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR
end
check(xr.xrEnumerateSwapchainImages(chain, count[0], count, imgs),
"xrEnumerateSwapchainImages")
local texids = {}
for i = 0, count[0] - 1 do texids[i + 1] = tonumber(imgs[i].image) end
return { swapchain = chain, images = texids, w = w, h = h }
end
-- The swapchain format: LOVE's canvases are plain RGBA8, so ask for that
-- and take sRGB8 only when it is all the runtime offers (colours come out
-- slightly lifted there; documented rather than corrected in v1).
local function pickFormat()
local count = ffi.new("uint32_t[1]")
check(xr.xrEnumerateSwapchainFormats(session, 0, count, nil),
"xrEnumerateSwapchainFormats(count)")
local fmts = ffi.new("int64_t[?]", count[0])
check(xr.xrEnumerateSwapchainFormats(session, count[0], count, fmts),
"xrEnumerateSwapchainFormats")
local has = {}
for i = 0, count[0] - 1 do has[tonumber(fmts[i])] = true end
if has[XR.GL_RGBA8] then return XR.GL_RGBA8 end
if has[XR.GL_SRGB8_ALPHA8] then return XR.GL_SRGB8_ALPHA8 end
return tonumber(fmts[0])
end
-- ------- input: one action set, the mod's fixed verbs
--
-- OpenXR input is semantic: the app declares ACTIONS (move, A, grip...),
-- suggests where they live on the controllers it knows, and the runtime
-- owns the actual binding (and lets the player rebind in its own UI).
-- The suggestions below cover Touch, Index and WMR sticks-and-buttons
-- style, plus the khr/simple fallback every runtime accepts. Input is
-- OPTIONAL: a runtime that refuses any of this leaves `input` nil and VR
-- stays view-only with the desk's own controls.
local input = nil -- { set, A = {name->XrAction}, spaces, active }
local function toPath(str)
local out = ffi.new("XrPath[1]")
check(xr.xrStringToPath(instance, str, out), "xrStringToPath " .. str)
return out[0]
end
local function makeAction(set, name, atype, localized)
local info = ffi.new("XrActionCreateInfo")
info.type = XR.TYPE_ACTION_CREATE_INFO
ffi.copy(info.actionName, name)
info.actionType = atype
ffi.copy(info.localizedActionName, localized)
local out = ffi.new("XrAction[1]")
check(xr.xrCreateAction(set, info, out), "xrCreateAction " .. name)
return out[0]
end
local function setupInput()
local sci = ffi.new("XrActionSetCreateInfo")
sci.type = XR.TYPE_ACTION_SET_CREATE_INFO
ffi.copy(sci.actionSetName, "gameplay")
ffi.copy(sci.localizedActionSetName, "Gameplay")
local setOut = ffi.new("XrActionSet[1]")
check(xr.xrCreateActionSet(instance, sci, setOut), "xrCreateActionSet")
local set = setOut[0]
local A = {
move = makeAction(set, "move", XR.ACTION_TYPE_VECTOR2F, "Move"),
look = makeAction(set, "look", XR.ACTION_TYPE_VECTOR2F, "Zoom / Look"),
a = makeAction(set, "btn_a", XR.ACTION_TYPE_BOOLEAN, "A"),
b = makeAction(set, "btn_b", XR.ACTION_TYPE_BOOLEAN, "B"),
start = makeAction(set, "btn_start", XR.ACTION_TYPE_BOOLEAN, "Start"),
toggle = makeAction(set, "viewtoggle", XR.ACTION_TYPE_BOOLEAN,
"First / Third Person"),
gripl = makeAction(set, "grip_l", XR.ACTION_TYPE_FLOAT, "Left Grip"),
gripr = makeAction(set, "grip_r", XR.ACTION_TYPE_FLOAT, "Right Grip"),
handl = makeAction(set, "hand_l", XR.ACTION_TYPE_POSE, "Left Hand"),
handr = makeAction(set, "hand_r", XR.ACTION_TYPE_POSE, "Right Hand"),
-- HORDE MODE's two. `fire` is bound ALONGSIDE start on the right
-- trigger rather than instead of it: bindings are suggested once,
-- before the session attaches its action sets, so they cannot be
-- swapped when the mode starts -- and OpenXR is happy for two actions
-- to share an input. Outside the mode nothing reads `fire`, so the
-- trigger is START exactly as it always was; inside it, lib/VR reads
-- `fire` and drops START on the floor (there is no pausing anyway).
-- `aimr` is the pose a runtime defines as "where the user is
-- pointing", which is what a gun wants -- the grip pose points along
-- the controller's own body, which is a wrist, not a barrel.
fire = makeAction(set, "fire", XR.ACTION_TYPE_BOOLEAN, "Fire"),
aimr = makeAction(set, "aim_r", XR.ACTION_TYPE_POSE, "Right Aim"),
}
local function suggest(profile, list)
local arr = ffi.new("XrActionSuggestedBinding[?]", #list)
for i, p in ipairs(list) do
arr[i - 1].action = p[1]
arr[i - 1].binding = toPath(p[2])
end
local info = ffi.new("XrInteractionProfileSuggestedBinding")
info.type = XR.TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING
info.interactionProfile = toPath(profile)
info.countSuggestedBindings = #list
info.suggestedBindings = arr
check(xr.xrSuggestInteractionProfileBindings(instance, info),
"suggest " .. profile)
end
-- the sticks-and-buttons layout Touch and Index share
local sticks = {
{ A.move, "/user/hand/left/input/thumbstick" },
{ A.look, "/user/hand/right/input/thumbstick" },
{ A.a, "/user/hand/right/input/a/click" },
{ A.b, "/user/hand/right/input/b/click" },
{ A.a, "/user/hand/left/input/x/click" },
{ A.b, "/user/hand/left/input/y/click" },
{ A.start, "/user/hand/left/input/trigger/value" },
{ A.start, "/user/hand/right/input/trigger/value" },
{ A.toggle, "/user/hand/left/input/thumbstick/click" },
{ A.gripl, "/user/hand/left/input/squeeze/value" },
{ A.gripr, "/user/hand/right/input/squeeze/value" },
{ A.handl, "/user/hand/left/input/grip/pose" },
{ A.handr, "/user/hand/right/input/grip/pose" },
{ A.fire, "/user/hand/right/input/trigger/value" },
{ A.aimr, "/user/hand/right/input/aim/pose" },
}
-- Index has X/Y on no hand -- its A/B exist on BOTH -- so the two X/Y
-- rows are swapped for left A/B there
local index = {}
for i, p in ipairs(sticks) do index[i] = p end
index[5] = { A.a, "/user/hand/left/input/a/click" }
index[6] = { A.b, "/user/hand/left/input/b/click" }
pcall(suggest, "/interaction_profiles/oculus/touch_controller", sticks)
pcall(suggest, "/interaction_profiles/valve/index_controller", index)
pcall(suggest, "/interaction_profiles/microsoft/motion_controller", {
{ A.move, "/user/hand/left/input/thumbstick" },
{ A.look, "/user/hand/right/input/thumbstick" },
{ A.a, "/user/hand/right/input/trackpad/click" },
{ A.b, "/user/hand/left/input/trackpad/click" },
{ A.start, "/user/hand/left/input/trigger/value" },
{ A.start, "/user/hand/right/input/trigger/value" },
{ A.toggle, "/user/hand/left/input/thumbstick/click" },
{ A.gripl, "/user/hand/left/input/squeeze/click" },
{ A.gripr, "/user/hand/right/input/squeeze/click" },
{ A.handl, "/user/hand/left/input/grip/pose" },
{ A.handr, "/user/hand/right/input/grip/pose" },
{ A.fire, "/user/hand/right/input/trigger/value" },
{ A.aimr, "/user/hand/right/input/aim/pose" },
})
pcall(suggest, "/interaction_profiles/khr/simple_controller", {
{ A.a, "/user/hand/right/input/select/click" },
{ A.a, "/user/hand/left/input/select/click" },
{ A.start, "/user/hand/right/input/menu/click" },
{ A.start, "/user/hand/left/input/menu/click" },
})
local function handSpace(action)
local info = ffi.new("XrActionSpaceCreateInfo")
info.type = XR.TYPE_ACTION_SPACE_CREATE_INFO
info.action = action
info.poseInActionSpace.orientation.w = 1
local out = ffi.new("XrSpace[1]")
check(xr.xrCreateActionSpace(session, info, out), "xrCreateActionSpace")
return out[0]
end
-- the aim pose gets a space of its own; a runtime that refused the
-- binding simply never locates it, and the gun falls back to the grip
local spaces = { handl = handSpace(A.handl), handr = handSpace(A.handr) }
local okAim, aimSpace = pcall(handSpace, A.aimr)
if okAim and aimSpace then spaces.aimr = aimSpace end
local sets = ffi.new("XrActionSet[1]")
sets[0] = set
local att = ffi.new("XrSessionActionSetsAttachInfo")
att.type = XR.TYPE_SESSION_ACTION_SETS_ATTACH_INFO
att.countActionSets = 1
att.actionSets = sets
check(xr.xrAttachSessionActionSets(session, att),
"xrAttachSessionActionSets")
local active = ffi.new("XrActiveActionSet[1]")
active[0].actionSet = set
input = { set = set, A = A, spaces = spaces, active = active }
end
-- Sync and read this frame's controller state as one plain table, or nil
-- when there is none to read (no input attached, session unfocused) --
-- which callers treat as everything released.
function VRXR.input(time)
if not (running and input) then return nil end
local out = nil
pcall(function()
local si = ffi.new("XrActionsSyncInfo")
si.type = XR.TYPE_ACTIONS_SYNC_INFO
si.countActiveActionSets = 1
si.activeActionSets = input.active
if tonumber(xr.xrSyncActions(session, si)) ~= 0 then return end
local gi = ffi.new("XrActionStateGetInfo")
gi.type = XR.TYPE_ACTION_STATE_GET_INFO
local function readBool(action)
gi.action = action
local st = ffi.new("XrActionStateBoolean")
st.type = XR.TYPE_ACTION_STATE_BOOLEAN
xr.xrGetActionStateBoolean(session, gi, st)
return st.currentState ~= 0, st.changedSinceLastSync ~= 0
end
local function readVec2(action)
gi.action = action
local st = ffi.new("XrActionStateVector2f")
st.type = XR.TYPE_ACTION_STATE_VECTOR2F
xr.xrGetActionStateVector2f(session, gi, st)
return st.currentState.x, st.currentState.y
end
local function readFloat(action)
gi.action = action
local st = ffi.new("XrActionStateFloat")
st.type = XR.TYPE_ACTION_STATE_FLOAT
xr.xrGetActionStateFloat(session, gi, st)
return st.currentState
end
local A = input.A
local o = {}
o.moveX, o.moveY = readVec2(A.move)
o.lookX, o.lookY = readVec2(A.look)
o.a, o.aChanged = readBool(A.a)
o.b, o.bChanged = readBool(A.b)
o.start, o.startChanged = readBool(A.start)
o.toggle, o.toggleChanged = readBool(A.toggle)
o.fire, o.fireChanged = readBool(A.fire)
o.gripL = readFloat(A.gripl)
o.gripR = readFloat(A.gripr)
-- hand heights in LOCAL metres, for the grab-drag; absent when the
-- runtime has no valid position for a hand this frame
local loc = ffi.new("XrSpaceLocation")
for name, spc in pairs(input.spaces) do
loc.type = XR.TYPE_SPACE_LOCATION
loc.next = nil
loc.locationFlags = 0
if tonumber(xr.xrLocateSpace(spc, space, time, loc)) == 0 then
local fl = tonumber(loc.locationFlags) or 0
if fl % 4 >= XR.SPACE_LOCATION_POSITION_VALID_BIT then
o[name .. "Y"] = loc.pose.position.y
-- and the whole pose when the orientation is valid too
-- (bit 0x1): the hand-held pokedex stands on it
if fl % 2 >= 1 then
local p, q = loc.pose.position, loc.pose.orientation
o[name] = { pos = { p.x, p.y, p.z },
quat = { q.x, q.y, q.z, q.w } }
end
end
end
end
out = o
end)
return out
end
-- ------- lifecycle
-- Bring the whole stack up, or say exactly why not. Never throws; call
-- again after fixing whatever status() names.
function VRXR.start(quadW, quadH)
if session then return true end
local ok, err = pcall(function()
if not VRGL.load() then error(VRGL.status(), 0) end
xr = xr or loadLoader()
-- instance, asking only for the one extension this rides on
local extName = ffi.new("const char*[1]")
local extStr = ffi.new("char[?]", 32, "XR_KHR_opengl_enable")
extName[0] = extStr
local ici = ffi.new("XrInstanceCreateInfo")
ici.type = XR.TYPE_INSTANCE_CREATE_INFO
ffi.copy(ici.applicationInfo.applicationName, "DramaticShapeVoxelMod")
ici.applicationInfo.applicationVersion = 1
ffi.copy(ici.applicationInfo.engineName, "LOVE")
ici.applicationInfo.engineVersion = 11
ici.applicationInfo.apiVersion = XR.API_VERSION_1_0
ici.enabledExtensionCount = 1
ici.enabledExtensionNames = extName
local inst = ffi.new("XrInstance[1]")
check(xr.xrCreateInstance(ici, inst), "xrCreateInstance")
instance = inst[0]
-- a headset
local sgi = ffi.new("XrSystemGetInfo")
sgi.type = XR.TYPE_SYSTEM_GET_INFO
sgi.formFactor = XR.FORM_FACTOR_HEAD_MOUNTED_DISPLAY
local sys = ffi.new("XrSystemId[1]")
check(xr.xrGetSystem(instance, sgi, sys), "xrGetSystem")
systemId = sys[0]
-- the spec REQUIRES this call before the session may exist
local pfn = ffi.new("void*[1]")
check(xr.xrGetInstanceProcAddr(instance,
"xrGetOpenGLGraphicsRequirementsKHR", pfn),
"xrGetInstanceProcAddr")
local reqs = ffi.new("XrGraphicsRequirementsOpenGLKHR")
reqs.type = XR.TYPE_GRAPHICS_REQUIREMENTS_OPENGL_KHR
check(ffi.cast("PFN_xrGetOpenGLGraphicsRequirementsKHR", pfn[0])(
instance, systemId, reqs), "xrGetOpenGLGraphicsRequirementsKHR")
-- the session, married to LOVE's own GL context. The binding is
-- allocated as a one-element ARRAY: LuaJIT decays arrays to pointers
-- on assignment, which is what chaining it into `next` needs -- a
-- bare struct would not convert.
local hdc, hglrc = VRGL.contexts()
if hdc == nil or hglrc == nil then
error("no current GL context to bind (is a window up?)", 0)
end
local binding = ffi.new("XrGraphicsBindingOpenGLWin32KHR[1]")
binding[0].type = XR.TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR
binding[0].hDC, binding[0].hGLRC = hdc, hglrc
local sci = ffi.new("XrSessionCreateInfo")
sci.type = XR.TYPE_SESSION_CREATE_INFO
sci.next = binding
sci.systemId = systemId
local ses = ffi.new("XrSession[1]")
check(xr.xrCreateSession(instance, sci, ses), "xrCreateSession")
session = ses[0]
-- LOCAL space: origin where the head was, +Y up, -Z ahead
local rsci = ffi.new("XrReferenceSpaceCreateInfo")
rsci.type = XR.TYPE_REFERENCE_SPACE_CREATE_INFO
rsci.referenceSpaceType = XR.REFERENCE_SPACE_TYPE_LOCAL
rsci.poseInReferenceSpace.orientation.w = 1
local spc = ffi.new("XrSpace[1]")
check(xr.xrCreateReferenceSpace(session, rsci, spc),
"xrCreateReferenceSpace")
space = spc[0]
-- the two eyes at the runtime's recommended size
local count = ffi.new("uint32_t[1]")
check(xr.xrEnumerateViewConfigurationViews(instance, systemId,
XR.VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, 0, count, nil),
"xrEnumerateViewConfigurationViews(count)")
if count[0] < 2 then error("stereo view configuration missing", 0) end
views = ffi.new("XrViewConfigurationView[?]", count[0])
for i = 0, count[0] - 1 do
views[i].type = XR.TYPE_VIEW_CONFIGURATION_VIEW
end
check(xr.xrEnumerateViewConfigurationViews(instance, systemId,
XR.VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, count[0], count, views),
"xrEnumerateViewConfigurationViews")
local format = pickFormat()
for i = 1, 2 do
eyes[i] = makeSwapchain(views[i - 1].recommendedImageRectWidth,
views[i - 1].recommendedImageRectHeight,
format)
end
quad = makeSwapchain(quadW or 1024, quadH or 768, format)
viewsBuf = ffi.new("XrView[2]")
end)
if not ok then
VRXR.stop()
status = tostring(err)
return false
end
-- controllers, in their own pcall: a runtime that refuses the action
-- system costs the controllers and nothing else -- the head still
-- tracks and the desk's own inputs still play
local okIn, errIn = pcall(setupInput)
if not okIn then
input = nil
print("[DRAMATIC_SHAPE] VR controllers unavailable: " .. tostring(errIn))
end
status = "session created"
return true
end
-- Tear the whole stack down (setting toggled off, or a fatal state).
function VRXR.stop()
running = false
if session then pcall(function() xr.xrDestroySession(session) end) end
if instance then pcall(function() xr.xrDestroyInstance(instance) end) end
session, space, instance, systemId = nil, nil, nil, nil
eyes, quad, views, viewsBuf, lastViews = {}, nil, nil, nil, nil
input = nil -- its handles died with the instance
end
-- ------- the frame loop
-- Pump the runtime's events: begin the session when it says READY, end it
-- when it says STOPPING. Returns false when the session is gone for good.
function VRXR.poll()
if not session then return false end
local alive = true
pcall(function()
-- a one-element array, so the retype cast below has a pointer to
-- decay from (ffi.cast does not take a bare struct's address)
local buf = ffi.new("XrEventDataBuffer[1]")
while true do
buf[0].type = XR.TYPE_EVENT_DATA_BUFFER
buf[0].next = nil
if tonumber(xr.xrPollEvent(instance, buf)) ~= 0 then break end
if buf[0].type == XR.TYPE_EVENT_DATA_SESSION_STATE_CHANGED then
local ev = ffi.cast("XrEventDataSessionStateChanged*", buf)
sessionState = tonumber(ev.state)
if sessionState == XR.SESSION_STATE_READY and not running then
local sbi = ffi.new("XrSessionBeginInfo")
sbi.type = XR.TYPE_SESSION_BEGIN_INFO
sbi.primaryViewConfigurationType =
XR.VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO
if tonumber(xr.xrBeginSession(session, sbi)) == 0 then
running = true
status = "running"
end
elseif sessionState == XR.SESSION_STATE_STOPPING then
pcall(function() xr.xrEndSession(session) end)
running = false
status = "stopped by runtime"
elseif sessionState == XR.SESSION_STATE_EXITING
or sessionState == XR.SESSION_STATE_LOSS_PENDING then
alive = false
end
end
end
end)
if not alive then VRXR.stop() end
return session ~= nil
end
function VRXR.isRunning()
return running
end
-- Block until the runtime wants the next frame; returns its predicted
-- display time and whether rendering is worth doing, or nil off-session.
function VRXR.waitFrame()
if not running then return nil end
local time, should = nil, false
local ok = pcall(function()
local fs = ffi.new("XrFrameState")
fs.type = XR.TYPE_FRAME_STATE
check(xr.xrWaitFrame(session, nil, fs), "xrWaitFrame")
check(xr.xrBeginFrame(session, nil), "xrBeginFrame")
time = fs.predictedDisplayTime
should = fs.shouldRender ~= 0
end)
if not ok then return nil end
return time, should
end
-- Where each eye is and what it sees, at `time`, as plain tables (and
-- kept as cdata for the layer submit). nil when tracking is not live.
function VRXR.locateViews(time)
if not running then return nil end
local out = nil
pcall(function()
local vli = ffi.new("XrViewLocateInfo")
vli.type = XR.TYPE_VIEW_LOCATE_INFO
vli.viewConfigurationType = XR.VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO
vli.displayTime = time
vli.space = space
local vs = ffi.new("XrViewState")
vs.type = XR.TYPE_VIEW_STATE
local count = ffi.new("uint32_t[1]")
viewsBuf[0].type, viewsBuf[1].type = XR.TYPE_VIEW, XR.TYPE_VIEW
check(xr.xrLocateViews(session, vli, vs, 2, count, viewsBuf),
"xrLocateViews")
if count[0] < 2 then return end
out = {}
for i = 1, 2 do
local v = viewsBuf[i - 1]
out[i] = {
pose = {
pos = { v.pose.position.x, v.pose.position.y, v.pose.position.z },
quat = { v.pose.orientation.x, v.pose.orientation.y,
v.pose.orientation.z, v.pose.orientation.w },
},
fov = { angleLeft = v.fov.angleLeft, angleRight = v.fov.angleRight,
angleUp = v.fov.angleUp, angleDown = v.fov.angleDown },
w = eyes[i].w, h = eyes[i].h,
}
end
end)
lastViews = out
return out
end
-- Acquire eye `i`'s next texture for writing. Pair with releaseEye.
function VRXR.acquireEye(i)
local e = eyes[i]
if not (running and e) then return nil end
local tex = nil
pcall(function()
local idx = ffi.new("uint32_t[1]")
check(xr.xrAcquireSwapchainImage(e.swapchain, nil, idx),
"xrAcquireSwapchainImage")
local wi = ffi.new("XrSwapchainImageWaitInfo")
wi.type = XR.TYPE_SWAPCHAIN_IMAGE_WAIT_INFO
wi.timeout = XR.INFINITE_DURATION
check(xr.xrWaitSwapchainImage(e.swapchain, wi), "xrWaitSwapchainImage")
tex = e.images[idx[0] + 1]
end)
return tex, e.w, e.h
end
function VRXR.releaseEye(i)
local e = eyes[i]
if not (running and e) then return end
pcall(function() xr.xrReleaseSwapchainImage(e.swapchain, nil) end)
end
function VRXR.acquireQuad()
if not (running and quad) then return nil end
local tex = nil
pcall(function()
local idx = ffi.new("uint32_t[1]")
check(xr.xrAcquireSwapchainImage(quad.swapchain, nil, idx),
"xrAcquireSwapchainImage(quad)")
local wi = ffi.new("XrSwapchainImageWaitInfo")
wi.type = XR.TYPE_SWAPCHAIN_IMAGE_WAIT_INFO
wi.timeout = XR.INFINITE_DURATION
check(xr.xrWaitSwapchainImage(quad.swapchain, wi), "xrWaitSwapchainImage")
tex = quad.images[idx[0] + 1]
end)
return tex, quad.w, quad.h
end
function VRXR.releaseQuad()
if not (running and quad) then return end
pcall(function() xr.xrReleaseSwapchainImage(quad.swapchain, nil) end)
end
-- Close the frame. `world` submits the projection layer from the LAST
-- locateViews (both eyes assumed blitted); `quadOpts` floats the UI panel:
-- { pos = {x,y,z}, quat = {x,y,z,w}, width = metres,
-- crop = {x, y, w, h} or nil }. Either may be nil. `crop` shows only
-- that sub-rect of the quad's texture -- GL image coordinates, origin
-- bottom-left, exactly as the front-buffer copy landed the window -- and
-- the panel's aspect follows the rect (the GB letterbox's near-square
-- frame) instead of the whole texture's.
function VRXR.endFrame(time, world, quadOpts)
if not running then return end
pcall(function()
-- every layer is a one-element ARRAY so it decays to a pointer when
-- stored into the layer list; the locals also anchor the cdata
-- against collection until xrEndFrame has read it
local layers = {}
local projViews, proj, quadLayer
if world and lastViews then
projViews = ffi.new("XrCompositionLayerProjectionView[2]")
for i = 0, 1 do
projViews[i].type = XR.TYPE_COMPOSITION_LAYER_PROJECTION_VIEW
projViews[i].pose = viewsBuf[i].pose
projViews[i].fov = viewsBuf[i].fov
projViews[i].subImage.swapchain = eyes[i + 1].swapchain
projViews[i].subImage.imageRect.extent.width = eyes[i + 1].w
projViews[i].subImage.imageRect.extent.height = eyes[i + 1].h
end
proj = ffi.new("XrCompositionLayerProjection[1]")
proj[0].type = XR.TYPE_COMPOSITION_LAYER_PROJECTION
proj[0].space = space
proj[0].viewCount = 2
proj[0].views = projViews
layers[#layers + 1] = proj
end
if quadOpts and quad then
quadLayer = ffi.new("XrCompositionLayerQuad[1]")
quadLayer[0].type = XR.TYPE_COMPOSITION_LAYER_QUAD
quadLayer[0].space = space
quadLayer[0].eyeVisibility = XR.EYE_VISIBILITY_BOTH
quadLayer[0].subImage.swapchain = quad.swapchain
local cr = quadOpts.crop
if cr then
quadLayer[0].subImage.imageRect.offset.x = cr[1]
quadLayer[0].subImage.imageRect.offset.y = cr[2]
quadLayer[0].subImage.imageRect.extent.width = cr[3]
quadLayer[0].subImage.imageRect.extent.height = cr[4]
else
quadLayer[0].subImage.imageRect.extent.width = quad.w
quadLayer[0].subImage.imageRect.extent.height = quad.h
end
local p, q = quadOpts.pos or { 0, 0, -1 }, quadOpts.quat or { 0, 0, 0, 1 }
quadLayer[0].pose.position.x = p[1]
quadLayer[0].pose.position.y = p[2]
quadLayer[0].pose.position.z = p[3]
quadLayer[0].pose.orientation.x = q[1]
quadLayer[0].pose.orientation.y = q[2]
quadLayer[0].pose.orientation.z = q[3]
quadLayer[0].pose.orientation.w = q[4]
local w = quadOpts.width or 1.0
quadLayer[0].size.width = w
if cr then
quadLayer[0].size.height = w * (cr[4] / math.max(1, cr[3]))
else
quadLayer[0].size.height = w * (quad.h / quad.w)
end
layers[#layers + 1] = quadLayer
end
local arr = ffi.new("const void*[?]", math.max(1, #layers))
for i = 1, #layers do arr[i - 1] = layers[i] end
local fei = ffi.new("XrFrameEndInfo")
fei.type = XR.TYPE_FRAME_END_INFO
fei.displayTime = time
fei.environmentBlendMode = XR.ENVIRONMENT_BLEND_MODE_OPAQUE
fei.layerCount = #layers
fei.layers = arr
xr.xrEndFrame(session, fei)
end)
end
function VRXR.quadSize()
if quad then return quad.w, quad.h end
return nil
end
function VRXR.status()
return status
end
return VRXR