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