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