Skip to main content

gpui_authoring/elements/
surface.rs

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