servo: Merge #17150 - Implemented the plumbing for paint worklets (from asajeffrey:script-paint-worklets-plumbing); r=jdm
authorAlan Jeffrey <ajeffrey@mozilla.com>
Wed, 07 Jun 2017 11:57:07 -0700
changeset 410972 814a9b5ef1866c988b6d97490f305d5a246b44c2
parent 410971 763730b2f816122b2bef3897325ea46099d2930c
child 410973 7c0b44852e1e5688040f6ff5d48d8d8ca0907cc1
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdm
bugs17150
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
servo: Merge #17150 - Implemented the plumbing for paint worklets (from asajeffrey:script-paint-worklets-plumbing); r=jdm <!-- Please describe your changes on the following line: --> This PR implements the plumbing for paint worklets: * Adding CSS values for paint worklets. * Implementing a skeleton for the `PaintWorkletGlobalScope` webidl. * Implementing an executor for paint worklet tasks, and passing it from script to layout. * Building the display list items for paint worklet images. This PR does not implement registering or calling paint worklets in JS. Before it merges, this PR needs a reftest added for basic paint worklet functionality. --- <!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `__` with appropriate data: --> - [X] `./mach build -d` does not report any errors - [X] `./mach test-tidy` does not report any errors - [ ] There are tests for these changes <!-- Also, please make sure that "Allow edits from maintainers" checkbox is checked, so that we can help you if you get stuck somewhere along the way.--> <!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. --> Source-Repo: https://github.com/servo/servo Source-Revision: bf46da065db58549a74c489d521f9375f4137637
servo/Cargo.lock
servo/components/layout/context.rs
servo/components/layout/display_list_builder.rs
servo/components/layout_thread/lib.rs
servo/components/net/image_cache.rs
servo/components/net_traits/image_cache.rs
servo/components/script/dom/mod.rs
servo/components/script/dom/paintworkletglobalscope.rs
servo/components/script/dom/webidls/PaintWorkletGlobalScope.webidl
servo/components/script/dom/webidls/Window.webidl
servo/components/script/dom/window.rs
servo/components/script/dom/worklet.rs
servo/components/script/dom/workletglobalscope.rs
servo/components/script_layout_interface/message.rs
servo/components/script_traits/Cargo.toml
servo/components/script_traits/lib.rs
servo/components/style/values/generics/image.rs
servo/components/style/values/specified/image.rs
--- a/servo/Cargo.lock
+++ b/servo/Cargo.lock
@@ -2479,16 +2479,17 @@ dependencies = [
  "script 0.0.1",
  "servo_url 0.0.1",
 ]
 
 [[package]]
 name = "script_traits"
 version = "0.0.1"
 dependencies = [
+ "app_units 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "bluetooth_traits 0.0.1",
  "canvas_traits 0.0.1",
  "cookie 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "devtools_traits 0.0.1",
  "euclid 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "gfx_traits 0.0.1",
  "heapsize 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "heapsize_derive 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2498,16 +2499,17 @@ dependencies = [
  "libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
  "msg 0.0.1",
  "net_traits 0.0.1",
  "offscreen_gl_context 0.8.10 (registry+https://github.com/rust-lang/crates.io-index)",
  "profile_traits 0.0.1",
  "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_derive 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
+ "servo_atoms 0.0.1",
  "servo_url 0.0.1",
  "style_traits 0.0.1",
  "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
  "url 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "webrender_traits 0.40.0 (git+https://github.com/servo/webrender)",
  "webvr_traits 0.0.1",
 ]
 
--- a/servo/components/layout/context.rs
+++ b/servo/components/layout/context.rs
@@ -10,16 +10,17 @@ use gfx::font_cache_thread::FontCacheThr
 use gfx::font_context::FontContext;
 use heapsize::HeapSizeOf;
 use msg::constellation_msg::PipelineId;
 use net_traits::image_cache::{CanRequestImages, ImageCache, ImageState};
 use net_traits::image_cache::{ImageOrMetadataAvailable, UsePlaceholder};
 use opaque_node::OpaqueNodeMethods;
 use parking_lot::RwLock;
 use script_layout_interface::{PendingImage, PendingImageState};
+use script_traits::PaintWorkletExecutor;
 use script_traits::UntrustedNodeAddress;
 use servo_url::ServoUrl;
 use std::borrow::{Borrow, BorrowMut};
 use std::cell::{RefCell, RefMut};
 use std::collections::HashMap;
 use std::hash::BuildHasherDefault;
 use std::sync::{Arc, Mutex};
 use std::thread;
@@ -90,16 +91,19 @@ pub struct LayoutContext<'a> {
     /// Interface to the font cache thread.
     pub font_cache_thread: Mutex<FontCacheThread>,
 
     /// A cache of WebRender image info.
     pub webrender_image_cache: Arc<RwLock<HashMap<(ServoUrl, UsePlaceholder),
                                                   WebRenderImageInfo,
                                                   BuildHasherDefault<FnvHasher>>>>,
 
+    /// The executor for worklets
+    pub paint_worklet_executor: Option<Arc<PaintWorkletExecutor>>,
+
     /// A list of in-progress image loads to be shared with the script thread.
     /// A None value means that this layout was not initiated by the script thread.
     pub pending_images: Option<Mutex<Vec<PendingImage>>>,
 
     /// A list of nodes that have just initiated a CSS transition.
     /// A None value means that this layout was not initiated by the script thread.
     pub newly_transitioning_nodes: Option<Mutex<Vec<UntrustedNodeAddress>>>,
 }
--- a/servo/components/layout/display_list_builder.rs
+++ b/servo/components/layout/display_list_builder.rs
@@ -59,16 +59,17 @@ use style::servo::restyle_damage::REPAIN
 use style::values::{Either, RGBA};
 use style::values::computed::{Gradient, GradientItem, LengthOrPercentage};
 use style::values::computed::{LengthOrPercentageOrAuto, NumberOrPercentage, Position, Shadow};
 use style::values::computed::image::{EndingShape, LineDirection};
 use style::values::generics::background::BackgroundSize;
 use style::values::generics::image::{Circle, Ellipse, EndingShape as GenericEndingShape};
 use style::values::generics::image::{GradientItem as GenericGradientItem, GradientKind};
 use style::values::generics::image::{Image, ShapeExtent};
+use style::values::generics::image::PaintWorklet;
 use style::values::specified::position::{X, Y};
 use style_traits::CSSPixel;
 use style_traits::cursor::Cursor;
 use table_cell::CollapsedBordersForCell;
 use webrender_helpers::{ToMixBlendMode, ToTransformStyle};
 use webrender_traits::{ColorF, ClipId, GradientStop, RepeatMode, ScrollPolicy, TransformStyle};
 
 trait ResolvePercentage {
@@ -390,16 +391,38 @@ pub trait FragmentDisplayListBuilding {
                                                state: &mut DisplayListBuildState,
                                                style: &ServoComputedValues,
                                                display_list_section: DisplayListSection,
                                                absolute_bounds: &Rect<Au>,
                                                clip: &ClippingRegion,
                                                image_url: &ServoUrl,
                                                background_index: usize);
 
+    /// Adds the display items necessary to paint a webrender image of this fragment to the
+    /// appropriate section of the display list.
+    fn build_display_list_for_webrender_image(&self,
+                                              state: &mut DisplayListBuildState,
+                                              style: &ServoComputedValues,
+                                              display_list_section: DisplayListSection,
+                                              absolute_bounds: &Rect<Au>,
+                                              clip: &ClippingRegion,
+                                              webrender_image: WebRenderImageInfo,
+                                              index: usize);
+
+    /// Adds the display items necessary to paint the background image created by this fragment's
+    /// worklet to the appropriate section of the display list.
+    fn build_display_list_for_background_paint_worklet(&self,
+                                                       state: &mut DisplayListBuildState,
+                                                       style: &ServoComputedValues,
+                                                       display_list_section: DisplayListSection,
+                                                       absolute_bounds: &Rect<Au>,
+                                                       clip: &ClippingRegion,
+                                                       paint_worklet: &PaintWorklet,
+                                                       index: usize);
+
     fn convert_linear_gradient(&self,
                                bounds: &Rect<Au>,
                                stops: &[GradientItem],
                                direction: &LineDirection,
                                repeating: bool,
                                style: &ServoComputedValues)
                                -> display_list::Gradient;
 
@@ -888,16 +911,25 @@ impl FragmentDisplayListBuilding for Fra
                                                                      style,
                                                                      display_list_section,
                                                                      &bounds,
                                                                      &clip,
                                                                      url,
                                                                      i);
                     }
                 }
