Skip to content

HtcEyeTracker.kt

src/jvmMain/kotlin/afield/graphics/vulkan/targets/HtcEyeTracker.kt · 167 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.XrPosef
import org.lwjgl.openxr.XrSession
import org.lwjgl.openxr.XrSpace
import org.lwjgl.openxr.XrSystemProperties
import org.lwjgl.system.JNI
import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryUtil.*
import org.lwjgl.system.Pointer

Native binding for XR_HTC_eye_tracker, which Field's LWJGL version does not generate.

Layouts and signatures follow HTC's ViveEyeTrackerHelper.cs and enum values in OpenXRUtils.cs: https://github.com/ViveSoftware/VIVE-OpenXR-Unity/tree/25a5fd212420688952ead9deba735357656278ec/com.htc.upm.vive.openxr/Runtime

Only 64-bit ABIs are supported: enum/bool32/float = 4 bytes, pointer/handle/time = 8 bytes, and create returns null on any other pointer width. The per-eye arrays are inline rather than pointers, and their elements carry no type/next header.

internal class HtcEyeTracker private constructor

Fills a HtcEyeTrackingState from a reference space: gaze, pupil and geometric, per eye.

internal class HtcEyeTracker private constructor(
    private val tracker: Long,
    private val pDestroy: Long,
    private val pGaze: Long,
    private val pPupil: Long,
    private val pGeometric: Long,
) {

val state = HtcEyeTrackingState()

fun update(referenceSpace: XrSpace, predictedTime: Long)

    fun update(referenceSpace: XrSpace, predictedTime: Long) {
        state.invalidate()
        MemoryStack.stackPush().use { stack ->
            val gazeInfo = struct(stack, 32, TYPE_GAZE_INFO)
            memPutAddress(gazeInfo + 16, referenceSpace.address())
            memPutLong(gazeInfo + 24, predictedTime)
            val gaze = struct(stack, 88, TYPE_GAZE)
            readGaze(state, JNI.callPPPI(tracker, gazeInfo, gaze, pGaze), gaze)

            val pupilInfo = struct(stack, 16, TYPE_PUPIL_INFO)
            val pupil = struct(stack, 64, TYPE_PUPIL)
            readPupil(state, JNI.callPPPI(tracker, pupilInfo, pupil, pPupil), pupil)

            val geometricInfo = struct(stack, 16, TYPE_GEOMETRIC_INFO)
            val geometric = struct(stack, 56, TYPE_GEOMETRIC)
            readGeometric(state, JNI.callPPPI(tracker, geometricInfo, geometric, pGeometric), geometric)
        }
    }

fun close()

Must be called before destroying the session.

    fun close() {
        state.invalidate()
        val result = JNI.callPI(tracker, pDestroy)
        if (result != XR_SUCCESS) lprintln("warning,openxr", "xrDestroyEyeTrackerHTC failed: $result")
    }

companion object

const val EXTENSION_NAME = "XR_HTC_eye_tracker"

        private const val TYPE_CREATE = 1000326001
        private const val TYPE_GAZE_INFO = 1000326002
        private const val TYPE_GAZE = 1000326003
        private const val TYPE_SYSTEM_PROPERTIES = 1000326005
        private const val TYPE_PUPIL_INFO = 1000326006
        private const val TYPE_PUPIL = 1000326007
        private const val TYPE_GEOMETRIC_INFO = 1000326009
        private const val TYPE_GEOMETRIC = 1000326010

        private fun struct(stack: MemoryStack, size: Int, type: Int): Long =
            stack.ncalloc(8, 1, size).also { memPutInt(it, type) }

fun create(session: XrSession, systemId: Long): HtcEyeTracker?

Caller must have enabled the extension. Missing functions or device support disable it.

body — 44 lines, from line 77
        fun create(session: XrSession, systemId: Long): HtcEyeTracker? {
            if (Pointer.POINTER_SIZE != 8) {
                lprintln("warning,openxr", "HTC eye tracking requires a 64-bit ABI")
                return null
            }
            val instance = session.instance
            MemoryStack.stackPush().use { stack ->
                val eyeProperties = struct(stack, 24, TYPE_SYSTEM_PROPERTIES)
                val properties = XrSystemProperties.calloc(stack).type(XR_TYPE_SYSTEM_PROPERTIES).next(eyeProperties)
                val propsResult = xrGetSystemProperties(instance, systemId, properties)
                if (propsResult != XR_SUCCESS) {
                    lprintln("warning,openxr", "HTC eye tracking system properties failed: $propsResult")
                    return null
                }
                if (memGetInt(eyeProperties + 16) == 0) return null

                val create = proc(instance, stack, "xrCreateEyeTrackerHTC")
                val destroy = proc(instance, stack, "xrDestroyEyeTrackerHTC")
                val gaze = proc(instance, stack, "xrGetEyeGazeDataHTC")
                val pupil = proc(instance, stack, "xrGetEyePupilDataHTC")
                val geometric = proc(instance, stack, "xrGetEyeGeometricDataHTC")
                if (create == 0L || destroy == 0L || gaze == 0L || pupil == 0L || geometric == 0L) return null

                val info = struct(stack, 16, TYPE_CREATE)
                val out = stack.callocPointer(1)
                val result = JNI.callPPPI(session.address(), info, out.address(), create)
                if (result != XR_SUCCESS) {
                    lprintln("warning,openxr", "xrCreateEyeTrackerHTC failed: $result")
                    return null
                }
                lprintln("info,openxr", "HTC eye tracker enabled")
                return HtcEyeTracker(out[0], destroy, gaze, pupil, geometric)
            }
        }

        private fun proc(instance: XrInstance, stack: MemoryStack, name: String): Long {
            val out = stack.callocPointer(1)
            val result = xrGetInstanceProcAddr(instance, name, out)
            if (result != XR_SUCCESS || out[0] == 0L) {
                lprintln("warning,openxr", "xrGetInstanceProcAddr($name) failed: $result")
                return 0L
            }
            return out[0]
        }

internal fun readGaze(state: HtcEyeTrackingState, result: Int, data: Long)

All output headers are { type@0, next@8, time@16, inline eyes[2]@24 }. The decoders clear validity on failure and never read undefined numeric fields.

        internal fun readGaze(state: HtcEyeTrackingState, result: Int, data: Long) {
            state.gazeTime = if (result == XR_SUCCESS) memGetLong(data + 16) else 0
            for (i in 0..1) {
                val eye = if (i == 0) state.left else state.right
                val entry = data + 24 + i * 32L // bool32, pose (quat xyzw then position xyz)
                eye.gazeValid = result == XR_SUCCESS && memGetInt(entry) != 0
                if (eye.gazeValid) {
                    val pose = XrPosef.create(entry + 4)
                    eye.position.set(pose.`position$`().x(), pose.`position$`().y(), pose.`position$`().z())
                    eye.orientation.set(
                        pose.orientation().x(), pose.orientation().y(), pose.orientation().z(), pose.orientation().w()
                    )
                }
            }
        }

internal fun readPupil(state: HtcEyeTrackingState, result: Int, data: Long)

        internal fun readPupil(state: HtcEyeTrackingState, result: Int, data: Long) {
            state.pupilTime = if (result == XR_SUCCESS) memGetLong(data + 16) else 0
            for (i in 0..1) {
                val eye = if (i == 0) state.left else state.right
                val entry = data + 24 + i * 20L // diameter-valid, position-valid, diameter, vec2
                eye.pupilDiameterValid = result == XR_SUCCESS && memGetInt(entry) != 0
                eye.pupilPositionValid = result == XR_SUCCESS && memGetInt(entry + 4) != 0
                if (eye.pupilDiameterValid) eye.pupilDiameter = memGetFloat(entry + 8)
                if (eye.pupilPositionValid) eye.pupilPosition.set(memGetFloat(entry + 12), memGetFloat(entry + 16))
            }
        }

internal fun readGeometric(state: HtcEyeTrackingState, result: Int, data: Long)

        internal fun readGeometric(state: HtcEyeTrackingState, result: Int, data: Long) {
            state.geometricTime = if (result == XR_SUCCESS) memGetLong(data + 16) else 0
            for (i in 0..1) {
                val eye = if (i == 0) state.left else state.right
                val entry = data + 24 + i * 16L // valid, openness, wide, squeeze (in that order)
                eye.geometricValid = result == XR_SUCCESS && memGetInt(entry) != 0
                if (eye.geometricValid) {
                    eye.eyeOpenness = memGetFloat(entry + 4)
                    eye.eyeWide = memGetFloat(entry + 8)
                    eye.eyeSqueeze = memGetFloat(entry + 12)
                }
            }
        }
    }
}