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