Skip to content

File plugin.hpp

File List > plugins > quest3 > unity > plugin.hpp

Go to the documentation of this file

#pragma once

#include "illixr/data_format/query_response.hpp"
#include "illixr/data_format/semantics.hpp"
#include "illixr/data_format/voice_query.hpp"
#include "illixr/phonebook.hpp"
#include "illixr/plugin.hpp"
#include "illixr/switchboard.hpp"
#include "illixr/threadloop.hpp"
// clang-format off

#include "ndk_encoder.hpp"

#include 
#include 
#include 
// clang-format on
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

namespace ILLIXR {

class xr_sensor_capture : public threadloop {
public:
    [[maybe_unused]] xr_sensor_capture(const std::string& name, phonebook* pb);
    ~xr_sensor_capture() override;

    // Called from Unity's LateUpdate() via illixr_acquire_depth() in ILLIXRBridge.
    // Must be called between xrBeginFrame and xrEndFrame (i.e. during Unity's frame).
    // Also samples the current head pose via xrLocateSpace(VIEW, LOCAL) and stores
    // it in latest_head_pose_ for on_capture_completed() to snapshot.
    void acquire_depth_unity_thread(int64_t predicted_display_time_ns, double ovr_plugin_time_sec,
                                    const float* rgb_camera_pose_lh, // Unity world space LH, 16 floats row-major
                                    const float* head_pose_lh,       // Unity world space LH, 16 floats row-major
                                    const float* tracking_to_world_lh);
    // Public so on_render_event callback can call them from outside the class.
    bool init_vulkan();
    void destroy_vulkan();

    // Set from UnityPluginLoad - must be called before the plugin constructs.
    static void set_unity_interfaces(IUnityInterfaces* interfaces);

    // Public so UnityPluginLoad/UnityPluginUnload free functions and the
    // on_render_event callback can access them from outside the class.
    static IUnityInterfaces*     s_unity_interfaces_;
    static IUnityGraphicsVulkan* s_vk_interface_;

    // Called from render thread via GL.IssuePluginEvent(EVENT_ACQUIRE).
    // Submits the Vulkan cmd buffer prepared by acquire_depth_unity_thread().
    void submit_depth_readback();

    // Called from LateUpdate() on the main thread immediately after
    // GL.IssuePluginEvent(EVENT_ACQUIRE) returns. Releases the depth
    // image back to the OpenXR runtime before xrEndFrame.
    // Must be called on the main thread - same constraint as xrBeginFrame.
    void release_depth_after_submit();

    // ---- Latest head pose (written by acquire_depth_unity_thread, read by
    //      on_capture_completed) ----
    //
    // acquire_depth_unity_thread() runs on Unity's main thread at 90Hz during
    // an active XR frame - the only valid context for xrLocateSpace with VIEW
    // space on Quest 3. It stores the current pose here. on_capture_completed()
    // snapshots this pose into capture_result_cache_ at sensor exposure time.
    struct latest_head_pose {
        float pose[16] = {};
        bool  valid    = false;
    };

    mutable std::mutex latest_head_pose_mutex_;
    latest_head_pose   latest_head_pose_;

    // ---- Per-frame capture result (written by on_capture_completed,
    //      read by _p_one_iteration) ----
    //
    // on_capture_completed() fires on the Camera2 callback thread immediately
    // after each sensor exposure. It snapshots latest_head_pose_ and the current
    // XrTime into this ring buffer. _p_one_iteration() picks the most recent
    // valid entry and attaches its pose to the outgoing semantic_frame.
    struct capture_result {
        float  pose[16]     = {};
        XrTime capture_time = 0;
        bool   valid        = false;
    };

    static constexpr size_t CAPTURE_RESULT_CACHE_SIZE = 16;

    // Public so the static on_capture_completed callback can write to them.
    mutable std::mutex                                    capture_result_mutex_;
    std::array<capture_result, CAPTURE_RESULT_CACHE_SIZE> capture_result_cache_;
    size_t                                                capture_result_next_ = 0;

    // Clock offset: CLOCK_BOOTTIME - CLOCK_MONOTONIC, computed at construction.
    // Added to MediaCodec presentationTimeUs (MONOTONIC-based) to get XrTime.
    int64_t clock_offset_ns_;
    double  ovr_time_offset_sec_ = 0.0; // OVRPlugin_time - boottime_sec

protected:
    void        _p_one_iteration() override;
    skip_option _p_should_skip() override;

private:
    // ---- Configuration ----
    // Set to true when any init step fails - no-ops _p_should_skip.
    bool init_failed_ = false;

    // ---- Timing ----
    int64_t tick_interval_ns_ = 500'000'000LL; // default 2fps; overridden in constructor
    int64_t last_tick_ns_     = 0;

    // ---- OpenXR ----
    bool init_openxr();
    void destroy_openxr();

    XrInstance xr_instance_ = XR_NULL_HANDLE;
    XrSession  xr_session_  = XR_NULL_HANDLE;
    XrSpace    local_space_ = XR_NULL_HANDLE;
    bool       owns_xr_     = false;