+                Either::Second(Image::PaintWorklet(ref paint_worklet)) => {
+                    self.build_display_list_for_background_paint_worklet(state,
+                                                                         style,
+                                                                         display_list_section,
+                                                                         &bounds,
+                                                                         &clip,
+                                                                         paint_worklet,
+                                                                         i);
+                }
                 Either::Second(Image::Rect(_)) => {
                     // TODO: Implement `-moz-image-rect`
                 }
                 Either::Second(Image::Element(_)) => {
                     // TODO: Implement `-moz-element`
                 }
             }
         }
@@ -951,154 +983,214 @@ impl FragmentDisplayListBuilding for Fra
     fn build_display_list_for_background_image(&self,
                                                state: &mut DisplayListBuildState,
                                                style: &ServoComputedValues,
                                                display_list_section: DisplayListSection,
                                                absolute_bounds: &Rect<Au>,
                                                clip: &ClippingRegion,
                                                image_url: &ServoUrl,
                                                index: usize) {
-        let background = style.get_background();
         let webrender_image = state.layout_context
                                    .get_webrender_image_for_url(self.node,
                                                                 image_url.clone(),
                                                                 UsePlaceholder::No);
 
         if let Some(webrender_image) = webrender_image {
-            debug!("(building display list) building background image");
-
-            // Use `background-size` to get the size.
-            let mut bounds = *absolute_bounds;
-            let image_size = self.compute_background_image_size(style, &bounds,
-                                                                &webrender_image, index);
+            self.build_display_list_for_webrender_image(state,
+                                                        style,
+                                                        display_list_section,
+                                                        absolute_bounds,
+                                                        clip,
+                                                        webrender_image,
+                                                        index);
+        }
+    }
 
-            // Clip.
-            //
-            // TODO: Check the bounds to see if a clip item is actually required.
-            let mut clip = clip.clone();
-            clip.intersect_rect(&bounds);
+    fn build_display_list_for_webrender_image(&self,
+                                              state: &mut DisplayListBuildState,
+                                              style: &ServoComputedValues,
+                                              display_list_section: DisplayListSection,
+                                              absolute_bounds: &Rect<Au>,
+                                              clip: &ClippingRegion,
+                                              webrender_image: WebRenderImageInfo,
+                                              index: usize) {
+        debug!("(building display list) building background image");
+        let background = style.get_background();
 
-            // Background image should be positioned on the padding box basis.
-            let border = style.logical_border_width().to_physical(style.writing_mode);
+        // Use `background-size` to get the size.
+        let mut bounds = *absolute_bounds;
+        let image_size = self.compute_background_image_size(style, &bounds,
+                                                            &webrender_image, index);
+
+        // Clip.
+        //
+        // TODO: Check the bounds to see if a clip item is actually required.
+        let mut clip = clip.clone();
+        clip.intersect_rect(&bounds);
+
+        // Background image should be positioned on the padding box basis.
+        let border = style.logical_border_width().to_physical(style.writing_mode);
 
-            // Use 'background-origin' to get the origin value.
-            let origin = get_cyclic(&background.background_origin.0, index);
-            let (mut origin_x, mut origin_y) = match *origin {
-                background_origin::single_value::T::padding_box => {
-                    (Au(0), Au(0))
-                }
-                background_origin::single_value::T::border_box => {
-                    (-border.left, -border.top)
-                }
-                background_origin::single_value::T::content_box => {
-                    let border_padding = self.border_padding.to_physical(self.style.writing_mode);
-                    (border_padding.left - border.left, border_padding.top - border.top)
-                }
-            };
+        // Use 'background-origin' to get the origin value.
+        let origin = get_cyclic(&background.background_origin.0, index);
+        let (mut origin_x, mut origin_y) = match *origin {
+            background_origin::single_value::T::padding_box => {
+                (Au(0), Au(0))
+            }
+            background_origin::single_value::T::border_box => {
+                (-border.left, -border.top)
+            }
+            background_origin::single_value::T::content_box => {
+                let border_padding = self.border_padding.to_physical(self.style.writing_mode);
+                (border_padding.left - border.left, border_padding.top - border.top)
+            }
+        };
 
-            // Use `background-attachment` to get the initial virtual origin
-            let attachment = get_cyclic(&background.background_attachment.0, index);
-            let (virtual_origin_x, virtual_origin_y) = match *attachment {
-                background_attachment::single_value::T::scroll => {
-                    (absolute_bounds.origin.x, absolute_bounds.origin.y)
-                }
-                background_attachment::single_value::T::fixed => {
-                    // If the ‘background-attachment’ value for this image is ‘fixed’, then
-                    // 'background-origin' has no effect.
-                    origin_x = Au(0);
-                    origin_y = Au(0);
-                    (Au(0), Au(0))
-                }
-            };
+        // Use `background-attachment` to get the initial virtual origin
+        let attachment = get_cyclic(&background.background_attachment.0, index);
+        let (virtual_origin_x, virtual_origin_y) = match *attachment {
+            background_attachment::single_value::T::scroll => {
+                (absolute_bounds.origin.x, absolute_bounds.origin.y)
+            }
+            background_attachment::single_value::T::fixed => {
+                // If the ‘background-attachment’ value for this image is ‘fixed’, then
+                // 'background-origin' has no effect.
+                origin_x = Au(0);
+                origin_y = Au(0);
+                (Au(0), Au(0))
+            }
+        };
 
-            let horiz_position = *get_cyclic(&background.background_position_x.0, index);
-            let vert_position = *get_cyclic(&background.background_position_y.0, index);
-            // Use `background-position` to get the offset.
-            let horizontal_position = horiz_position.to_used_value(bounds.size.width - image_size.width);
-            let vertical_position = vert_position.to_used_value(bounds.size.height - image_size.height);
+        let horiz_position = *get_cyclic(&background.background_position_x.0, index);
+        let vert_position = *get_cyclic(&background.background_position_y.0, index);
+        // Use `background-position` to get the offset.
+        let horizontal_position = horiz_position.to_used_value(bounds.size.width - image_size.width);
+        let vertical_position = vert_position.to_used_value(bounds.size.height - image_size.height);
 
-            // The anchor position for this background, based on both the background-attachment
-            // and background-position properties.
-            let anchor_origin_x = border.left + virtual_origin_x + origin_x + horizontal_position;
-            let anchor_origin_y = border.top + virtual_origin_y + origin_y + vertical_position;
+        // The anchor position for this background, based on both the background-attachment
+        // and background-position properties.
+        let anchor_origin_x = border.left + virtual_origin_x + origin_x + horizontal_position;
+        let anchor_origin_y = border.top + virtual_origin_y + origin_y + vertical_position;
 
-            let mut tile_spacing = Size2D::zero();
-            let mut stretch_size = image_size;
+        let mut tile_spacing = Size2D::zero();
+        let mut stretch_size = image_size;
 
-            // Adjust origin and size based on background-repeat
-            let background_repeat = get_cyclic(&background.background_repeat.0, index);
-            match background_repeat.0 {
-                background_repeat::single_value::RepeatKeyword::NoRepeat => {
-                    bounds.origin.x = anchor_origin_x;
-                    bounds.size.width = image_size.width;
-                }
-                background_repeat::single_value::RepeatKeyword::Repeat => {
-                    ImageFragmentInfo::tile_image(&mut bounds.origin.x,
-                                                  &mut bounds.size.width,
-                                                  anchor_origin_x,
-                                                  image_size.width);
-                }
-                background_repeat::single_value::RepeatKeyword::Space => {
-                    ImageFragmentInfo::tile_image_spaced(&mut bounds.origin.x,
-                                                         &mut bounds.size.width,
-                                                         &mut tile_spacing.width,
-                                                         anchor_origin_x,
-                                                         image_size.width);
+        // Adjust origin and size based on background-repeat
+        let background_repeat = get_cyclic(&background.background_repeat.0, index);
+        match background_repeat.0 {
+            background_repeat::single_value::RepeatKeyword::NoRepeat => {
+                bounds.origin.x = anchor_origin_x;
+                bounds.size.width = image_size.width;
+            }
+            background_repeat::single_value::RepeatKeyword::Repeat => {
+                ImageFragmentInfo::tile_image(&mut bounds.origin.x,
+                                              &mut bounds.size.width,
+                                              anchor_origin_x,
+                                              image_size.width);
+            }
+            background_repeat::single_value::RepeatKeyword::Space => {
+                ImageFragmentInfo::tile_image_spaced(&mut bounds.origin.x,
+                                                     &mut bounds.size.width,
+                                                     &mut tile_spacing.width,
+                                                     anchor_origin_x,
+                                                     image_size.width);
+
+            }
+            background_repeat::single_value::RepeatKeyword::Round => {
+                ImageFragmentInfo::tile_image_round(&mut bounds.origin.x,
+                                                    &mut bounds.size.width,
+                                                    anchor_origin_x,
+                                                    &mut stretch_size.width);
+            }
+        };
+        match background_repeat.1 {
+            background_repeat::single_value::RepeatKeyword::NoRepeat => {
+                bounds.origin.y = anchor_origin_y;
+                bounds.size.height = image_size.height;
+            }
+            background_repeat::single_value::RepeatKeyword::Repeat => {
+                ImageFragmentInfo::tile_image(&mut bounds.origin.y,
+                                              &mut bounds.size.height,
+                                              anchor_origin_y,
+                                              image_size.height);
+            }
+            background_repeat::single_value::RepeatKeyword::Space => {
+                ImageFragmentInfo::tile_image_spaced(&mut bounds.origin.y,
+                                                     &mut bounds.size.height,
+                                                     &mut tile_spacing.height,
+                                                     anchor_origin_y,
+                                                     image_size.height);
+
+            }
+            background_repeat::single_value::RepeatKeyword::Round => {
+                ImageFragmentInfo::tile_image_round(&mut bounds.origin.y,
+                                                    &mut bounds.size.height,
+                                                    anchor_origin_y,
+                                                    &mut stretch_size.height);
+            }
+        };
 
-                }
-                background_repeat::single_value::RepeatKeyword::Round => {
-                    ImageFragmentInfo::tile_image_round(&mut bounds.origin.x,
-                                                        &mut bounds.size.width,
-                                                        anchor_origin_x,
-                                                        &mut stretch_size.width);
-                }
-            };
-            match background_repeat.1 {
-                background_repeat::single_value::RepeatKeyword::NoRepeat => {
-                    bounds.origin.y = anchor_origin_y;
-                    bounds.size.height = image_size.height;
-                }
-                background_repeat::single_value::RepeatKeyword::Repeat => {
-                    ImageFragmentInfo::tile_image(&mut bounds.origin.y,
-                                                  &mut bounds.size.height,
-                                                  anchor_origin_y,
-                                                  image_size.height);
-                }
-                background_repeat::single_value::RepeatKeyword::Space => {
-                    ImageFragmentInfo::tile_image_spaced(&mut bounds.origin.y,
-                                                         &mut bounds.size.height,
-                                                         &mut tile_spacing.height,
-                                                         anchor_origin_y,
-                                                         image_size.height);
+        // Create the image display item.
+        let base = state.create_base_display_item(&bounds,
+                                                  &clip,
+                                                  self.node,
+                                                  style.get_cursor(Cursor::Default),
+                                                  display_list_section);
+
+        debug!("(building display list) adding background image.");
+        state.add_display_item(DisplayItem::Image(box ImageDisplayItem {
+            base: base,
+            webrender_image: webrender_image,
+            image_data: None,
+            stretch_size: stretch_size,
+            tile_spacing: tile_spacing,
+            image_rendering: style.get_inheritedbox().image_rendering.clone(),
+        }));
+
+    }
 
-                }
-                background_repeat::single_value::RepeatKeyword::Round => {
-                    ImageFragmentInfo::tile_image_round(&mut bounds.origin.y,
-                                                        &mut bounds.size.height,
-                                                        anchor_origin_y,
-                                                        &mut stretch_size.height);
-                }
-            };
+    fn build_display_list_for_background_paint_worklet(&self,
+                                                       state: &mut DisplayListBuildState,
+                                                       style: &ServoComputedValues,
+                                                       display_list_section: DisplayListSection,
+                                                       absolute_bounds: &Rect<Au>,
+                                                       clip: &ClippingRegion,
+                                                       paint_worklet: &PaintWorklet,
+                                                       index: usize)
+    {
+        // TODO: check that this is the servo equivalent of "concrete object size".
+        // https://drafts.css-houdini.org/css-paint-api/#draw-a-paint-image
+        // https://drafts.csswg.org/css-images-3/#concrete-object-size
+        let size = self.content_box().size.to_physical(style.writing_mode);
+        let name = paint_worklet.name.clone();
 
-            // Create the image display item.
-            let base = state.create_base_display_item(&bounds,
-                                                      &clip,
-                                                      self.node,
-                                                      style.get_cursor(Cursor::Default),
-                                                      display_list_section);
-            state.add_display_item(DisplayItem::Image(box ImageDisplayItem {
-              base: base,
-              webrender_image: webrender_image,
-              image_data: None,
-              stretch_size: stretch_size,
-              tile_spacing: tile_spacing,
-              image_rendering: style.get_inheritedbox().image_rendering.clone(),
-            }));
+        // If the script thread has not added any paint worklet modules, there is nothing to do!
+        let executor = match state.layout_context.paint_worklet_executor {
+            Some(ref executor) => executor,
+            None => return debug!("Worklet {} called before any paint modules are added.", name),
+        };
 
-        }
+        // TODO: add a one-place cache to avoid drawing the paint image every time.
+        debug!("Drawing a paint image {}({},{}).", name, size.width.to_px(), size.height.to_px());
+        let mut image = match executor.draw_a_paint_image(name, size) {
+            Ok(image) => image,
+            Err(err) => return warn!("Error running paint worklet ({:?}).", err),
+        };
+
+        // Make sure the image has a webrender key.
+        state.layout_context.image_cache.set_webrender_image_key(&mut image);
+
+        debug!("Drew a paint image ({},{}).", image.width, image.height);
+        self.build_display_list_for_webrender_image(state,
+                                                    style,
+                                                    display_list_section,
+                                                    absolute_bounds,
+                                                    clip,
+                                                    WebRenderImageInfo::from_image(&image),
+                                                    index);
     }
 
     fn convert_linear_gradient(&self,
                                bounds: &Rect<Au>,
                                stops: &[GradientItem],
                                direction: &LineDirection,
                                repeating: bool,
                                style: &ServoComputedValues)
