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