Skip to main content

wasdk_wgpu/
surface.rs

1//! Surfaces: wgpu's way to draw on a window, here on a program's view, as wgpu's browser backend
2//! draws on a canvas. The window is a [`SharedView`], the program's view shared with the surfaces
3//! made on it, and its window handle a `WebCanvasWindowHandle` pointing at it, as a browser's
4//! points at a canvas: a component has no browser canvas for one to point at, and a view's id is
5//! a `u64`, which the numbered `WebWindowHandle` cannot carry. A surface configures a canvas
6//! context of the view's, hands out the context's texture each frame, and the frame's present
7//! presents the view (`Queue::present`, in `queue`).
8//!
9//! The canvas is the size the surface is configured at: `configure` sets the view's canvas to its
10//! width and height, so every frame's texture is the size wgpu tells for it, and a program
11//! configures again as its view's size changes, as it would a window's surface anywhere.
12//!
13//! The invariant: the handle's pointer is read only in `create_surface`, while the window it was
14//! taken from lends it; from there the surface, and each frame it hands out, hold the view through
15//! its `Arc`, so nothing after needs the pointer.
16
17use std::ops::Deref;
18use std::ptr::NonNull;
19use std::sync::Arc;
20
21use wasdk::views::View;
22use wasdk::webgpu::{GpuCanvasConfiguration, GpuCanvasContext};
23use wgpu::custom::{
24    DispatchAdapter, DispatchDevice, DispatchSurfaceOutputDetail, DispatchTexture,
25    SurfaceInterface, SurfaceOutputDetailInterface,
26};
27use wgpu::rwh::{
28    DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawWindowHandle,
29    WebCanvasWindowHandle, WindowHandle,
30};
31use wgpu::{
32    CompositeAlphaMode, PresentMode, SurfaceCapabilities, SurfaceColorSpaces,
33    SurfaceFormatCapabilities, SurfaceStatus, TextureFormat, TextureUsages,
34};
35
36use crate::convert;
37use crate::device::WasdkDevice;
38use crate::gap::wasi_gap;
39use crate::texture::WasdkTexture;
40
41/// A program's view, shared by the program and the surfaces made on it: the window wgpu makes a
42/// surface for, as an HTML canvas is on the web. It is the view's `Arc` with the window handle
43/// Rust's orphan rule keeps off `Arc<View>`, so it dereferences to the view; every clone is the
44/// same view.
45///
46/// ```ignore
47/// let view = wasdk_wgpu::SharedView::new(view);
48/// let surface = instance.create_surface(view.clone())?;
49/// view.request_frame();
50/// ```
51#[derive(Clone, Debug)]
52pub struct SharedView(Arc<View>);
53
54impl SharedView {
55    pub fn new(view: View) -> SharedView {
56        SharedView(Arc::new(view))
57    }
58}
59
60impl Deref for SharedView {
61    type Target = View;
62
63    fn deref(&self) -> &View {
64        &self.0
65    }
66}
67
68impl HasWindowHandle for SharedView {
69    /// A `WebCanvas` handle pointing at this view.
70    #[allow(unsafe_code)]
71    fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
72        let handle = WebCanvasWindowHandle::new(NonNull::from(self).cast());
73        // SAFETY: the handle points at `self`, which the handle borrows for as long as it lives.
74        Ok(unsafe { WindowHandle::borrow_raw(handle.into()) })
75    }
76}
77
78impl HasDisplayHandle for SharedView {
79    /// The web's display, which carries nothing: the view is reached through its window handle.
80    fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
81        Ok(DisplayHandle::web())
82    }
83}
84
85/// Where the view a window handle points at is: a `WebCanvas` handle's object, which in a
86/// component is a [`SharedView`], since a component has no browser canvas. Any other handle is a
87/// gap: wgpu gives a backend of its own no way to make the `CreateSurfaceError` it would answer.
88pub(crate) fn shared_view_of(handle: RawWindowHandle) -> NonNull<SharedView> {
89    match handle {
90        RawWindowHandle::WebCanvas(handle) => handle.obj.cast(),
91        other => wasi_gap(format_args!(
92            "a surface from a window handle other than a view's (a {other:?})"
93        )),
94    }
95}
96
97/// What the view's canvas serves: its one format, presented at the view's frames, blended as
98/// WebGPU's canvas blends, for drawing into, sampling and copying to and from.
99pub(crate) fn capabilities(format: TextureFormat) -> SurfaceCapabilities {
100    SurfaceCapabilities {
101        formats: vec![format],
102        format_capabilities: vec![SurfaceFormatCapabilities {
103            format,
104            color_spaces: SurfaceColorSpaces::SRGB,
105        }],
106        present_modes: vec![PresentMode::Fifo],
107        alpha_modes: vec![
108            CompositeAlphaMode::Opaque,
109            CompositeAlphaMode::PreMultiplied,
110        ],
111        usages: TextureUsages::RENDER_ATTACHMENT
112            | TextureUsages::TEXTURE_BINDING
113            | TextureUsages::COPY_SRC
114            | TextureUsages::COPY_DST,
115    }
116}
117
118/// Refuses a present mode other than the view's: its frames come at the screen's refresh, which
119/// is `Fifo`, and what an automatic mode falls back to.
120fn check_present_mode(mode: PresentMode) {
121    match mode {
122        PresentMode::Fifo | PresentMode::AutoVsync | PresentMode::AutoNoVsync => {}
123        other => wasi_gap(format_args!(
124            "the present mode {other:?}: a view presents at its frames"
125        )),
126    }
127}
128
129/// The surface behind a [`wgpu::Surface`] on a view: `surface.as_custom::<WasdkSurface>()`.
130#[derive(Debug)]
131pub struct WasdkSurface {
132    view: Arc<View>,
133    context: GpuCanvasContext,
134    /// The one format the view's canvas is drawn in.
135    format: TextureFormat,
136}
137
138impl WasdkSurface {
139    /// A surface on `view`, through a canvas context of its own, drawn in `format`.
140    pub(crate) fn new(view: &SharedView, format: TextureFormat) -> WasdkSurface {
141        WasdkSurface {
142            view: Arc::clone(&view.0),
143            context: view.context(),
144            format,
145        }
146    }
147}
148
149impl SurfaceInterface for WasdkSurface {
150    fn get_capabilities(&self, _adapter: &DispatchAdapter) -> SurfaceCapabilities {
151        capabilities(self.format)
152    }
153
154    /// Sets the view's canvas to `width` by `height`, then configures its canvas context as
155    /// WebGPU's canvas is configured; the present mode and the frame latency have no counterpart,
156    /// a view presenting at its frames.
157    fn configure(&self, device: &DispatchDevice, config: &wgpu::SurfaceConfiguration) {
158        check_present_mode(config.present_mode);
159        self.view.set_canvas_size(config.width, config.height);
160        let (color_space, tone_mapping) = convert::color_space_to_wasi(config.color_space);
161        let view_formats = config
162            .view_formats
163            .iter()
164            .map(|format| convert::texture_format_to_wasi(*format))
165            .collect();
166        self.context.configure(&GpuCanvasConfiguration {
167            device: WasdkDevice::of_dispatch(device).gpu_device(),
168            format: convert::texture_format_to_wasi(config.format),
169            usage: Some(convert::texture_usages_to_wasi(config.usage)),
170            view_formats: Some(view_formats),
171            color_space,
172            tone_mapping,
173            alpha_mode: Some(convert::alpha_mode_to_wasi(config.alpha_mode)),
174        });
175    }
176
177    /// The canvas's texture for this frame, of the size configured. A lost device hands out an
178    /// invalid texture, which the canvas cannot tell from a good one, so it is handed out as good:
179    /// the program hears of the loss through the device.
180    fn get_current_texture(
181        &self,
182    ) -> (
183        Option<DispatchTexture>,
184        SurfaceStatus,
185        DispatchSurfaceOutputDetail,
186    ) {
187        let texture = self.context.get_current_texture();
188        let frame = WasdkSurfaceOutputDetail {
189            view: Arc::clone(&self.view),
190        };
191        (
192            Some(DispatchTexture::custom(WasdkTexture::new(texture))),
193            SurfaceStatus::Good,
194            DispatchSurfaceOutputDetail::custom(frame),
195        )
196    }
197}
198
199/// A frame a surface handed out: the view it is presented on.
200#[derive(Debug)]
201pub struct WasdkSurfaceOutputDetail {
202    view: Arc<View>,
203}
204
205downcast!(WasdkSurfaceOutputDetail: dispatch DispatchSurfaceOutputDetail);
206
207impl WasdkSurfaceOutputDetail {
208    /// Presents the frame on its view, with the layers the view holds beneath it.
209    pub(crate) fn present(&self) {
210        self.view.present();
211    }
212}
213
214impl SurfaceOutputDetailInterface for WasdkSurfaceOutputDetail {
215    /// A frame dropped without a present is not presented: the view presents only when asked, and
216    /// the next frame taken replaces it.
217    fn texture_discard(&self) {}
218
219    fn texture_release(&self) {}
220}
221
222#[cfg(test)]
223mod tests {
224    use wgpu::rwh::{AppKitWindowHandle, WebWindowHandle};
225
226    use super::*;
227
228    #[test]
229    fn a_view_serves_its_canvas_format_at_its_frames() {
230        let capabilities = capabilities(TextureFormat::Bgra8Unorm);
231        assert_eq!(capabilities.formats, [TextureFormat::Bgra8Unorm]);
232        assert_eq!(
233            capabilities.color_spaces(TextureFormat::Bgra8Unorm),
234            SurfaceColorSpaces::SRGB
235        );
236        assert_eq!(capabilities.present_modes, [PresentMode::Fifo]);
237        assert_eq!(capabilities.alpha_modes[0], CompositeAlphaMode::Opaque);
238        assert!(
239            capabilities
240                .usages
241                .contains(TextureUsages::RENDER_ATTACHMENT)
242        );
243    }
244
245    #[test]
246    fn an_automatic_present_mode_is_the_views_frames() {
247        check_present_mode(PresentMode::AutoNoVsync);
248        check_present_mode(PresentMode::Fifo);
249    }
250
251    #[test]
252    #[should_panic(expected = "Mailbox")]
253    fn a_present_mode_other_than_the_views_frames_is_a_gap() {
254        check_present_mode(PresentMode::Mailbox);
255    }
256
257    /// `instance.create_surface(view.clone())` takes it, and Bevy's `RawHandleWrapper` its two
258    /// handles.
259    #[test]
260    fn a_shared_view_is_a_window_wgpu_makes_a_surface_for() {
261        fn surface_target<T: Into<wgpu::SurfaceTarget<'static>>>() {}
262        fn window<T: wgpu::WindowHandle + HasDisplayHandle>() {}
263        surface_target::<SharedView>();
264        window::<SharedView>();
265    }
266
267    #[test]
268    fn a_web_canvas_handle_points_at_the_shared_view() {
269        let pointer = NonNull::<SharedView>::dangling();
270        let handle = WebCanvasWindowHandle::new(pointer.cast());
271        assert_eq!(shared_view_of(handle.into()), pointer);
272    }
273
274    #[test]
275    #[should_panic(expected = "other than a view's")]
276    fn a_numbered_web_handle_is_not_a_view() {
277        shared_view_of(WebWindowHandle::new(1).into());
278    }
279
280    #[test]
281    #[should_panic(expected = "other than a view's")]
282    fn a_native_window_is_not_a_view() {
283        let window = NonNull::<u8>::dangling().cast();
284        shared_view_of(AppKitWindowHandle::new(window).into());
285    }
286}