    PFN_xrCreateEnvironmentDepthProviderMETA           xr_create_depth_provider_   = nullptr;
    PFN_xrDestroyEnvironmentDepthProviderMETA          xr_destroy_depth_provider_  = nullptr;
    PFN_xrStartEnvironmentDepthProviderMETA            xr_start_depth_provider_    = nullptr;
    PFN_xrCreateEnvironmentDepthSwapchainMETA          xr_create_depth_swapchain_  = nullptr;
    PFN_xrDestroyEnvironmentDepthSwapchainMETA         xr_destroy_depth_swapchain_ = nullptr;
    PFN_xrEnumerateEnvironmentDepthSwapchainImagesMETA xr_enum_depth_images_       = nullptr;
    PFN_xrGetEnvironmentDepthSwapchainStateMETA        xr_get_depth_state_         = nullptr;
    PFN_xrAcquireEnvironmentDepthImageMETA             xr_acquire_depth_image_     = nullptr;
    // PFN_xrReleaseEnvironmentDepthImageMETA takes only the provider handle.
    using xr_release_depth_fn                   = XrResult(XRAPI_PTR*)(XrEnvironmentDepthProviderMETA);
    xr_release_depth_fn xr_release_depth_image_ = nullptr;

    bool needs_depth_release_ = false; // true after acquire, until release

    XrEnvironmentDepthProviderMETA  depth_provider_         = XR_NULL_HANDLE;
    XrEnvironmentDepthSwapchainMETA depth_swapchain_        = XR_NULL_HANDLE;
    bool                            depth_ext_available_    = false;
    int32_t                         depth_swapchain_width_  = 0;
    int32_t                         depth_swapchain_height_ = 0;

    // Vulkan swapchain images - VkImage per slot, populated at swapchain creation.
    std::vector<VkImage> depth_vk_images_;

    VkDevice         vk_device_       = VK_NULL_HANDLE;
    VkPhysicalDevice vk_physical_     = VK_NULL_HANDLE;
    VkQueue          vk_queue_        = VK_NULL_HANDLE;
    uint32_t         vk_queue_family_ = 0;
    VkCommandPool    vk_cmd_pool_     = VK_NULL_HANDLE;
    VkCommandBuffer  vk_cmd_buf_      = VK_NULL_HANDLE;
    VkFence          vk_fence_        = VK_NULL_HANDLE;

    // Host-visible staging buffer - sized for one full depth frame.
    VkBuffer       vk_staging_buf_  = VK_NULL_HANDLE;
    VkDeviceMemory vk_staging_mem_  = VK_NULL_HANDLE;
    VkDeviceSize   vk_staging_size_ = 0;

    // Find a memory type index satisfying required property flags.
    uint32_t find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags props) const;

    // ---- Depth cache (written by Unity thread, read by threadloop) ----
    //
    // acquire_depth_unity_thread() is called every LateUpdate tick (~90Hz on
    // Quest 3) but depth frames are only stored every DEPTH_ACQUIRE_EVERY calls
    // (~10fps). _p_one_iteration() picks the entry whose timestamp is closest
    // to each encoded RGB frame.
    static constexpr int    DEPTH_ACQUIRE_EVERY = 9;  // ~10fps at 90Hz
    static constexpr size_t DEPTH_CACHE_SIZE    = 16; // ~1.6s of history

    struct depth_frame_data {
        std::vector<uint8_t>           data; // R16F, 2 bytes/pixel, top-down
        data_format::camera_intrinsics intrinsics;
        float                          near_z    = 0.f;
        float                          far_z     = 0.f;
        float                          pose[16]  = {};
        XrTime                         timestamp = 0;
        bool                           valid     = false;
    };

    // Returns the cached entry with the smallest |timestamp - rgb_ts|,
    // or nullptr if the cache is empty.
    const depth_frame_data* find_closest_depth(XrTime rgb_ts) const;

    mutable std::mutex                             depth_mutex_;
    std::array<depth_frame_data, DEPTH_CACHE_SIZE> depth_cache_;
    size_t                                         depth_cache_next_      = 0;
    int                                            depth_acquire_counter_ = 0;

    // Pending Vulkan readback: set by acquire_depth_unity_thread() on the
    // main thread, consumed by submit_depth_readback() on the render thread.
    struct pending_readback {
        VkImage                        image  = VK_NULL_HANDLE;
        int32_t                        width  = 0;
        int32_t                        height = 0;
        data_format::camera_intrinsics intrinsics;
        float                          near_z    = 0.f;
        float                          far_z     = 0.f;
        float                          pose[16]  = {};
        XrTime                         timestamp = 0;
        bool                           valid     = false;
    };

    std::mutex       pending_readback_mutex_;
    pending_readback pending_readback_;

    // ---- Camera2 RGB ----
    bool init_camera();
    void destroy_camera();

    ACameraManager*                 camera_mgr_               = nullptr;
    ACameraDevice*                  camera_device_            = nullptr;
    ACameraCaptureSession*          capture_session_          = nullptr;
    ACaptureRequest*                capture_request_          = nullptr;
    ACameraOutputTarget*            camera_output_target_     = nullptr;
    ACaptureSessionOutputContainer* session_output_container_ = nullptr;
    ACaptureSessionOutput*          session_output_           = nullptr;

    data_format::camera_intrinsics rgb_intrinsics_;
    bool                           rgb_intrinsics_valid_ = false;

    // ---- Switchboard output ----
    const std::shared_ptr<switchboard>                       switchboard_;
    switchboard::network_writer<data_format::semantic_frame> writer_;
    int32_t                                                  frame_number_ = 0;
    float                                                    max_depth_m_  = 0.f;

    std::unique_ptr<ndk_encoder> encoder_;
};

} // namespace ILLIXR