ailloli_ui_openxr/target.rs
1//! Legacy/generic `wgpu` RenderTarget adapters for external frame producers.
2//!
3//! These helpers are retained for custom host integrations that already use wgpu.
4//! The OpenXR production path uses `ailloli_ui_render_vulkan` and Vulkan/SPIR-V instead.
5
6use std::sync::{Arc, Mutex};
7
8use ailloli_ui_render::render_target::{PhysicalExtent, RenderFrame, RenderTarget};
9use ailloli_ui_render::RendererError;
10use wgpu::Texture;
11
12use crate::error::{OpenXrHostError, OpenXrHostResult};
13
14/// Backing token carried between acquire/present when the host needs lifecycle hooks
15/// (e.g. a host swapchain image index).
16///
17/// # Examples
18///
19/// ```
20/// use ailloli_ui_openxr::target::FrameToken;
21/// let image_index: FrameToken = 3;
22/// assert_eq!(image_index, 3_u64);
23/// ```
24pub type FrameToken = u64;
25
26/// One acquired host texture together with a token consumed on presentation.
27///
28/// # Examples
29///
30/// ```
31/// use ailloli_ui_openxr::OpenXrImageFrame;
32///
33/// fn recover_token<T>(frame: OpenXrImageFrame<T>) -> T {
34/// frame.token
35/// }
36/// # let _ = recover_token::<u64>;
37/// ```
38#[derive(Debug)]
39pub struct OpenXrImageFrame<T> {
40 /// Writable texture owned by the acquired frame.
41 pub texture: Texture,
42 /// Host token returned unchanged to [`OpenXrImageSource::present`].
43 pub token: T,
44}
45
46/// Abstraction over external wgpu frame providers (mock backends,
47/// render-to-texture pipelines, tests).
48///
49/// Implementations must not hand the same image to overlapping acquisitions.
50/// A successful [`Self::acquire`] must be paired with exactly one
51/// [`Self::present`] using the returned token.
52///
53/// # Examples
54///
55/// ```no_run
56/// use ailloli_ui_openxr::OpenXrImageSource;
57///
58/// fn inspect<S: OpenXrImageSource>(source: &S) {
59/// let _size = source.size();
60/// let _format: S::TextureFormat = source.format();
61/// }
62/// ```
63pub trait OpenXrImageSource {
64 /// Token that identifies an acquired image until presentation.
65 ///
66 /// # Examples
67 ///
68 /// ```
69 /// use ailloli_ui_openxr::OpenXrImageSource;
70 /// fn require_clone<S: OpenXrImageSource>() where S::FrameToken: Clone {}
71 /// ```
72 type FrameToken: Clone + Send + 'static;
73 /// Backend texture-format value returned by [`Self::format`].
74 ///
75 /// # Examples
76 ///
77 /// ```
78 /// use ailloli_ui_openxr::OpenXrImageSource;
79 /// fn require_send<S: OpenXrImageSource>() where S::TextureFormat: Send {}
80 /// ```
81 type TextureFormat: Send + 'static;
82
83 /// Returns the physical pixel extent of future acquired textures.
84 ///
85 /// # Examples
86 ///
87 /// ```no_run
88 /// use ailloli_ui_openxr::OpenXrImageSource;
89 /// fn width<S: OpenXrImageSource>(source: &S) -> u32 { source.size().width }
90 /// ```
91 fn size(&self) -> PhysicalExtent;
92
93 /// Returns the format of future acquired textures.
94 ///
95 /// # Examples
96 ///
97 /// ```no_run
98 /// use ailloli_ui_openxr::OpenXrImageSource;
99 /// fn format<S: OpenXrImageSource>(source: &S) -> S::TextureFormat { source.format() }
100 /// ```
101 fn format(&self) -> Self::TextureFormat;
102
103 /// Acquires one writable texture and its presentation token.
104 ///
105 /// # Errors
106 ///
107 /// Returns a host error when no image is available or acquisition fails.
108 ///
109 /// # Examples
110 ///
111 /// ```no_run
112 /// use ailloli_ui_openxr::{OpenXrHostResult, OpenXrImageFrame, OpenXrImageSource};
113 /// fn acquire<S: OpenXrImageSource>(source: &S) -> OpenXrHostResult<OpenXrImageFrame<S::FrameToken>> { source.acquire() }
114 /// ```
115 fn acquire(&self) -> OpenXrHostResult<OpenXrImageFrame<Self::FrameToken>>;
116
117 /// Presents or releases the image identified by `token`.
118 ///
119 /// # Errors
120 ///
121 /// Returns a host error if the token cannot be submitted or released.
122 ///
123 /// # Examples
124 ///
125 /// ```no_run
126 /// use ailloli_ui_openxr::{OpenXrHostResult, OpenXrImageSource};
127 /// fn present<S: OpenXrImageSource>(source: &S, token: S::FrameToken) -> OpenXrHostResult<()> { source.present(token) }
128 /// ```
129 fn present(&self, token: Self::FrameToken) -> OpenXrHostResult<()>;
130
131 /// Notifies the source immediately before renderer presentation work.
132 ///
133 /// The default is a no-op. Hosts may use this hook for backend ordering.
134 ///
135 /// # Examples
136 ///
137 /// ```no_run
138 /// use ailloli_ui_openxr::OpenXrImageSource;
139 /// fn notify<S: OpenXrImageSource>(source: &S) { source.pre_present_notify(); }
140 /// ```
141 fn pre_present_notify(&self) {}
142}
143
144/// Legacy wgpu `RenderTarget` backed by a generic `OpenXrImageSource`.
145///
146/// The target serializes source callbacks behind a mutex and converts source
147/// failures into renderer errors. A poisoned mutex reports a conservative 1x1
148/// BGRA target for metadata queries and fails acquisition.
149///
150/// # Examples
151///
152/// ```no_run
153/// use ailloli_ui_openxr::{OpenXrImageSource, OpenXrRenderTarget};
154/// fn accept<S>(target: OpenXrRenderTarget<S>)
155/// where S: OpenXrImageSource<TextureFormat = wgpu::TextureFormat> + Send + 'static
156/// { drop(target); }
157/// ```
158pub struct OpenXrRenderTarget<S>
159where
160 S: OpenXrImageSource<TextureFormat = wgpu::TextureFormat> + Send + 'static,
161{
162 /// Thread-safe adapter around the host-owned image source.
163 source: Arc<dyn SourceProxy<S>>,
164}
165
166impl<S> OpenXrRenderTarget<S>
167where
168 S: OpenXrImageSource<TextureFormat = wgpu::TextureFormat> + Send + 'static,
169{
170 /// Wraps an image source in a renderer-compatible target.
171 ///
172 /// # Examples
173 ///
174 /// ```no_run
175 /// use ailloli_ui_openxr::{CallbackImageSource, OpenXrHostResult, OpenXrImageFrame, OpenXrRenderTarget};
176 /// use ailloli_ui_render::render_target::PhysicalExtent;
177 /// let source = CallbackImageSource::new(
178 /// PhysicalExtent::new(640, 480),
179 /// wgpu::TextureFormat::Bgra8Unorm,
180 /// || -> OpenXrHostResult<OpenXrImageFrame<u64>> { panic!("host callback") },
181 /// |_| Ok(()),
182 /// );
183 /// let _target = OpenXrRenderTarget::new(source);
184 /// ```
185 pub fn new(source: S) -> Self {
186 Self {
187 source: Arc::new(SimpleSourceProxy::new(source)),
188 }
189 }
190
191 /// Replaces the source while preserving the target value used by a renderer.
192 ///
193 /// Any unpresented frame from the old source remains the caller's lifecycle
194 /// responsibility.
195 ///
196 /// # Examples
197 ///
198 /// ```no_run
199 /// use ailloli_ui_openxr::{CallbackImageSource, OpenXrHostResult, OpenXrImageFrame, OpenXrRenderTarget};
200 /// use ailloli_ui_render::render_target::PhysicalExtent;
201 /// fn source() -> CallbackImageSource<impl FnMut() -> OpenXrHostResult<OpenXrImageFrame<u64>> + Send, impl FnMut(u64) -> OpenXrHostResult<()> + Send> {
202 /// CallbackImageSource::new(PhysicalExtent::new(1, 1), wgpu::TextureFormat::Bgra8Unorm, || panic!(), |_| Ok(()))
203 /// }
204 /// let mut target = OpenXrRenderTarget::new(source());
205 /// target.replace_source(source());
206 /// ```
207 pub fn replace_source(&mut self, source: S) {
208 self.source = Arc::new(SimpleSourceProxy::new(source));
209 }
210}
211
212/// Object-safe synchronization boundary around an image source.
213trait SourceProxy<S: OpenXrImageSource<TextureFormat = wgpu::TextureFormat>>: Send + Sync {
214 /// Reports the current physical extent.
215 fn size(&self) -> PhysicalExtent;
216 /// Reports the current wgpu texture format.
217 fn format(&self) -> wgpu::TextureFormat;
218 /// Forwards the source's pre-presentation hook.
219 fn pre_present_notify(&self);
220 /// Acquires a frame and erases the structured host error to text.
221 ///
222 /// # Errors
223 ///
224 /// Returns the source acquisition error converted to its display string, or
225 /// a synchronization error when the proxy cannot lock its source.
226 fn acquire(&self) -> Result<OpenXrImageFrame<S::FrameToken>, String>;
227 /// Presents a token and erases the structured host error to text.
228 ///
229 /// # Errors
230 ///
231 /// Returns the source presentation error converted to its display string, or
232 /// a synchronization error when the proxy cannot lock its source.
233 fn present(&self, token: S::FrameToken) -> Result<(), String>;
234}
235
236/// Mutex-backed [`SourceProxy`] used by [`OpenXrRenderTarget`].
237struct SimpleSourceProxy<S: OpenXrImageSource<TextureFormat = wgpu::TextureFormat>> {
238 /// Serialized image source.
239 inner: Mutex<S>,
240}
241
242impl<S: OpenXrImageSource<TextureFormat = wgpu::TextureFormat>> SimpleSourceProxy<S> {
243 /// Creates a synchronized proxy around `inner`.
244 fn new(inner: S) -> Self {
245 Self {
246 inner: Mutex::new(inner),
247 }
248 }
249}
250
251impl<S> SourceProxy<S> for SimpleSourceProxy<S>
252where
253 S: OpenXrImageSource<TextureFormat = wgpu::TextureFormat> + Send + 'static,
254{
255 fn size(&self) -> PhysicalExtent {
256 self.inner
257 .lock()
258 .map(|s| s.size())
259 .unwrap_or(PhysicalExtent::new(1, 1))
260 }
261
262 fn format(&self) -> wgpu::TextureFormat {
263 self.inner
264 .lock()
265 .map(|s| s.format())
266 .unwrap_or(wgpu::TextureFormat::Bgra8Unorm)
267 }
268
269 fn pre_present_notify(&self) {
270 if let Ok(source) = self.inner.lock() {
271 source.pre_present_notify();
272 }
273 }
274
275 fn acquire(&self) -> Result<OpenXrImageFrame<S::FrameToken>, String> {
276 self.inner
277 .lock()
278 .map_err(|_| "source mutex poisoned".to_string())?
279 .acquire()
280 .map_err(|err| err.to_string())
281 }
282
283 fn present(&self, token: S::FrameToken) -> Result<(), String> {
284 self.inner
285 .lock()
286 .map_err(|_| "source mutex poisoned".to_string())
287 .and_then(|source| source.present(token).map_err(|err| err.to_string()))
288 }
289}
290
291impl<S> RenderTarget for OpenXrRenderTarget<S>
292where
293 S: OpenXrImageSource<TextureFormat = wgpu::TextureFormat> + Send + 'static,
294{
295 fn size(&self) -> PhysicalExtent {
296 self.source.size()
297 }
298
299 fn format(&self) -> wgpu::TextureFormat {
300 self.source.format()
301 }
302
303 fn acquire_frame(&mut self) -> Result<RenderFrame, RendererError> {
304 let source = Arc::clone(&self.source);
305 let source_frame = source.acquire().map_err(|err| {
306 RendererError::SurfaceAcquireFailed(format!("openxr source acquire failed: {err}"))
307 })?;
308 let view = source_frame
309 .texture
310 .create_view(&wgpu::TextureViewDescriptor::default());
311 let size = self.size();
312 let format = self.format();
313
314 let source = Arc::clone(&source);
315 let present_token = source_frame.token.clone();
316 let present = Some(Box::new(move || {
317 if let Err(err) = source.present(present_token.clone()) {
318 eprintln!("ailloli_ui_openxr: present callback failed: {err}");
319 }
320 }) as Box<dyn FnOnce()>);
321
322 Ok(RenderFrame::from_texture(
323 view,
324 source_frame.texture,
325 size,
326 format,
327 present,
328 ))
329 }
330
331 fn pre_present_notify(&self) {
332 self.source.pre_present_notify();
333 }
334}
335
336/// Convenience source helper for custom runtime glue.
337///
338/// Both callbacks are serialized independently. Callback failures are returned
339/// to the target; a poisoned callback mutex is converted to the matching host
340/// error variant.
341///
342/// # Examples
343///
344/// ```no_run
345/// use ailloli_ui_openxr::{CallbackImageSource, OpenXrHostResult, OpenXrImageFrame};
346/// use ailloli_ui_render::render_target::PhysicalExtent;
347/// let _source = CallbackImageSource::new(
348/// PhysicalExtent::new(1024, 1024),
349/// wgpu::TextureFormat::Rgba8Unorm,
350/// || -> OpenXrHostResult<OpenXrImageFrame<u64>> { panic!("provided by host") },
351/// |_token| Ok(()),
352/// );
353/// ```
354pub struct CallbackImageSource<Fa, Fp>
355where
356 Fa: FnMut() -> OpenXrHostResult<OpenXrImageFrame<FrameToken>> + Send,
357 Fp: FnMut(FrameToken) -> OpenXrHostResult<()> + Send,
358{
359 /// Physical width and height of every acquired image.
360 size: PhysicalExtent,
361 /// WGPU texture format shared by acquired images.
362 format: wgpu::TextureFormat,
363 /// Serialized callback that acquires the next host image.
364 acquire: Arc<Mutex<Fa>>,
365 /// Serialized callback that releases or presents an acquired image.
366 present: Arc<Mutex<Fp>>,
367}
368
369impl<Fa, Fp> CallbackImageSource<Fa, Fp>
370where
371 Fa: FnMut() -> OpenXrHostResult<OpenXrImageFrame<FrameToken>> + Send,
372 Fp: FnMut(FrameToken) -> OpenXrHostResult<()> + Send,
373{
374 /// Creates a callback source with fixed physical size and texture format.
375 ///
376 /// The callbacks are not invoked by this constructor. `acquire` must return
377 /// a texture matching `size` and `format`; `present` receives its token.
378 ///
379 /// # Examples
380 ///
381 /// ```no_run
382 /// use ailloli_ui_openxr::{CallbackImageSource, OpenXrHostResult, OpenXrImageFrame};
383 /// use ailloli_ui_render::render_target::PhysicalExtent;
384 /// let source = CallbackImageSource::new(
385 /// PhysicalExtent::new(800, 600),
386 /// wgpu::TextureFormat::Bgra8Unorm,
387 /// || -> OpenXrHostResult<OpenXrImageFrame<u64>> { panic!() },
388 /// |_token| Ok(()),
389 /// );
390 /// assert_eq!(ailloli_ui_openxr::OpenXrImageSource::size(&source).width, 800);
391 /// ```
392 pub fn new(
393 size: PhysicalExtent,
394 format: wgpu::TextureFormat,
395 acquire: Fa,
396 present: Fp,
397 ) -> Self {
398 Self {
399 size,
400 format,
401 acquire: Arc::new(Mutex::new(acquire)),
402 present: Arc::new(Mutex::new(present)),
403 }
404 }
405}
406
407impl<Fa, Fp> OpenXrImageSource for CallbackImageSource<Fa, Fp>
408where
409 Fa: FnMut() -> OpenXrHostResult<OpenXrImageFrame<FrameToken>> + Send,
410 Fp: FnMut(FrameToken) -> OpenXrHostResult<()> + Send,
411{
412 type FrameToken = FrameToken;
413 type TextureFormat = wgpu::TextureFormat;
414
415 fn size(&self) -> PhysicalExtent {
416 self.size
417 }
418
419 fn format(&self) -> wgpu::TextureFormat {
420 self.format
421 }
422
423 fn acquire(&self) -> OpenXrHostResult<OpenXrImageFrame<Self::FrameToken>> {
424 let mut acquire = self.acquire.lock().map_err(|_| {
425 OpenXrHostError::FrameSourceUnavailable("callback acquire mutex poisoned".into())
426 })?;
427 (&mut *acquire)()
428 }
429
430 fn present(&self, token: Self::FrameToken) -> OpenXrHostResult<()> {
431 let mut present = self.present.lock().map_err(|_| {
432 OpenXrHostError::PresentFailed("callback present mutex poisoned".into())
433 })?;
434 (&mut *present)(token)
435 }
436}
437
438/// Construct a callback-backed target in one line for host/runtime integrations.
439///
440/// # Examples
441///
442/// ```no_run
443/// use ailloli_ui_openxr::{build_callback_source, OpenXrHostResult, OpenXrImageFrame};
444/// use ailloli_ui_render::render_target::PhysicalExtent;
445/// let _target = build_callback_source(
446/// PhysicalExtent::new(1280, 720),
447/// wgpu::TextureFormat::Bgra8Unorm,
448/// || -> OpenXrHostResult<OpenXrImageFrame<u64>> { panic!("host acquires") },
449/// |_token| Ok(()),
450/// );
451/// ```
452pub fn build_callback_source<Fa, Fp>(
453 size: PhysicalExtent,
454 format: wgpu::TextureFormat,
455 acquire: Fa,
456 present: Fp,
457) -> OpenXrRenderTarget<CallbackImageSource<Fa, Fp>>
458where
459 Fa: FnMut() -> OpenXrHostResult<OpenXrImageFrame<FrameToken>> + Send + 'static,
460 Fp: FnMut(FrameToken) -> OpenXrHostResult<()> + Send + 'static,
461{
462 OpenXrRenderTarget::new(CallbackImageSource::new(size, format, acquire, present))
463}