Skip to main content

gpui_engine/
custom_render.rs

1use std::{any::Any, fmt, sync::Arc};
2
3use crate::{BorderStyle, Quad};
4use gpui_types::{Background, Bounds, ContentMask, Corners, DrawOrder, Edges, Hsla, ScaledPixels};
5
6/// A token for a texture produced outside GPUI, on the renderer's own device.
7///
8/// The payload is erased because the handle has exactly one counterparty — the application
9/// that made the texture and the renderer that application chose — and those two agree on the
10/// concrete type it holds. Keeping the engine blind to that type is what keeps `wgpu` and the
11/// platform graphics APIs out of this crate. It is also why there is no id and no registry
12/// beside it: the handle *is* the resource, so it cannot be wrong.
13#[derive(Clone)]
14pub struct ImportedTextureHandle {
15    /// The backend's own payload, and the one thing the engine never looks at.
16    pub payload: Arc<dyn Any + Send + Sync>,
17}
18
19impl ImportedTextureHandle {
20    /// Wrap a backend payload — a `wgpu::TextureView`, a `MetalTexture` — as a handle.
21    pub fn new(payload: impl Any + Send + Sync) -> Self {
22        Self {
23            payload: Arc::new(payload),
24        }
25    }
26}
27
28impl fmt::Debug for ImportedTextureHandle {
29    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30        formatter.write_str("ImportedTextureHandle")
31    }
32}
33
34/// A drawable the application produced itself, drawn in the composite pass rather than
35/// translated into GPUI's own primitives.
36///
37/// It rides [`crate::PaintSurface`]'s machinery — the ordering, the content mask, one
38/// accumulation list and one batch — but is drawn by a fragment path of its own: what it
39/// samples is RGBA, where that surface is YCbCr video.
40#[derive(Clone)]
41pub enum CustomRenderPrimitive {
42    /// A texture produced outside GPUI, sampled in the composite pass.
43    Texture {
44        /// Assigned by the scene, so the texture composites with its siblings in order.
45        order: DrawOrder,
46        /// The token the renderer resolves to the texture it samples.
47        handle: ImportedTextureHandle,
48        /// Where the texture lands, in scaled pixels.
49        bounds: Bounds<ScaledPixels>,
50        /// What may clip it.
51        content_mask: ContentMask<ScaledPixels>,
52        /// Corner radii, in scaled pixels: the element converts at paint time, as `paint_quad`
53        /// does, because the renderer has no scale factor of its own.
54        radii: Corners<ScaledPixels>,
55        /// Multiplied into the sample.
56        opacity: f32,
57        /// The producer's textures have a top-left origin; the scene's quads do not.
58        flip_v: bool,
59    },
60    /* Inline, in inline-commands.md */
61}
62
63impl CustomRenderPrimitive {
64    /// The order the scene assigned it.
65    pub fn order(&self) -> DrawOrder {
66        match self {
67            Self::Texture { order, .. } => *order,
68        }
69    }
70
71    /// The order, for the scene to assign.
72    pub fn order_mut(&mut self) -> &mut DrawOrder {
73        match self {
74            Self::Texture { order, .. } => order,
75        }
76    }
77
78    /// Where it lands.
79    pub fn bounds(&self) -> &Bounds<ScaledPixels> {
80        match self {
81            Self::Texture { bounds, .. } => bounds,
82        }
83    }
84
85    /// What may clip it.
86    pub fn content_mask(&self) -> &ContentMask<ScaledPixels> {
87        match self {
88            Self::Texture { content_mask, .. } => content_mask,
89        }
90    }
91
92    /// Encode this primitive into the quad instance record the backends' shaders read.
93    ///
94    /// Both renderers draw it through their quad pipeline, so its geometry, its
95    /// content-mask clip and its corner SDF are the quads' rather than a second
96    /// implementation of them. That leaves two values with no field of their own: the
97    /// opacity rides in the solid background's alpha, which the quad vertex entry point
98    /// forwards as a scratch field the texturing path does not read, and `flip_v` rides in
99    /// the border style, which a textured quad has no other use for. Encoding it here
100    /// rather than in each renderer is what keeps the two in step with the shaders.
101    pub fn to_quad_record(&self) -> Quad {
102        match self {
103            Self::Texture {
104                order,
105                bounds,
106                content_mask,
107                radii,
108                opacity,
109                flip_v,
110                ..
111            } => {
112                let mut background = Background::default();
113                background.solid = Hsla {
114                    h: 0.0,
115                    s: 0.0,
116                    l: 0.0,
117                    a: *opacity,
118                };
119
120                Quad {
121                    order: *order,
122                    border_style: if *flip_v {
123                        BorderStyle::Dashed
124                    } else {
125                        BorderStyle::Solid
126                    },
127                    bounds: *bounds,
128                    content_mask: *content_mask,
129                    background,
130                    border_color: Hsla::default(),
131                    corner_radii: *radii,
132                    border_widths: Edges::default(),
133                }
134            }
135        }
136    }
137}
138
139impl fmt::Debug for CustomRenderPrimitive {
140    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
141        match self {
142            Self::Texture {
143                handle,
144                bounds,
145                opacity,
146                flip_v,
147                ..
148            } => formatter
149                .debug_struct("Texture")
150                .field("handle", handle)
151                .field("bounds", bounds)
152                .field("opacity", opacity)
153                .field("flip_v", flip_v)
154                .finish_non_exhaustive(),
155        }
156    }
157}
158
159/// A native texture handle for the Metal backend.
160///
161/// This is what a macOS renderer resolves [`ImportedTextureHandle`] against: a raw
162/// `id<MTLTexture>`, which is not `Send`, but the handle crosses no thread. The assertions are
163/// only what [`ImportedTextureHandle`]'s `Arc<dyn Any + Send + Sync>` asks for.
164#[cfg(target_os = "macos")]
165#[derive(Clone, Copy)]
166pub struct MetalTexture(pub *mut std::ffi::c_void);
167
168#[cfg(target_os = "macos")]
169unsafe impl Send for MetalTexture {}
170
171#[cfg(target_os = "macos")]
172unsafe impl Sync for MetalTexture {}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use gpui_types::{Point, Size};
178
179    fn bounds(x: f32, y: f32, width: f32, height: f32) -> Bounds<ScaledPixels> {
180        Bounds {
181            origin: Point {
182                x: ScaledPixels(x),
183                y: ScaledPixels(y),
184            },
185            size: Size {
186                width: ScaledPixels(width),
187                height: ScaledPixels(height),
188            },
189        }
190    }
191
192    fn texture(
193        bounds: Bounds<ScaledPixels>,
194        radii: Corners<ScaledPixels>,
195        opacity: f32,
196        flip_v: bool,
197    ) -> CustomRenderPrimitive {
198        CustomRenderPrimitive::Texture {
199            order: 0,
200            handle: ImportedTextureHandle::new(()),
201            bounds,
202            content_mask: ContentMask { bounds },
203            radii,
204            opacity,
205            flip_v,
206        }
207    }
208
209    /// The record the imported-texture pipeline reads is the quad record, so the geometry, the
210    /// clip and the radii have to survive the encoding, and the two values with no field of their
211    /// own have to land in the fields the quad path does not otherwise use. Both shaders read it
212    /// that way, which is why it is written down once, here.
213    #[test]
214    fn a_texture_primitive_encodes_into_the_quad_record() {
215        let bounds = bounds(1.0, 2.0, 3.0, 4.0);
216        let radii = Corners {
217            top_left: ScaledPixels(5.0),
218            top_right: ScaledPixels(6.0),
219            bottom_right: ScaledPixels(7.0),
220            bottom_left: ScaledPixels(8.0),
221        };
222
223        let quad = texture(bounds, radii, 0.25, true).to_quad_record();
224
225        assert_eq!(quad.bounds, bounds);
226        assert_eq!(quad.content_mask, ContentMask { bounds });
227        assert_eq!(quad.corner_radii, radii);
228        assert_eq!(
229            quad.background.solid.a, 0.25,
230            "the opacity rides in the solid background's alpha"
231        );
232        assert_eq!(
233            quad.border_style,
234            BorderStyle::Dashed,
235            "flip_v rides in the border style"
236        );
237    }
238
239    /// A producer whose texture already has GPUI's origin must not be flipped, so the encoding
240    /// has to distinguish the two cases rather than always marking the record.
241    #[test]
242    fn a_texture_primitive_without_a_flip_encodes_a_solid_border_style() {
243        let bounds = bounds(0.0, 0.0, 10.0, 10.0);
244
245        let quad = texture(bounds, Corners::default(), 1.0, false).to_quad_record();
246
247        assert_eq!(quad.border_style, BorderStyle::Solid);
248        assert_eq!(quad.background.solid.a, 1.0);
249    }
250}