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