script/dom/
paintworkletglobalscope.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::collections::hash_map::Entry;
7use std::ptr::null_mut;
8use std::rc::Rc;
9use std::sync::{Arc, Mutex};
10use std::thread;
11use std::time::Duration;
12
13use base::id::PipelineId;
14use crossbeam_channel::{Sender, unbounded};
15use dom_struct::dom_struct;
16use euclid::{Scale, Size2D};
17use js::jsapi::{
18    HandleValueArray, Heap, IsCallable, IsConstructor, JS_ClearPendingException,
19    JS_IsExceptionPending, JSAutoRealm, JSObject, NewArrayObject, Value,
20};
21use js::jsval::{JSVal, ObjectValue, UndefinedValue};
22use js::rust::HandleValue;
23use js::rust::wrappers::{Call, Construct1};
24use net_traits::image_cache::ImageCache;
25use pixels::PixelFormat;
26use script_traits::{DrawAPaintImageResult, PaintWorkletError, Painter};
27use servo_config::pref;
28use servo_url::ServoUrl;
29use style_traits::{CSSPixel, SpeculativePainter};
30use stylo_atoms::Atom;
31use webrender_api::units::DevicePixel;
32
33use super::bindings::trace::HashMapTracedValues;
34use crate::dom::bindings::callback::CallbackContainer;
35use crate::dom::bindings::cell::DomRefCell;
36use crate::dom::bindings::codegen::Bindings::PaintWorkletGlobalScopeBinding;
37use crate::dom::bindings::codegen::Bindings::PaintWorkletGlobalScopeBinding::PaintWorkletGlobalScopeMethods;
38use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction;
39use crate::dom::bindings::conversions::{get_property, get_property_jsval};
40use crate::dom::bindings::error::{Error, Fallible};
41use crate::dom::bindings::inheritance::Castable;
42use crate::dom::bindings::reflector::DomObject;
43use crate::dom::bindings::root::{Dom, DomRoot};
44use crate::dom::bindings::str::DOMString;
45use crate::dom::cssstylevalue::CSSStyleValue;
46use crate::dom::globalscope::GlobalScope;
47use crate::dom::paintrenderingcontext2d::PaintRenderingContext2D;
48use crate::dom::paintsize::PaintSize;
49use crate::dom::stylepropertymapreadonly::StylePropertyMapReadOnly;
50use crate::dom::worklet::WorkletExecutor;
51use crate::dom::workletglobalscope::{WorkletGlobalScope, WorkletGlobalScopeInit, WorkletTask};
52use crate::script_runtime::CanGc;
53
54/// <https://drafts.css-houdini.org/css-paint-api/#paintworkletglobalscope>
55#[dom_struct]
56pub(crate) struct PaintWorkletGlobalScope {
57    /// The worklet global for this object
58    worklet_global: WorkletGlobalScope,
59    /// The image cache
60    #[ignore_malloc_size_of = "Arc"]
61    #[no_trace]
62    image_cache: Arc<dyn ImageCache>,
63    /// <https://drafts.css-houdini.org/css-paint-api/#paint-definitions>
64    paint_definitions: DomRefCell<HashMapTracedValues<Atom, Box<PaintDefinition>>>,
65    /// <https://drafts.css-houdini.org/css-paint-api/#paint-class-instances>
66    #[ignore_malloc_size_of = "mozjs"]
67    paint_class_instances: DomRefCell<HashMapTracedValues<Atom, Box<Heap<JSVal>>>>,
68    /// The most recent name the worklet was called with
69    #[no_trace]
70    cached_name: DomRefCell<Atom>,
71    /// The most recent size the worklet was drawn at
72    #[no_trace]
73    cached_size: Cell<Size2D<f32, CSSPixel>>,
74    /// The most recent device pixel ratio the worklet was drawn at
75    #[no_trace]
76    cached_device_pixel_ratio: Cell<Scale<f32, CSSPixel, DevicePixel>>,
77    /// The most recent properties the worklet was drawn at
78    #[no_trace]
79    cached_properties: DomRefCell<Vec<(Atom, String)>>,
80    /// The most recent arguments the worklet was drawn at
81    cached_arguments: DomRefCell<Vec<String>>,
82    /// The most recent result
83    #[no_trace]
84    cached_result: DomRefCell<DrawAPaintImageResult>,
85}
86
87impl PaintWorkletGlobalScope {
88    #[allow(unsafe_code)]
89    pub(crate) fn new(
90        pipeline_id: PipelineId,
91        base_url: ServoUrl,
92        executor: WorkletExecutor,
93        init: &WorkletGlobalScopeInit,
94    ) -> DomRoot<PaintWorkletGlobalScope> {
95        debug!(
96            "Creating paint worklet global scope for pipeline {}.",
97            pipeline_id
98        );
99        let global = Box::new(PaintWorkletGlobalScope {
100            worklet_global: WorkletGlobalScope::new_inherited(
101                pipeline_id,
102                base_url,
103                executor,
104                init,
105            ),
106            image_cache: init.image_cache.clone(),
107            paint_definitions: Default::default(),
108            paint_class_instances: Default::default(),
109            cached_name: DomRefCell::new(Atom::from("")),
110            cached_size: Cell::new(Size2D::zero()),
111            cached_device_pixel_ratio: Cell::new(Scale::new(1.0)),
112            cached_properties: Default::default(),
113            cached_arguments: Default::default(),
114            cached_result: DomRefCell::new(DrawAPaintImageResult {
115                width: 0,
116                height: 0,
117                format: PixelFormat::BGRA8,
118                image_key: None,
119                missing_image_urls: Vec::new(),
120            }),
121        });
122        PaintWorkletGlobalScopeBinding::Wrap::<crate::DomTypeHolder>(GlobalScope::get_cx(), global)
123    }
124
125    pub(crate) fn image_cache(&self) -> Arc<dyn ImageCache> {
126        self.image_cache.clone()
127    }
128
129    pub(crate) fn perform_a_worklet_task(&self, task: PaintWorkletTask) {
130        match task {
131            PaintWorkletTask::DrawAPaintImage(
132                name,
133                size,
134                device_pixel_ratio,
135                properties,
136                arguments,
137                sender,
138            ) => {
139                let cache_hit = (*self.cached_name.borrow() == name) &&
140                    (self.cached_size.get() == size) &&
141                    (self.cached_device_pixel_ratio.get() == device_pixel_ratio) &&
142                    (*self.cached_properties.borrow() == properties) &&
143                    (*self.cached_arguments.borrow() == arguments);
144                let result = if cache_hit {
145                    debug!("Cache hit on paint worklet {}!", name);
146                    self.cached_result.borrow().clone()
147                } else {
148                    debug!("Cache miss on paint worklet {}!", name);
149                    let map = StylePropertyMapReadOnly::from_iter(
150                        self.upcast(),
151                        properties.iter().cloned(),
152                        CanGc::note(),
153                    );
154                    let result =
155                        self.draw_a_paint_image(&name, size, device_pixel_ratio, &map, &arguments);
156                    if (result.image_key.is_some()) && (result.missing_image_urls.is_empty()) {
157                        *self.cached_name.borrow_mut() = name;
158                        self.cached_size.set(size);
159                        self.cached_device_pixel_ratio.set(device_pixel_ratio);
160                        *self.cached_properties.borrow_mut() = properties;
161                        *self.cached_arguments.borrow_mut() = arguments;
162                        *self.cached_result.borrow_mut() = result.clone();
163                    }
164                    result
165                };
166                let _ = sender.send(result);
167            },
168            PaintWorkletTask::SpeculativelyDrawAPaintImage(name, properties, arguments) => {
169                let should_speculate = (*self.cached_name.borrow() != name) ||
170                    (*self.cached_properties.borrow() != properties) ||
171                    (*self.cached_arguments.borrow() != arguments);
172                if should_speculate {
173                    let size = self.cached_size.get();
174                    let device_pixel_ratio = self.cached_device_pixel_ratio.get();
175                    let map = StylePropertyMapReadOnly::from_iter(
176                        self.upcast(),
177                        properties.iter().cloned(),
178                        CanGc::note(),
179                    );
180                    let result =
181                        self.draw_a_paint_image(&name, size, device_pixel_ratio, &map, &arguments);
182                    if (result.image_key.is_some()) && (result.missing_image_urls.is_empty()) {
183                        *self.cached_name.borrow_mut() = name;
184                        *self.cached_properties.borrow_mut() = properties;
185                        *self.cached_arguments.borrow_mut() = arguments;
186                        *self.cached_result.borrow_mut() = result;
187                    }
188                }
189            },
190        }
191    }
192
193    /// <https://drafts.css-houdini.org/css-paint-api/#draw-a-paint-image>
194    fn draw_a_paint_image(
195        &self,
196        name: &Atom,
197        size_in_px: Size2D<f32, CSSPixel>,
198        device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>,
199        properties: &StylePropertyMapReadOnly,
200        arguments: &[String],
201    ) -> DrawAPaintImageResult {
202        let size_in_dpx = size_in_px * device_pixel_ratio;
203        let size_in_dpx = Size2D::new(
204            size_in_dpx.width.abs() as u32,
205            size_in_dpx.height.abs() as u32,
206        );
207
208        // TODO: Steps 1-5.
209
210        // TODO: document paint definitions.
211        self.invoke_a_paint_callback(
212            name,
213            size_in_px,
214            size_in_dpx,
215            device_pixel_ratio,
216            properties,
217            arguments,
218            CanGc::note(),
219        )
220    }
221
222    /// <https://drafts.css-houdini.org/css-paint-api/#invoke-a-paint-callback>
223    #[allow(clippy::too_many_arguments)]
224    #[allow(unsafe_code)]
225    fn invoke_a_paint_callback(
226        &self,
227        name: &Atom,
228        size_in_px: Size2D<f32, CSSPixel>,
229        size_in_dpx: Size2D<u32, DevicePixel>,
230        device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>,
231        properties: &StylePropertyMapReadOnly,
232        arguments: &[String],
233        can_gc: CanGc,
234    ) -> DrawAPaintImageResult {
235        debug!(
236            "Invoking a paint callback {}({},{}) at {:?}.",
237            name, size_in_px.width, size_in_px.height, device_pixel_ratio
238        );
239
240        let cx = WorkletGlobalScope::get_cx();
241        let _ac = JSAutoRealm::new(*cx, self.worklet_global.reflector().get_jsobject().get());
242
243        // TODO: Steps 1-2.1.
244        // Step 2.2-5.1.
245        rooted!(in(*cx) let mut class_constructor = UndefinedValue());
246        rooted!(in(*cx) let mut paint_function = UndefinedValue());
247        let rendering_context = match self.paint_definitions.borrow().get(name) {
248            None => {
249                // Step 2.2.
250                warn!("Drawing un-registered paint definition {}.", name);
251                return self.invalid_image(size_in_dpx, vec![]);
252            },
253            Some(definition) => {
254                // Step 5.1
255                if !definition.constructor_valid_flag.get() {
256                    debug!("Drawing invalid paint definition {}.", name);
257                    return self.invalid_image(size_in_dpx, vec![]);
258                }
259                class_constructor.set(definition.class_constructor.get());
260                paint_function.set(definition.paint_function.get());
261                DomRoot::from_ref(&*definition.context)
262            },
263        };
264
265        // Steps 5.2-5.4
266        // TODO: the spec requires calling the constructor now, but we might want to
267        // prepopulate the paint instance in `RegisterPaint`, to avoid calling it in
268        // the primary worklet thread.
269        // https://github.com/servo/servo/issues/17377
270        rooted!(in(*cx) let mut paint_instance = UndefinedValue());
271        match self.paint_class_instances.borrow_mut().entry(name.clone()) {
272            Entry::Occupied(entry) => paint_instance.set(entry.get().get()),
273            Entry::Vacant(entry) => {
274                // Step 5.2-5.3
275                let args = HandleValueArray::empty();
276                rooted!(in(*cx) let mut result = null_mut::<JSObject>());
277                unsafe {
278                    Construct1(*cx, class_constructor.handle(), &args, result.handle_mut());
279                }
280                paint_instance.set(ObjectValue(result.get()));
281                if unsafe { JS_IsExceptionPending(*cx) } {
282                    debug!("Paint constructor threw an exception {}.", name);
283                    unsafe {
284                        JS_ClearPendingException(*cx);
285                    }
286                    self.paint_definitions
287                        .borrow_mut()
288                        .get_mut(name)
289                        .expect("Vanishing paint definition.")
290                        .constructor_valid_flag
291                        .set(false);
292                    return self.invalid_image(size_in_dpx, vec![]);
293                }
294                // Step 5.4
295                entry
296                    .insert(Box::<Heap<Value>>::default())
297                    .set(paint_instance.get());
298            },
299        };
300
301        // TODO: Steps 6-7
302        // Step 8
303        // TODO: the spec requires creating a new paint rendering context each time,
304        // this code recycles the same one.
305        rendering_context.set_bitmap_dimensions(size_in_px, device_pixel_ratio);
306
307        // Step 9
308        let paint_size = PaintSize::new(self, size_in_px, can_gc);
309
310        // TODO: Step 10
311        // Steps 11-12
312        debug!("Invoking paint function {}.", name);
313        rooted_vec!(let mut arguments_values);
314        for argument in arguments {
315            let style_value = CSSStyleValue::new(self.upcast(), argument.clone(), can_gc);
316            arguments_values.push(ObjectValue(style_value.reflector().get_jsobject().get()));
317        }
318        let arguments_value_array = HandleValueArray::from(&arguments_values);
319        rooted!(in(*cx) let argument_object = unsafe { NewArrayObject(*cx, &arguments_value_array) });
320
321        rooted_vec!(let mut callback_args);
322        callback_args.push(ObjectValue(
323            rendering_context.reflector().get_jsobject().get(),
324        ));
325        callback_args.push(ObjectValue(paint_size.reflector().get_jsobject().get()));
326        callback_args.push(ObjectValue(properties.reflector().get_jsobject().get()));
327        callback_args.push(ObjectValue(argument_object.get()));
328        let args = HandleValueArray::from(&callback_args);
329
330        rooted!(in(*cx) let mut result = UndefinedValue());
331        unsafe {
332            Call(
333                *cx,
334                paint_instance.handle(),
335                paint_function.handle(),
336                &args,
337                result.handle_mut(),
338            );
339        }
340        let missing_image_urls = rendering_context.take_missing_image_urls();
341
342        // Step 13.
343        if unsafe { JS_IsExceptionPending(*cx) } {
344            debug!("Paint function threw an exception {}.", name);
345            unsafe {
346                JS_ClearPendingException(*cx);
347            }
348            return self.invalid_image(size_in_dpx, missing_image_urls);
349        }
350
351        rendering_context.update_rendering();
352
353        DrawAPaintImageResult {
354            width: size_in_dpx.width,
355            height: size_in_dpx.height,
356            format: PixelFormat::BGRA8,
357            image_key: Some(rendering_context.image_key()),
358            missing_image_urls,
359        }
360    }
361
362    // https://drafts.csswg.org/css-images-4/#invalid-image
363    fn invalid_image(
364        &self,
365        size: Size2D<u32, DevicePixel>,
366        missing_image_urls: Vec<ServoUrl>,
367    ) -> DrawAPaintImageResult {
368        debug!("Returning an invalid image.");
369        DrawAPaintImageResult {
370            width: size.width,
371            height: size.height,
372            format: PixelFormat::BGRA8,
373            image_key: None,
374            missing_image_urls,
375        }
376    }
377
378    fn painter(&self, name: Atom) -> Box<dyn Painter> {
379        // Rather annoyingly we have to use a mutex here to make the painter Sync.
380        struct WorkletPainter {
381            name: Atom,
382            executor: Mutex<WorkletExecutor>,
383        }
384        impl SpeculativePainter for WorkletPainter {
385            fn speculatively_draw_a_paint_image(
386                &self,
387                properties: Vec<(Atom, String)>,
388                arguments: Vec<String>,
389            ) {
390                let name = self.name.clone();
391                let task =
392                    PaintWorkletTask::SpeculativelyDrawAPaintImage(name, properties, arguments);
393                self.executor
394                    .lock()
395                    .expect("Locking a painter.")
396                    .schedule_a_worklet_task(WorkletTask::Paint(task));
397            }
398        }
399        impl Painter for WorkletPainter {
400            fn draw_a_paint_image(
401                &self,
402                size: Size2D<f32, CSSPixel>,
403                device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>,
404                properties: Vec<(Atom, String)>,
405                arguments: Vec<String>,
406            ) -> Result<DrawAPaintImageResult, PaintWorkletError> {
407                let name = self.name.clone();
408                let (sender, receiver) = unbounded();
409                let task = PaintWorkletTask::DrawAPaintImage(
410                    name,
411                    size,
412                    device_pixel_ratio,
413                    properties,
414                    arguments,
415                    sender,
416                );
417                self.executor
418                    .lock()
419                    .expect("Locking a painter.")
420                    .schedule_a_worklet_task(WorkletTask::Paint(task));
421
422                let timeout = pref!(dom_worklet_timeout_ms) as u64;
423
424                receiver
425                    .recv_timeout(Duration::from_millis(timeout))
426                    .map_err(PaintWorkletError::from)
427            }
428        }
429        Box::new(WorkletPainter {
430            name,
431            executor: Mutex::new(self.worklet_global.executor()),
432        })
433    }
434}
435
436/// Tasks which can be peformed by a paint worklet
437pub(crate) enum PaintWorkletTask {
438    DrawAPaintImage(
439        Atom,
440        Size2D<f32, CSSPixel>,
441        Scale<f32, CSSPixel, DevicePixel>,
442        Vec<(Atom, String)>,
443        Vec<String>,
444        Sender<DrawAPaintImageResult>,
445    ),
446    SpeculativelyDrawAPaintImage(Atom, Vec<(Atom, String)>, Vec<String>),
447}
448
449/// A paint definition
450/// <https://drafts.css-houdini.org/css-paint-api/#paint-definition>
451/// This type is dangerous, because it contains uboxed `Heap<JSVal>` values,
452/// which can't be moved.
453#[derive(JSTraceable, MallocSizeOf)]
454#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
455struct PaintDefinition {
456    #[ignore_malloc_size_of = "mozjs"]
457    class_constructor: Heap<JSVal>,
458    #[ignore_malloc_size_of = "mozjs"]
459    paint_function: Heap<JSVal>,
460    constructor_valid_flag: Cell<bool>,
461    context_alpha_flag: bool,
462    // TODO: this should be a list of CSS syntaxes.
463    input_arguments_len: usize,
464    // TODO: the spec calls for fresh rendering contexts each time a paint image is drawn,
465    // but to avoid having the primary worklet thread create a new renering context,
466    // we recycle them.
467    context: Dom<PaintRenderingContext2D>,
468}
469
470impl PaintDefinition {
471    fn new(
472        class_constructor: HandleValue,
473        paint_function: HandleValue,
474        alpha: bool,
475        input_arguments_len: usize,
476        context: &PaintRenderingContext2D,
477    ) -> Box<PaintDefinition> {
478        let result = Box::new(PaintDefinition {
479            class_constructor: Heap::default(),
480            paint_function: Heap::default(),
481            constructor_valid_flag: Cell::new(true),
482            context_alpha_flag: alpha,
483            input_arguments_len,
484            context: Dom::from_ref(context),
485        });
486        result.class_constructor.set(class_constructor.get());
487        result.paint_function.set(paint_function.get());
488        result
489    }
490}
491
492impl PaintWorkletGlobalScopeMethods<crate::DomTypeHolder> for PaintWorkletGlobalScope {
493    #[allow(unsafe_code)]
494    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
495    /// <https://drafts.css-houdini.org/css-paint-api/#dom-paintworkletglobalscope-registerpaint>
496    fn RegisterPaint(&self, name: DOMString, paint_ctor: Rc<VoidFunction>) -> Fallible<()> {
497        let name = Atom::from(name);
498        let cx = WorkletGlobalScope::get_cx();
499        rooted!(in(*cx) let paint_obj = paint_ctor.callback_holder().get());
500        rooted!(in(*cx) let paint_val = ObjectValue(paint_obj.get()));
501
502        debug!("Registering paint image name {}.", name);
503
504        // Step 1.
505        if name.is_empty() {
506            return Err(Error::Type(String::from("Empty paint name.")));
507        }
508
509        // Step 2-3.
510        if self.paint_definitions.borrow().contains_key(&name) {
511            return Err(Error::InvalidModification);
512        }
513
514        // Step 4-6.
515        let mut property_names: Vec<String> =
516            unsafe { get_property(*cx, paint_obj.handle(), "inputProperties", ()) }?
517                .unwrap_or_default();
518        let properties = property_names.drain(..).map(Atom::from).collect();
519
520        // Step 7-9.
521        let input_arguments: Vec<String> =
522            unsafe { get_property(*cx, paint_obj.handle(), "inputArguments", ()) }?
523                .unwrap_or_default();
524
525        // TODO: Steps 10-11.
526
527        // Steps 12-13.
528        let alpha: bool =
529            unsafe { get_property(*cx, paint_obj.handle(), "alpha", ()) }?.unwrap_or(true);
530
531        // Step 14
532        if unsafe { !IsConstructor(paint_obj.get()) } {
533            return Err(Error::Type(String::from("Not a constructor.")));
534        }
535
536        // Steps 15-16
537        rooted!(in(*cx) let mut prototype = UndefinedValue());
538        unsafe {
539            get_property_jsval(*cx, paint_obj.handle(), "prototype", prototype.handle_mut())?;
540        }
541        if !prototype.is_object() {
542            return Err(Error::Type(String::from("Prototype is not an object.")));
543        }
544        rooted!(in(*cx) let prototype = prototype.to_object());
545
546        // Steps 17-18
547        rooted!(in(*cx) let mut paint_function = UndefinedValue());
548        unsafe {
549            get_property_jsval(
550                *cx,
551                prototype.handle(),
552                "paint",
553                paint_function.handle_mut(),
554            )?;
555        }
556        if !paint_function.is_object() || unsafe { !IsCallable(paint_function.to_object()) } {
557            return Err(Error::Type(String::from("Paint function is not callable.")));
558        }
559
560        // Step 19.
561        let Some(context) = PaintRenderingContext2D::new(self, CanGc::note()) else {
562            return Err(Error::Operation);
563        };
564        let definition = PaintDefinition::new(
565            paint_val.handle(),
566            paint_function.handle(),
567            alpha,
568            input_arguments.len(),
569            &context,
570        );
571
572        // Step 20.
573        debug!("Registering definition {}.", name);
574        self.paint_definitions
575            .borrow_mut()
576            .insert(name.clone(), definition);
577
578        // TODO: Step 21.
579
580        // Inform layout that there is a registered paint worklet.
581        // TODO: layout will end up getting this message multiple times.
582        let painter = self.painter(name.clone());
583        self.worklet_global
584            .register_paint_worklet(name, properties, painter);
585
586        Ok(())
587    }
588
589    /// This is a blocking sleep function available in the paint worklet
590    /// global scope behind the dom.worklet.enabled +
591    /// dom.worklet.blockingsleep.enabled prefs. It is to be used only for
592    /// testing, e.g., timeouts, where otherwise one would need busy waiting
593    /// to make sure a certain timeout is triggered.
594    /// check-tidy: no specs after this line
595    fn Sleep(&self, ms: u64) {
596        thread::sleep(Duration::from_millis(ms));
597    }
598}