deft_softbuffer/backends/
web.rs

1//! Implementation of software buffering for web targets.
2
3#![allow(clippy::uninlined_format_args)]
4
5use js_sys::Object;
6use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle};
7use wasm_bindgen::{JsCast, JsValue};
8use web_sys::ImageData;
9use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
10use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d};
11
12use crate::backend_interface::*;
13use crate::error::{InitError, SwResultExt};
14use crate::{util, NoDisplayHandle, NoWindowHandle, Rect, SoftBufferError};
15use std::marker::PhantomData;
16use std::num::NonZeroU32;
17
18/// Display implementation for the web platform.
19///
20/// This just caches the document to prevent having to query it every time.
21pub struct WebDisplayImpl<D> {
22    document: web_sys::Document,
23    _display: D,
24}
25
26impl<D: HasDisplayHandle> ContextInterface<D> for WebDisplayImpl<D> {
27    fn new(display: D) -> Result<Self, InitError<D>> {
28        let raw = display.display_handle()?.as_raw();
29        let RawDisplayHandle::Web(..) = raw else {
30            return Err(InitError::Unsupported(display));
31        };
32
33        let document = web_sys::window()
34            .swbuf_err("`Window` is not present in this runtime")?
35            .document()
36            .swbuf_err("`Document` is not present in this runtime")?;
37
38        Ok(Self {
39            document,
40            _display: display,
41        })
42    }
43}
44
45pub struct WebImpl<D, W> {
46    /// The handle and context to the canvas that we're drawing to.
47    canvas: Canvas,
48
49    /// The buffer that we're drawing to.
50    buffer: Vec<u32>,
51
52    /// Buffer has been presented.
53    buffer_presented: bool,
54
55    /// The current canvas width/height.
56    size: Option<(NonZeroU32, NonZeroU32)>,
57
58    /// The underlying window handle.
59    window_handle: W,
60
61    /// The underlying display handle.
62    _display: PhantomData<D>,
63}
64
65/// Holding canvas and context for [`HtmlCanvasElement`] or [`OffscreenCanvas`],
66/// since they have different types.
67enum Canvas {
68    Canvas {
69        canvas: HtmlCanvasElement,
70        ctx: CanvasRenderingContext2d,
71    },
72    OffscreenCanvas {
73        canvas: OffscreenCanvas,
74        ctx: OffscreenCanvasRenderingContext2d,
75    },
76}
77
78impl<D: HasDisplayHandle, W: HasWindowHandle> WebImpl<D, W> {
79    fn from_canvas(canvas: HtmlCanvasElement, window: W) -> Result<Self, SoftBufferError> {
80        let ctx = Self::resolve_ctx(canvas.get_context("2d").ok(), "CanvasRenderingContext2d")?;
81
82        Ok(Self {
83            canvas: Canvas::Canvas { canvas, ctx },
84            buffer: Vec::new(),
85            buffer_presented: false,
86            size: None,
87            window_handle: window,
88            _display: PhantomData,
89        })
90    }
91
92    fn from_offscreen_canvas(canvas: OffscreenCanvas, window: W) -> Result<Self, SoftBufferError> {
93        let ctx = Self::resolve_ctx(
94            canvas.get_context("2d").ok(),
95            "OffscreenCanvasRenderingContext2d",
96        )?;
97
98        Ok(Self {
99            canvas: Canvas::OffscreenCanvas { canvas, ctx },
100            buffer: Vec::new(),
101            buffer_presented: false,
102            size: None,
103            window_handle: window,
104            _display: PhantomData,
105        })
106    }
107
108    fn resolve_ctx<T: JsCast>(
109        result: Option<Option<Object>>,
110        name: &str,
111    ) -> Result<T, SoftBufferError> {
112        let ctx = result
113            .swbuf_err("Canvas already controlled using `OffscreenCanvas`")?
114            .swbuf_err(format!(
115                "A canvas context other than `{name}` was already created"
116            ))?
117            .dyn_into()
118            .unwrap_or_else(|_| panic!("`getContext(\"2d\") didn't return a `{name}`"));
119
120        Ok(ctx)
121    }
122
123    fn present_with_damage(&mut self, damage: &[Rect]) -> Result<(), SoftBufferError> {
124        let (buffer_width, _buffer_height) = self
125            .size
126            .expect("Must set size of surface before calling `present_with_damage()`");
127
128        let union_damage = if let Some(rect) = util::union_damage(damage) {
129            rect
130        } else {
131            return Ok(());
132        };
133
134        // Create a bitmap from the buffer.
135        let bitmap: Vec<_> = self
136            .buffer
137            .chunks_exact(buffer_width.get() as usize)
138            .skip(union_damage.y as usize)
139            .take(union_damage.height.get() as usize)
140            .flat_map(|row| {
141                row.iter()
142                    .skip(union_damage.x as usize)
143                    .take(union_damage.width.get() as usize)
144            })
145            .copied()
146            .flat_map(|pixel| [(pixel >> 16) as u8, (pixel >> 8) as u8, pixel as u8, 255])
147            .collect();
148
149        debug_assert_eq!(
150            bitmap.len() as u32,
151            union_damage.width.get() * union_damage.height.get() * 4
152        );
153
154        #[cfg(target_feature = "atomics")]
155        #[allow(non_local_definitions)]
156        let result = {
157            // When using atomics, the underlying memory becomes `SharedArrayBuffer`,
158            // which can't be shared with `ImageData`.
159            use js_sys::{Uint8Array, Uint8ClampedArray};
160            use wasm_bindgen::prelude::wasm_bindgen;
161
162            #[wasm_bindgen]
163            extern "C" {
164                #[wasm_bindgen(js_name = ImageData)]
165                type ImageDataExt;
166
167                #[wasm_bindgen(catch, constructor, js_class = ImageData)]
168                fn new(array: Uint8ClampedArray, sw: u32) -> Result<ImageDataExt, JsValue>;
169            }
170
171            let array = Uint8Array::new_with_length(bitmap.len() as u32);
172            array.copy_from(&bitmap);
173            let array = Uint8ClampedArray::new(&array);
174            ImageDataExt::new(array, union_damage.width.get())
175                .map(JsValue::from)
176                .map(ImageData::unchecked_from_js)
177        };
178        #[cfg(not(target_feature = "atomics"))]
179        let result = ImageData::new_with_u8_clamped_array(
180            wasm_bindgen::Clamped(&bitmap),
181            union_damage.width.get(),
182        );
183        // This should only throw an error if the buffer we pass's size is incorrect.
184        let image_data = result.unwrap();
185
186        for rect in damage {
187            // This can only throw an error if `data` is detached, which is impossible.
188            self.canvas
189                .put_image_data(
190                    &image_data,
191                    union_damage.x.into(),
192                    union_damage.y.into(),
193                    (rect.x - union_damage.x).into(),
194                    (rect.y - union_damage.y).into(),
195                    rect.width.get().into(),
196                    rect.height.get().into(),
197                )
198                .unwrap();
199        }
200
201        self.buffer_presented = true;
202
203        Ok(())
204    }
205}
206
207impl<D: HasDisplayHandle, W: HasWindowHandle> SurfaceInterface<D, W> for WebImpl<D, W> {
208    type Context = WebDisplayImpl<D>;
209    type Buffer<'a>
210        = BufferImpl<'a, D, W>
211    where
212        Self: 'a;
213
214    fn new(window: W, display: &WebDisplayImpl<D>) -> Result<Self, InitError<W>> {
215        let raw = window.window_handle()?.as_raw();
216        let canvas: HtmlCanvasElement = match raw {
217            RawWindowHandle::Web(handle) => {
218                display
219                    .document
220                    .query_selector(&format!("canvas[data-raw-handle=\"{}\"]", handle.id))
221                    // `querySelector` only throws an error if the selector is invalid.
222                    .unwrap()
223                    .swbuf_err("No canvas found with the given id")?
224                    // We already made sure this was a canvas in `querySelector`.
225                    .unchecked_into()
226            }
227            RawWindowHandle::WebCanvas(handle) => {
228                let value: &JsValue = unsafe { handle.obj.cast().as_ref() };
229                value.clone().unchecked_into()
230            }
231            RawWindowHandle::WebOffscreenCanvas(handle) => {
232                let value: &JsValue = unsafe { handle.obj.cast().as_ref() };
233                let canvas: OffscreenCanvas = value.clone().unchecked_into();
234
235                return Self::from_offscreen_canvas(canvas, window).map_err(InitError::Failure);
236            }
237            _ => return Err(InitError::Unsupported(window)),
238        };
239
240        Self::from_canvas(canvas, window).map_err(InitError::Failure)
241    }
242
243    /// Get the inner window handle.
244    #[inline]
245    fn window(&self) -> &W {
246        &self.window_handle
247    }
248
249    /// De-duplicates the error handling between `HtmlCanvasElement` and `OffscreenCanvas`.
250    /// Resize the canvas to the given dimensions.
251    fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> {
252        if self.size != Some((width, height)) {
253            self.buffer_presented = false;
254            self.buffer.resize(total_len(width.get(), height.get()), 0);
255            self.canvas.set_width(width.get());
256            self.canvas.set_height(height.get());
257            self.size = Some((width, height));
258        }
259
260        Ok(())
261    }
262
263    fn buffer_mut(&mut self) -> Result<BufferImpl<'_, D, W>, SoftBufferError> {
264        Ok(BufferImpl { imp: self })
265    }
266
267    fn fetch(&mut self) -> Result<Vec<u32>, SoftBufferError> {
268        let (width, height) = self
269            .size
270            .expect("Must set size of surface before calling `fetch()`");
271
272        let image_data = self
273            .canvas
274            .get_image_data(0., 0., width.get().into(), height.get().into())
275            .ok()
276            // TODO: Can also error if width or height are 0.
277            .swbuf_err("`Canvas` contains pixels from a different origin")?;
278
279        Ok(image_data
280            .data()
281            .0
282            .chunks_exact(4)
283            .map(|chunk| u32::from_be_bytes([0, chunk[0], chunk[1], chunk[2]]))
284            .collect())
285    }
286}
287
288/// Extension methods for the Wasm target on [`Surface`](crate::Surface).
289pub trait SurfaceExtWeb: Sized {
290    /// Creates a new instance of this struct, using the provided [`HtmlCanvasElement`].
291    ///
292    /// # Errors
293    /// - If the canvas was already controlled by an `OffscreenCanvas`.
294    /// - If a another context then "2d" was already created for this canvas.
295    fn from_canvas(canvas: HtmlCanvasElement) -> Result<Self, SoftBufferError>;
296
297    /// Creates a new instance of this struct, using the provided [`OffscreenCanvas`].
298    ///
299    /// # Errors
300    /// If a another context then "2d" was already created for this canvas.
301    fn from_offscreen_canvas(offscreen_canvas: OffscreenCanvas) -> Result<Self, SoftBufferError>;
302}
303
304impl SurfaceExtWeb for crate::Surface<NoDisplayHandle, NoWindowHandle> {
305    fn from_canvas(canvas: HtmlCanvasElement) -> Result<Self, SoftBufferError> {
306        let imple = crate::SurfaceDispatch::Web(WebImpl::from_canvas(canvas, NoWindowHandle(()))?);
307
308        Ok(Self {
309            surface_impl: Box::new(imple),
310            _marker: PhantomData,
311        })
312    }
313
314    fn from_offscreen_canvas(offscreen_canvas: OffscreenCanvas) -> Result<Self, SoftBufferError> {
315        let imple = crate::SurfaceDispatch::Web(WebImpl::from_offscreen_canvas(
316            offscreen_canvas,
317            NoWindowHandle(()),
318        )?);
319
320        Ok(Self {
321            surface_impl: Box::new(imple),
322            _marker: PhantomData,
323        })
324    }
325}
326
327impl Canvas {
328    fn set_width(&self, width: u32) {
329        match self {
330            Self::Canvas { canvas, .. } => canvas.set_width(width),
331            Self::OffscreenCanvas { canvas, .. } => canvas.set_width(width),
332        }
333    }
334
335    fn set_height(&self, height: u32) {
336        match self {
337            Self::Canvas { canvas, .. } => canvas.set_height(height),
338            Self::OffscreenCanvas { canvas, .. } => canvas.set_height(height),
339        }
340    }
341
342    fn get_image_data(&self, sx: f64, sy: f64, sw: f64, sh: f64) -> Result<ImageData, JsValue> {
343        match self {
344            Canvas::Canvas { ctx, .. } => ctx.get_image_data(sx, sy, sw, sh),
345            Canvas::OffscreenCanvas { ctx, .. } => ctx.get_image_data(sx, sy, sw, sh),
346        }
347    }
348
349    // NOTE: suppress the lint because we mirror `CanvasRenderingContext2D.putImageData()`, and
350    // this is just an internal API used by this module only, so it's not too relevant.
351    #[allow(clippy::too_many_arguments)]
352    fn put_image_data(
353        &self,
354        imagedata: &ImageData,
355        dx: f64,
356        dy: f64,
357        dirty_x: f64,
358        dirty_y: f64,
359        width: f64,
360        height: f64,
361    ) -> Result<(), JsValue> {
362        match self {
363            Self::Canvas { ctx, .. } => ctx
364                .put_image_data_with_dirty_x_and_dirty_y_and_dirty_width_and_dirty_height(
365                    imagedata, dx, dy, dirty_x, dirty_y, width, height,
366                ),
367            Self::OffscreenCanvas { ctx, .. } => ctx
368                .put_image_data_with_dirty_x_and_dirty_y_and_dirty_width_and_dirty_height(
369                    imagedata, dx, dy, dirty_x, dirty_y, width, height,
370                ),
371        }
372    }
373}
374
375pub struct BufferImpl<'a, D, W> {
376    imp: &'a mut WebImpl<D, W>,
377}
378
379impl<D: HasDisplayHandle, W: HasWindowHandle> BufferInterface for BufferImpl<'_, D, W> {
380    fn pixels(&self) -> &[u32] {
381        &self.imp.buffer
382    }
383
384    fn pixels_mut(&mut self) -> &mut [u32] {
385        &mut self.imp.buffer
386    }
387
388    fn age(&self) -> u8 {
389        if self.imp.buffer_presented {
390            1
391        } else {
392            0
393        }
394    }
395
396    /// Push the buffer to the canvas.
397    fn present(self) -> Result<(), SoftBufferError> {
398        let (width, height) = self
399            .imp
400            .size
401            .expect("Must set size of surface before calling `present()`");
402        self.imp.present_with_damage(&[Rect {
403            x: 0,
404            y: 0,
405            width,
406            height,
407        }])
408    }
409
410    fn present_with_damage(self, damage: &[Rect]) -> Result<(), SoftBufferError> {
411        self.imp.present_with_damage(damage)
412    }
413}
414
415#[inline(always)]
416fn total_len(width: u32, height: u32) -> usize {
417    // Convert width and height to `usize`, then multiply.
418    width
419        .try_into()
420        .ok()
421        .and_then(|w: usize| height.try_into().ok().and_then(|h| w.checked_mul(h)))
422        .unwrap_or_else(|| {
423            panic!(
424                "Overflow when calculating total length of buffer: {}x{}",
425                width, height
426            );
427        })
428}