Skip to content

ExtEyeGazeInteraction.kt

src/jvmMain/kotlin/afield/graphics/vulkan/targets/ExtEyeGazeInteraction.kt · 115 lines

package afield.graphics.vulkan.targets

import afield.app.diagnostics.logging.lprintln
import org.lwjgl.openxr.*
import org.lwjgl.openxr.EXTEyeGazeInteraction.*
import org.lwjgl.openxr.XR10.*
import org.lwjgl.system.MemoryStack

Combined eye gaze from OpenXR's XR_EXT_eye_gaze_interaction, as a single pose action inside the HMD target's existing gameplay action set.

Creation returns null on a runtime that does not advertise the extension, reports no eye-gaze support in its system properties, or refuses the action, its suggested binding or its action space, so a target asks once and then has either gaze or none. It has to happen before the action sets are attached to the session. Each frame the gaze pose is located against the target's reference space and written into an EyeGazeState with the sample time the runtime reports for it; the state is invalidated first, so a frame in which the action is not active reads as inactive rather than as the last pose that worked.

internal class ExtEyeGazeInteraction private constructor

Optional combined gaze action, owned by the target's existing gameplay action set.

internal class ExtEyeGazeInteraction private constructor(
    private val action: XrAction,
    private val space: XrSpace,
) {

val state = EyeGazeState()

fun update(session: XrSession, referenceSpace: XrSpace, predictedTime: Long)

Called only after a successful sync of the gameplay action set.

    fun update(session: XrSession, referenceSpace: XrSpace, predictedTime: Long) {
        state.invalidate()
        MemoryStack.stackPush().use { stack ->
            val getInfo = XrActionStateGetInfo.calloc(stack).type(XR_TYPE_ACTION_STATE_GET_INFO)
                .action(action).subactionPath(XR_NULL_PATH)
            val actionState = XrActionStatePose.calloc(stack).type(XR_TYPE_ACTION_STATE_POSE)
            if (xrGetActionStatePose(session, getInfo, actionState) != XR_SUCCESS || !actionState.isActive()) return
            state.isActive = true
            val sample = XrEyeGazeSampleTimeEXT.calloc(stack).type(XR_TYPE_EYE_GAZE_SAMPLE_TIME_EXT)
            val location = XrSpaceLocation.calloc(stack).type(XR_TYPE_SPACE_LOCATION).next(sample.address())
            if (xrLocateSpace(space, referenceSpace, predictedTime, location) == XR_SUCCESS) {
                state.update(location, sample.time())
            }
        }
    }

fun close()

Destroy the space before the containing action set/session. The action set owns the action.

    fun close() {
        state.invalidate()
        xrDestroySpace(space)
    }

companion object

    companion object {

fun create(session: XrSession, systemId: Long, actionSet: XrActionSet): ExtEyeGazeInteraction?

Must run before xrAttachSessionActionSets; gaze has no hand subaction paths.

body — 61 lines, from line 54
        fun create(session: XrSession, systemId: Long, actionSet: XrActionSet): ExtEyeGazeInteraction? {
            val instance = session.instance
            if (!instance.capabilities.XR_EXT_eye_gaze_interaction) return null
            MemoryStack.stackPush().use { stack ->
                val eyeProperties = XrSystemEyeGazeInteractionPropertiesEXT.calloc(stack)
                    .type(XR_TYPE_SYSTEM_EYE_GAZE_INTERACTION_PROPERTIES_EXT)
                val properties = XrSystemProperties.calloc(stack).type(XR_TYPE_SYSTEM_PROPERTIES)
                    .next(eyeProperties.address())
                val propsResult = xrGetSystemProperties(instance, systemId, properties)
                if (propsResult != XR_SUCCESS) {
                    lprintln("warning,openxr", "eye gaze system properties failed: $propsResult")
                    return null
                }
                if (!eyeProperties.supportsEyeGazeInteraction()) return null

                // Resolve paths before creating resources, so partial initialization cannot leak them.
                val profile = stack.callocLong(1)
                val binding = stack.callocLong(1)
                if (xrStringToPath(instance, "/interaction_profiles/ext/eye_gaze_interaction", profile) != XR_SUCCESS ||
                    xrStringToPath(instance, "/user/eyes_ext/input/gaze_ext/pose", binding) != XR_SUCCESS
                ) {
                    lprintln("warning,openxr", "eye gaze binding paths unavailable")
                    return null
                }
                val info = XrActionCreateInfo.calloc(stack).type(XR_TYPE_ACTION_CREATE_INFO)
                    .actionType(XR_ACTION_TYPE_POSE_INPUT)
                    .actionName(stack.UTF8("eye_gaze_pose")).localizedActionName(stack.UTF8("Eye Gaze Pose"))
                val outAction = stack.callocPointer(1)
                var result = xrCreateAction(actionSet, info, outAction)
                if (result != XR_SUCCESS) {
                    lprintln("warning,openxr", "xrCreateAction(eye gaze) failed: $result")
                    return null
                }
                val action = XrAction(outAction[0], actionSet)
                val bindings = XrActionSuggestedBinding.calloc(1, stack)
                bindings[0].action(action).binding(binding[0])
                val suggested = XrInteractionProfileSuggestedBinding.calloc(stack)
                    .type(XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING)
                    .interactionProfile(profile[0]).suggestedBindings(bindings)
                result = xrSuggestInteractionProfileBindings(instance, suggested)
                if (result != XR_SUCCESS) {
                    lprintln("warning,openxr", "eye gaze suggested binding failed: $result")
                    xrDestroyAction(action)
                    return null
                }
                val spaceInfo = XrActionSpaceCreateInfo.calloc(stack).type(XR_TYPE_ACTION_SPACE_CREATE_INFO)
                    .action(action).subactionPath(XR_NULL_PATH)
                spaceInfo.poseInActionSpace().orientation().w(1f)
                val outSpace = stack.callocPointer(1)
                result = xrCreateActionSpace(session, spaceInfo, outSpace)
                if (result != XR_SUCCESS) {
                    lprintln("warning,openxr", "xrCreateActionSpace(eye gaze) failed: $result")
                    xrDestroyAction(action)
                    return null
                }
                lprintln("info,openxr", "EXT eye gaze interaction enabled")
                return ExtEyeGazeInteraction(action, XrSpace(outSpace[0], session))
            }
        }
    }
}