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}