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(bounds: Bounds<ScaledPixels>, radii: Corners<ScaledPixels>, opacity: f32, flip_v: bool) -> CustomRenderPrimitive {
193        CustomRenderPrimitive::Texture {
194            order: 0,
195            handle: ImportedTextureHandle::new(()),
196            bounds,
197            content_mask: ContentMask { bounds },
198            radii,
199            opacity,
200            flip_v,
201        }
202    }
203
204    /// The record the imported-texture pipeline reads is the quad record, so the geometry, the
205    /// clip and the radii have to survive the encoding, and the two values with no field of their
206    /// own have to land in the fields the quad path does not otherwise use. Both shaders read it
207    /// that way, which is why it is written down once, here.
208    #[test]
209    fn a_texture_primitive_encodes_into_the_quad_record() {
210        let bounds = bounds(1.0, 2.0, 3.0, 4.0);
211        let radii = Corners {
212            top_left: ScaledPixels(5.0),
213            top_right: ScaledPixels(6.0),
214            bottom_right: ScaledPixels(7.0),
215            bottom_left: ScaledPixels(8.0),
216        };
217
218        let quad = texture(bounds, radii, 0.25, true).to_quad_record();
219
220        assert_eq!(quad.bounds, bounds);
221        assert_eq!(quad.content_mask, ContentMask { bounds });
222        assert_eq!(quad.corner_radii, radii);
223        assert_eq!(
224            quad.background.solid.a, 0.25,
225            "the opacity rides in the solid background's alpha"
226        );
227        assert_eq!(
228            quad.border_style,
229            BorderStyle::Dashed,
230            "flip_v rides in the border style"
231        );
232    }
233
234    /// A producer whose texture already has GPUI's origin must not be flipped, so the encoding
235    /// has to distinguish the two cases rather than always marking the record.
236    #[test]
237    fn a_texture_primitive_without_a_flip_encodes_a_solid_border_style() {
238        let bounds = bounds(0.0, 0.0, 10.0, 10.0);
239
240        let quad = texture(bounds, Corners::default(), 1.0, false).to_quad_record();
241
242        assert_eq!(quad.border_style, BorderStyle::Solid);
243        assert_eq!(quad.background.solid.a, 1.0);
244    }
245}