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