The open source OpenXR runtime

doc: adds docs for future usage & async functions

Part-of: <https://gitlab.freedesktop.org/monado/monado/-/merge_requests/2554>

authored by

Korcan Hussein and committed by
Marge Bot
7e964820 d580fcdf

+200
+200
doc/async-functions-and-futures.md
··· 1 + # OpenXR async functions (XrFutureEXT) 2 + 3 + <!-- 4 + Copyright 2025, Collabora, Ltd. and the Monado contributors 5 + SPDX-License-Identifier: BSL-1.0 6 + --> 7 + 8 + This document describes the extra steps required when implementing OpenXR extensions that expose asynchronous functions returning `XrFutureEXT` in Monado. The overall process is the same as implementing normal extensions (see [implementing-extension](./implementing-extension.md)), but there are a few additional points to keep in mind — mostly around future result types, lifetime management, and IPC support. 9 + 10 + --- 11 + 12 + ## Future result types 13 + 14 + 1. If the future's data result is a struct, the data type must be an **xrt** data-type (typically defined in `xrt_defines.h`). 15 + 2. Register that xrt data-type in `xrt_future_value` (in `xrt_future_value.h`) by adding a new entry to `XRT_FUTURE_VALUE_TYPES[_WITH]`. 16 + 17 + --- 18 + 19 + ## Server-side / driver overview 20 + 21 + OpenXR async functions come in pairs of `xrDoWorkAsync[Suffix]` and `xrDoWorkComplete[Suffix]`. When adding these functions to the server-side/driver (and for IPC) you typically **do not** need to implement the `Complete` function — for simple use-cases where you only need to obtain results, the `Complete` implementation is not required. For more complex scenarios you may still need to implement the `Complete` function. 22 + 23 + --- 24 + 25 + ## Server-side / driver — implementing async callbacks 26 + 27 + 1. Add a callback to the device (for example `xrt_device::create_foo_object_async`). 28 + 29 + - Name the callback data member with a `_async` suffix. 30 + - The last parameter **must** be an output parameter of type `struct xrt_future **`. 31 + 32 + 2. In the implementation that is hooked up to this callback: 33 + 34 + - Create/derive a specific future instance (for example via `u_future_create`). 35 + - `xrt_future` objects are reference-counted for shared access and thread-safe destruction. 36 + - If the asynchronous work runs on a different thread than the callback caller, or if you need a local reference to the future, increment and decrement the reference count when crossing thread/object boundaries using `xrt_future_reference`. 37 + 38 + ### Example callback (driver-side) 39 + 40 + ```cpp 41 + //! simulated driver 42 + static xrt_result_t 43 + simulated_create_foo_object_async(struct xrt_device *xdev, struct xrt_future **out_future) 44 + { 45 + struct simulated_hmd *hmd = simulated_hmd(xdev); 46 + if (hmd == nullptr || out_future == nullptr) { 47 + return XRT_ERROR_ALLOCATION; 48 + } 49 + 50 + struct xrt_future *xft = u_future_create(); 51 + if (xft == nullptr) { 52 + return XRT_ERROR_ALLOCATION; 53 + } 54 + 55 + struct xrt_future *th_xft = NULL; 56 + xrt_future_reference(&xft, xft); 57 + assert(th_xft != nullptr); 58 + 59 + std::thread t([th_xft]() mutable { 60 + using namespace std::chrono_literals; 61 + 62 + bool is_cancel_requested = false; 63 + for (uint32_t x = 0; x < 100; ++x) { 64 + if (xrt_future_is_cancel_requested(th_xft, &is_cancel_requested) != XRT_SUCCESS || 65 + is_cancel_requested) { 66 + U_LOG_I("cancelling work..."); 67 + break; 68 + } 69 + U_LOG_I("doing work..."); 70 + std::this_thread::sleep_for(250ms); 71 + } 72 + 73 + struct xrt_future_result rest; 74 + if (!is_cancel_requested) { 75 + struct xrt_foo foo_bar = { 76 + .foo = 65.f, 77 + .bar = 19, 78 + }; 79 + rest = XRT_FUTURE_RESULT(foo_bar, XRT_SUCCESS); 80 + } 81 + xrt_future_complete(th_xft, &rest); 82 + xrt_future_reference(&th_xft, nullptr); 83 + U_LOG_I("work finished..."); 84 + }); 85 + t.detach(); 86 + 87 + *out_future = xft; 88 + 89 + return XRT_SUCCESS; 90 + } 91 + ``` 92 + 93 + --- 94 + 95 + ## Server-side IPC 96 + 97 + When adding async support to IPC, keep these conventions in mind: 98 + 99 + 1. In `proto.json`: 100 + 101 + - `xrt_future**` output parameters are represented by integer IDs. 102 + - Use `uint32_t` for future IDs. Example: 103 + 104 + ```json 105 + "device_create_foo_object_async": { 106 + "in": [ 107 + {"name": "id", "type": "uint32_t"} 108 + ], 109 + "out": [ 110 + {"name": "out_future_id", "type": "uint32_t"} 111 + ] 112 + } 113 + ``` 114 + 115 + 2. In the server-side handler (`ipc_server_handler.c`): 116 + 117 + - Use `get_new_future_id` to obtain a new future ID. 118 + - Add the newly created `xrt_future` returned from the callback into the client's future list (`struct ipc_client_state::xfts`) using that ID. 119 + 120 + ### Example server handler 121 + 122 + ```c 123 + xrt_result_t 124 + ipc_handle_device_create_foo_object_async(volatile struct ipc_client_state *ics, uint32_t id, uint32_t *out_future_id) 125 + { 126 + struct xrt_device *xdev = NULL; 127 + GET_XDEV_OR_RETURN(ics, id, xdev); 128 + 129 + uint32_t new_future_id; 130 + xrt_result_t xret = get_new_future_id(ics, &new_future_id); 131 + if (xret != XRT_SUCCESS) { 132 + return xret; 133 + } 134 + 135 + struct xrt_future *xft = NULL; 136 + xret = xrt_device_create_foo_object_async(xdev, &xft); 137 + if (xret != XRT_SUCCESS) { 138 + return xret; 139 + } 140 + 141 + assert(xft != NULL); 142 + assert(new_future_id < IPC_MAX_CLIENT_FUTURES); 143 + ics->xfts[new_future_id] = xft; 144 + 145 + *out_future_id = new_future_id; 146 + return XRT_SUCCESS; 147 + } 148 + ``` 149 + 150 + --- 151 + 152 + ## Client-side IPC 153 + 154 + On the client side, create an IPC-backed future using the ID returned by the server; call `ipc_client_future_create` to construct a client-side `xrt_future` wrapper for that ID. 155 + 156 + ### Example client handler 157 + 158 + ```c 159 + static xrt_result_t 160 + ipc_client_xdev_create_foo_object_async(struct xrt_device *xdev, struct xrt_future **out_future) 161 + { 162 + if (xdev == NULL || out_future == NULL) { 163 + return XRT_ERROR_INVALID_ARGUMENT; 164 + } 165 + struct ipc_client_xdev *icx = ipc_client_xdev(xdev); 166 + 167 + uint32_t future_id; 168 + xrt_result_t r = ipc_call_device_create_foo_object_async(icx->ipc_c, icx->device_id, &future_id); 169 + if (r != XRT_SUCCESS) { 170 + IPC_ERROR(icx->ipc_c, "Error sending create_foo_object_async!"); 171 + return r; 172 + } 173 + 174 + struct xrt_future *new_future = ipc_client_future_create(icx->ipc_c, future_id); 175 + if (new_future == NULL) { 176 + return XRT_ERROR_ALLOCATION; 177 + } 178 + 179 + *out_future = new_future; 180 + return XRT_SUCCESS; 181 + } 182 + ``` 183 + 184 + --- 185 + 186 + ## State tracker (OpenXR layer) 187 + 188 + Implementing the OpenXR async function hooks requires implementing both functions in the async pair. In those functions: 189 + 190 + - Use `oxr_future_ext` and the operations declared in `oxr_object.h`. This provides a close 1:1 mapping between OpenXR futures/async functions and Monado's internal future types. 191 + - Note: OpenXR describes `XrFutureEXT` as a new primitive that is neither a handle nor an atom. In Monado, however, `oxr_future_ext` is still represented like other `oxr_*` types (i.e., it behaves like a handle/atom in the internal mapping). 192 + - The lifetime of `oxr_future_ext` is tied to the parent handle passed into `oxr_future_create`. This means: 193 + - You do not need to store the future by value in parent `oxr_` types. 194 + - You *must* ensure the future's parent handle is set correctly. Do not parent the future to `oxr_instance` or `oxr_session` unless that truly represents the future's intended lifetime scope. 195 + 196 + --- 197 + 198 + ## Example / reference 199 + 200 + A full end-to-end example of adding an OpenXR extension with async functions is available [here](https://gitlab.freedesktop.org/korejan/monado/-/commits/korejan/ext_future_example)