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};
53use crate::runtime::microtask::MicrotaskQueue;
54
55#[dom_struct]
57pub(crate) struct PaintWorkletGlobalScope {
58 worklet_global: WorkletGlobalScope,
60 #[ignore_malloc_size_of = "ImageCache"]
62 #[no_trace]
63 image_cache: Arc<dyn ImageCache>,
64 paint_definitions: DomRefCell<HashMapTracedValues<Atom, Box<PaintDefinition>>>,
66 #[ignore_malloc_size_of = "mozjs"]
68 paint_class_instances: DomRefCell<HashMapTracedValues<Atom, Box<Heap<JSVal>>>>,
69 #[no_trace]
71 cached_name: DomRefCell<Atom>,
72 #[no_trace]
74 cached_size: Cell<Size2D<f32, CSSPixel>>,
75 #[no_trace]
77 cached_device_pixel_ratio: Cell<Scale<f32, CSSPixel, DevicePixel>>,
78 #[no_trace]
80 cached_properties: DomRefCell<Vec<(Atom, String)>>,
81 cached_arguments: DomRefCell<Vec<String>>,
83 #[no_trace]
85 cached_result: DomRefCell<DrawAPaintImageResult>,
86}
87
88impl PaintWorkletGlobalScope {
89 #[allow(clippy::too_many_arguments)]
90 pub(crate) fn new(
91 cx: &mut JSContext,
92 pipeline_id: PipelineId,
93 base_url: ServoUrl,
94 inherited_secure_context: Option<bool>,
95 executor: WorkletExecutor,
96 init: &WorkletGlobalScopeInit,
97 closing: Arc<AtomicBool>,
98 microtask_queue: Rc<MicrotaskQueue>,
99 ) -> DomRoot<PaintWorkletGlobalScope> {
100 debug!(
101 "Creating paint worklet global scope for pipeline {}.",
102 pipeline_id
103 );
104 let global = Box::new(PaintWorkletGlobalScope {
105 worklet_global: WorkletGlobalScope::new_inherited(
106 pipeline_id,
107 base_url,
108 inherited_secure_context,
109 executor,
110 init,
111 closing,
112 microtask_queue,
113 ),
114 image_cache: init.image_cache.clone(),
115 paint_definitions: Default::default(),
116 paint_class_instances: Default::default(),
117 cached_name: DomRefCell::new(Atom::from("")),
118 cached_size: Cell::new(Size2D::zero()),
119 cached_device_pixel_ratio: Cell::new(Scale::new(1.0)),
120 cached_properties: Default::default(),
121 cached_arguments: Default::default(),
122 cached_result: DomRefCell::new(DrawAPaintImageResult {
123 width: 0,
124 height: 0,
125 format: PixelFormat::BGRA8,
126 image_key: None,
127 missing_image_urls: Vec::new(),
128 }),
129 });
130 PaintWorkletGlobalScopeBinding::Wrap::<crate::DomTypeHolder>(cx, &global.origin(), global)
131 }
132
133 pub(crate) fn image_cache(&self) -> Arc<dyn ImageCache> {
134 self.image_cache.clone()
135 }
136
137 fn has_cached_paint_image(
138 &self,
139 name: &Atom,
140 size: Size2D<f32, CSSPixel>,
141 device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>,
142 properties: &[(Atom, String)],
143 arguments: &[String],
144 ) -> bool {
145 (&*self.cached_name.borrow() == name) &&
146 (self.cached_size.get() == size) &&
147 (self.cached_device_pixel_ratio.get() == device_pixel_ratio) &&
148 (*self.cached_properties.borrow() == properties) &&
149 (*self.cached_arguments.borrow() == arguments)
150 }
151
152 fn set_cached_paint_image(
153 &self,
154 name: Atom,
155 size: Size2D<f32, CSSPixel>,
156 device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>,
157 properties: Vec<(Atom, String)>,
158 arguments: Vec<String>,
159 result: DrawAPaintImageResult,
160 ) {
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;
167 }
168
169 fn draw_a_paint_image(
171 &self,
172 cx: &mut JSContext,
173 name: &Atom,
174 size_in_px: Size2D<f32, CSSPixel>,
175 device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>,
176 properties: &StylePropertyMapReadOnly,
177 arguments: &[String],
178 ) -> DrawAPaintImageResult {
179 let size_in_dpx = size_in_px * device_pixel_ratio;
180 let size_in_dpx = Size2D::new(
181 size_in_dpx.width.abs() as u32,
182 size_in_dpx.height.abs() as u32,
183 );
184
185 self.invoke_a_paint_callback(
189 cx,
190 name,
191 size_in_px,
192 size_in_dpx,
193 device_pixel_ratio,
194 properties,
195 arguments,
196 )
197 }
198
199 #[expect(unsafe_code)]
200 fn get_or_create_paint_instance(
204 &self,
205 cx: &mut JSContext,
206 mut paint_instance: MutableHandle<Value>,
207 name: Atom,
208 class_constructor: HandleValue,
209 size_in_dpx: Size2D<u32, DevicePixel>,
210 ) -> Result<(), DrawAPaintImageResult> {
211 if let Some(entry) = self.paint_class_instances.borrow().get(&name) {
212 paint_instance.set(entry.get());
213 return Ok(());
214 }
215
216 let args = HandleValueArray::empty();
222 rooted!(&in(cx) let mut result = null_mut::<JSObject>());
223 unsafe {
224 Construct1(cx, class_constructor, &args, result.handle_mut());
225 }
226 paint_instance.set(ObjectValue(result.get()));
227 if unsafe { JS_IsExceptionPending(cx) } {
228 debug!("Paint constructor threw an exception {}.", name);
229 unsafe {
230 JS_ClearPendingException(cx);
231 }
232 self.paint_definitions
233 .safe_borrow_mut(cx)
234 .get_mut(&name)
235 .expect("Vanishing paint definition.")
236 .constructor_valid_flag
237 .set(false);
238 return Err(self.invalid_image(size_in_dpx, vec![]));
240 }
241 self.paint_class_instances
243 .safe_borrow_mut(cx)
244 .entry(name)
245 .or_default()
246 .set(paint_instance.get());
247 Ok(())
248 }
249
250 #[expect(clippy::too_many_arguments)]
252 #[expect(unsafe_code)]
253 fn invoke_a_paint_callback(
254 &self,
255 cx: &mut JSContext,
256 name: &Atom,
257 size_in_px: Size2D<f32, CSSPixel>,
258 size_in_dpx: Size2D<u32, DevicePixel>,
259 device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>,
260 properties: &StylePropertyMapReadOnly,
261 arguments: &[String],
262 ) -> DrawAPaintImageResult {
263 debug!(
264 "Invoking a paint callback {}({},{}) at {:?}.",
265 name, size_in_px.width, size_in_px.height, device_pixel_ratio
266 );
267
268 let mut realm = AutoRealm::new(
269 cx,
270 NonNull::new(self.worklet_global.reflector().get_jsobject().get()).unwrap(),
271 );
272 let cx = &mut *realm;
273
274 rooted!(&in(cx) let mut class_constructor = UndefinedValue());
277 rooted!(&in(cx) let mut paint_function = UndefinedValue());
278 let rendering_context = match self.paint_definitions.borrow().get(name) {
279 None => {
280 warn!("Drawing un-registered paint definition {}.", name);
282 return self.invalid_image(size_in_dpx, vec![]);
283 },
284 Some(definition) => {
285 if !definition.constructor_valid_flag.get() {
287 debug!("Drawing invalid paint definition {}.", name);
288 return self.invalid_image(size_in_dpx, vec![]);
289 }
290 class_constructor.set(definition.class_constructor.get());
291 paint_function.set(definition.paint_function.get());
292 DomRoot::from_ref(&*definition.context)
293 },
294 };
295
296 rooted!(&in(cx) let mut paint_instance = UndefinedValue());
302 if let Err(early_return) = self.get_or_create_paint_instance(
303 cx,
304 paint_instance.handle_mut(),
305 name.clone(),
306 class_constructor.handle(),
307 size_in_dpx,
308 ) {
309 return early_return;
310 }
311
312 rendering_context.set_bitmap_dimensions(size_in_px, device_pixel_ratio);
317
318 let paint_size = PaintSize::new(cx, self, size_in_px);
320
321 debug!("Invoking paint function {}.", name);
324 rooted_vec!(let mut arguments_values);
325 for argument in arguments {
326 let style_value = CSSStyleValue::new(cx, self.upcast(), argument.clone());
327 arguments_values.push(ObjectValue(style_value.reflector().get_jsobject().get()));
328 }
329 let arguments_value_array = HandleValueArray::from(&arguments_values);
330 rooted!(&in(cx) let argument_object = unsafe { NewArrayObject(cx, &arguments_value_array) });
331
332 rooted_vec!(let mut callback_args);
333 callback_args.push(ObjectValue(
334 rendering_context.reflector().get_jsobject().get(),
335 ));
336 callback_args.push(ObjectValue(paint_size.reflector().get_jsobject().get()));
337 callback_args.push(ObjectValue(properties.reflector().get_jsobject().get()));
338 callback_args.push(ObjectValue(argument_object.get()));
339 let args = HandleValueArray::from(&callback_args);
340
341 rooted!(&in(cx) let mut result = UndefinedValue());
342 unsafe {
343 Call(
344 cx,
345 paint_instance.handle(),
346 paint_function.handle(),
347 &args,
348 result.handle_mut(),
349 );
350 }
351 let missing_image_urls = rendering_context.take_missing_image_urls();
352
353 if unsafe { JS_IsExceptionPending(cx) } {
355 debug!("Paint function threw an exception {}.", name);
356 unsafe {
357 JS_ClearPendingException(cx);
358 }
359 return self.invalid_image(size_in_dpx, missing_image_urls);
360 }
361
362 rendering_context.update_rendering();
363
364 DrawAPaintImageResult {
365 width: size_in_dpx.width,
366 height: size_in_dpx.height,
367 format: PixelFormat::BGRA8,
368 image_key: Some(rendering_context.image_key()),
369 missing_image_urls,
370 }
371 }
372
373 fn invalid_image(
375 &self,
376 size: Size2D<u32, DevicePixel>,
377 missing_image_urls: Vec<ServoUrl>,
378 ) -> DrawAPaintImageResult {
379 debug!("Returning an invalid image.");
380 DrawAPaintImageResult {
381 width: size.width,
382 height: size.height,
383 format: PixelFormat::BGRA8,
384 image_key: None,
385 missing_image_urls,
386 }
387 }
388
389 fn painter(&self, name: Atom) -> Box<dyn Painter> {
390 Box::new(WorkletPainter {
391 name,
392 executor: Mutex::new(self.worklet_global.executor()),
393 })
394 }
395}
396
397struct WorkletPainter {
399 name: Atom,
400 executor: Mutex<WorkletExecutor>,
401}
402
403impl SpeculativePainter for WorkletPainter {
404 fn speculatively_draw_a_paint_image(
405 &self,
406 properties: Vec<(Atom, String)>,
407 arguments: Vec<String>,
408 ) {
409 let name = self.name.clone();
410
411 let speculatively_draw_a_paint_image_task =
412 move |cx: &mut JSContext, global_scope: &WorkletGlobalScope| {
413 let paint_worklet_global_scope = global_scope
414 .downcast::<PaintWorkletGlobalScope>()
415 .expect("PaintWorklet's task should be run only on PaintWorkletGlobalScope.");
416
417 let should_speculate = (*paint_worklet_global_scope.cached_name.borrow() != name) ||
418 (*paint_worklet_global_scope.cached_properties.borrow() != properties) ||
419 (*paint_worklet_global_scope.cached_arguments.borrow() != arguments);
420 if should_speculate {
421 let size = paint_worklet_global_scope.cached_size.get();
422 let device_pixel_ratio =
423 paint_worklet_global_scope.cached_device_pixel_ratio.get();
424 let map = StylePropertyMapReadOnly::from_iter(
425 cx,
426 paint_worklet_global_scope.upcast(),
427 properties.iter().cloned(),
428 );
429 let result = paint_worklet_global_scope.draw_a_paint_image(
430 cx,
431 &name,
432 size,
433 device_pixel_ratio,
434 &map,
435 &arguments,
436 );
437 if (result.image_key.is_some()) && (result.missing_image_urls.is_empty()) {
438 paint_worklet_global_scope.set_cached_paint_image(
439 name,
440 size,
441 device_pixel_ratio,
442 properties,
443 arguments,
444 result,
445 );
446 }
447 }
448 };
449
450 self.executor
451 .lock()
452 .expect("Locking a painter.")
453 .schedule_a_worklet_task(Box::new(speculatively_draw_a_paint_image_task));
454 }
455}
456
457impl Painter for WorkletPainter {
458 fn draw_a_paint_image(
459 &self,
460 size: Size2D<f32, CSSPixel>,
461 device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>,
462 properties: Vec<(Atom, String)>,
463 arguments: Vec<String>,
464 ) -> Result<DrawAPaintImageResult, PaintWorkletError> {
465 let name = self.name.clone();
466 let (sender, receiver) = unbounded();
467
468 let draw_a_paint_image_task =
469 move |cx: &mut JSContext, global_scope: &WorkletGlobalScope| {
470 let paint_worklet_global_scope = global_scope
471 .downcast::<PaintWorkletGlobalScope>()
472 .expect("PaintWorklet's task should be run only on PaintWorkletGlobalScope.");
473
474 let cache_hit = paint_worklet_global_scope.has_cached_paint_image(
475 &name,
476 size,
477 device_pixel_ratio,
478 &properties,
479 &arguments,
480 );
481 let result = if cache_hit {
482 debug!("Cache hit on paint worklet {}!", name);
483 paint_worklet_global_scope.cached_result.borrow().clone()
484 } else {
485 debug!("Cache miss on paint worklet {}!", name);
486 let map = StylePropertyMapReadOnly::from_iter(
487 cx,
488 paint_worklet_global_scope.upcast(),
489 properties.iter().cloned(),
490 );
491 let result = paint_worklet_global_scope.draw_a_paint_image(
492 cx,
493 &name,
494 size,
495 device_pixel_ratio,
496 &map,
497 &arguments,
498 );
499 if (result.image_key.is_some()) && (result.missing_image_urls.is_empty()) {
500 paint_worklet_global_scope.set_cached_paint_image(
501 name,
502 size,
503 device_pixel_ratio,
504 properties,
505 arguments,
506 result.clone(),
507 );
508 }
509 result
510 };
511 let _ = sender.send(result);
512 };
513
514 self.executor
515 .lock()
516 .expect("Locking a painter.")
517 .schedule_a_worklet_task(Box::new(draw_a_paint_image_task));
518
519 let timeout = pref!(dom_worklet_timeout_ms) as u64;
520
521 receiver
522 .recv_timeout(Duration::from_millis(timeout))
523 .map_err(PaintWorkletError::from)
524 }
525}
526
527#[derive(JSTraceable, MallocSizeOf)]
532#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
533struct PaintDefinition {
534 #[ignore_malloc_size_of = "mozjs"]
535 class_constructor: Heap<JSVal>,
536 #[ignore_malloc_size_of = "mozjs"]
537 paint_function: Heap<JSVal>,
538 constructor_valid_flag: Cell<bool>,
539 context_alpha_flag: bool,
540 input_arguments_len: usize,
542 context: Dom<PaintRenderingContext2D>,
546}
547
548impl PaintDefinition {
549 fn new(
550 class_constructor: HandleValue,
551 paint_function: HandleValue,
552 alpha: bool,
553 input_arguments_len: usize,
554 context: &PaintRenderingContext2D,
555 ) -> Box<PaintDefinition> {
556 let result = Box::new(PaintDefinition {
557 class_constructor: Heap::default(),
558 paint_function: Heap::default(),
559 constructor_valid_flag: Cell::new(true),
560 context_alpha_flag: alpha,
561 input_arguments_len,
562 context: Dom::from_ref(context),
563 });
564 result.class_constructor.set(class_constructor.get());
565 result.paint_function.set(paint_function.get());
566 result
567 }
568}
569
570impl PaintWorkletGlobalScopeMethods<crate::DomTypeHolder> for PaintWorkletGlobalScope {
571 #[expect(unsafe_code)]
572 #[cfg_attr(crown, expect(crown::unrooted_must_root))]
573 fn RegisterPaint(
575 &self,
576 cx: &mut JSContext,
577 name: DOMString,
578 paint_ctor: Rc<VoidFunction>,
579 ) -> Fallible<()> {
580 let name = Atom::from(name);
581 rooted!(&in(cx) let paint_obj = paint_ctor.callback_holder().get());
582 rooted!(&in(cx) let paint_val = ObjectValue(paint_obj.get()));
583
584 debug!("Registering paint image name {}.", name);
585
586 if name.is_empty() {
588 return Err(Error::Type(c"Empty paint name.".to_owned()));
589 }
590
591 if self.paint_definitions.borrow().contains_key(&name) {
593 return Err(Error::InvalidModification(None));
594 }
595
596 let property_names: Vec<String> =
598 get_property(cx, paint_obj.handle(), c"inputProperties", ())?.unwrap_or_default();
599 let properties = property_names.into_iter().map(Atom::from).collect();
600
601 let input_arguments: Vec<String> =
603 get_property(cx, paint_obj.handle(), c"inputArguments", ())?.unwrap_or_default();
604
605 let alpha: bool = get_property(cx, paint_obj.handle(), c"alpha", ())?.unwrap_or(true);
609
610 if unsafe { !IsConstructor(paint_obj.get()) } {
612 return Err(Error::Type(c"Not a constructor.".to_owned()));
613 }
614
615 rooted!(&in(cx) let mut prototype = UndefinedValue());
617 get_property_jsval(cx, paint_obj.handle(), c"prototype", prototype.handle_mut())?;
618 if !prototype.is_object() {
619 return Err(Error::Type(c"Prototype is not an object.".to_owned()));
620 }
621 rooted!(&in(cx) let prototype = prototype.to_object());
622
623 rooted!(&in(cx) let mut paint_function = UndefinedValue());
625 get_property_jsval(
626 cx,
627 prototype.handle(),
628 c"paint",
629 paint_function.handle_mut(),
630 )?;
631 if !paint_function.is_object() || unsafe { !IsCallable(paint_function.to_object()) } {
632 return Err(Error::Type(c"Paint function is not callable.".to_owned()));
633 }
634
635 let Some(context) = PaintRenderingContext2D::new(cx, self) else {
637 return Err(Error::Operation(None));
638 };
639 let definition = PaintDefinition::new(
640 paint_val.handle(),
641 paint_function.handle(),
642 alpha,
643 input_arguments.len(),
644 &context,
645 );
646
647 debug!("Registering definition {}.", name);
649 self.paint_definitions
650 .borrow_mut()
651 .insert(name.clone(), definition);
652
653 let painter = self.painter(name.clone());
658 self.worklet_global
659 .register_paint_worklet(name, properties, painter);
660
661 Ok(())
662 }
663
664 fn Sleep(&self, ms: u64) {
671 thread::sleep(Duration::from_millis(ms));
672 }
673}
674
675impl HasOrigin for PaintWorkletGlobalScope {
676 fn origin(&self) -> MutableOrigin {
677 self.upcast::<WorkletGlobalScope>().origin()
678 }
679}