ailloli_ui_render_wgpu/
render_target.rs

1//! Host-independent targets for managed presentation or borrowed command recording.
2
3use crate::error::RendererError;
4use crate::pipeline_cache::ResizeOutcome;
5
6/// A host-owned, single-sampled color target borrowed for UI recording.
7///
8/// Use this with [`crate::Renderer::record_layered_to_target_scaled`] when the
9/// application already owns the image and command encoder. Unlike [`RenderFrame`],
10/// this descriptor transfers no ownership and carries no presentation callback.
11/// The host acquires, submits, and presents the image itself.
12///
13/// # Attachment contract
14///
15/// - The view and encoder belong to [`crate::Renderer::device`].
16/// - The view is `D2`, covers mip zero only and array layer zero only, and uses
17///   a single-sampled color texture with `RENDER_ATTACHMENT` usage.
18/// - `size` is the full physical-pixel extent, not a logical size or a crop.
19///   Both axes are nonzero and within the device's `max_texture_dimension_2d`.
20/// - `format` matches the renderer's pipeline format. Format reinterpretation
21///   through a differently formatted view is not supported by this contract.
22/// - If supplied, `texture` backs exactly this view, with matching dimensions
23///   and format. It is a single-layer `D2` image, not a multisampled attachment.
24///
25/// Wgpu 0.20 does not expose a view's descriptor or backing texture for inspection.
26/// The caller must attest the view metadata and identity. The recorder checks the
27/// supplied extent, format and texture descriptor, but cannot prove that the view
28/// refers to that texture or that all handles share a device. Wgpu validates
29/// invalid GPU bindings separately; such failures are not typed recorder errors.
30///
31/// # Examples
32///
33/// ```no_run
34/// use ailloli_ui_render_wgpu::{BorrowedRenderTarget, PhysicalExtent};
35/// fn describe<'a>(
36///     texture: &'a wgpu::Texture,
37///     view: &'a wgpu::TextureView,
38/// ) -> BorrowedRenderTarget<'a> {
39///     BorrowedRenderTarget {
40///         view,
41///         texture: Some(texture),
42///         size: PhysicalExtent::new(texture.width(), texture.height()),
43///         format: texture.format(),
44///     }
45/// }
46/// ```
47#[derive(Debug, Clone, Copy)]
48pub struct BorrowedRenderTarget<'a> {
49    /// Full-size destination color view, on the renderer's device.
50    pub view: &'a wgpu::TextureView,
51    /// Optional backing image for destination-dependent effects.
52    ///
53    /// `None` is sufficient for ordinary UI and foreground blur. Backdrop blur,
54    /// `Multiply`, and `Screen` blends require `Some` with `COPY_SRC` usage.
55    /// The recorder never acquires, owns, or presents this texture.
56    pub texture: Option<&'a wgpu::Texture>,
57    /// Full physical dimensions; both axes must be nonzero.
58    pub size: PhysicalExtent,
59    /// Attachment format, exactly matching the renderer's pipeline format.
60    pub format: wgpu::TextureFormat,
61}
62
63/// How the UI recorder treats colors already present in the host target.
64///
65/// Neither policy submits GPU work. It takes effect when the host submits the
66/// encoder. Use `Load` after host drawing; use `Clear` for standalone UI or to
67/// intentionally replace the host background. The policy covers the full target,
68/// independently of layer clips and whether the layer list is empty.
69///
70/// # Examples
71///
72/// ```
73/// use ailloli_ui_core::Color;
74/// use ailloli_ui_render_wgpu::TargetLoadOp;
75/// let overlay = TargetLoadOp::Load;
76/// let standalone = TargetLoadOp::Clear(Color::BLACK);
77/// assert_ne!(overlay, standalone);
78/// ```
79#[derive(Debug, Clone, Copy, PartialEq)]
80pub enum TargetLoadOp {
81    /// Preserve the initialized destination, including when the UI has no layers.
82    ///
83    /// Initialize it in an earlier submitted command or an earlier pass in the
84    /// same encoder. Backdrop effects and destination blends read that content.
85    Load,
86    /// Clear the full attachment before destination reads or UI drawing.
87    ///
88    /// The clear still runs for empty UI or a backdrop as the first layer. The
89    /// color follows the same conventions as [`crate::Renderer::render_layered`].
90    Clear(ailloli_ui_core::Color),
91}
92
93/// Physical pixel dimensions of a render destination.
94///
95/// This renderer-local type keeps presentation adapters such as winit or
96/// OpenXR outside the render-target contract. Hosts convert their native size
97/// type at the adapter boundary.
98///
99/// # Examples
100///
101/// ```
102/// use ailloli_ui_render_wgpu::PhysicalExtent;
103/// let extent = PhysicalExtent { width: 800, height: 600 };
104/// assert_eq!(extent, PhysicalExtent::new(800, 600));
105/// ```
106#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
107pub struct PhysicalExtent {
108    /// Width in physical device pixels.
109    pub width: u32,
110    /// Height in physical device pixels.
111    pub height: u32,
112}
113
114impl PhysicalExtent {
115    /// Creates an extent without clamping either axis.
116    ///
117    /// # Examples
118    ///
119    /// ```
120    /// use ailloli_ui_render_wgpu::PhysicalExtent;
121    /// let extent = PhysicalExtent::new(1920, 1080);
122    /// assert_eq!((extent.width, extent.height), (1920, 1080));
123    /// ```
124    pub const fn new(width: u32, height: u32) -> Self {
125        Self { width, height }
126    }
127
128    /// Returns `true` when either axis is zero and no renderable area exists.
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use ailloli_ui_render_wgpu::PhysicalExtent;
134    /// assert!(PhysicalExtent::new(0, 10).is_zero());
135    /// assert!(!PhysicalExtent::new(1, 1).is_zero());
136    /// ```
137    pub const fn is_zero(self) -> bool {
138        self.width == 0 || self.height == 0
139    }
140}
141
142/// One acquired render destination from a host target.
143///
144/// # Examples
145///
146/// ```no_run
147/// use ailloli_ui_render_wgpu::RenderFrame;
148/// fn inspect(frame: &RenderFrame) {
149///     let _: u32 = frame.size.width;
150///     let _: wgpu::TextureFormat = frame.format;
151/// }
152/// ```
153pub struct RenderFrame {
154    /// Texture view bound to the target image.
155    pub view: wgpu::TextureView,
156    /// Owned source keeping the view's texture alive through submission.
157    source: RenderFrameSource,
158    /// Active frame dimensions.
159    pub size: PhysicalExtent,
160    /// Output format for this frame.
161    pub format: wgpu::TextureFormat,
162    /// Optional target-specific callback executed before source presentation.
163    present: Option<Box<dyn FnOnce()>>, // no-op for targets that do not need explicit present.
164}
165
166#[allow(dead_code)]
167/// Ownership form backing a frame's texture view and presentation behavior.
168enum RenderFrameSource {
169    /// Surface-backed texture that must be explicitly presented.
170    Surface(wgpu::SurfaceTexture),
171    /// Owned render texture for host-provided targets.
172    Texture(wgpu::Texture),
173    /// Target implementation only has a view (no copy-back source available).
174    Unknown,
175}
176
177impl RenderFrameSource {
178    /// Returns the copyable underlying texture when the ownership form exposes one.
179    fn as_texture(&self) -> Option<&wgpu::Texture> {
180        match self {
181            Self::Surface(frame) => Some(&frame.texture),
182            Self::Texture(texture) => Some(texture),
183            Self::Unknown => None,
184        }
185    }
186
187    /// Presents a surface texture and otherwise drops the owned source.
188    fn present(self) {
189        if let Self::Surface(frame) = self {
190            frame.present();
191        }
192    }
193}
194
195impl RenderFrame {
196    /// Packages one acquired target view with its ownership and present callback.
197    fn new(
198        view: wgpu::TextureView,
199        source: RenderFrameSource,
200        size: PhysicalExtent,
201        format: wgpu::TextureFormat,
202        present: Option<Box<dyn FnOnce()>>,
203    ) -> Self {
204        Self {
205            view,
206            source,
207            size,
208            format,
209            present,
210        }
211    }
212
213    /// Returns the backing texture for effects that require reads or copies.
214    ///
215    /// View-only targets return `None`; surface and owned-texture frames return
216    /// `Some` until the frame is consumed by [`Self::present`].
217    ///
218    /// # Examples
219    ///
220    /// ```no_run
221    /// use ailloli_ui_render_wgpu::RenderFrame;
222    /// fn can_capture(frame: &RenderFrame) -> bool { frame.texture().is_some() }
223    /// ```
224    pub fn texture(&self) -> Option<&wgpu::Texture> {
225        self.source.as_texture()
226    }
227
228    #[allow(dead_code)]
229    /// Wraps a view-only target with no copyable backing texture.
230    ///
231    /// # Examples
232    ///
233    /// ```no_run
234    /// // Used internally when a host can supply only a TextureView.
235    /// assert!(true);
236    /// ```
237    pub(crate) fn from_unknown_view(
238        view: wgpu::TextureView,
239        size: PhysicalExtent,
240        format: wgpu::TextureFormat,
241        present: Option<Box<dyn FnOnce()>>,
242    ) -> Self {
243        Self::new(view, RenderFrameSource::Unknown, size, format, present)
244    }
245
246    /// Wraps an acquired surface texture and arranges explicit presentation.
247    ///
248    /// # Examples
249    ///
250    /// ```no_run
251    /// // The surface adapter calls this after acquiring a wgpu SurfaceTexture.
252    /// assert!(true);
253    /// ```
254    pub(crate) fn from_surface_texture(
255        view: wgpu::TextureView,
256        frame: wgpu::SurfaceTexture,
257        size: PhysicalExtent,
258        format: wgpu::TextureFormat,
259        present: Option<Box<dyn FnOnce()>>,
260    ) -> Self {
261        Self::new(
262            view,
263            RenderFrameSource::Surface(frame),
264            size,
265            format,
266            present,
267        )
268    }
269
270    /// Wraps a host-owned texture and its view as a render frame.
271    ///
272    /// `present` is an optional one-shot host callback invoked after consuming
273    /// the texture source.
274    ///
275    /// # Examples
276    ///
277    /// ```no_run
278    /// use ailloli_ui_render_wgpu::{PhysicalExtent, RenderFrame};
279    /// fn wrap(view: wgpu::TextureView, texture: wgpu::Texture) -> RenderFrame {
280    ///     RenderFrame::from_texture(view, texture, PhysicalExtent::new(64, 64),
281    ///         wgpu::TextureFormat::Rgba8Unorm, None)
282    /// }
283    /// ```
284    pub fn from_texture(
285        view: wgpu::TextureView,
286        texture: wgpu::Texture,
287        size: PhysicalExtent,
288        format: wgpu::TextureFormat,
289        present: Option<Box<dyn FnOnce()>>,
290    ) -> Self {
291        Self::new(
292            view,
293            RenderFrameSource::Texture(texture),
294            size,
295            format,
296            present,
297        )
298    }
299
300    /// Present the acquired frame to its consumer.
301    ///
302    /// Surface frames call `SurfaceTexture::present` first, then the optional
303    /// host callback exactly once. Owned and view-only frames run only the
304    /// callback.
305    ///
306    /// # Examples
307    ///
308    /// ```no_run
309    /// use ailloli_ui_render_wgpu::RenderFrame;
310    /// fn finish(frame: RenderFrame) { frame.present(); }
311    /// ```
312    pub fn present(mut self) {
313        self.source.present();
314        if let Some(present) = self.present.take() {
315            present();
316        }
317    }
318}
319
320/// Host-agnostic render target that owns swapchain-like image acquisition.
321///
322/// Sizes are physical pixels and formats must match the renderer pipelines.
323/// A target may defer resizing by returning [`ResizeOutcome::Deferred`].
324///
325/// # Examples
326///
327/// ```no_run
328/// use ailloli_ui_render_wgpu::{PhysicalExtent, RenderTarget};
329/// fn dimensions(target: &impl RenderTarget) -> PhysicalExtent { target.size() }
330/// ```
331pub trait RenderTarget {
332    /// Returns the current physical-pixel extent.
333    ///
334    /// # Examples
335    ///
336    /// ```no_run
337    /// use ailloli_ui_render_wgpu::RenderTarget;
338    /// fn width(target: &impl RenderTarget) -> u32 { target.size().width }
339    /// ```
340    fn size(&self) -> PhysicalExtent;
341
342    /// Returns the texture format of subsequently acquired frames.
343    ///
344    /// # Examples
345    ///
346    /// ```no_run
347    /// use ailloli_ui_render_wgpu::RenderTarget;
348    /// fn format(target: &impl RenderTarget) -> wgpu::TextureFormat { target.format() }
349    /// ```
350    fn format(&self) -> wgpu::TextureFormat;
351
352    /// Acquires one renderable frame or a typed renderer error.
353    ///
354    /// # Errors
355    ///
356    /// Returns the target's acquisition error, including typed surface timeout,
357    /// loss, outdated, and out-of-memory conditions.
358    ///
359    /// # Examples
360    ///
361    /// ```no_run
362    /// use ailloli_ui_render_wgpu::{RenderFrame, RendererError, RenderTarget};
363    /// fn acquire(target: &mut impl RenderTarget) -> Result<RenderFrame, RendererError> {
364    ///     target.acquire_frame()
365    /// }
366    /// ```
367    fn acquire_frame(&mut self) -> Result<RenderFrame, RendererError>;
368
369    /// Gives the host a chance to notify its window system before presentation.
370    ///
371    /// The default implementation is a no-op.
372    ///
373    /// # Examples
374    ///
375    /// ```no_run
376    /// use ailloli_ui_render_wgpu::RenderTarget;
377    /// fn notify(target: &impl RenderTarget) { target.pre_present_notify(); }
378    /// ```
379    fn pre_present_notify(&self) {}
380
381    /// Requests a new physical-pixel extent.
382    ///
383    /// The default leaves the target unchanged. Implementations may configure
384    /// immediately or return a deferred outcome for a zero-sized surface.
385    ///
386    /// # Errors
387    ///
388    /// Propagates target-specific reconfiguration failures.
389    ///
390    /// # Examples
391    ///
392    /// ```no_run
393    /// use ailloli_ui_render_wgpu::{PhysicalExtent, RenderTarget};
394    /// fn resize(target: &mut impl RenderTarget) {
395    ///     let _ = target.try_resize(PhysicalExtent::new(800, 600));
396    /// }
397    /// ```
398    fn try_resize(&mut self, _size: PhysicalExtent) -> Result<ResizeOutcome, RendererError> {
399        Ok(ResizeOutcome::Unchanged)
400    }
401}
402
403#[cfg(test)]
404/// Verifies physical extent preservation and zero-dimension detection.
405mod tests {
406    use super::PhysicalExtent;
407
408    #[test]
409    fn physical_extent_preserves_host_pixels() {
410        let extent = PhysicalExtent::new(1920, 1080);
411
412        assert_eq!(extent.width, 1920);
413        assert_eq!(extent.height, 1080);
414        assert!(!extent.is_zero());
415    }
416
417    #[test]
418    fn physical_extent_is_zero_when_either_axis_is_zero() {
419        assert!(PhysicalExtent::new(0, 10).is_zero());
420        assert!(PhysicalExtent::new(10, 0).is_zero());
421    }
422}