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