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}