1use std::cell::Cell;
6use std::ptr::{NonNull, null_mut};
7use std::rc::Rc;
8use std::sync::atomic::AtomicBool;
9use std::sync::{Arc, Mutex};
10use std::thread;
11use std::time::Duration;
12
13use crossbeam_channel::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::wrappers2::{
21 Call, Construct1, JS_ClearPendingException, JS_IsExceptionPending, NewArrayObject,
22};
23use js::rust::{HandleValue, MutableHandle};
24use net_traits::image_cache::ImageCache;
25use pixels::PixelFormat;
26use script_bindings::cell::DomRefCell;
27use script_bindings::interfaces::HasOrigin;
28use script_bindings::reflector::DomObject;
29use script_traits::{DrawAPaintImageResult, PaintWorkletError, Painter};
30use servo_base::id::PipelineId;
31use servo_config::pref;
32use servo_url::{MutableOrigin, ServoUrl};
33use style_traits::{CSSPixel, SpeculativePainter};
34use stylo_atoms::Atom;
35use webrender_api::units::DevicePixel;
36
37use crate::dom::bindings::callback::CallbackContainer;
38use crate::dom::bindings::codegen::Bindings::PaintWorkletGlobalScopeBinding;
39use crate::dom::bindings::codegen::Bindings::PaintWorkletGlobalScopeBinding::PaintWorkletGlobalScopeMethods;
40use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction;
41use crate::dom::bindings::conversions::{get_property, get_property_jsval};
42use crate::dom::bindings::error::{Error, Fallible};
43use crate::dom::bindings::inheritance::Castable;
44use crate::dom::bindings::root::{Dom, DomRoot};
45use crate::dom::bindings::str::DOMString;
46use crate::dom::bindings::trace::HashMapTracedValues;
47use crate::dom::css::cssstylevalue::CSSStyleValue;
48use crate::dom::css::stylepropertymapreadonly::StylePropertyMapReadOnly;
49use crate::dom::paintrenderingcontext2d::PaintRenderingContext2D;
50use crate::dom::paintsize::PaintSize;
51use crate::dom::worklet::WorkletExecutor;
52use crate::dom::workletglobalscope::{WorkletGlobalScope, WorkletGlobalScopeInit};
53
54#[dom_struct]
56pub(crate) struct PaintWorkletGlobalScope {
57 worklet_global: WorkletGlobalScope,
59 #[ignore_malloc_size_of = "ImageCache"]
61 #[no_trace]
62 image_cache: Arc<dyn ImageCache>,
63 paint_definitions: DomRefCell<HashMapTracedValues<Atom, Box<PaintDefinition>>>,
65 #[ignore_malloc_size_of = "mozjs"]
67 paint_class_instances: DomRefCell<HashMapTracedValues<Atom, Box<Heap<JSVal>>>>,
68 #[no_trace]
70 cached_name: DomRefCell<Atom>,
71 #[no_trace]
73 cached_size: Cell<Size2D<f32, CSSPixel>>,
74 #[no_trace]
76 cached_device_pixel_ratio: Cell<Scale<f32, CSSPixel, DevicePixel>>,
77 #[no_trace]
79 cached_properties: DomRefCell<Vec<(Atom, String)>>,
80 cached_arguments: DomRefCell<Vec<String>>,
82 #[no_trace]
84 cached_result: DomRefCell<DrawAPaintImageResult>,
85}
86
87impl PaintWorkletGlobalScope {
88 #[allow(clippy::too_many_arguments)]
89 pub(crate) fn new(
90 cx: &mut JSContext,
91 pipeline_id: PipelineId,
92 base_url: ServoUrl,
93 inherited_secure_context: Option<bool>,
94 executor: WorkletExecutor,
95 init: &WorkletGlobalScopeInit,
96 closing: Arc<AtomicBool>,
97 ) -> DomRoot<PaintWorkletGlobalScope> {
98 debug!(
99 "Creating paint worklet global scope for pipeline {}.",
100 pipeline_id
101 );
102 let global = Box::new(PaintWorkletGlobalScope {
103 worklet_global: WorkletGlobalScope::new_inherited(
104 pipeline_id,
105 base_url,
106 inherited_secure_context,
107 executor,
108 init,
109 closing,
110 ),
111 image_cache: init.image_cache.clone(),
112 paint_definitions: Default::default(),
113 paint_class_instances: Default::default(),
114 cached_name: DomRefCell::new(Atom::from("")),
115 cached_size: Cell::new(Size2D::zero()),
116 cached_device_pixel_ratio: Cell::new(Scale::new(1.0)),
117 cached_properties: Default::default(),
118 cached_arguments: Default::default(),
119 cached_result: DomRefCell::new(DrawAPaintImageResult {
120 width: 0,
121 height: 0,
122 format: PixelFormat::BGRA8,
123 image_key: None,
124 missing_image_urls: Vec::new(),
125 }),
126 });
127 PaintWorkletGlobalScopeBinding::Wrap::<crate::DomTypeHolder>(cx, &global.origin(), global)
128 }
129
130 pub(crate) fn image_cache(&self) -> Arc<dyn ImageCache> {
131 self.image_cache.clone()
132 }
133
134 fn has_cached_paint_image(
135 &self,
136 name: &Atom,
137 size: Size2D<f32, CSSPixel>,
138 device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>,
139 properties: &[(Atom, String)],
140 arguments: &[String],
141 ) -> bool {
142 (&*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 }
148
149 fn set_cached_paint_image(
150 &self,
151 name: Atom,
152 size: Size2D<f32, CSSPixel>,
153 device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>,
154 properties: Vec<(Atom, String)>,
155 arguments: Vec<String>,
156 result: DrawAPaintImageResult,
157 ) {
158 *self.cached_name.borrow_mut() = name;
159 self.cached_size.set(size);
160 self.cached_device_pixel_ratio.set(device_pixel_ratio);
161 *self.cached_properties.borrow_mut() = properties;
162 *self.cached_arguments.borrow_mut() = arguments;
163 *self.cached_result.borrow_mut() = result;
164 }
165
166 fn draw_a_paint_image(
168 &self,
169 cx: &mut JSContext,
170 name: &Atom,
171 size_in_px: Size2D<f32, CSSPixel>,
172 device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>,
173 properties: &StylePropertyMapReadOnly,
174 arguments: &[String],
175 ) -> DrawAPaintImageResult {
176 let size_in_dpx = size_in_px * device_pixel_ratio;
177 let size_in_dpx = Size2D::new(
178 size_in_dpx.width.abs() as u32,
179 size_in_dpx.height.abs() as u32,
180 );
181
182 self.invoke_a_paint_callback(
186 cx,
187 name,
188 size_in_px,
189 size_in_dpx,
190 device_pixel_ratio,
191 properties,
192 arguments,
193 )
194 }
195
196 #[expect(unsafe_code)]
197 fn get_or_create_paint_instance(
201 &self,
202 cx: &mut JSContext,
203 mut paint_instance: MutableHandle<Value>,
204 name: Atom,
205 class_constructor: HandleValue,
206 size_in_dpx: Size2D<u32, DevicePixel>,
207 ) -> Result<(), DrawAPaintImageResult> {
208 if let Some(entry) = self.paint_class_instances.borrow().get(&name) {
209 paint_instance.set(entry.get());
210 return Ok(());
211 }
212
213 let args = HandleValueArray::empty();
219 rooted!(&in(cx) let mut result = null_mut::<JSObject>());
220 unsafe {
221 Construct1(cx, class_constructor, &args, result.handle_mut());
222 }
223 paint_instance.set(ObjectValue(result.get()));
224 if unsafe { JS_IsExceptionPending(cx) } {
225 debug!("Paint constructor threw an exception {}.", name);
226 unsafe {
227 JS_ClearPendingException(cx);
228 }
229 self.paint_definitions
230 .safe_borrow_mut(cx)
231 .get_mut(&name)
232 .expect("Vanishing paint definition.")
233 .constructor_valid_flag
234 .set(false);
235 return Err(self.invalid_image(size_in_dpx, vec![]));
237 }
238 self.paint_class_instances
240 .safe_borrow_mut(cx)
241 .entry(name)
242 .or_default()
243 .set(paint_instance.get());
244 Ok(())
245 }
246
247 #[expect(clippy::too_many_arguments)]
249 #[expect(unsafe_code)]
250 fn invoke_a_paint_callback(
251 &self,
252 cx: &mut JSContext,
253 name: &Atom,
254 size_in_px: Size2D<f32, CSSPixel>,
255 size_in_dpx: Size2D<u32, DevicePixel>,
256 device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>,
257 properties: &StylePropertyMapReadOnly,
258 arguments: &[String],
259 ) -> DrawAPaintImageResult {
260 debug!(
261 "Invoking a paint callback {}({},{}) at {:?}.",
262 name, size_in_px.width, size_in_px.height, device_pixel_ratio
263 );
264
265 let mut realm = AutoRealm::new(
266 cx,
267 NonNull::new(self.worklet_global.reflector().get_jsobject().get()).unwrap(),
268 );
269 let cx = &mut *realm;
270
271 rooted!(&in(cx) let mut class_constructor = UndefinedValue());
274 rooted!(&in(cx) let mut paint_function = UndefinedValue());
275 let rendering_context = match self.paint_definitions.borrow().get(name) {
276 None => {
277 warn!("Drawing un-registered paint definition {}.", name);
279 return self.invalid_image(size_in_dpx, vec![]);
280 },
281 Some(definition) => {
282 if !definition.constructor_valid_flag.get() {
284 debug!("Drawing invalid paint definition {}.", name);
285 return self.invalid_image(size_in_dpx, vec![]);
286 }
287 class_constructor.set(definition.class_constructor.get());
288 paint_function.set(definition.paint_function.get());
289 DomRoot::from_ref(&*definition.context)
290 },
291 };
292
293 rooted!(&in(cx) let mut paint_instance = UndefinedValue());
299 if let Err(early_return) = self.get_or_create_paint_instance(
300 cx,
301 paint_instance.handle_mut(),
302 name.clone(),
303 class_constructor.handle(),
304 size_in_dpx,
305 ) {
306 return early_return;
307 }
308
309 rendering_context.set_bitmap_dimensions(size_in_px, device_pixel_ratio);
314
315 let paint_size = PaintSize::new(cx, self, size_in_px);
317
318 debug!("Invoking paint function {}.", name);
321 rooted_vec!(let mut arguments_values);
322 for argument in arguments {
323 let style_value = CSSStyleValue::new(cx, self.upcast(), argument.clone());
324 arguments_values.push(ObjectValue(style_value.reflector().get_jsobject().get()));
325 }
326 let arguments_value_array = HandleValueArray::from(&arguments_values);
327 rooted!(&in(cx) let argument_object = unsafe { NewArrayObject(cx, &arguments_value_array) });
328
329 rooted_vec!(let mut callback_args);
330 callback_args.push(ObjectValue(
331 rendering_context.reflector().get_jsobject().get(),
332 ));
333 callback_args.push(ObjectValue(paint_size.reflector().get_jsobject().get()));
334 callback_args.push(ObjectValue(properties.reflector().get_jsobject().get()));
335 callback_args.push(ObjectValue(argument_object.get()));
336 let args = HandleValueArray::from(&callback_args);
337
338 rooted!(&in(cx) let mut result = UndefinedValue());
339 unsafe {
340 Call(
341 cx,
342 paint_instance.handle(),
343 paint_function.handle(),
344 &args,
345 result.handle_mut(),
346 );
347 }
348 let missing_image_urls = rendering_context.take_missing_image_urls();
349
350 if unsafe { JS_IsExceptionPending(cx) } {
352 debug!("Paint function threw an exception {}.", name);
353 unsafe {
354 JS_ClearPendingException(cx);
355 }
356 return self.invalid_image(size_in_dpx, missing_image_urls);
357 }
358
359 rendering_context.update_rendering();
360
361 DrawAPaintImageResult {
362 width: size_in_dpx.width,
363 height: size_in_dpx.height,
364 format: PixelFormat::BGRA8,
365 image_key: Some(rendering_context.image_key()),
366 missing_image_urls,
367 }
368 }
369
370 fn invalid_image(
372 &self,
373 size: Size2D<u32, DevicePixel>,
374 missing_image_urls: Vec<ServoUrl>,
375 ) -> DrawAPaintImageResult {
376 debug!("Returning an invalid image.");
377 DrawAPaintImageResult {
378 width: size.width,
379 height: size.height,
380 format: PixelFormat::BGRA8,
381 image_key: None,
382 missing_image_urls,
383 }
384 }
385
386 fn painter(&self, name: Atom) -> Box<dyn Painter> {
387 Box::new(WorkletPainter {
388 name,
389 executor: Mutex::new(self.worklet_global.executor()),
390 })
391 }
392}
393
394struct WorkletPainter {
396 name: Atom,
397 executor: Mutex<WorkletExecutor>,
398}
399
400impl SpeculativePainter for WorkletPainter {
401 fn speculatively_draw_a_paint_image(
402 &self,
403 properties: Vec<(Atom, String)>,
404 arguments: Vec<String>,
405 ) {
406 let name = self.name.clone();
407
408 let speculatively_draw_a_paint_image_task =
409 move |cx: &mut JSContext, global_scope: &WorkletGlobalScope| {
410 let paint_worklet_global_scope = global_scope
411 .downcast::<PaintWorkletGlobalScope>()
412 .expect("PaintWorklet's task should be run only on PaintWorkletGlobalScope.");
413
414 let should_speculate = (*paint_worklet_global_scope.cached_name.borrow() != name) ||
415 (*paint_worklet_global_scope.cached_properties.borrow() != properties) ||
416 (*paint_worklet_global_scope.cached_arguments.borrow() != arguments);
417 if should_speculate {
418 let size = paint_worklet_global_scope.cached_size.get();
419 let device_pixel_ratio =
420 paint_worklet_global_scope.cached_device_pixel_ratio.get();
421 let map = StylePropertyMapReadOnly::from_iter(
422 cx,
423 paint_worklet_global_scope.upcast(),
424 properties.iter().cloned(),
425 );
426 let result = paint_worklet_global_scope.draw_a_paint_image(
427 cx,
428 &name,
429 size,
430 device_pixel_ratio,
431 &map,
432 &arguments,
433 );
434 if (result.image_key.is_some()) && (result.missing_image_urls.is_empty()) {
435 paint_worklet_global_scope.set_cached_paint_image(
436 name,
437 size,
438 device_pixel_ratio,
439 properties,
440 arguments,
441 result,
442 );
443 }
444 }
445 };
446
447 self.executor
448 .lock()
449 .expect("Locking a painter.")
450 .schedule_a_worklet_task(Box::new(speculatively_draw_a_paint_image_task));
451 }
452}
453
454impl Painter for WorkletPainter {
455 fn draw_a_paint_image(
456 &self,
457 size: Size2D<f32, CSSPixel>,
458 device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>,
459 properties: Vec<(Atom, String)>,
460 arguments: Vec<String>,
461 ) -> Result<DrawAPaintImageResult, PaintWorkletError> {
462 let name = self.name.clone();
463 let (sender, receiver) = unbounded();
464
465 let draw_a_paint_image_task =
466 move |cx: &mut JSContext, global_scope: &WorkletGlobalScope| {
467 let paint_worklet_global_scope = global_scope
468 .downcast::<PaintWorkletGlobalScope>()
469 .expect("PaintWorklet's task should be run only on PaintWorkletGlobalScope.");
470
471 let cache_hit = paint_worklet_global_scope.has_cached_paint_image(
472 &name,
473 size,
474 device_pixel_ratio,
475 &properties,
476 &arguments,
477 );
478 let result = if cache_hit {
479 debug!("Cache hit on paint worklet {}!", name);
480 paint_worklet_global_scope.cached_result.borrow().clone()
481 } else {
482 debug!("Cache miss on paint worklet {}!", name);
483 let map = StylePropertyMapReadOnly::from_iter(
484 cx,
485 paint_worklet_global_scope.upcast(),
486 properties.iter().cloned(),
487 );
488 let result = paint_worklet_global_scope.draw_a_paint_image(
489 cx,
490 &name,
491 size,
492 device_pixel_ratio,
493 &map,
494 &arguments,
495 );
496 if (result.image_key.is_some()) && (result.missing_image_urls.is_empty()) {
497 paint_worklet_global_scope.set_cached_paint_image(
498 name,
499 size,
500 device_pixel_ratio,
501 properties,
502 arguments,
503 result.clone(),
504 );
505 }
506 result
507 };
508 let _ = sender.send(result);
509 };
510
511 self.executor
512 .lock()
513 .expect("Locking a painter.")
514 .schedule_a_worklet_task(Box::new(draw_a_paint_image_task));
515
516 let timeout = pref!(dom_worklet_timeout_ms) as u64;
517
518 receiver
519 .recv_timeout(Duration::from_millis(timeout))
520 .map_err(PaintWorkletError::from)
521 }
522}
523
524#[derive(JSTraceable, MallocSizeOf)]
529#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
530struct PaintDefinition {
531 #[ignore_malloc_size_of = "mozjs"]
532 class_constructor: Heap<JSVal>,
533 #[ignore_malloc_size_of = "mozjs"]
534 paint_function: Heap<JSVal>,
535 constructor_valid_flag: Cell<bool>,
536 context_alpha_flag: bool,
537 input_arguments_len: usize,
539 context: Dom<PaintRenderingContext2D>,
543}
544
545impl PaintDefinition {
546 fn new(
547 class_constructor: HandleValue,
548 paint_function: HandleValue,
549 alpha: bool,
550 input_arguments_len: usize,
551 context: &PaintRenderingContext2D,
552 ) -> Box<PaintDefinition> {
553 let result = Box::new(PaintDefinition {
554 class_constructor: Heap::default(),
555 paint_function: Heap::default(),
556 constructor_valid_flag: Cell::new(true),
557 context_alpha_flag: alpha,
558 input_arguments_len,
559 context: Dom::from_ref(context),
560 });
561 result.class_constructor.set(class_constructor.get());
562 result.paint_function.set(paint_function.get());
563 result
564 }
565}
566
567impl PaintWorkletGlobalScopeMethods<crate::DomTypeHolder> for PaintWorkletGlobalScope {
568 #[expect(unsafe_code)]
569 #[cfg_attr(crown, expect(crown::unrooted_must_root))]
570 fn RegisterPaint(
572 &self,
573 cx: &mut JSContext,
574 name: DOMString,
575 paint_ctor: Rc<VoidFunction>,
576 ) -> Fallible<()> {
577 let name = Atom::from(name);
578 rooted!(&in(cx) let paint_obj = paint_ctor.callback_holder().get());
579 rooted!(&in(cx) let paint_val = ObjectValue(paint_obj.get()));
580
581 debug!("Registering paint image name {}.", name);
582
583 if name.is_empty() {
585 return Err(Error::Type(c"Empty paint name.".to_owned()));
586 }
587
588 if self.paint_definitions.borrow().contains_key(&name) {
590 return Err(Error::InvalidModification(None));
591 }
592
593 let property_names: Vec<String> =
595 get_property(cx, paint_obj.handle(), c"inputProperties", ())?.unwrap_or_default();
596 let properties = property_names.into_iter().map(Atom::from).collect();
597
598 let input_arguments: Vec<String> =
600 get_property(cx, paint_obj.handle(), c"inputArguments", ())?.unwrap_or_default();
601
602 let alpha: bool = get_property(cx, paint_obj.handle(), c"alpha", ())?.unwrap_or(true);
606
607 if unsafe { !IsConstructor(paint_obj.get()) } {
609 return Err(Error::Type(c"Not a constructor.".to_owned()));
610 }
611
612 rooted!(&in(cx) let mut prototype = UndefinedValue());
614 get_property_jsval(cx, paint_obj.handle(), c"prototype", prototype.handle_mut())?;
615 if !prototype.is_object() {
616 return Err(Error::Type(c"Prototype is not an object.".to_owned()));
617 }
618 rooted!(&in(cx) let prototype = prototype.to_object());
619
620 rooted!(&in(cx) let mut paint_function = UndefinedValue());
622 get_property_jsval(
623 cx,
624 prototype.handle(),
625 c"paint",
626 paint_function.handle_mut(),
627 )?;
628 if !paint_function.is_object() || unsafe { !IsCallable(paint_function.to_object()) } {
629 return Err(Error::Type(c"Paint function is not callable.".to_owned()));
630 }
631
632 let Some(context) = PaintRenderingContext2D::new(cx, self) else {
634 return Err(Error::Operation(None));
635 };
636 let definition = PaintDefinition::new(
637 paint_val.handle(),
638 paint_function.handle(),
639 alpha,
640 input_arguments.len(),
641 &context,
642 );
643
644 debug!("Registering definition {}.", name);
646 self.paint_definitions
647 .borrow_mut()
648 .insert(name.clone(), definition);
649
650 let painter = self.painter(name.clone());
655 self.worklet_global
656 .register_paint_worklet(name, properties, painter);
657
658 Ok(())
659 }
660
661 fn Sleep(&self, ms: u64) {
668 thread::sleep(Duration::from_millis(ms));
669 }
670}
671
672impl HasOrigin for PaintWorkletGlobalScope {
673 fn origin(&self) -> MutableOrigin {
674 self.upcast::<WorkletGlobalScope>().origin()
675 }
676}