/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use dom::bindings::callback::CallbackContainer; use dom::bindings::cell::DomRefCell; use dom::bindings::codegen::Bindings::PaintWorkletGlobalScopeBinding; use dom::bindings::codegen::Bindings::PaintWorkletGlobalScopeBinding::PaintWorkletGlobalScopeMethods; use dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction; use dom::bindings::conversions::get_property; use dom::bindings::conversions::get_property_jsval; use dom::bindings::error::Error; use dom::bindings::error::Fallible; use dom::bindings::inheritance::Castable; use dom::bindings::reflector::DomObject; use dom::bindings::root::{Dom, DomRoot}; use dom::bindings::str::DOMString; use dom::cssstylevalue::CSSStyleValue; use dom::paintrenderingcontext2d::PaintRenderingContext2D; use dom::paintsize::PaintSize; use dom::stylepropertymapreadonly::StylePropertyMapReadOnly; use dom::worklet::WorkletExecutor; use dom::workletglobalscope::WorkletGlobalScope; use dom::workletglobalscope::WorkletGlobalScopeInit; use dom::workletglobalscope::WorkletTask; use dom_struct::dom_struct; use euclid::ScaleFactor; use euclid::TypedSize2D; use ipc_channel::ipc; use js::jsapi::Call; use js::jsapi::Construct1; use js::jsapi::HandleValue; use js::jsapi::HandleValueArray; use js::jsapi::Heap; use js::jsapi::IsCallable; use js::jsapi::IsConstructor; use js::jsapi::JSAutoCompartment; use js::jsapi::JS_ClearPendingException; use js::jsapi::JS_IsExceptionPending; use js::jsapi::JS_NewArrayObject; use js::jsval::JSVal; use js::jsval::ObjectValue; use js::jsval::UndefinedValue; use js::rust::Runtime; use msg::constellation_msg::PipelineId; use net_traits::image::base::PixelFormat; use net_traits::image_cache::ImageCache; use script_traits::DrawAPaintImageResult; use script_traits::Painter; use servo_atoms::Atom; use servo_url::ServoUrl; use std::cell::Cell; use std::collections::HashMap; use std::collections::hash_map::Entry; use std::ptr::null_mut; use std::rc::Rc; use std::sync::Arc; use std::sync::Mutex; use std::sync::mpsc; use std::sync::mpsc::Sender; use style_traits::CSSPixel; use style_traits::DevicePixel; use style_traits::SpeculativePainter; /// #[dom_struct] pub struct PaintWorkletGlobalScope { /// The worklet global for this object worklet_global: WorkletGlobalScope, /// The image cache #[ignore_malloc_size_of = "Arc"] image_cache: Arc, /// paint_definitions: DomRefCell>>, /// paint_class_instances: DomRefCell>>>, /// The most recent name the worklet was called with cached_name: DomRefCell, /// The most recent size the worklet was drawn at cached_size: Cell>, /// The most recent device pixel ratio the worklet was drawn at cached_device_pixel_ratio: Cell>, /// The most recent properties the worklet was drawn at cached_properties: DomRefCell>, /// The most recent arguments the worklet was drawn at cached_arguments: DomRefCell>, /// The most recent result cached_result: DomRefCell, } impl PaintWorkletGlobalScope { #[allow(unsafe_code)] pub fn new(runtime: &Runtime, pipeline_id: PipelineId, base_url: ServoUrl, executor: WorkletExecutor, init: &WorkletGlobalScopeInit) -> DomRoot { debug!("Creating paint worklet global scope for pipeline {}.", pipeline_id); let global = Box::new(PaintWorkletGlobalScope { worklet_global: WorkletGlobalScope::new_inherited(pipeline_id, base_url, executor, init), image_cache: init.image_cache.clone(), paint_definitions: Default::default(), paint_class_instances: Default::default(), cached_name: DomRefCell::new(Atom::from("")), cached_size: Cell::new(TypedSize2D::zero()), cached_device_pixel_ratio: Cell::new(ScaleFactor::new(1.0)), cached_properties: Default::default(), cached_arguments: Default::default(), cached_result: DomRefCell::new(DrawAPaintImageResult { width: 0, height: 0, format: PixelFormat::BGRA8, image_key: None, missing_image_urls: Vec::new(), }), }); unsafe { PaintWorkletGlobalScopeBinding::Wrap(runtime.cx(), global) } } pub fn image_cache(&self) -> Arc { self.image_cache.clone() } pub fn perform_a_worklet_task(&self, task: PaintWorkletTask) { match task { PaintWorkletTask::DrawAPaintImage(name, size, device_pixel_ratio, properties, arguments, sender) => { let cache_hit = (&*self.cached_name.borrow() == &name) && (self.cached_size.get() == size) && (self.cached_device_pixel_ratio.get() == device_pixel_ratio) && (&*self.cached_properties.borrow() == &properties) && (&*self.cached_arguments.borrow() == &arguments); let result = if cache_hit { debug!("Cache hit on paint worklet {}!", name); self.cached_result.borrow().clone() } else { debug!("Cache miss on paint worklet {}!", name); let map = StylePropertyMapReadOnly::from_iter(self.upcast(), properties.iter().cloned()); let result = self.draw_a_paint_image(&name, size, device_pixel_ratio, &*map, &*arguments); if (result.image_key.is_some()) && (result.missing_image_urls.is_empty()) { *self.cached_name.borrow_mut() = name; self.cached_size.set(size); self.cached_device_pixel_ratio.set(device_pixel_ratio); *self.cached_properties.borrow_mut() = properties; *self.cached_arguments.borrow_mut() = arguments; *self.cached_result.borrow_mut() = result.clone(); } result }; let _ = sender.send(result); } PaintWorkletTask::SpeculativelyDrawAPaintImage(name, properties, arguments) => { let should_speculate = (&*self.cached_name.borrow() != &name) || (&*self.cached_properties.borrow() != &properties) || (&*self.cached_arguments.borrow() != &arguments); if should_speculate { let size = self.cached_size.get(); let device_pixel_ratio = self.cached_device_pixel_ratio.get(); let map = StylePropertyMapReadOnly::from_iter(self.upcast(), properties.iter().cloned()); let result = self.draw_a_paint_image(&name, size, device_pixel_ratio, &*map, &*arguments); if (result.image_key.is_some()) && (result.missing_image_urls.is_empty()) { *self.cached_name.borrow_mut() = name; *self.cached_properties.borrow_mut() = properties; *self.cached_arguments.borrow_mut() = arguments; *self.cached_result.borrow_mut() = result; } } } } } /// fn draw_a_paint_image(&self, name: &Atom, size_in_px: TypedSize2D, device_pixel_ratio: ScaleFactor, properties: &StylePropertyMapReadOnly, arguments: &[String]) -> DrawAPaintImageResult { let size_in_dpx = size_in_px * device_pixel_ratio; let size_in_dpx = TypedSize2D::new(size_in_dpx.width.abs() as u32, size_in_dpx.height.abs() as u32); // TODO: Steps 1-5. // TODO: document paint definitions. self.invoke_a_paint_callback(name, size_in_px, size_in_dpx, device_pixel_ratio, properties, arguments) } /// #[allow(unsafe_code)] fn invoke_a_paint_callback(&self, name: &Atom, size_in_px: TypedSize2D, size_in_dpx: TypedSize2D, device_pixel_ratio: ScaleFactor, properties: &StylePropertyMapReadOnly, arguments: &[String]) -> DrawAPaintImageResult { debug!("Invoking a paint callback {}({},{}) at {}.", name, size_in_px.width, size_in_px.height, device_pixel_ratio); let cx = self.worklet_global.get_cx(); let _ac = JSAutoCompartment::new(cx, self.worklet_global.reflector().get_jsobject().get()); // TODO: Steps 1-2.1. // Step 2.2-5.1. rooted!(in(cx) let mut class_constructor = UndefinedValue()); rooted!(in(cx) let mut paint_function = UndefinedValue()); let rendering_context = match self.paint_definitions.borrow().get(name) { None => { // Step 2.2. warn!("Drawing un-registered paint definition {}.", name); return self.invalid_image(size_in_dpx, vec![]); } Some(definition) => { // Step 5.1 if !definition.constructor_valid_flag.get() { debug!("Drawing invalid paint definition {}.", name); return self.invalid_image(size_in_dpx, vec![]); } class_constructor.set(definition.class_constructor.get()); paint_function.set(definition.paint_function.get()); DomRoot::from_ref(&*definition.context) } }; // Steps 5.2-5.4 // TODO: the spec requires calling the constructor now, but we might want to // prepopulate the paint instance in `RegisterPaint`, to avoid calling it in // the primary worklet thread. // https://github.com/servo/servo/issues/17377 rooted!(in(cx) let mut paint_instance = UndefinedValue()); match self.paint_class_instances.borrow_mut().entry(name.clone()) { Entry::Occupied(entry) => paint_instance.set(entry.get().get()), Entry::Vacant(entry) => { // Step 5.2-5.3 let args = HandleValueArray::new(); rooted!(in(cx) let mut result = null_mut()); unsafe { Construct1(cx, class_constructor.handle(), &args, result.handle_mut()); } paint_instance.set(ObjectValue(result.get())); if unsafe { JS_IsExceptionPending(cx) } { debug!("Paint constructor threw an exception {}.", name); unsafe { JS_ClearPendingException(cx); } self.paint_definitions.borrow_mut().get_mut(name) .expect("Vanishing paint definition.") .constructor_valid_flag.set(false); return self.invalid_image(size_in_dpx, vec![]); } // Step 5.4 entry.insert(Box::new(Heap::default())).set(paint_instance.get()); } }; // TODO: Steps 6-7 // Step 8 // TODO: the spec requires creating a new paint rendering context each time, // this code recycles the same one. rendering_context.set_bitmap_dimensions(size_in_px, device_pixel_ratio); // Step 9 let paint_size = PaintSize::new(self, size_in_px); // TODO: Step 10 // Steps 11-12 debug!("Invoking paint function {}.", name); rooted_vec!(let arguments_values <- arguments.iter().cloned() .map(|argument| CSSStyleValue::new(self.upcast(), argument))); let arguments_value_vec: Vec = arguments_values.iter() .map(|argument| ObjectValue(argument.reflector().get_jsobject().get())) .collect(); let arguments_value_array = unsafe { HandleValueArray::from_rooted_slice(&*arguments_value_vec) }; rooted!(in(cx) let argument_object = unsafe { JS_NewArrayObject(cx, &arguments_value_array) }); let args_slice = [ ObjectValue(rendering_context.reflector().get_jsobject().get()), ObjectValue(paint_size.reflector().get_jsobject().get()), ObjectValue(properties.reflector().get_jsobject().get()), ObjectValue(argument_object.get()), ]; let args = unsafe { HandleValueArray::from_rooted_slice(&args_slice) }; rooted!(in(cx) let mut result = UndefinedValue()); unsafe { Call(cx, paint_instance.handle(), paint_function.handle(), &args, result.handle_mut()); } let missing_image_urls = rendering_context.take_missing_image_urls(); // Step 13. if unsafe { JS_IsExceptionPending(cx) } { debug!("Paint function threw an exception {}.", name); unsafe { JS_ClearPendingException(cx); } return self.invalid_image(size_in_dpx, missing_image_urls); } let (sender, receiver) = ipc::channel().expect("IPC channel creation."); rendering_context.send_data(sender); let image_key = match receiver.recv() { Ok(data) => Some(data.image_key), _ => None, }; DrawAPaintImageResult { width: size_in_dpx.width, height: size_in_dpx.height, format: PixelFormat::BGRA8, image_key: image_key, missing_image_urls: missing_image_urls, } } // https://drafts.csswg.org/css-images-4/#invalid-image fn invalid_image(&self, size: TypedSize2D, missing_image_urls: Vec) -> DrawAPaintImageResult { debug!("Returning an invalid image."); DrawAPaintImageResult { width: size.width as u32, height: size.height as u32, format: PixelFormat::BGRA8, image_key: None, missing_image_urls: missing_image_urls, } } fn painter(&self, name: Atom) -> Box { // Rather annoyingly we have to use a mutex here to make the painter Sync. struct WorkletPainter { name: Atom, executor: Mutex, } impl SpeculativePainter for WorkletPainter { fn speculatively_draw_a_paint_image(&self, properties: Vec<(Atom, String)>, arguments: Vec) { let name = self.name.clone(); let task = PaintWorkletTask::SpeculativelyDrawAPaintImage(name, properties, arguments); self.executor.lock().expect("Locking a painter.") .schedule_a_worklet_task(WorkletTask::Paint(task)); } } impl Painter for WorkletPainter { fn draw_a_paint_image(&self, size: TypedSize2D, device_pixel_ratio: ScaleFactor, properties: Vec<(Atom, String)>, arguments: Vec) -> DrawAPaintImageResult { let name = self.name.clone(); let (sender, receiver) = mpsc::channel(); let task = PaintWorkletTask::DrawAPaintImage(name, size, device_pixel_ratio, properties, arguments, sender); self.executor.lock().expect("Locking a painter.") .schedule_a_worklet_task(WorkletTask::Paint(task)); receiver.recv().expect("Worklet thread died?") } } Box::new(WorkletPainter { name: name, executor: Mutex::new(self.worklet_global.executor()), }) } } impl PaintWorkletGlobalScopeMethods for PaintWorkletGlobalScope { #[allow(unsafe_code)] #[allow(unrooted_must_root)] /// fn RegisterPaint(&self, name: DOMString, paint_ctor: Rc) -> Fallible<()> { let name = Atom::from(name); let cx = self.worklet_global.get_cx(); rooted!(in(cx) let paint_obj = paint_ctor.callback_holder().get()); rooted!(in(cx) let paint_val = ObjectValue(paint_obj.get())); debug!("Registering paint image name {}.", name); // Step 1. if name.is_empty() { return Err(Error::Type(String::from("Empty paint name."))) ; } // Step 2-3. if self.paint_definitions.borrow().contains_key(&name) { return Err(Error::InvalidModification); } // Step 4-6. let mut property_names: Vec = unsafe { get_property(cx, paint_obj.handle(), "inputProperties", ()) }? .unwrap_or_default(); let properties = property_names.drain(..).map(Atom::from).collect(); // Step 7-9. let input_arguments: Vec = unsafe { get_property(cx, paint_obj.handle(), "inputArguments", ()) }? .unwrap_or_default(); // TODO: Steps 10-11. // Steps 12-13. let alpha: bool = unsafe { get_property(cx, paint_obj.handle(), "alpha", ()) }? .unwrap_or(true); // Step 14 if unsafe { !IsConstructor(paint_obj.get()) } { return Err(Error::Type(String::from("Not a constructor."))); } // Steps 15-16 rooted!(in(cx) let mut prototype = UndefinedValue()); unsafe { get_property_jsval(cx, paint_obj.handle(), "prototype", prototype.handle_mut())?; } if !prototype.is_object() { return Err(Error::Type(String::from("Prototype is not an object."))); } rooted!(in(cx) let prototype = prototype.to_object()); // Steps 17-18 rooted!(in(cx) let mut paint_function = UndefinedValue()); unsafe { get_property_jsval(cx, prototype.handle(), "paint", paint_function.handle_mut())?; } if !paint_function.is_object() || unsafe { !IsCallable(paint_function.to_object()) } { return Err(Error::Type(String::from("Paint function is not callable."))); } // Step 19. let context = PaintRenderingContext2D::new(self); let definition = PaintDefinition::new(paint_val.handle(), paint_function.handle(), alpha, input_arguments.len(), &*context); // Step 20. debug!("Registering definition {}.", name); self.paint_definitions.borrow_mut().insert(name.clone(), definition); // TODO: Step 21. // Inform layout that there is a registered paint worklet. // TODO: layout will end up getting this message multiple times. let painter = self.painter(name.clone()); self.worklet_global.register_paint_worklet(name, properties, painter); Ok(()) } } /// Tasks which can be peformed by a paint worklet pub enum PaintWorkletTask { DrawAPaintImage(Atom, TypedSize2D, ScaleFactor, Vec<(Atom, String)>, Vec, Sender), SpeculativelyDrawAPaintImage(Atom, Vec<(Atom, String)>, Vec), } /// A paint definition /// /// This type is dangerous, because it contains uboxed `Heap` values, /// which can't be moved. #[derive(JSTraceable, MallocSizeOf)] #[must_root] struct PaintDefinition { class_constructor: Heap, paint_function: Heap, constructor_valid_flag: Cell, context_alpha_flag: bool, // TODO: this should be a list of CSS syntaxes. input_arguments_len: usize, // TODO: the spec calls for fresh rendering contexts each time a paint image is drawn, // but to avoid having the primary worklet thread create a new renering context, // we recycle them. context: Dom, } impl PaintDefinition { fn new(class_constructor: HandleValue, paint_function: HandleValue, alpha: bool, input_arguments_len: usize, context: &PaintRenderingContext2D) -> Box { let result = Box::new(PaintDefinition { class_constructor: Heap::default(), paint_function: Heap::default(), constructor_valid_flag: Cell::new(true), context_alpha_flag: alpha, input_arguments_len: input_arguments_len, context: Dom::from_ref(context), }); result.class_constructor.set(class_constructor.get()); result.paint_function.set(paint_function.get()); result } }