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}