aetna_core/surface.rs
1//! App-owned GPU textures composited into the paint stream.
2//!
3//! Where [`crate::image::Image`] hands Aetna a CPU pixel buffer that the
4//! backend uploads and content-hash caches, an [`AppTexture`] wraps a
5//! GPU texture the *app* allocates, fills, and resizes itself. Aetna
6//! samples it during paint — no upload, no per-frame copy.
7//!
8//! This is the affordance for content that doesn't fit the quad-instance
9//! shader model: 3D viewports, video frames, externally rasterised
10//! canvases. The widget that displays one is [`crate::tree::surface`].
11//!
12//! # Sizing contract
13//!
14//! The source texture's pixel dimensions are **independent of the
15//! rendered size**. By default, `surface()` samples the full texture
16//! across its resolved layout rect with bilinear filtering; use
17//! [`crate::tree::El::surface_fit`] for `Contain`, `Cover`, or natural
18//! size projection, and [`crate::tree::El::surface_transform`] for
19//! destination-space affine transforms. See [`crate::tree::surface`]
20//! for sizing strategies (pixel-accurate, viewport-driven
21//! re-allocation, aspect-ratio wrappers).
22//!
23//! # Backend dispatch
24//!
25//! Backend-neutral: [`AppTexture`] is an `Arc<dyn AppTextureBackend>`,
26//! and each Aetna backend (`aetna-wgpu`, `aetna-vulkano`) supplies its
27//! own concrete impl plus a constructor (e.g. `aetna_wgpu::app_texture`).
28//! The runtime downcasts in the backend's record path; everything above
29//! the backend boundary stays neutral.
30
31use std::any::Any;
32use std::fmt;
33use std::sync::Arc;
34use std::sync::atomic::{AtomicU64, Ordering};
35
36/// Pixel format of an [`AppTexture`]. The widget composites by sampling
37/// the texture; the backend picks a sampler / shader path that matches.
38///
39/// 0.3.4 ships the three RGBA8 variants below — enough for 3D viewport
40/// output (typically a surface-format-matching `*Srgb`), video decoded
41/// to RGBA, and rumble-style animated frames. Future variants (HDR,
42/// YUV) slot in here without breaking the widget surface.
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum SurfaceFormat {
45 /// 8-bit RGBA, sRGB-encoded. Sampling decodes to linear, matching
46 /// the rest of Aetna's pipeline (`stock::image`, text, rounded_rect).
47 Rgba8UnormSrgb,
48 /// 8-bit BGRA, sRGB-encoded. The native swapchain format on most
49 /// platforms — apps that render their 3D scene into a swapchain-
50 /// shaped texture can hand it in directly.
51 Bgra8UnormSrgb,
52 /// 8-bit RGBA, linear. For content that's already in linear space
53 /// (e.g. tone-mapped HDR collapsed to 8-bit, ink rasterisers) and
54 /// shouldn't go through an extra sRGB decode.
55 Rgba8Unorm,
56}
57
58/// How an [`AppTexture`] composes with widgets painted underneath it.
59///
60/// The choice affects blend state and lets opaque content skip blend
61/// math; it does *not* change z-order. Widgets above the surface in the
62/// paint stream still paint over it, regardless of mode.
63#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
64pub enum SurfaceAlpha {
65 /// Texture carries premultiplied alpha. Default; matches Aetna's
66 /// internal blend convention.
67 #[default]
68 Premultiplied,
69 /// Texture is fully opaque. Backend skips blending — pixels written
70 /// to the surface rect replace whatever was there. Pick this for 3D
71 /// viewports and video where every output pixel is non-transparent.
72 Opaque,
73 /// Texture carries straight (unpremultiplied) alpha. Backend
74 /// premultiplies in the shader before blending. Convenient for
75 /// content authored in a paint app or rasterised by a third-party
76 /// vector library that doesn't premultiply.
77 Straight,
78}
79
80/// Stable identity for an [`AppTexture`]. Allocated by the constructor
81/// that wraps the underlying GPU texture; backends cache their bind
82/// groups / descriptor sets keyed on this id, so it must not be reused
83/// for a different texture during the lifetime of the wrapping
84/// `AppTexture`.
85///
86/// Apps that recreate their texture (resize, format change) get a fresh
87/// id — the previous bind group falls off the cache after one frame,
88/// like any other unused entry.
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
90pub struct AppTextureId(pub u64);
91
92/// Allocate a fresh [`AppTextureId`]. Used by backend constructors. App
93/// code should not call this directly — go through the backend's
94/// `app_texture(...)` constructor instead.
95pub fn next_app_texture_id() -> AppTextureId {
96 static COUNTER: AtomicU64 = AtomicU64::new(1);
97 AppTextureId(COUNTER.fetch_add(1, Ordering::Relaxed))
98}
99
100/// Backend implementation of an [`AppTexture`]. Implemented by
101/// `aetna-wgpu` and `aetna-vulkano` against their native texture types;
102/// the runtime downcasts via [`Self::as_any`] in the backend's record
103/// path.
104pub trait AppTextureBackend: Send + Sync + fmt::Debug + 'static {
105 /// Stable identity allocated by the constructor — must round-trip
106 /// the same value on every call for the lifetime of `self`.
107 fn id(&self) -> AppTextureId;
108
109 /// Pixel size of the underlying texture. The backend uses this for
110 /// sanity checks; the widget rect comes from layout, not from here.
111 fn size_px(&self) -> (u32, u32);
112
113 /// Pixel format of the underlying texture. Used by the backend to
114 /// pick a sampler / shader path.
115 fn format(&self) -> SurfaceFormat;
116
117 /// Downcast hatch for the backend's record path. Each backend
118 /// asserts the trait object is its own concrete type; mixing
119 /// backends in one runtime is unsupported.
120 fn as_any(&self) -> &dyn Any;
121
122 /// Human-readable concrete backend type for diagnostics.
123 fn backend_name(&self) -> &'static str {
124 std::any::type_name::<Self>()
125 }
126}
127
128/// An app-owned GPU texture handed to Aetna for compositing. Cheap
129/// `Arc`-backed clone; pass into [`crate::tree::surface`] to display.
130///
131/// Construct via the backend constructor — `aetna_wgpu::app_texture` or
132/// `aetna_vulkano::app_texture`. The wrapper is type-erased so the El
133/// tree and paint stream stay backend-neutral.
134#[derive(Clone)]
135pub struct AppTexture {
136 inner: Arc<dyn AppTextureBackend>,
137}
138
139impl AppTexture {
140 /// Wrap a backend-supplied implementation. Constructors in
141 /// `aetna-wgpu` / `aetna-vulkano` are the intended entry points.
142 pub fn from_backend(inner: Arc<dyn AppTextureBackend>) -> Self {
143 Self { inner }
144 }
145
146 pub fn id(&self) -> AppTextureId {
147 self.inner.id()
148 }
149
150 pub fn size_px(&self) -> (u32, u32) {
151 self.inner.size_px()
152 }
153
154 pub fn format(&self) -> SurfaceFormat {
155 self.inner.format()
156 }
157
158 /// Borrow the backend impl as a trait object. Backends call this
159 /// from their record path and downcast to their concrete type.
160 pub fn backend(&self) -> &dyn AppTextureBackend {
161 &*self.inner
162 }
163
164 /// Human-readable concrete backend type for diagnostics.
165 pub fn backend_name(&self) -> &'static str {
166 self.inner.backend_name()
167 }
168}
169
170impl fmt::Debug for AppTexture {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 let (w, h) = self.size_px();
173 f.debug_struct("AppTexture")
174 .field("id", &self.id().0)
175 .field("size_px", &(w, h))
176 .field("format", &self.format())
177 .finish()
178 }
179}
180
181/// Source of pixels for a [`crate::tree::Kind::Surface`] widget.
182///
183/// Today only [`Self::Texture`] is shipped. A `Callback(...)` variant
184/// is planned as a future, more efficient path that hands the backend
185/// encoder to the app during paint; the `Source` enum exists from day
186/// one so that addition is non-breaking for callers.
187#[derive(Clone, Debug)]
188pub enum SurfaceSource {
189 /// App-owned, app-filled GPU texture. Sampled by the backend during
190 /// the existing paint pass — no shared encoder, no extra render
191 /// pass.
192 Texture(AppTexture),
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn ids_are_unique_and_stable() {
201 let a = next_app_texture_id();
202 let b = next_app_texture_id();
203 assert_ne!(a, b);
204 assert_eq!(a, AppTextureId(a.0));
205 }
206
207 #[test]
208 fn surface_alpha_default_is_premultiplied() {
209 assert_eq!(SurfaceAlpha::default(), SurfaceAlpha::Premultiplied);
210 }
211}