Skip to main content

gpui_authoring/elements/
surface.rs

1#[cfg(any(
2    target_os = "linux",
3    target_os = "macos",
4    target_os = "ios",
5    target_os = "windows"
6))]
7use crate::GpuCanvasContext;
8use crate::{
9    App, Bounds, Element, ElementId, GlobalElementId, IntoElement, LayoutId, ObjectFit, Pixels,
10    Style, StyleRefinement, Styled, Window,
11};
12// The payload lives in the engine beside `PaintSurface`, the primitive it becomes: `surface()`
13// takes one and `draw_surfaces` reads it, and neither the element nor a renderer invents a
14// transport of its own.
15#[cfg(target_os = "windows")]
16use gpui_engine::DirectXSource;
17use gpui_engine::SurfaceSource;
18#[cfg(any(target_os = "macos", target_os = "ios"))]
19use objc2_core_video::{CVPixelBufferGetHeight, CVPixelBufferGetWidth};
20use refineable::Refineable;
21
22/// A surface element: an external pixel source composited into the scene.
23///
24/// Built by [`surface`] and configured with [`Surface::object_fit`]. It holds the [`SurfaceSource`]
25/// it was given until the window's renderer resolves that source into something it can sample; it
26/// carries no pixel data itself. See [`surface`] for what a surface is and where a source comes
27/// from.
28pub struct Surface {
29    source: SurfaceSource,
30    object_fit: ObjectFit,
31    style: StyleRefinement,
32}
33
34/// Composites pixels GPUI did not draw into the scene, as an ordinary element.
35///
36/// A surface is how an application shows content produced outside GPUI — a video decoder's frame, a
37/// texture another graphics API rendered, or a resource that lives on another device — without
38/// copying those pixels through the CPU. The producer fills a GPU resource and hands it here as a
39/// source; the window's renderer samples that resource directly while it composites the frame.
40///
41/// The element lays out and stacks like any other child: it occupies space in the layout tree, and
42/// it is painted in the frame's own pass in child order, so ordinary elements drawn beside or over
43/// it compose with it rather than floating above it.
44///
45/// `source` is anything that converts into a [`SurfaceSource`]. On Windows that is a Direct3D
46/// texture or shader-resource view, on macOS a CoreVideo image buffer, and on Linux a dma-buf
47/// handle; [`SurfaceSource`] documents each platform's payloads and, where a platform offers more
48/// than one, which to reach for. The Windows pair is the one that comes up most: handing over a
49/// texture is the ergonomic default, since the renderer makes the view on the device it owns, while
50/// handing over a view is the escape for a producer that is the authority on its own format and mip
51/// interpretation.
52///
53/// A source is produced by someone else, before this element paints. A same-device resource is made
54/// inside a [`gpu_canvas`](crate::gpu_canvas) callback, where the window renderer's device is
55/// reachable through [`GpuCanvasContext::device`](crate::GpuCanvasContext::device); a source this
56/// element takes was made off the window's device and shared with it — a dma-buf, a CoreVideo
57/// buffer, a shared Direct3D view — the route a cross-process bridge takes.
58///
59/// ```rust
60/// use gpui::{div, surface};
61///
62/// // `source` is a `SurfaceSource` for this platform; see the type's docs.
63/// div().size_full().child(surface(source))
64/// ```
65///
66/// For a working producer, run `cargo run -p gpui --example surface`: it shows this element and the
67/// `gpu_canvas()` callback side by side, one payload at a time, on the host renderer.
68pub fn surface(source: impl Into<SurfaceSource>) -> Surface {
69    Surface {
70        source: source.into(),
71        object_fit: ObjectFit::Contain,
72        style: Default::default(),
73    }
74}
75
76impl Surface {
77    /// Sets how the source's pixels are fitted into this element's bounds.
78    ///
79    /// The source carries its own pixel size; the element carries its layout size. The fit decides
80    /// how the former maps onto the latter, using the same [`ObjectFit`] modes images use — for
81    /// example [`ObjectFit::Contain`] (the default) letterboxes the source inside the bounds while
82    /// preserving its aspect ratio, and [`ObjectFit::Fill`] stretches it to fill the bounds
83    /// exactly. When the source's size cannot be determined, the pixels are drawn into the full
84    /// bounds.
85    pub fn object_fit(mut self, object_fit: ObjectFit) -> Self {
86        self.object_fit = object_fit;
87        self
88    }
89}
90
91impl Element for Surface {
92    type RequestLayoutState = ();
93    type PrepaintState = ();
94
95    fn id(&self) -> Option<ElementId> {
96        None
97    }
98
99    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
100        None
101    }
102
103    fn request_layout(
104        &mut self,
105        _global_id: Option<&GlobalElementId>,
106        window: &mut Window,
107        cx: &mut App,
108    ) -> (LayoutId, Self::RequestLayoutState) {
109        let mut style = Style::default();
110        style.refine(&self.style);
111        let layout_id = window.request_layout(style, [], cx);
112        (layout_id, ())
113    }
114
115    fn prepaint(
116        &mut self,
117        _global_id: Option<&GlobalElementId>,
118        _bounds: Bounds<Pixels>,
119        _request_layout: &mut Self::RequestLayoutState,
120        _window: &mut Window,
121        _cx: &mut App,
122    ) -> Self::PrepaintState {
123    }
124
125    fn paint(
126        &mut self,
127        _global_id: Option<&GlobalElementId>,
128        #[cfg_attr(
129            not(any(target_os = "macos", target_os = "ios", target_os = "windows")),
130            allow(unused_variables)
131        )]
132        bounds: Bounds<Pixels>,
133        _: &mut Self::RequestLayoutState,
134        _: &mut Self::PrepaintState,
135        #[cfg_attr(
136            not(any(target_os = "macos", target_os = "ios", target_os = "windows")),
137            allow(unused_variables)
138        )]
139        window: &mut Window,
140        #[cfg_attr(
141            not(any(target_os = "macos", target_os = "ios", target_os = "windows")),
142            allow(unused_variables)
143        )]
144        cx: &mut App,
145    ) {
146        match &self.source {
147            #[cfg(any(target_os = "macos", target_os = "ios"))]
148            SurfaceSource::CoreVideo(image_buffer) => {
149                let size = crate::size(
150                    CVPixelBufferGetWidth(image_buffer).into(),
151                    CVPixelBufferGetHeight(image_buffer).into(),
152                );
153                let new_bounds = self.object_fit.get_bounds(bounds, size);
154                // TODO: Add support for corner_radii
155                let mut ctx = GpuCanvasContext::new(window, cx, new_bounds);
156                ctx.paint_surface(image_buffer.clone());
157            }
158            #[cfg(target_os = "windows")]
159            SurfaceSource::DirectX(source) => {
160                let new_bounds = match directx_source_size(source) {
161                    Some(size) => self.object_fit.get_bounds(bounds, size),
162                    None => bounds,
163                };
164                // TODO: Add support for corner_radii
165                let mut ctx = GpuCanvasContext::new(window, cx, new_bounds);
166                ctx.paint_surface(SurfaceSource::DirectX(source.clone()));
167            }
168            #[cfg(target_os = "linux")]
169            SurfaceSource::DmaBuf(handle) => {
170                let size = crate::size(
171                    crate::DevicePixels::from(handle.width as i32),
172                    crate::DevicePixels::from(handle.height as i32),
173                );
174                let new_bounds = self.object_fit.get_bounds(bounds, size);
175                // TODO: Add support for corner_radii
176                let mut ctx = GpuCanvasContext::new(window, cx, new_bounds);
177                ctx.paint_surface(handle.clone());
178            }
179            #[allow(unreachable_patterns)]
180            _ => {}
181        }
182    }
183}
184
185impl IntoElement for Surface {
186    type Element = Self;
187
188    fn into_element(self) -> Self::Element {
189        self
190    }
191}
192
193impl Styled for Surface {
194    fn style(&mut self) -> &mut StyleRefinement {
195        &mut self.style
196    }
197}
198
199/// The pixel size of the texture behind a Direct3D surface source.
200///
201/// The Windows backend fits the surface to its bounds exactly as the macOS backend does, and neither
202/// a texture nor a view carries a size of its own: the texture is the resource, and the view's size is
203/// the resource behind it. A source whose resource is not a texture, or whose query fails, has no
204/// size to fit, and the element falls back to its bounds.
205#[cfg(target_os = "windows")]
206fn directx_source_size(source: &DirectXSource) -> Option<crate::Size<crate::DevicePixels>> {
207    use windows::Win32::Graphics::Direct3D11::{D3D11_TEXTURE2D_DESC, ID3D11Texture2D};
208    use windows::core::Interface as _;
209
210    unsafe {
211        let mut desc = D3D11_TEXTURE2D_DESC::default();
212        match source {
213            DirectXSource::Texture(texture) => texture.GetDesc(&mut desc),
214            DirectXSource::View(view) => {
215                let resource = view.GetResource().ok()?;
216                let texture: ID3D11Texture2D = resource.cast().ok()?;
217                texture.GetDesc(&mut desc);
218            }
219            // A shared texture carries its size, because the renderer has not opened it yet.
220            DirectXSource::Shared(shared) => {
221                return Some(crate::size(
222                    crate::DevicePixels::from(shared.width as i32),
223                    crate::DevicePixels::from(shared.height as i32),
224                ));
225            }
226        }
227        Some(crate::size(
228            crate::DevicePixels::from(desc.Width as i32),
229            crate::DevicePixels::from(desc.Height as i32),
230        ))
231    }
232}
233
234#[cfg(all(test, target_os = "linux"))]
235mod tests {
236    use super::*;
237    use crate::{Context, Render, TestAppContext, Window};
238    use gpui_engine::{DmaBufHandle, DmaBufPlane, SurfaceFormatKind, SurfaceSource};
239    use std::os::fd::OwnedFd;
240
241    struct SurfaceView {
242        handle: DmaBufHandle,
243    }
244
245    impl Render for SurfaceView {
246        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
247            surface(self.handle.clone())
248        }
249    }
250
251    /// A descriptor without a GPU: any open file is a valid one as far as the element cares.
252    fn descriptor() -> OwnedFd {
253        std::fs::File::open("/dev/null").expect("/dev/null").into()
254    }
255
256    /// The element's whole job is to put the handle into the scene as a surface: it names no
257    /// renderer, so this needs no GPU.
258    #[gpui::test]
259    fn a_surface_handle_reaches_the_scene(cx: &mut TestAppContext) {
260        let handle = DmaBufHandle::new(
261            4,
262            4,
263            SurfaceFormatKind::rgba8(),
264            DmaBufHandle::LINEAR,
265            [DmaBufPlane::new(descriptor(), 0, 16)],
266            None,
267        );
268
269        let expected = handle.clone();
270        // `add_window_view` draws the frame the way the platform does; drawing by hand from inside
271        // a `window.update` would re-borrow the view that closure already holds.
272        let (_view, cx) = cx.add_window_view(|_, _| SurfaceView { handle });
273        cx.run_until_parked();
274
275        let surfaces = cx.update(|window, _cx| window.painted_surfaces());
276        assert_eq!(
277            surfaces.len(),
278            1,
279            "the surface should be in the scene exactly once"
280        );
281        assert_eq!(surfaces[0].source, SurfaceSource::DmaBuf(expected));
282    }
283}