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}