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}