Skip to main content

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