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