MndxXDevSpace.kt
src/jvmMain/kotlin/afield/graphics/vulkan/targets/MndxXDevSpace.kt · 214 lines
package afield.graphics.vulkan.targets
import afield.app.diagnostics.logging.lprintln
import org.lwjgl.openxr.XR10
import org.lwjgl.openxr.XrInstance
import org.lwjgl.openxr.XrSession
import org.lwjgl.openxr.XrSpace
import org.lwjgl.system.JNI
import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryUtil
Hand-rolled bindings for XR_MNDX_xdev_space.
This is a Monado vendor extension: it never made it into the official OpenXR registry, so
LWJGL generates no bindings for it. We resolve the entry points through xrGetInstanceProcAddr
and lay the structs out by hand. Definitions transcribed from Monado's
src/external/openxr_includes/openxr/XR_MNDX_xdev_space.h (spec version 3, extension 445).
It is the only route to WiVRn's body trackers. The obvious one, XR_FB_body_tracking, is a dead
end there; OpenXRHmdTarget's body-tracking setup says why, and cites the WiVRn source.
The extension enumerates every "xdev" (tracked device) the runtime knows about (HMD, controllers,
body trackers) and hands back an XrSpace per device, which the caller then locates with the
ordinary xrLocateSpace.
internal class MndxXDevSpace private constructor
The extension's entry points, resolved per instance by load, which is null unless all are there.
internal class MndxXDevSpace private constructor(
private val pCreateXDevList: Long,
private val pGetGenerationNumber: Long,
private val pEnumerateXDevs: Long,
private val pGetXDevProperties: Long,
private val pDestroyXDevList: Long,
private val pCreateXDevSpace: Long,
) {
class XDev(val id: Long, val name: String, val serial: String, val canCreateSpace: Boolean)
One tracked device, as reported by xrGetXDevPropertiesMNDX.
override fun toString()
override fun toString() = "XDev(id=$id, name='$name', serial='$serial', canCreateSpace=$canCreateSpace)"
}
fun createXDevList(session: XrSession): Long?
Returns the XrXDevListMNDX handle, or null on failure.
fun createXDevList(session: XrSession): Long? {
MemoryStack.stackPush().use { stack ->
val info = stack.ncalloc(8, 1, CREATE_XDEV_LIST_INFO_SIZE)
MemoryUtil.memPutInt(info + TYPE, XR_TYPE_CREATE_XDEV_LIST_INFO_MNDX)
val out = stack.ncalloc(8, 1, 8)
val res = JNI.callPPPI(session.address(), info, out, pCreateXDevList)
if (res != XR10.XR_SUCCESS) {
lprintln("warning,openxr", "xrCreateXDevListMNDX failed: $res")
return null
}
return MemoryUtil.memGetAddress(out)
}
}
fun destroyXDevList(xdevList: Long)
fun destroyXDevList(xdevList: Long) {
val res = JNI.callPI(xdevList, pDestroyXDevList)
if (res != XR10.XR_SUCCESS) lprintln("warning,openxr", "xrDestroyXDevListMNDX failed: $res")
}
fun generationNumber(xdevList: Long): Long?
Bumps whenever the device list changes. Cheap to poll, so it's how we notice trackers appearing after the session started (the user enabling body tracking mid-stream, or WiVRn reconnecting).
Null on failure rather than a sentinel value: callers compare this against the generation they last saw, and any in-band sentinel could collide with a real generation number and wedge them into never refreshing again.
fun generationNumber(xdevList: Long): Long? {
MemoryStack.stackPush().use { stack ->
val out = stack.ncalloc(8, 1, 8)
val res = JNI.callPPI(xdevList, out, pGetGenerationNumber)
if (res != XR10.XR_SUCCESS) return null
return MemoryUtil.memGetLong(out)
}
}
fun enumerateXDevs(xdevList: Long): LongArray
fun enumerateXDevs(xdevList: Long): LongArray {
MemoryStack.stackPush().use { stack ->
val count = stack.ncalloc(4, 1, 4)
var res = JNI.callPPPI(xdevList, 0, count, 0L, pEnumerateXDevs)
if (res != XR10.XR_SUCCESS) {
lprintln("warning,openxr", "xrEnumerateXDevsMNDX(count) failed: $res")
return LongArray(0)
}
val n = MemoryUtil.memGetInt(count)
if (n <= 0) return LongArray(0)
val ids = stack.ncalloc(8, n, 8)
res = JNI.callPPPI(xdevList, n, count, ids, pEnumerateXDevs)
if (res != XR10.XR_SUCCESS) {
lprintln("warning,openxr", "xrEnumerateXDevsMNDX failed: $res")
return LongArray(0)
}
return LongArray(MemoryUtil.memGetInt(count)) { MemoryUtil.memGetLong(ids + it * 8L) }
}
}
fun getProperties(xdevList: Long, id: Long): XDev?
fun getProperties(xdevList: Long, id: Long): XDev? {
MemoryStack.stackPush().use { stack ->
val info = stack.ncalloc(8, 1, GET_XDEV_INFO_SIZE)
MemoryUtil.memPutInt(info + TYPE, XR_TYPE_GET_XDEV_INFO_MNDX)
MemoryUtil.memPutLong(info + GET_XDEV_INFO_ID, id)
val props = stack.ncalloc(8, 1, XDEV_PROPERTIES_SIZE)
MemoryUtil.memPutInt(props + TYPE, XR_TYPE_XDEV_PROPERTIES_MNDX)
val res = JNI.callPPPI(xdevList, info, props, pGetXDevProperties)
if (res != XR10.XR_SUCCESS) {
lprintln("warning,openxr", "xrGetXDevPropertiesMNDX($id) failed: $res")
return null
}
return XDev(
id = id,
name = MemoryUtil.memUTF8(props + XDEV_PROPERTIES_NAME, strlen(props + XDEV_PROPERTIES_NAME)),
serial = MemoryUtil.memUTF8(props + XDEV_PROPERTIES_SERIAL, strlen(props + XDEV_PROPERTIES_SERIAL)),
canCreateSpace = MemoryUtil.memGetInt(props + XDEV_PROPERTIES_CAN_CREATE_SPACE) != 0,
)
}
}
fun createXDevSpace(session: XrSession, xdevList: Long, id: Long): XrSpace?
Creates an identity-offset space tracking the given device. Caller owns the space.
fun createXDevSpace(session: XrSession, xdevList: Long, id: Long): XrSpace? {
MemoryStack.stackPush().use { stack ->
val info = stack.ncalloc(8, 1, CREATE_XDEV_SPACE_INFO_SIZE)
MemoryUtil.memPutInt(info + TYPE, XR_TYPE_CREATE_XDEV_SPACE_INFO_MNDX)
MemoryUtil.memPutAddress(info + CREATE_XDEV_SPACE_INFO_XDEV_LIST, xdevList)
MemoryUtil.memPutLong(info + CREATE_XDEV_SPACE_INFO_ID, id)
// XrPosef offset: identity. Quaternion is (x, y, z, w), so only w is non-zero.
MemoryUtil.memPutFloat(info + CREATE_XDEV_SPACE_INFO_OFFSET + 12, 1f)
val out = stack.ncalloc(8, 1, 8)
val res = JNI.callPPPI(session.address(), info, out, pCreateXDevSpace)
if (res != XR10.XR_SUCCESS) {
lprintln("warning,openxr", "xrCreateXDevSpaceMNDX($id) failed: $res")
return null
}
return XrSpace(MemoryUtil.memGetAddress(out), session)
}
}
The struct fields are fixed-capacity char arrays, not pointers, so find the terminator.
private fun strlen(address: Long): Int {
var i = 0
while (i < XDEV_NAME_CAPACITY && MemoryUtil.memGetByte(address + i) != 0.toByte()) i++
return i
}
companion object
const val EXTENSION_NAME = "XR_MNDX_xdev_space"
Struct type enums, from the 1000444xxx block.
private const val XR_TYPE_CREATE_XDEV_LIST_INFO_MNDX = 1000444002
private const val XR_TYPE_GET_XDEV_INFO_MNDX = 1000444003
private const val XR_TYPE_XDEV_PROPERTIES_MNDX = 1000444004
private const val XR_TYPE_CREATE_XDEV_SPACE_INFO_MNDX = 1000444005
Struct layouts for LP64 (4-byte enum, 8-byte pointer, natural alignment). Every struct
starts with XrStructureType type; void *next;, so type is always at 0 and the first
real field lands at 16.
private const val TYPE = 0L
private const val CREATE_XDEV_LIST_INFO_SIZE = 16
private const val GET_XDEV_INFO_SIZE = 24
private const val GET_XDEV_INFO_ID = 16L // XrXDevIdMNDX (atom, uint64)
private const val XDEV_PROPERTIES_SIZE = 536 // 532 rounded up to pointer alignment
private const val XDEV_PROPERTIES_NAME = 16L // char name[256]
private const val XDEV_PROPERTIES_SERIAL = 272L // char serial[256]
private const val XDEV_PROPERTIES_CAN_CREATE_SPACE = 528L
private const val XDEV_NAME_CAPACITY = 256
private const val CREATE_XDEV_SPACE_INFO_SIZE = 64 // 60 rounded up
private const val CREATE_XDEV_SPACE_INFO_XDEV_LIST = 16L
private const val CREATE_XDEV_SPACE_INFO_ID = 24L
private const val CREATE_XDEV_SPACE_INFO_OFFSET = 32L // XrPosef: quat xyzw, then vec3 xyz
fun load(instance: XrInstance): MndxXDevSpace?
Resolves the entry points. Returns null unless every one is present, so callers get an all-or-nothing handle rather than having to null-check each call.
The instance must have been created with EXTENSION_NAME enabled.
fun load(instance: XrInstance): MndxXDevSpace? {
MemoryStack.stackPush().use { stack ->
fun proc(name: String): Long {
val out = stack.callocPointer(1)
val res = XR10.xrGetInstanceProcAddr(instance, name, out)
if (res != XR10.XR_SUCCESS || out[0] == 0L) {
lprintln("warning,openxr", "xrGetInstanceProcAddr($name) failed: $res")
return 0L
}
return out[0]
}
val create = proc("xrCreateXDevListMNDX")
val generation = proc("xrGetXDevListGenerationNumberMNDX")
val enumerate = proc("xrEnumerateXDevsMNDX")
val properties = proc("xrGetXDevPropertiesMNDX")
val destroy = proc("xrDestroyXDevListMNDX")
val space = proc("xrCreateXDevSpaceMNDX")
if (create == 0L || generation == 0L || enumerate == 0L ||
properties == 0L || destroy == 0L || space == 0L
) return null
return MndxXDevSpace(create, generation, enumerate, properties, destroy, space)
}
}
}
}