Skip to main content

damascene_core/paint/
surface.rs

1//! App-owned GPU textures composited into the paint stream.
2//!
3//! Where [`crate::image::Image`] hands Damascene 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. Damascene
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 Damascene backend (`damascene-wgpu`, `damascene-vulkano`) supplies its
27//! own concrete impl plus a constructor (e.g. `damascene_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.6 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 Damascene'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    /// 16-bit float RGBA, linear extended-range. For HDR content authored
57    /// in scene-linear light — values may exceed `1.0` (and the surface
58    /// holds them verbatim). Composited like [`Self::Rgba8Unorm`] (linear,
59    /// no sRGB decode); the extra range only carries through to the display
60    /// when the swapchain is itself an extended-range float surface (see
61    /// the host's HDR swapchain selection), otherwise it clamps at output.
62    Rgba16Float,
63}
64
65/// How an [`AppTexture`] composes with widgets painted underneath it.
66///
67/// The choice affects blend state and lets opaque content skip blend
68/// math; it does *not* change z-order. Widgets above the surface in the
69/// paint stream still paint over it, regardless of mode.
70#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
71pub enum SurfaceAlpha {
72    /// Texture carries premultiplied alpha. Default; matches Damascene's
73    /// internal blend convention.
74    #[default]
75    Premultiplied,
76    /// Texture is fully opaque. Backend skips blending — pixels written
77    /// to the surface rect replace whatever was there. Pick this for 3D
78    /// viewports and video where every output pixel is non-transparent.
79    Opaque,
80    /// Texture carries straight (unpremultiplied) alpha. Backend
81    /// premultiplies in the shader before blending. Convenient for
82    /// content authored in a paint app or rasterised by a third-party
83    /// vector library that doesn't premultiply.
84    Straight,
85}
86
87/// Stable identity for an [`AppTexture`]. Allocated by the constructor
88/// that wraps the underlying GPU texture; backends cache their bind
89/// groups / descriptor sets keyed on this id, so it must not be reused
90/// for a different texture during the lifetime of the wrapping
91/// `AppTexture`.
92///
93/// Apps that recreate their texture (resize, format change) get a fresh
94/// id — the previous bind group falls off the cache after one frame,
95/// like any other unused entry.
96#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
97pub struct AppTextureId(pub u64);
98
99/// Allocate a fresh [`AppTextureId`]. Used by backend constructors. App
100/// code should not call this directly — go through the backend's
101/// `app_texture(...)` constructor instead.
102pub fn next_app_texture_id() -> AppTextureId {
103    static COUNTER: AtomicU64 = AtomicU64::new(1);
104    AppTextureId(COUNTER.fetch_add(1, Ordering::Relaxed))
105}
106
107/// Backend implementation of an [`AppTexture`]. Implemented by
108/// `damascene-wgpu` and `damascene-vulkano` against their native texture types;
109/// the runtime downcasts via [`Self::as_any`] in the backend's record
110/// path.
111pub trait AppTextureBackend: Send + Sync + fmt::Debug + 'static {
112    /// Stable identity allocated by the constructor — must round-trip
113    /// the same value on every call for the lifetime of `self`.
114    fn id(&self) -> AppTextureId;
115
116    /// Pixel size of the underlying texture. The backend uses this for
117    /// sanity checks; the widget rect comes from layout, not from here.
118    fn size_px(&self) -> (u32, u32);
119
120    /// Pixel format of the underlying texture. Used by the backend to
121    /// pick a sampler / shader path.
122    fn format(&self) -> SurfaceFormat;
123
124    /// Downcast hatch for the backend's record path. Each backend
125    /// asserts the trait object is its own concrete type; mixing
126    /// backends in one runtime is unsupported.
127    fn as_any(&self) -> &dyn Any;
128
129    /// Human-readable concrete backend type for diagnostics.
130    fn backend_name(&self) -> &'static str {
131        std::any::type_name::<Self>()
132    }
133}
134
135/// An app-owned GPU texture handed to Damascene for compositing. Cheap
136/// `Arc`-backed clone; pass into [`crate::tree::surface`] to display.
137///
138/// Construct via the backend constructor — `damascene_wgpu::app_texture` or
139/// `damascene_vulkano::app_texture`. The wrapper is type-erased so the El
140/// tree and paint stream stay backend-neutral.
141#[derive(Clone)]
142pub struct AppTexture {
143    inner: Arc<dyn AppTextureBackend>,
144}
145
146impl AppTexture {
147    /// Wrap a backend-supplied implementation. Constructors in
148    /// `damascene-wgpu` / `damascene-vulkano` are the intended entry points.
149    pub fn from_backend(inner: Arc<dyn AppTextureBackend>) -> Self {
150        Self { inner }
151    }
152
153    pub fn id(&self) -> AppTextureId {
154        self.inner.id()
155    }
156
157    pub fn size_px(&self) -> (u32, u32) {
158        self.inner.size_px()
159    }
160
161    pub fn format(&self) -> SurfaceFormat {
162        self.inner.format()
163    }
164
165    /// Borrow the backend impl as a trait object. Backends call this
166    /// from their record path and downcast to their concrete type.
167    pub fn backend(&self) -> &dyn AppTextureBackend {
168        &*self.inner
169    }
170
171    /// Human-readable concrete backend type for diagnostics.
172    pub fn backend_name(&self) -> &'static str {
173        self.inner.backend_name()
174    }
175}
176
177impl fmt::Debug for AppTexture {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        let (w, h) = self.size_px();
180        f.debug_struct("AppTexture")
181            .field("id", &self.id().0)
182            .field("size_px", &(w, h))
183            .field("format", &self.format())
184            .finish()
185    }
186}
187
188/// Source of pixels for a [`crate::tree::Kind::Surface`] widget.
189///
190/// Today only [`Self::Texture`] is shipped. A `Callback(...)` variant
191/// is planned as a future, more efficient path that hands the backend
192/// encoder to the app during paint; the `Source` enum exists from day
193/// one so that addition is non-breaking for callers.
194#[derive(Clone, Debug)]
195pub enum SurfaceSource {
196    /// App-owned, app-filled GPU texture. Sampled by the backend during
197    /// the existing paint pass — no shared encoder, no extra render
198    /// pass.
199    Texture(AppTexture),
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn ids_are_unique_and_stable() {
208        let a = next_app_texture_id();
209        let b = next_app_texture_id();
210        assert_ne!(a, b);
211        assert_eq!(a, AppTextureId(a.0));
212    }
213
214    #[test]
215    fn surface_alpha_default_is_premultiplied() {
216        assert_eq!(SurfaceAlpha::default(), SurfaceAlpha::Premultiplied);
217    }
218}