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