@@ -1397,16 +1489,19 @@ impl FragmentDisplayListBuilding for Fra
 
                                     // TODO(gw): Support border-image-outset
                                     outset: SideOffsets2D::zero(),
                                 }),
                         }));
                     }
                 }
             }
+            Either::Second(Image::PaintWorklet(..)) => {
+                // TODO: Handle border-image with `paint()`.
+            }
             Either::Second(Image::Rect(..)) => {
                 // TODO: Handle border-image with `-moz-image-rect`.
             }
             Either::Second(Image::Element(..)) => {
                 // TODO: Handle border-image with `-moz-element`.
             }
             Either::Second(Image::Url(ref image_url)) => {
                 if let Some(url) = image_url.url() {
--- a/servo/components/layout_thread/lib.rs
+++ b/servo/components/layout_thread/lib.rs
@@ -85,16 +85,17 @@ use script::layout_wrapper::{ServoLayout
 use script_layout_interface::message::{Msg, NewLayoutThreadInfo, Reflow, ReflowQueryType};
 use script_layout_interface::message::{ScriptReflow, ReflowComplete};
 use script_layout_interface::reporter::CSSErrorReporter;
 use script_layout_interface::rpc::{LayoutRPC, MarginStyleResponse, NodeOverflowResponse, OffsetParentResponse};
 use script_layout_interface::rpc::TextIndexResponse;
 use script_layout_interface::wrapper_traits::LayoutNode;
 use script_traits::{ConstellationControlMsg, LayoutControlMsg, LayoutMsg as ConstellationMsg};
 use script_traits::{ScrollState, UntrustedNodeAddress};
+use script_traits::PaintWorkletExecutor;
 use selectors::Element;
 use servo_config::opts;
 use servo_config::prefs::PREFS;
 use servo_config::resource_files::read_resource_file;
 use servo_geometry::max_rect;
 use servo_url::ServoUrl;
 use std::borrow::ToOwned;
 use std::cell::{Cell, RefCell};
@@ -220,16 +221,19 @@ pub struct LayoutThread {
     /// All the other elements of this struct are read-only.
     rw_data: Arc<Mutex<LayoutThreadData>>,
 
     /// The CSS error reporter for all CSS loaded in this layout thread
     error_reporter: CSSErrorReporter,
 
     webrender_image_cache: Arc<RwLock<FnvHashMap<(ServoUrl, UsePlaceholder),
                                                  WebRenderImageInfo>>>,
+    /// The executor for paint worklets.
+    /// Will be None if the script thread hasn't added any paint worklet modules.
+    paint_worklet_executor: Option<Arc<PaintWorkletExecutor>>,
 
     /// Webrender interface.
     webrender_api: webrender_traits::RenderApi,
 
     /// The timer object to control the timing of the animations. This should
     /// only be a test-mode timer during testing for animations.
     timer: Timer,
 
@@ -472,16 +476,17 @@ impl LayoutThread {
             url: url,
             is_iframe: is_iframe,
             port: port,
             pipeline_port: pipeline_receiver,
             script_chan: script_chan.clone(),
             constellation_chan: constellation_chan.clone(),
             time_profiler_chan: time_profiler_chan,
             mem_profiler_chan: mem_profiler_chan,
+            paint_worklet_executor: None,
             image_cache: image_cache.clone(),
             font_cache_thread: font_cache_thread,
             first_reflow: Cell::new(true),
             font_cache_receiver: font_cache_receiver,
             font_cache_sender: ipc_font_cache_sender,
             parallel_traversal: parallel_traversal,
             parallel_flag: true,
             generation: Cell::new(0),
@@ -569,16 +574,17 @@ impl LayoutThread {
                 traversal_flags: TraversalFlags::empty(),
                 snapshot_map: snapshot_map,
             },
             image_cache: self.image_cache.clone(),
             font_cache_thread: Mutex::new(self.font_cache_thread.clone()),
             webrender_image_cache: self.webrender_image_cache.clone(),
             pending_images: if script_initiated_layout { Some(Mutex::new(vec![])) } else { None },
             newly_transitioning_nodes: if script_initiated_layout { Some(Mutex::new(vec![])) } else { None },
+            paint_worklet_executor: self.paint_worklet_executor.clone(),
         }
     }
 
     /// Receives and dispatches messages from the script and constellation threads
     fn handle_request<'a, 'b>(&mut self, possibly_locked_rw_data: &mut RwData<'a, 'b>) -> bool {
         enum Request {
             FromPipeline(LayoutControlMsg),
             FromScript(Msg),
@@ -684,16 +690,21 @@ impl LayoutThread {
                 sender.send(outstanding_web_fonts != 0).unwrap();
             },
             Msg::CreateLayoutThread(info) => {
                 self.create_layout_thread(info)
             }
             Msg::SetFinalUrl(final_url) => {
                 self.url = final_url;
             },
+            Msg::SetPaintWorkletExecutor(executor) => {
+                debug!("Setting the paint worklet executor");
+                debug_assert!(self.paint_worklet_executor.is_none());
+                self.paint_worklet_executor = Some(executor);
+            },
             Msg::PrepareToExit(response_chan) => {
                 self.prepare_to_exit(response_chan);
                 return false
             },
             Msg::ExitNow => {
                 debug!("layout: ExitNow received");
                 self.exit_now();
                 return false
--- a/servo/components/net/image_cache.rs
+++ b/servo/components/net/image_cache.rs
@@ -52,32 +52,40 @@ fn decode_bytes_sync(key: LoadKey, bytes
     }
 }
 
 fn get_placeholder_image(webrender_api: &webrender_traits::RenderApi, path: &PathBuf) -> io::Result<Arc<Image>> {
     let mut file = try!(File::open(path));
     let mut image_data = vec![];
     try!(file.read_to_end(&mut image_data));
     let mut image = load_from_memory(&image_data).unwrap();
+    set_webrender_image_key(webrender_api, &mut image);
+    Ok(Arc::new(image))
+}
+
+fn set_webrender_image_key(webrender_api: &webrender_traits::RenderApi, image: &mut Image) {
+    if image.id.is_some() { return; }
     let format = convert_format(image.format);
     let mut bytes = Vec::new();
     bytes.extend_from_slice(&*image.bytes);
+    if format == webrender_traits::ImageFormat::RGBA8 {
+        premultiply(bytes.as_mut_slice());
+    }
     let descriptor = webrender_traits::ImageDescriptor {
         width: image.width,
         height: image.height,
         stride: None,
         format: format,
         offset: 0,
         is_opaque: is_image_opaque(format, &bytes),
     };
     let data = webrender_traits::ImageData::new(bytes);
     let image_key = webrender_api.generate_image_key();
     webrender_api.add_image(image_key, descriptor, data, None);
     image.id = Some(image_key);
-    Ok(Arc::new(image))
 }
 
 // TODO(gw): This is a port of the old is_image_opaque code from WR.
 //           Consider using SIMD to speed this up if it shows in profiles.
 fn is_image_opaque(format: webrender_traits::ImageFormat, bytes: &[u8]) -> bool {
     match format {
         webrender_traits::ImageFormat::RGBA8 => {
             let mut is_opaque = true;
@@ -333,36 +341,17 @@ impl ImageCacheStore {
     // Change state of a url from pending -> loaded.
     fn complete_load(&mut self, key: LoadKey, mut load_result: LoadResult) {
         let pending_load = match self.pending_loads.remove(&key) {
             Some(load) => load,
             None => return,
         };
 
         match load_result {
-            LoadResult::Loaded(ref mut image) => {
-                let format = convert_format(image.format);
-                let mut bytes = Vec::new();
-                bytes.extend_from_slice(&*image.bytes);
-                if format == webrender_traits::ImageFormat::RGBA8 {
-                    premultiply(bytes.as_mut_slice());
-                }
-                let descriptor = webrender_traits::ImageDescriptor {
-                    width: image.width,
-                    height: image.height,
-                    stride: None,
-                    format: format,
-                    offset: 0,
-                    is_opaque: is_image_opaque(format, &bytes),
-                };
-                let data = webrender_traits::ImageData::new(bytes);
-                let image_key = self.webrender_api.generate_image_key();
-                self.webrender_api.add_image(image_key, descriptor, data, None);
-                image.id = Some(image_key);
-            }
+            LoadResult::Loaded(ref mut image) => set_webrender_image_key(&self.webrender_api, image),
             LoadResult::PlaceholderLoaded(..) | LoadResult::None => {}
         }
 
         let url = pending_load.final_url.clone();
         let image_response = match load_result {
             LoadResult::Loaded(image) => ImageResponse::Loaded(Arc::new(image), url.unwrap()),
             LoadResult::PlaceholderLoaded(image) =>
                 ImageResponse::PlaceholderLoaded(image, self.placeholder_url.clone()),
@@ -571,9 +560,14 @@ impl ImageCache for ImageCacheImpl {
                             }
                             None => store.complete_load(id, LoadResult::None),
                         }
                     }
                 }
             }
         }
     }
+
+    /// Ensure an image has a webrender key.
+    fn set_webrender_image_key(&self, image: &mut Image) {
+        set_webrender_image_key(&self.store.lock().unwrap().webrender_api, image);
+    }
 }
--- a/servo/components/net_traits/image_cache.rs
+++ b/servo/components/net_traits/image_cache.rs
@@ -113,9 +113,12 @@ pub trait ImageCache: Sync + Send {
                               -> Result<ImageOrMetadataAvailable, ImageState>;
 
     /// Add a new listener for the given pending image id. If the image is already present,
     /// the responder will still receive the expected response.
     fn add_listener(&self, id: PendingImageId, listener: ImageResponder);
 
     /// Inform the image cache about a response for a pending request.
     fn notify_pending_response(&self, id: PendingImageId, action: FetchResponseMsg);
+
+    /// Ensure an image has a webrender key.
+    fn set_webrender_image_key(&self, image: &mut Image);
 }
--- a/servo/components/script/dom/mod.rs
+++ b/servo/components/script/dom/mod.rs
@@ -386,16 +386,17 @@ pub mod mutationobserver;
 pub mod mutationrecord;
 pub mod namednodemap;
 pub mod navigator;
 pub mod navigatorinfo;
 pub mod node;
 pub mod nodeiterator;
 pub mod nodelist;
 pub mod pagetransitionevent;
+pub mod paintworkletglobalscope;
 pub mod performance;
 pub mod performancetiming;
 pub mod permissions;
 pub mod permissionstatus;
 pub mod plugin;
 pub mod pluginarray;
 pub mod popstateevent;
 pub mod processinginstruction;
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/paintworkletglobalscope.rs
@@ -0,0 +1,97 @@
+/* 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 app_units::Au;
+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::js::Root;
+use dom::bindings::str::DOMString;
+use dom::workletglobalscope::WorkletGlobalScope;
+use dom::workletglobalscope::WorkletGlobalScopeInit;
+use dom_struct::dom_struct;
+use euclid::Size2D;
+use ipc_channel::ipc::IpcSharedMemory;
+use js::rust::Runtime;
+use msg::constellation_msg::PipelineId;
+use net_traits::image::base::Image;
+use net_traits::image::base::PixelFormat;
+use script_traits::PaintWorkletError;
+use servo_atoms::Atom;
+use servo_url::ServoUrl;
+use std::rc::Rc;
+use std::sync::mpsc::Sender;
+
+#[dom_struct]
+/// https://drafts.css-houdini.org/css-paint-api/#paintworkletglobalscope
+pub struct PaintWorkletGlobalScope {
+    /// The worklet global for this object
+    worklet_global: WorkletGlobalScope,
+    /// A buffer to draw into
+    buffer: DOMRefCell<Vec<u8>>,
+}
+
+impl PaintWorkletGlobalScope {
+    #[allow(unsafe_code)]
+    pub fn new(runtime: &Runtime,
+               pipeline_id: PipelineId,
+               base_url: ServoUrl,
+               init: &WorkletGlobalScopeInit)
+               -> Root<PaintWorkletGlobalScope> {
+        debug!("Creating paint worklet global scope for pipeline {}.", pipeline_id);
+        let global = box PaintWorkletGlobalScope {
+            worklet_global: WorkletGlobalScope::new_inherited(pipeline_id, base_url, init),
+            buffer: Default::default(),
+        };
+        unsafe { PaintWorkletGlobalScopeBinding::Wrap(runtime.cx(), global) }
+    }
+
+    pub fn perform_a_worklet_task(&self, task: PaintWorkletTask) {
+        match task {
+            PaintWorkletTask::DrawAPaintImage(name, size, sender) => self.draw_a_paint_image(name, size, sender),
+        }
+    }
+
+    fn draw_a_paint_image(&self,
+                          name: Atom,
+                          concrete_object_size: Size2D<Au>,
+                          sender: Sender<Result<Image, PaintWorkletError>>) {
+        let width = concrete_object_size.width.to_px().abs() as u32;
+        let height = concrete_object_size.height.to_px().abs() as u32;
+        let area = (width as usize) * (height as usize);
+        let old_buffer_size = self.buffer.borrow().len();
+        let new_buffer_size = area * 4;
+        debug!("Drawing a paint image {}({},{}).", name, width, height);
+        // TODO: call into script to create the image.
+        // For now, we just build a dummy.
+        if new_buffer_size > old_buffer_size {
+            let pixel = [0xFF, 0x00, 0x00, 0xFF];
+            self.buffer.borrow_mut().extend(pixel.iter().cycle().take(new_buffer_size - old_buffer_size));
+        } else {
+            self.buffer.borrow_mut().truncate(new_buffer_size);
+        }
+        let image = Image {
+            width: width,
+            height: height,
+            format: PixelFormat::RGBA8,
+            bytes: IpcSharedMemory::from_bytes(&*self.buffer.borrow()),
+            id: None,
+        };
+        let _ = sender.send(Ok(image));
+    }
+}
+
+impl PaintWorkletGlobalScopeMethods for PaintWorkletGlobalScope {
+    /// https://drafts.css-houdini.org/css-paint-api/#dom-paintworkletglobalscope-registerpaint
+    fn RegisterPaint(&self, name: DOMString, _paintCtor: Rc<VoidFunction>) {
+        debug!("Registering paint image name {}.", name);
+        // TODO
+    }
+}
+
+/// Tasks which can be peformed by a paint worklet
+pub enum PaintWorkletTask {
+    DrawAPaintImage(Atom, Size2D<Au>, Sender<Result<Image, PaintWorkletError>>)
+}
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/webidls/PaintWorkletGlobalScope.webidl
@@ -0,0 +1,9 @@
+/* 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/. */
+
+// https://drafts.css-houdini.org/css-paint-api/#paintworkletglobalscope
+[Global=(Worklet,PaintWorklet), Exposed=PaintWorklet]
+interface PaintWorkletGlobalScope : WorkletGlobalScope {
+    void registerPaint(DOMString name, VoidFunction paintCtor);
+};
--- a/servo/components/script/dom/webidls/Window.webidl
+++ b/servo/components/script/dom/webidls/Window.webidl
@@ -199,8 +199,12 @@ callback FrameRequestCallback = void (DO
 
 // https://webbluetoothcg.github.io/web-bluetooth/tests#test-interfaces
 partial interface Window {
    [Pref="dom.bluetooth.testing.enabled", Exposed=Window]
    readonly attribute TestRunner testRunner;
    //readonly attribute EventSender eventSender;
 };
 
+// https://drafts.css-houdini.org/css-paint-api-1/#paint-worklet
+partial interface Window {
+    [SameObject] readonly attribute Worklet paintWorklet;
+};
--- a/servo/components/script/dom/window.rs
+++ b/servo/components/script/dom/window.rs
@@ -46,16 +46,17 @@ use dom::navigator::Navigator;
 use dom::node::{Node, NodeDamage, document_from_node, from_untrusted_node_address};
 use dom::performance::Performance;
 use dom::promise::Promise;
 use dom::screen::Screen;
 use dom::storage::Storage;
 use dom::testrunner::TestRunner;
 use dom::windowproxy::WindowProxy;
 use dom::worklet::Worklet;
+use dom::workletglobalscope::WorkletGlobalScopeType;
 use dom_struct::dom_struct;
 use euclid::{Point2D, Rect, Size2D};
 use fetch;
 use ipc_channel::ipc::{self, IpcSender};
 use ipc_channel::router::ROUTER;
 use js::jsapi::{HandleObject, HandleValue, JSAutoCompartment, JSContext};
 use js::jsapi::{JS_GC, JS_GetRuntime};
 use js::jsval::UndefinedValue;
@@ -274,16 +275,18 @@ pub struct Window {
     pending_layout_images: DOMRefCell<HashMap<PendingImageId, Vec<JS<Node>>>>,
 
     /// Directory to store unminified scripts for this window if unminify-js
     /// opt is enabled.
     unminified_js_dir: DOMRefCell<Option<String>>,
 
     /// Worklets
     test_worklet: MutNullableJS<Worklet>,
+    /// https://drafts.css-houdini.org/css-paint-api-1/#paint-worklet
+    paint_worklet: MutNullableJS<Worklet>,
 }
 
 impl Window {
     #[allow(unsafe_code)]
     pub fn clear_js_runtime_for_script_deallocation(&self) {
         unsafe {
             *self.js_runtime.borrow_for_script_deallocation() = None;
             self.window_proxy.set(None);
@@ -368,16 +371,24 @@ impl Window {
     pub fn current_viewport(&self) -> Rect<Au> {
         self.current_viewport.clone().get()
     }
 
     pub fn webvr_thread(&self) -> Option<IpcSender<WebVRMsg>> {
         self.webvr_thread.clone()
     }
 
+    fn new_paint_worklet(&self) -> Root<Worklet> {
+        debug!("Creating new paint worklet.");
+        let worklet = Worklet::new(self, WorkletGlobalScopeType::Paint);
+        let executor = Arc::new(worklet.executor());
+        let _ = self.layout_chan.send(Msg::SetPaintWorkletExecutor(executor));
+        worklet
+    }
+
     pub fn permission_state_invocation_results(&self) -> &DOMRefCell<HashMap<String, PermissionState>> {
         &self.permission_state_invocation_results
     }
 
     pub fn pending_image_notification(&self, response: PendingImageResponse) {
         //XXXjdm could be more efficient to send the responses to the layout thread,
         //       rather than making the layout thread talk to the image cache to
         //       obtain the same data.
@@ -1006,16 +1017,21 @@ impl WindowMethods for Window {
     }
 
     #[allow(unrooted_must_root)]
     // https://fetch.spec.whatwg.org/#fetch-method
     fn Fetch(&self, input: RequestOrUSVString, init: RootedTraceableBox<RequestInit>) -> Rc<Promise> {
         fetch::Fetch(&self.upcast(), input, init)
     }
 
+    // https://drafts.css-houdini.org/css-paint-api-1/#paint-worklet
+    fn PaintWorklet(&self) -> Root<Worklet> {
+        self.paint_worklet.or_init(|| self.new_paint_worklet())
+    }
+
     fn TestRunner(&self) -> Root<TestRunner> {
         self.test_runner.or_init(|| TestRunner::new(self.upcast()))
     }
 }
 
 impl Window {
     pub fn get_runnable_wrapper(&self) -> RunnableWrapper {
         RunnableWrapper {
@@ -1851,16 +1867,17 @@ impl Window {
             scroll_offsets: DOMRefCell::new(HashMap::new()),
             media_query_lists: WeakMediaQueryListVec::new(),
             test_runner: Default::default(),
             webvr_thread: webvr_thread,
             permission_state_invocation_results: DOMRefCell::new(HashMap::new()),
             pending_layout_images: DOMRefCell::new(HashMap::new()),
             unminified_js_dir: DOMRefCell::new(None),
             test_worklet: Default::default(),
+            paint_worklet: Default::default(),
         };
 
         unsafe {
             WindowBinding::Wrap(runtime.cx(), win)
         }
     }
 }
 
--- a/servo/components/script/dom/worklet.rs
+++ b/servo/components/script/dom/worklet.rs
@@ -5,16 +5,17 @@
 //! An implementation of Houdini worklets.
 //!
 //! The goal of this implementation is to maximize responsiveness of worklets,
 //! and in particular to ensure that the thread performing worklet tasks
 //! is never busy GCing or loading worklet code. We do this by providing a custom
 //! thread pool implementation, which only performs GC or code loading on
 //! a backup thread, not on the primary worklet thread.
 
+use app_units::Au;
 use dom::bindings::codegen::Bindings::RequestBinding::RequestCredentials;
 use dom::bindings::codegen::Bindings::WindowBinding::WindowBinding::WindowMethods;
 use dom::bindings::codegen::Bindings::WorkletBinding::WorkletMethods;
 use dom::bindings::codegen::Bindings::WorkletBinding::WorkletOptions;
 use dom::bindings::codegen::Bindings::WorkletBinding::Wrap;
 use dom::bindings::error::Error;
 use dom::bindings::inheritance::Castable;
 use dom::bindings::js::JS;
@@ -22,65 +23,74 @@ use dom::bindings::js::Root;
 use dom::bindings::js::RootCollection;
 use dom::bindings::refcounted::TrustedPromise;
 use dom::bindings::reflector::Reflector;
 use dom::bindings::reflector::reflect_dom_object;
 use dom::bindings::str::USVString;
 use dom::bindings::trace::JSTraceable;
 use dom::bindings::trace::RootedTraceableBox;
 use dom::globalscope::GlobalScope;
+use dom::paintworkletglobalscope::PaintWorkletTask;
 use dom::promise::Promise;
 use dom::testworkletglobalscope::TestWorkletTask;
 use dom::window::Window;
 use dom::workletglobalscope::WorkletGlobalScope;
 use dom::workletglobalscope::WorkletGlobalScopeInit;
 use dom::workletglobalscope::WorkletGlobalScopeType;
 use dom::workletglobalscope::WorkletTask;
 use dom_struct::dom_struct;
+use euclid::Size2D;
 use js::jsapi::JSGCParamKey;
 use js::jsapi::JSTracer;
 use js::jsapi::JS_GC;
 use js::jsapi::JS_GetGCParameter;
 use js::rust::Runtime;
 use msg::constellation_msg::PipelineId;
 use net_traits::IpcSend;
+use net_traits::image::base::Image;
 use net_traits::load_whole_resource;
 use net_traits::request::Destination;
 use net_traits::request::RequestInit;
 use net_traits::request::RequestMode;
 use net_traits::request::Type as RequestType;
 use script_runtime::CommonScriptMsg;
 use script_runtime::ScriptThreadEventCategory;
 use script_runtime::StackRootTLS;
 use script_runtime::new_rt_and_cx;
 use script_thread::MainThreadScriptMsg;
 use script_thread::Runnable;
 use script_thread::ScriptThread;
+use script_traits::PaintWorkletError;
+use script_traits::PaintWorkletExecutor;
+use servo_atoms::Atom;
 use servo_rand;
 use servo_url::ImmutableOrigin;
 use servo_url::ServoUrl;
 use std::cmp::max;
 use std::collections::HashMap;
 use std::collections::hash_map;
 use std::rc::Rc;
 use std::sync::Arc;
+use std::sync::Mutex;
 use std::sync::atomic::AtomicIsize;
 use std::sync::atomic::Ordering;
 use std::sync::mpsc;
 use std::sync::mpsc::Receiver;
 use std::sync::mpsc::Sender;
 use std::thread;
+use std::time::Duration;
 use style::thread_state;
 use swapper::Swapper;
 use swapper::swapper;
 use uuid::Uuid;
 
 // Magic numbers
 const WORKLET_THREAD_POOL_SIZE: u32 = 3;
 const MIN_GC_THRESHOLD: u32 = 1_000_000;
+const PAINT_TIMEOUT_MILLISECONDS: u64 = 10;
 
 #[dom_struct]
 /// https://drafts.css-houdini.org/worklets/#worklet
 pub struct Worklet {
     reflector: Reflector,
     window: JS<Window>,
     worklet_id: WorkletId,
     global_type: WorkletGlobalScopeType,
@@ -104,16 +114,23 @@ impl Worklet {
     pub fn worklet_id(&self) -> WorkletId {
         self.worklet_id
     }
 
     #[allow(dead_code)]
     pub fn worklet_global_scope_type(&self) -> WorkletGlobalScopeType {
         self.global_type
     }
+
+    pub fn executor(&self) -> WorkletExecutor {
+        WorkletExecutor {
+            worklet_id: self.worklet_id,
+            primary_sender: Mutex::new(ScriptThread::worklet_thread_pool().primary_sender.clone()),
+        }
+    }
 }
 
 impl WorkletMethods for Worklet {
     #[allow(unrooted_must_root)]
     /// https://drafts.css-houdini.org/worklets/#dom-worklet-addmodule
     fn AddModule(&self, module_url: USVString, options: &WorkletOptions) -> Rc<Promise> {
         // Step 1.
         let promise = Promise::new(self.window.upcast());
@@ -556,17 +573,18 @@ impl WorkletThread {
         // TODO: Settings object?
 
         // Step 2.
         // TODO: Fetch a module graph, not just a single script.
         // TODO: Fetch the script asynchronously?
         // TODO: Caching.
         // TODO: Avoid re-parsing the origin as a URL.
         let resource_fetcher = self.global_init.resource_threads.sender();
-        let origin_url = ServoUrl::parse(&*origin.unicode_serialization()).expect("Failed to parse origin as URL.");
+        let origin_url = ServoUrl::parse(&*origin.unicode_serialization())
+            .unwrap_or_else(|_| ServoUrl::parse("about:blank").unwrap());
         let request = RequestInit {
             url: script_url,
             type_: RequestType::Script,
             destination: Destination::Script,
             mode: RequestMode::CorsMode,
             origin: origin_url,
             credentials_mode: credentials.into(),
             .. RequestInit::default()
@@ -630,8 +648,40 @@ impl WorkletThread {
     fn run_in_script_thread<R>(&self, runnable: R) where
         R: 'static + Send + Runnable,
     {
         let msg = CommonScriptMsg::RunnableMsg(ScriptThreadEventCategory::WorkletEvent, box runnable);
         let msg = MainThreadScriptMsg::Common(msg);
         self.script_sender.send(msg).expect("Worklet thread outlived script thread.");
     }
 }
+
+/// An executor of worklet tasks
+pub struct WorkletExecutor {
+    worklet_id: WorkletId,
+    // Rather annoyingly, we have to use a mutex here because
+    // layout threads share their context rather than cloning it.
+    primary_sender: Mutex<Sender<WorkletData>>,
+}
+
+impl WorkletExecutor {
+    /// Schedule a worklet task to be peformed by the worklet thread pool.
+    fn schedule_a_worklet_task(&self, task: WorkletTask) {
+        let _ = self.primary_sender.lock()
+            .expect("Locking the worklet channel.")
+            .send(WorkletData::Task(self.worklet_id, task));
+    }
+}
+
+impl PaintWorkletExecutor for WorkletExecutor {
+    /// https://drafts.css-houdini.org/css-paint-api/#draw-a-paint-image
+    fn draw_a_paint_image(&self,
+                          name: Atom,
+                          concrete_object_size: Size2D<Au>)
+                          -> Result<Image, PaintWorkletError>
+    {
+        let (sender, receiver) = mpsc::channel();
+        let task = WorkletTask::Paint(PaintWorkletTask::DrawAPaintImage(name, concrete_object_size, sender));
+        let timeout = Duration::from_millis(PAINT_TIMEOUT_MILLISECONDS);
+        self.schedule_a_worklet_task(task);
+        receiver.recv_timeout(timeout)?
+    }
+}
--- a/servo/components/script/dom/workletglobalscope.rs
+++ b/servo/components/script/dom/workletglobalscope.rs
@@ -1,16 +1,18 @@
 /* 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 devtools_traits::ScriptToDevtoolsControlMsg;
 use dom::bindings::inheritance::Castable;
 use dom::bindings::js::Root;
 use dom::globalscope::GlobalScope;
+use dom::paintworkletglobalscope::PaintWorkletGlobalScope;
+use dom::paintworkletglobalscope::PaintWorkletTask;
 use dom::testworkletglobalscope::TestWorkletGlobalScope;
 use dom::testworkletglobalscope::TestWorkletTask;
 use dom_struct::dom_struct;
 use ipc_channel::ipc;
 use ipc_channel::ipc::IpcSender;
 use js::jsval::UndefinedValue;
 use js::rust::Runtime;
 use microtask::Microtask;
@@ -87,16 +89,20 @@ impl WorkletGlobalScope {
 
     /// Perform a worklet task
     pub fn perform_a_worklet_task(&self, task: WorkletTask) {
         match task {
             WorkletTask::Test(task) => match self.downcast::<TestWorkletGlobalScope>() {
                 Some(global) => global.perform_a_worklet_task(task),
                 None => warn!("This is not a test worklet."),
             },
+            WorkletTask::Paint(task) => match self.downcast::<PaintWorkletGlobalScope>() {
+                Some(global) => global.perform_a_worklet_task(task),
+                None => warn!("This is not a paint worklet."),
+            },
         }
     }
 }
 
 /// Resources required by workletglobalscopes
 #[derive(Clone)]
 pub struct WorkletGlobalScopeInit {
     /// Channel to a resource thread
@@ -111,33 +117,37 @@ pub struct WorkletGlobalScopeInit {
     pub constellation_chan: IpcSender<ScriptMsg>,
     /// Message to send to the scheduler
     pub scheduler_chan: IpcSender<TimerSchedulerMsg>,
 }
 
 /// https://drafts.css-houdini.org/worklets/#worklet-global-scope-type
 #[derive(Clone, Copy, Debug, HeapSizeOf, JSTraceable)]
 pub enum WorkletGlobalScopeType {
-    /// https://drafts.css-houdini.org/worklets/#examples
+    /// A servo-specific testing worklet
     Test,
+    /// A paint worklet
+    Paint,
 }
 
 impl WorkletGlobalScopeType {
     /// Create a new heap-allocated `WorkletGlobalScope`.
     pub fn new(&self,
                runtime: &Runtime,
                pipeline_id: PipelineId,
                base_url: ServoUrl,
                init: &WorkletGlobalScopeInit)
                -> Root<WorkletGlobalScope>
     {
         match *self {
             WorkletGlobalScopeType::Test =>
                 Root::upcast(TestWorkletGlobalScope::new(runtime, pipeline_id, base_url, init)),
+            WorkletGlobalScopeType::Paint =>
+                Root::upcast(PaintWorkletGlobalScope::new(runtime, pipeline_id, base_url, init)),
         }
     }
 }
 
 /// A task which can be performed in the context of a worklet global.
 pub enum WorkletTask {
     Test(TestWorkletTask),
+    Paint(PaintWorkletTask),
 }
-
--- a/servo/components/script_layout_interface/message.rs
+++ b/servo/components/script_layout_interface/message.rs
@@ -9,16 +9,17 @@ use euclid::rect::Rect;
 use gfx_traits::Epoch;
 use ipc_channel::ipc::{IpcReceiver, IpcSender};
 use msg::constellation_msg::PipelineId;
 use net_traits::image_cache::ImageCache;
 use profile_traits::mem::ReportsChan;
 use rpc::LayoutRPC;
 use script_traits::{ConstellationControlMsg, LayoutControlMsg, LayoutMsg as ConstellationMsg};
 use script_traits::{ScrollState, UntrustedNodeAddress, WindowSizeData};
+use script_traits::PaintWorkletExecutor;
 use servo_url::ServoUrl;
 use std::sync::Arc;
 use std::sync::mpsc::{Receiver, Sender};
 use style::context::{QuirksMode, ReflowGoal};
 use style::properties::PropertyId;
 use style::selector_parser::PseudoElement;
 use style::stylesheets::Stylesheet;
 
@@ -79,16 +80,19 @@ pub enum Msg {
     SetFinalUrl(ServoUrl),
 
     /// Tells layout about the new scrolling offsets of each scrollable stacking context.
     SetScrollStates(Vec<ScrollState>),
 
     /// Tells layout about a single new scrolling offset from the script. The rest will
     /// remain untouched and layout won't forward this back to script.
     UpdateScrollStateFromScript(ScrollState),
+
+    /// Tells layout that script has added some paint worklet modules.
+    SetPaintWorkletExecutor(Arc<PaintWorkletExecutor>),
 }
 
 
 /// Any query to perform with this reflow.
 #[derive(Debug, PartialEq)]
 pub enum ReflowQueryType {
     NoQuery,
     ContentBoxQuery(TrustedNodeAddress),
--- a/servo/components/script_traits/Cargo.toml
+++ b/servo/components/script_traits/Cargo.toml
@@ -5,16 +5,17 @@ authors = ["The Servo Project Developers
 license = "MPL-2.0"
 publish = false
 
 [lib]
 name = "script_traits"
 path = "lib.rs"
 
 [dependencies]
+app_units = "0.4"
 bluetooth_traits = {path = "../bluetooth_traits"}
 canvas_traits = {path = "../canvas_traits"}
 cookie = "0.6"
 devtools_traits = {path = "../devtools_traits"}
 euclid = "0.13"
 gfx_traits = {path = "../gfx_traits"}
 heapsize = "0.4"
 heapsize_derive = "0.1"
@@ -24,14 +25,15 @@ ipc-channel = "0.7"
 libc = "0.2"
 msg = {path = "../msg"}
 net_traits = {path = "../net_traits"}
 offscreen_gl_context = "0.8"
 profile_traits = {path = "../profile_traits"}
 rustc-serialize = "0.3.4"
 serde = "0.9"
 serde_derive = "0.9"
+servo_atoms = {path = "../atoms"}
 servo_url = {path = "../url"}
 style_traits = {path = "../style_traits", features = ["servo"]}
 time = "0.1.12"
 url = {version = "1.2", features = ["heap_size"]}
 webrender_traits = {git = "https://github.com/servo/webrender", features = ["ipc"]}
 webvr_traits = {path = "../webvr_traits"}
--- a/servo/components/script_traits/lib.rs
+++ b/servo/components/script_traits/lib.rs
@@ -4,16 +4,17 @@
 
 //! This module contains traits in script used generically in the rest of Servo.
 //! The traits are here instead of in script so that these modules won't have
 //! to depend on script.
 
 #![deny(missing_docs)]
 #![deny(unsafe_code)]
 
+extern crate app_units;
 extern crate bluetooth_traits;
 extern crate canvas_traits;
 extern crate cookie as cookie_rs;
 extern crate devtools_traits;
 extern crate euclid;
 extern crate gfx_traits;
 extern crate heapsize;
 #[macro_use]
@@ -25,25 +26,27 @@ extern crate libc;
 extern crate msg;
 extern crate net_traits;
 extern crate offscreen_gl_context;
 extern crate profile_traits;
 extern crate rustc_serialize;
 extern crate serde;
 #[macro_use]
 extern crate serde_derive;
+extern crate servo_atoms;
 extern crate servo_url;
 extern crate style_traits;
 extern crate time;
 extern crate webrender_traits;
 extern crate webvr_traits;
 
 mod script_msg;
 pub mod webdriver_msg;
 
+use app_units::Au;
 use bluetooth_traits::BluetoothRequest;
 use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, WorkerId};
 use euclid::Size2D;
 use euclid::length::Length;
 use euclid::point::Point2D;
 use euclid::rect::Rect;
 use euclid::scale_factor::ScaleFactor;
 use euclid::size::TypedSize2D;
@@ -58,22 +61,23 @@ use msg::constellation_msg::{PipelineId,
 use net_traits::{FetchResponseMsg, ReferrerPolicy, ResourceThreads};
 use net_traits::image::base::Image;
 use net_traits::image_cache::ImageCache;
 use net_traits::response::HttpsState;
 use net_traits::storage_thread::StorageType;
 use profile_traits::mem;
 use profile_traits::time as profile_time;
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use servo_atoms::Atom;
 use servo_url::ImmutableOrigin;
 use servo_url::ServoUrl;
 use std::collections::HashMap;
 use std::fmt;
 use std::sync::Arc;
-use std::sync::mpsc::{Receiver, Sender};
+use std::sync::mpsc::{Receiver, Sender, RecvTimeoutError};
 use style_traits::CSSPixel;
 use webdriver_msg::{LoadStatus, WebDriverScriptCommand};
 use webrender_traits::ClipId;
 use webvr_traits::{WebVREvent, WebVRMsg};
 
 pub use script_msg::{LayoutMsg, ScriptMsg, EventResult, LogEntry};
 pub use script_msg::{ServiceWorkerMsg, ScopeThings, SWManagerMsg, SWManagerSenders, DOMMessage};
 
@@ -808,8 +812,33 @@ pub struct WorkerGlobalScopeInit {
 pub struct WorkerScriptLoadOrigin {
     /// referrer url
     pub referrer_url: Option<ServoUrl>,
     /// the referrer policy which is used
     pub referrer_policy: Option<ReferrerPolicy>,
     /// the pipeline id of the entity requesting the load
     pub pipeline_id: Option<PipelineId>,
 }
+
+/// Errors from executing a paint worklet
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub enum PaintWorkletError {
+    /// Execution timed out.
+    Timeout,
+    /// No such worklet.
+    WorkletNotFound,
+}
+
+impl From<RecvTimeoutError> for PaintWorkletError {
+    fn from(_: RecvTimeoutError) -> PaintWorkletError {
+        PaintWorkletError::Timeout
+    }
+}
+
+/// Execute paint code in the worklet thread pool.<
+pub trait PaintWorkletExecutor: Sync + Send {
+    /// https://drafts.css-houdini.org/css-paint-api/#draw-a-paint-image
+    fn draw_a_paint_image(&self,
+                          name: Atom,
+                          concrete_object_size: Size2D<Au>)
+                          -> Result<Image, PaintWorkletError>;
+}
+
--- a/servo/components/style/values/generics/image.rs
+++ b/servo/components/style/values/generics/image.rs
@@ -22,16 +22,20 @@ pub enum Image<Gradient, ImageRect> {
     /// A `<url()>` image.
     Url(SpecifiedUrl),
     /// A `<gradient>` image.
     Gradient(Gradient),
     /// A `-moz-image-rect` image
     Rect(ImageRect),
     /// A `-moz-element(# <element-id>)`
     Element(Atom),
+    /// A paint worklet image.
+    /// https://drafts.css-houdini.org/css-paint-api/
+    #[cfg(feature = "servo")]
+    PaintWorklet(PaintWorklet),
 }
 
 /// A CSS gradient.
 /// https://drafts.csswg.org/css-images/#gradients
 #[derive(Clone, Debug, HasViewportPercentage, PartialEq, ToComputedValue)]
 #[cfg_attr(feature = "servo", derive(HeapSizeOf))]
 pub struct Gradient<LineDirection, Length, LengthOrPercentage, Position, Color> {
     /// Gradients can be linear or radial.
@@ -123,16 +127,33 @@ pub enum GradientItem<Color, LengthOrPer
 #[cfg_attr(feature = "servo", derive(HeapSizeOf))]
 pub struct ColorStop<Color, LengthOrPercentage> {
     /// The color of this stop.
     pub color: Color,
     /// The position of this stop.
     pub position: Option<LengthOrPercentage>,
 }
 
+/// Specified values for a paint worklet.
+/// https://drafts.css-houdini.org/css-paint-api/
+#[derive(Clone, Debug, PartialEq, ToComputedValue)]
+#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
+pub struct PaintWorklet {
+    /// The name the worklet was registered with.
+    pub name: Atom,
+}
+
+impl ToCss for PaintWorklet {
+    fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
+        dest.write_str("paint(")?;
+        serialize_identifier(&*self.name.to_string(), dest)?;
+        dest.write_str(")")
+    }
+}
+
 /// Values for `moz-image-rect`.
 ///
 /// `-moz-image-rect(<uri>, top, right, bottom, left);`
 #[derive(Clone, Debug, PartialEq, ToComputedValue)]
 #[cfg_attr(feature = "servo", derive(HeapSizeOf))]
 #[allow(missing_docs)]
 pub struct ImageRect<NumberOrPercentage> {
     pub url: SpecifiedUrl,
@@ -145,16 +166,18 @@ pub struct ImageRect<NumberOrPercentage>
 impl<G, R> fmt::Debug for Image<G, R>
     where G: fmt::Debug, R: fmt::Debug,
 {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match *self {
             Image::Url(ref url) => url.to_css(f),
             Image::Gradient(ref grad) => grad.fmt(f),
             Image::Rect(ref rect) => rect.fmt(f),
+            #[cfg(feature = "servo")]
+            Image::PaintWorklet(ref paint_worklet) => paint_worklet.fmt(f),
             Image::Element(ref selector) => {
                 f.write_str("-moz-element(#")?;
                 serialize_identifier(&selector.to_string(), f)?;
                 f.write_str(")")
             },
         }
     }
 }
@@ -162,16 +185,18 @@ impl<G, R> fmt::Debug for Image<G, R>
 impl<G, R> ToCss for Image<G, R>
     where G: ToCss, R: ToCss,
 {
     fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
         match *self {
             Image::Url(ref url) => url.to_css(dest),
             Image::Gradient(ref gradient) => gradient.to_css(dest),
             Image::Rect(ref rect) => rect.to_css(dest),
+            #[cfg(feature = "servo")]
+            Image::PaintWorklet(ref paint_worklet) => paint_worklet.to_css(dest),
             Image::Element(ref selector) => {
                 dest.write_str("-moz-element(#")?;
                 serialize_identifier(&selector.to_string(), dest)?;
                 dest.write_str(")")
             },
         }
     }
 }
--- a/servo/components/style/values/specified/image.rs
+++ b/servo/components/style/values/specified/image.rs
@@ -17,16 +17,17 @@ use std::f32::consts::PI;
 use std::fmt;
 use style_traits::ToCss;
 use values::{Either, None_};
 use values::generics::image::{Circle, CompatMode, Ellipse, ColorStop as GenericColorStop};
 use values::generics::image::{EndingShape as GenericEndingShape, Gradient as GenericGradient};
 use values::generics::image::{GradientItem as GenericGradientItem, GradientKind as GenericGradientKind};
 use values::generics::image::{Image as GenericImage, ImageRect as GenericImageRect};
 use values::generics::image::{LineDirection as GenericsLineDirection, ShapeExtent};
+use values::generics::image::PaintWorklet;
 use values::generics::position::Position as GenericPosition;
 use values::specified::{Angle, CSSColor, Color, Length, LengthOrPercentage};
 use values::specified::{Number, NumberOrPercentage, Percentage};
 use values::specified::position::{Position, PositionComponent, Side, X, Y};
 use values::specified::url::SpecifiedUrl;
 
 /// A specified image layer.
 pub type ImageLayer = Either<None_, Image>;
@@ -93,16 +94,22 @@ impl Parse for Image {
         {
           if let Ok(url) = input.try(|input| SpecifiedUrl::parse(context, input)) {
               return Ok(GenericImage::Url(url));
           }
         }
         if let Ok(gradient) = input.try(|i| Gradient::parse(context, i)) {
             return Ok(GenericImage::Gradient(gradient));
         }
+        #[cfg(feature = "servo")]
+        {
+            if let Ok(paint_worklet) = input.try(|i| PaintWorklet::parse(context, i)) {
+                return Ok(GenericImage::PaintWorklet(paint_worklet));
+            }
+        }
         #[cfg(feature = "gecko")]
         {
             if let Ok(mut image_rect) = input.try(|input| ImageRect::parse(context, input)) {
                 image_rect.url.build_image_value();
                 return Ok(GenericImage::Rect(image_rect));
             }
         }
         #[cfg(feature = "servo")]
@@ -668,16 +675,28 @@ impl Parse for ColorStop {
     fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ()> {
         Ok(ColorStop {
             color: try!(CSSColor::parse(context, input)),
             position: input.try(|i| LengthOrPercentage::parse(context, i)).ok(),
         })
     }
 }
 
+impl Parse for PaintWorklet {
+    fn parse(_context: &ParserContext, input: &mut Parser) -> Result<Self, ()> {
+        input.expect_function_matching("paint")?;
+        input.parse_nested_block(|i| {
+            let name = i.expect_ident()?;
+            Ok(PaintWorklet {
+                name: Atom::from(name),
+            })
+        })
+    }
+}
+
 impl Parse for ImageRect {
     fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ()> {
         input.try(|i| i.expect_function_matching("-moz-image-rect"))?;
         input.parse_nested_block(|i| {
             let string = i.expect_url_or_string()?;
             let url = SpecifiedUrl::parse_from_string(string, context)?;
             i.expect_comma()?;
             let top = NumberOrPercentage::parse_non_negative(context, i)?;