From 0b9015230536c5dd002f1e8fcbf59ca3d43b1b20 Mon Sep 17 00:00:00 2001 From: Nicolas Silva Date: Fri, 28 Apr 2017 11:25:13 +0200 Subject: [PATCH] Make the example blob renderer multi-threaded. --- webrender/examples/blob.rs | 210 ++++++++++++++++++++++++++----------- 1 file changed, 148 insertions(+), 62 deletions(-) diff --git a/webrender/examples/blob.rs b/webrender/examples/blob.rs index 554cffe9de..4e785f6ea4 100644 --- a/webrender/examples/blob.rs +++ b/webrender/examples/blob.rs @@ -8,11 +8,17 @@ extern crate gleam; extern crate glutin; extern crate webrender; extern crate webrender_traits; +extern crate rayon; use gleam::gl; +use rayon::ThreadPool; +use rayon::Configuration as ThreadPoolConfig; use std::collections::HashMap; +use std::collections::hash_map::Entry; +use std::sync::Arc; +use std::sync::mpsc::{channel, Sender, Receiver}; use webrender_traits::{BlobImageData, BlobImageDescriptor, BlobImageError, BlobImageRenderer, BlobImageRequest}; -use webrender_traits::{BlobImageResult, ImageStore, ColorF, ColorU, Epoch}; +use webrender_traits::{BlobImageResult, TileOffset, ImageStore, ColorF, ColorU, Epoch}; use webrender_traits::{DeviceUintSize, DeviceUintRect, LayoutPoint, LayoutRect, LayoutSize}; use webrender_traits::{ImageData, ImageDescriptor, ImageFormat, ImageRendering, ImageKey, TileSize}; use webrender_traits::{PipelineId, RasterizedBlobImage, TransformStyle}; @@ -39,32 +45,109 @@ fn deserialize_blob(blob: &[u8]) -> Result { } } +// This is the function that applies the deserialized drawing commands and generates +// actual image data. +fn render_blob( + commands: Arc, + descriptor: &BlobImageDescriptor, + tile: Option, +) -> BlobImageResult { + let color = *commands; + + // Allocate storage for the result. Right now the resource cache expects the + // tiles to have have no stride or offset. + let mut texels = Vec::with_capacity((descriptor.width * descriptor.height * 4) as usize); + + // Generate a per-tile pattern to see it in the demo. For a real use case it would not + // make sense for the rendered content to depend on its tile. + let tile_checker = match tile { + Some(tile) => (tile.x % 2 == 0) != (tile.y % 2 == 0), + None => true, + }; + + for y in 0..descriptor.height { + for x in 0..descriptor.width { + // Apply the tile's offset. This is important: all drawing commands should be + // translated by this offset to give correct results with tiled blob images. + let x2 = x + descriptor.offset.x as u32; + let y2 = y + descriptor.offset.y as u32; + + // Render a simple checkerboard pattern + let checker = if (x2 % 20 >= 10) != (y2 % 20 >= 10) { 1 } else { 0 }; + // ..nested in the per-tile cherkerboard pattern + let tc = if tile_checker { 0 } else { (1 - checker) * 40 }; + + match descriptor.format { + ImageFormat::RGBA8 => { + texels.push(color.b * checker + tc); + texels.push(color.g * checker + tc); + texels.push(color.r * checker + tc); + texels.push(color.a * checker + tc); + } + ImageFormat::A8 => { + texels.push(color.a * checker + tc); + } + _ => { + return Err(BlobImageError::Other(format!( + "Usupported image format {:?}", + descriptor.format + ))); + } + } + } + } + + Ok(RasterizedBlobImage { + data: texels, + width: descriptor.width, + height: descriptor.height, + }) +} + struct CheckerboardRenderer { - // The deserialized drawing commands. - image_cmds: HashMap, + // We are going to defer the rendering work to worker threads. + // Using a pre-built Arc rather than creating our own threads + // makes it possible to share the same thread pool as the glyph renderer (if we + // want to). + workers: Arc, - // The images rendered in the current frame (not kept here between frames) - rendered_images: HashMap, + // the workers will use an mpsc channel to communicate the result. + tx: Sender<(BlobImageRequest, BlobImageResult)>, + rx: Receiver<(BlobImageRequest, BlobImageResult)>, + + // The deserialized drawing commands. + // In this example we store them in Arcs. This isn't necessary since in this simplified + // case the command list is a simple 32 bits value and would be cheap to clone before sending + // to the workers. But in a more realistic scenario the commands would typically be bigger + // and more expensive to clone, so let's pretend it is also the case here. + image_cmds: HashMap>, + + // The images rendered in the current frame (not kept here between frames). + rendered_images: HashMap>, } impl CheckerboardRenderer { - fn new() -> Self { + fn new(workers: Arc) -> Self { + let (tx, rx) = channel(); CheckerboardRenderer { image_cmds: HashMap::new(), rendered_images: HashMap::new(), + workers: workers, + tx: tx, + rx: rx, } } } impl BlobImageRenderer for CheckerboardRenderer { fn add(&mut self, key: ImageKey, cmds: BlobImageData, _: Option) { - self.image_cmds.insert(key, deserialize_blob(&cmds[..]).unwrap()); + self.image_cmds.insert(key, Arc::new(deserialize_blob(&cmds[..]).unwrap())); } fn update(&mut self, key: ImageKey, cmds: BlobImageData) { // Here, updating is just replacing the current version of the commands with - // the new one (no incremental updates) - self.image_cmds.insert(key, deserialize_blob(&cmds[..]).unwrap()); + // the new one (no incremental updates). + self.image_cmds.insert(key, Arc::new(deserialize_blob(&cmds[..]).unwrap())); } fn delete(&mut self, key: ImageKey) { @@ -76,63 +159,57 @@ impl BlobImageRenderer for CheckerboardRenderer { descriptor: &BlobImageDescriptor, _dirty_rect: Option, _images: &ImageStore) { - let color = self.image_cmds.get(&request.key).unwrap().clone(); - - // Allocate storage for the result. Right now the resource cache expects the - // tiles to have have no stride or offset. - let mut texels = Vec::with_capacity((descriptor.width * descriptor.height * 4) as usize); - - // Generate a per-tile pattern to see it in the demo. For a real use case it would not - // make sense for the rendered content to depend on its tile. - let tile_checker = match request.tile { - Some(tile) => (tile.x % 2 == 0) != (tile.y % 2 == 0), - None => true, - }; - - for y in 0..descriptor.height { - for x in 0..descriptor.width { - // Apply the tile's offset. This is important: all drawing commands should be - // translated by this offset to give correct results with tiled blob images. - let x2 = x + descriptor.offset.x as u32; - let y2 = y + descriptor.offset.y as u32; - - // Render a simple checkerboard pattern - let checker = if (x2 % 20 >= 10) != (y2 % 20 >= 10) { 1 } else { 0 }; - // ..nested in the per-tile cherkerboard pattern - let tc = if tile_checker { 0 } else { (1 - checker) * 40 }; - - match descriptor.format { - ImageFormat::RGBA8 => { - texels.push(color.b * checker + tc); - texels.push(color.g * checker + tc); - texels.push(color.r * checker + tc); - texels.push(color.a * checker + tc); - } - ImageFormat::A8 => { - texels.push(color.a * checker + tc); - } - _ => { - self.rendered_images.insert(request, - Err(BlobImageError::Other(format!( - "Usupported image format {:?}", - descriptor.format - ))) - ); - return; - } + // This method is where we kick off our rendering jobs. + // It should avoid doing work on the calling thread as much as possible. + // In this example we will use the thread pool to render individual tiles. + + // Gather the input data to send to a worker thread. + let cmds = Arc::clone(&self.image_cmds.get(&request.key).unwrap()); + let tx = self.tx.clone(); + let descriptor = descriptor.clone(); + + self.workers.spawn_async(move || { + let result = render_blob(cmds, &descriptor, request.tile); + tx.send((request, result)).unwrap(); + }); + + // Add None in the map of rendered images. This makes it possible to differentiate + // between commands that aren't finished yet (entry in the map is equal to None) and + // keys that have never been requested (entry not in the map), which would cause deadlocks + // if we were to block upon receing their result in resolve! + self.rendered_images.insert(request, None); + } + + fn resolve(&mut self, request: BlobImageRequest) -> BlobImageResult { + // In this method we wait until the work is complete on the worker threads and + // gather the results. + + // First look at whether we have already received the rendered image + // that we are looking for. + match self.rendered_images.entry(request) { + Entry::Vacant(_) => { + return Err(BlobImageError::InvalidKey); + } + Entry::Occupied(entry) => { + // None means we haven't yet received the result. + if entry.get().is_some() { + let result = entry.remove(); + return result.unwrap(); } } } - self.rendered_images.insert(request, Ok(RasterizedBlobImage { - data: texels, - width: descriptor.width, - height: descriptor.height, - })); - } + // We haven't received it yet, pull from the channel until we receive it. + while let Ok((req, result)) = self.rx.recv() { + if req == request { + // There it is! + return result + } + self.rendered_images.insert(req, Some(result)); + } - fn resolve(&mut self, request: BlobImageRequest) -> BlobImageResult { - self.rendered_images.remove(&request).unwrap_or(Err(BlobImageError::InvalidKey)) + // If we break out of the loop above it means the channel closed unexpectedly. + Err(BlobImageError::Other("Channel closed".into())) } } @@ -160,9 +237,18 @@ fn main() { let (width, height) = window.get_inner_size_pixels().unwrap(); + let worker_config = ThreadPoolConfig::new().thread_name(|idx|{ + format!("WebRender:Worker#{}", idx) + }); + + let workers = Arc::new(ThreadPool::new(worker_config).unwrap()); + let opts = webrender::RendererOptions { debug: true, - blob_image_renderer: Some(Box::new(CheckerboardRenderer::new())), + workers: Some(Arc::clone(&workers)), + // Register our blob renderer, so that WebRender integrates it in the resource cache.. + // Share the same pool of worker threads between WebRender and our blob renderer. + blob_image_renderer: Some(Box::new(CheckerboardRenderer::new(Arc::clone(&workers)))), device_pixel_ratio: window.hidpi_factor(), .. Default::default() };