From 8f6610ce54a252711a3ba2dfc59d3f2d6eb62759 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Thu, 29 Oct 2015 18:30:56 -0700 Subject: [PATCH] Optimize large tiled backgrounds by prerendering a few repeats into the texture cache. Timings: * Page with nothing but a 1x1 tiled background -- 100. ms -> 0.423 ms (236x improvement) * https://miiverse.nintendo.net/guide/ -- 3.11 ms -> 2.12 ms (47% improvement) * http://en.wikipedia.org/wiki/Servomechanism -- 0.951 ms -> 0.842 ms (13% improvement) --- res/tile.fs.glsl | 8 ++++ res/tile.vs.glsl | 7 +++ src/internal_types.rs | 11 +++++ src/render_backend.rs | 102 +++++++++++++++++++++++++++++------------- src/renderer.rs | 84 ++++++++++++++++++++++++++++++++++ src/resource_cache.rs | 19 +++++++- src/resource_list.rs | 65 ++++++++++++++++++++++++++- src/texture_cache.rs | 25 +++++++++++ 8 files changed, 287 insertions(+), 34 deletions(-) create mode 100644 res/tile.fs.glsl create mode 100644 res/tile.vs.glsl diff --git a/res/tile.fs.glsl b/res/tile.fs.glsl new file mode 100644 index 0000000000..023793f703 --- /dev/null +++ b/res/tile.fs.glsl @@ -0,0 +1,8 @@ +void main(void) { + vec2 textureSize = vBorderPosition.zw - vBorderPosition.xy; + vec3 colorTexCoord = vec3(vBorderPosition.xy + mod(vColorTexCoord.xy, 1.0) * textureSize, + vColorTexCoord.z); + vec4 diffuse = Texture(sDiffuse, colorTexCoord); + SetFragColor(diffuse); +} + diff --git a/res/tile.vs.glsl b/res/tile.vs.glsl new file mode 100644 index 0000000000..7d26996c51 --- /dev/null +++ b/res/tile.vs.glsl @@ -0,0 +1,7 @@ +void main(void) +{ + vColorTexCoord = vec3(aBorderRadii.xy, aMisc.y); + vBorderPosition = aBorderPosition; + gl_Position = uTransform * vec4(aPosition, 1.0); +} + diff --git a/src/internal_types.rs b/src/internal_types.rs index b4a513d639..7ee945617b 100644 --- a/src/internal_types.rs +++ b/src/internal_types.rs @@ -204,6 +204,8 @@ pub enum TextureUpdateDetails { BorderRadius(Au, Au, Au, Au, bool), /// Blur radius border radius, and whether inverted, respectively. BoxShadowCorner(Au, Au, bool), + /// Bytes, stretch size, and scratch texture image, respectively. + Tile(Vec, Size2D, TextureImage), } #[derive(Clone, Copy, Debug)] @@ -730,3 +732,12 @@ pub enum RasterItem { BoxShadowCorner(BoxShadowCornerRasterOp), } +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct TiledImageKey { + pub image_key: ImageKey, + pub tiled_width: u32, + pub tiled_height: u32, + pub stretch_width: u32, + pub stretch_height: u32, +} + diff --git a/src/render_backend.rs b/src/render_backend.rs index fb6acc4bce..209ed8ae6f 100644 --- a/src/render_backend.rs +++ b/src/render_backend.rs @@ -12,7 +12,7 @@ use internal_types::{PackedVertex, WorkVertex, DisplayList, DrawCommand, DrawCom use internal_types::{ClipRectToRegionResult, DrawListIndex, DrawListItemIndex, DisplayItemKey}; use internal_types::{CompositeInfo, BorderEdgeDirection, RenderTargetIndex, GlyphKey}; use internal_types::{FontTemplate, Glyph, PolygonPosColorUv, RectPosUv, TextureTarget}; -use internal_types::{ResourceId, IdNamespace}; +use internal_types::{ResourceId, IdNamespace, TiledImageKey}; use layer::Layer; use optimizer; use platform::font::{FontContext, RasterizedGlyph}; @@ -951,6 +951,27 @@ impl Scene { }); }); + // Update texture cache with any new image tile operations. + resource_list.for_each_tiled_image(|tiled_image_key| { + resource_cache.cache_tiled_image_if_required(tiled_image_key, + |tiled_image_template| { + let image_id = texture_cache.new_item_id(); + // TODO: Can we avoid the clone of the bytes here? + let stretch_size = Size2D::new(tiled_image_key.stretch_width, + tiled_image_key.stretch_height); + texture_cache.insert(image_id, + 0, + 0, + tiled_image_key.tiled_width, + tiled_image_key.tiled_height, + tiled_image_template.format, + TextureInsertOp::Tile( + tiled_image_template.bytes.clone(), + stretch_size)); + image_id + }); + }); + // Update texture cache with any newly rasterized glyphs. resource_list.for_each_glyph(|glyph_key| { resource_cache.cache_glyph_if_required(glyph_key, || { @@ -993,7 +1014,7 @@ impl Scene { (*native_font_handle).clone()); } } - job.result = font_context.get_glyph(job.glyph_key.font_key, + job.result = font_context.get_glyph(&job.glyph_key.font_key, job.glyph_key.size, job.glyph_key.index, device_pixel_ratio); @@ -1608,12 +1629,12 @@ impl DrawCommandBuilder { let image_id = resource_cache.get_image(image_key); let image_info = texture_cache.get(image_id); - let uv_origin = Point2D::new(image_info.u0, image_info.v0); - let uv_size = Size2D::new(image_info.u1 - image_info.u0, - image_info.v1 - image_info.v0); - let uv = Rect::new(uv_origin, uv_size); - if rect.size.width == stretch_size.width && rect.size.height == stretch_size.height { + let uv_origin = Point2D::new(image_info.u0, image_info.v0); + let uv_size = Size2D::new(image_info.u1 - image_info.u0, + image_info.v1 - image_info.v0); + let uv = Rect::new(uv_origin, uv_size); + self.push_image_rect(color, image_info, dummy_mask_image, @@ -1625,32 +1646,49 @@ impl DrawCommandBuilder { clip_buffers, rect, &uv); - } else { - let mut y_offset = 0.0; - while y_offset < rect.size.height { - let mut x_offset = 0.0; - while x_offset < rect.size.width { - - let origin = Point2D::new(rect.origin.x + x_offset, rect.origin.y + y_offset); - let tiled_rect = Rect::new(origin, stretch_size.clone()); - - self.push_image_rect(color, - image_info, - dummy_mask_image, - clip_rect, - clip_region, - &sort_key, - resource_cache, - texture_cache, - clip_buffers, - &tiled_rect, - &uv); + return + } - x_offset = x_offset + stretch_size.width; - } + let (image_info, tiled_size) = match TiledImageKey::create_if_necessary(image_key, + &rect.size, + stretch_size) { + Some(ref tiled_image_key) => { + let tiled_image_id = resource_cache.get_tiled_image(tiled_image_key); + (texture_cache.get(tiled_image_id), + Size2D::new(tiled_image_key.tiled_width as f32, + tiled_image_key.tiled_height as f32)) + } + None => (image_info, *stretch_size), + }; - y_offset = y_offset + stretch_size.height; + let uv_origin = Point2D::new(image_info.u0, image_info.v0); + let uv_size = Size2D::new(image_info.u1 - image_info.u0, + image_info.v1 - image_info.v0); + let uv = Rect::new(uv_origin, uv_size); + + let mut y_offset = 0.0; + while y_offset < rect.size.height { + let mut x_offset = 0.0; + while x_offset < rect.size.width { + let origin = Point2D::new(rect.origin.x + x_offset, rect.origin.y + y_offset); + let tiled_rect = Rect::new(origin, tiled_size.clone()); + + self.push_image_rect(color, + image_info, + dummy_mask_image, + clip_rect, + clip_region, + &sort_key, + resource_cache, + texture_cache, + clip_buffers, + &tiled_rect, + &uv); + + x_offset = x_offset + tiled_size.width; } + + y_offset = y_offset + tiled_size.height; } } @@ -2903,7 +2941,9 @@ impl BuildRequiredResources for AABBTreeNode { match display_item.item { SpecificDisplayItem::Image(ref info) => { - resource_list.add_image(info.image_key); + resource_list.add_image(info.image_key, + &display_item.rect.size, + &info.stretch_size); } SpecificDisplayItem::Text(ref info) => { for glyph in &info.glyphs { diff --git a/src/renderer.rs b/src/renderer.rs index 443d214b4c..dfe1a006ea 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -69,6 +69,8 @@ pub struct Renderer { blur_program_id: ProgramId, u_direction: UniformLocation, + + tile_program_id: ProgramId, } impl Renderer { @@ -92,6 +94,7 @@ impl Renderer { let box_shadow_corner_program_id = device.create_program("box-shadow-corner.vs.glsl", "box-shadow-corner.fs.glsl"); let blur_program_id = device.create_program("blur.vs.glsl", "blur.fs.glsl"); + let tile_program_id = device.create_program("tile.vs.glsl", "tile.fs.glsl"); let u_quad_transform_array = device.get_uniform_location(quad_program_id, "uMatrixPalette"); @@ -165,6 +168,7 @@ impl Renderer { quad_program_id: quad_program_id, box_shadow_corner_program_id: box_shadow_corner_program_id, blur_program_id: blur_program_id, + tile_program_id: tile_program_id, u_blend_params: u_blend_params, u_filter_params: u_filter_params, u_filter_texture_size: u_filter_texture_size, @@ -558,6 +562,86 @@ impl Renderer { border_radius, inverted) } + TextureUpdateDetails::Tile(bytes, + stretch_size, + scratch_texture_image) => { + self.device.update_texture_for_noncomposite_operation( + scratch_texture_image.texture_id, + scratch_texture_image.texture_index, + scratch_texture_image.pixel_uv.x, + scratch_texture_image.pixel_uv.y, + stretch_size.width, + stretch_size.height, + bytes.as_slice()); + + let white = ColorF::new(1.0, 1.0, 1.0, 1.0); + let zero_point = Point2D::new(0.0, 0.0); + let zero_size = Size2D::new(0.0, 0.0); + let scaled_bottom_right = + Point2D::new((width as f32) / (stretch_size.width as f32), + (height as f32) / (stretch_size.height as f32)); + + let tile_program_id = self.tile_program_id; + let mut batch = self.get_or_create_raster_batch( + update.id, + update.index, + scratch_texture_image.texture_id, + tile_program_id, + None); + let vertices = [ + PackedVertexForTextureCacheUpdate::new( + &Point2D::new(x as f32, y as f32), + &white, + &zero_point, + scratch_texture_image.texture_index, + &Point2D::new(0.0, 0.0), + &zero_point, + &scratch_texture_image.texel_uv.origin, + &scratch_texture_image.texel_uv.bottom_right(), + &zero_size, + &zero_size, + 0.0), + PackedVertexForTextureCacheUpdate::new( + &Point2D::new((x + width) as f32, y as f32), + &white, + &zero_point, + scratch_texture_image.texture_index, + &Point2D::new(scaled_bottom_right.x, 0.0), + &zero_point, + &scratch_texture_image.texel_uv.origin, + &scratch_texture_image.texel_uv.bottom_right(), + &zero_size, + &zero_size, + 0.0), + PackedVertexForTextureCacheUpdate::new( + &Point2D::new(x as f32, (y + height) as f32), + &white, + &zero_point, + scratch_texture_image.texture_index, + &Point2D::new(0.0, scaled_bottom_right.y), + &zero_point, + &scratch_texture_image.texel_uv.origin, + &scratch_texture_image.texel_uv.bottom_right(), + &zero_size, + &zero_size, + 0.0), + PackedVertexForTextureCacheUpdate::new( + &Point2D::new((x + width) as f32, (y + height) as f32), + &white, + &zero_point, + scratch_texture_image.texture_index, + &scaled_bottom_right, + &zero_point, + &scratch_texture_image.texel_uv.origin, + &scratch_texture_image.texel_uv.bottom_right(), + &zero_size, + &zero_size, + 0.0), + ]; + batch.add_draw_item(update.id, + scratch_texture_image.texture_id, + &vertices); + } } } } diff --git a/src/resource_cache.rs b/src/resource_cache.rs index c5be2a7602..50ca45f7da 100644 --- a/src/resource_cache.rs +++ b/src/resource_cache.rs @@ -1,5 +1,5 @@ use fnv::FnvHasher; -use internal_types::{FontTemplate, ImageResource, GlyphKey, RasterItem}; +use internal_types::{FontTemplate, ImageResource, GlyphKey, RasterItem, TiledImageKey}; use std::collections::HashMap; use std::collections::hash_state::DefaultState; use texture_cache::TextureCacheItemId; @@ -9,6 +9,7 @@ pub struct ResourceCache { cached_glyphs: HashMap>, cached_rasters: HashMap>, cached_images: HashMap>, + cached_tiled_images: HashMap>, font_templates: HashMap>, image_templates: HashMap>, @@ -20,6 +21,7 @@ impl ResourceCache { cached_glyphs: HashMap::with_hash_state(Default::default()), cached_rasters: HashMap::with_hash_state(Default::default()), cached_images: HashMap::with_hash_state(Default::default()), + cached_tiled_images: HashMap::with_hash_state(Default::default()), font_templates: HashMap::with_hash_state(Default::default()), image_templates: HashMap::with_hash_state(Default::default()), } @@ -71,6 +73,17 @@ impl ResourceCache { } } + pub fn cache_tiled_image_if_required(&mut self, tiled_image_key: &TiledImageKey, mut f: F) + where F: FnMut(&ImageResource) -> TextureCacheItemId { + if !self.cached_tiled_images.contains_key(&tiled_image_key) { + let image_id = { + let image_template = self.get_image_template(tiled_image_key.image_key); + f(image_template) + }; + self.cached_tiled_images.insert((*tiled_image_key).clone(), image_id); + } + } + pub fn get_glyph(&self, glyph_key: &GlyphKey) -> TextureCacheItemId { self.cached_glyphs[glyph_key] } @@ -82,4 +95,8 @@ impl ResourceCache { pub fn get_image(&self, image_key: ImageKey) -> TextureCacheItemId { self.cached_images[&image_key] } + + pub fn get_tiled_image(&self, tiled_image_key: &TiledImageKey) -> TextureCacheItemId { + self.cached_tiled_images[tiled_image_key] + } } diff --git a/src/resource_list.rs b/src/resource_list.rs index 03e88dd72c..f1a0c58026 100644 --- a/src/resource_list.rs +++ b/src/resource_list.rs @@ -2,7 +2,7 @@ use app_units::Au; use euclid::Size2D; use fnv::FnvHasher; use internal_types::{BorderRadiusRasterOp, BoxShadowCornerRasterOp}; -use internal_types::{Glyph, GlyphKey, RasterItem}; +use internal_types::{Glyph, GlyphKey, RasterItem, TiledImageKey}; use std::collections::{HashMap, HashSet}; use std::collections::hash_map::Entry::{Occupied, Vacant}; use std::collections::hash_state::DefaultState; @@ -11,11 +11,24 @@ use types::{FontKey, ImageFormat, ImageKey}; type RequiredImageSet = HashSet>; type RequiredGlyphMap = HashMap, DefaultState>; type RequiredRasterSet = HashSet>; +type RequiredTiledImageSet = HashSet>; + +/// The number of repeats of an image we allow within the viewport before we add a tile +/// rasterization op. +const MAX_IMAGE_REPEATS: u32 = 64; + +/// The dimensions (horizontal and vertical) of the area that we tile an image to. +const TILE_SIZE: u32 = 128; + +/// The size of the virtual viewport used to estimate the number of image repeats we'll have to +/// display. +const APPROXIMATE_VIEWPORT_SIZE: u32 = 1024; pub struct ResourceList { required_images: RequiredImageSet, required_glyphs: RequiredGlyphMap, required_rasters: RequiredRasterSet, + required_tiled_images: RequiredTiledImageSet, } impl ResourceList { @@ -24,11 +37,16 @@ impl ResourceList { required_glyphs: HashMap::with_hash_state(Default::default()), required_images: HashSet::with_hash_state(Default::default()), required_rasters: HashSet::with_hash_state(Default::default()), + required_tiled_images: HashSet::with_hash_state(Default::default()), } } - pub fn add_image(&mut self, key: ImageKey) { + pub fn add_image(&mut self, + key: ImageKey, + tiled_size: &Size2D, + stretch_size: &Size2D) { self.required_images.insert(key); + self.add_tiled_image(key, tiled_size, stretch_size); } pub fn add_glyph(&mut self, font_key: FontKey, glyph: Glyph) { @@ -68,6 +86,17 @@ impl ResourceList { } } + pub fn add_tiled_image(&mut self, + image_key: ImageKey, + tiled_size: &Size2D, + stretch_size: &Size2D) { + if let Some(tiled_image_op) = TiledImageKey::create_if_necessary(image_key, + tiled_size, + stretch_size) { + self.required_tiled_images.insert(tiled_image_op); + } + } + pub fn for_each_image(&self, mut f: F) where F: FnMut(ImageKey) { for image_id in &self.required_images { f(*image_id); @@ -80,6 +109,12 @@ impl ResourceList { } } + pub fn for_each_tiled_image(&self, mut f: F) where F: FnMut(&TiledImageKey) { + for tiled_image_key in &self.required_tiled_images { + f(tiled_image_key); + } + } + pub fn for_each_glyph(&self, mut f: F) where F: FnMut(&GlyphKey) { for (font_id, glyphs) in &self.required_glyphs { let mut glyph_key = GlyphKey::new(font_id.clone(), Au(0), Au(0), 0); @@ -94,3 +129,29 @@ impl ResourceList { } } } + +impl TiledImageKey { + pub fn create_if_necessary(image_key: ImageKey, + tiled_size: &Size2D, + stretch_size: &Size2D) + -> Option { + let tiled_size = Size2D::new(tiled_size.width.min(APPROXIMATE_VIEWPORT_SIZE as f32), + tiled_size.height.min(APPROXIMATE_VIEWPORT_SIZE as f32)); + let image_repeats = ((tiled_size.width / stretch_size.width).ceil() * + (tiled_size.height / stretch_size.height).ceil()) as u32; + if image_repeats <= MAX_IMAGE_REPEATS { + return None + } + let prerendered_tile_size = Size2D::new( + (((TILE_SIZE as f32) / stretch_size.width).ceil() * stretch_size.width) as u32, + (((TILE_SIZE as f32) / stretch_size.height).ceil() * stretch_size.height) as u32); + Some(TiledImageKey { + image_key: image_key, + tiled_width: prerendered_tile_size.width, + tiled_height: prerendered_tile_size.height, + stretch_width: stretch_size.width as u32, + stretch_height: stretch_size.height as u32, + }) + } +} + diff --git a/src/texture_cache.rs b/src/texture_cache.rs index 32379d38df..8f0e48d3d6 100644 --- a/src/texture_cache.rs +++ b/src/texture_cache.rs @@ -230,6 +230,7 @@ struct TextureCacheArena { pages_rgb8: Vec, pages_rgba8: Vec, alternate_pages_a8: Vec, + alternate_pages_rgba8: Vec, } impl TextureCacheArena { @@ -239,6 +240,7 @@ impl TextureCacheArena { pages_rgb8: Vec::new(), pages_rgba8: Vec::new(), alternate_pages_a8: Vec::new(), + alternate_pages_rgba8: Vec::new(), } } } @@ -355,6 +357,9 @@ impl TextureCache { (ImageFormat::RGBA8, false) => { (&mut self.arena.pages_rgba8, RenderTargetMode::RenderTarget) } + (ImageFormat::RGBA8, true) => { + (&mut self.arena.alternate_pages_rgba8, RenderTargetMode::RenderTarget) + } (ImageFormat::RGB8, false) => (&mut self.arena.pages_rgb8, RenderTargetMode::None), (ImageFormat::Invalid, false) | (_, true) => unreachable!(), }; @@ -557,6 +562,20 @@ impl TextureCache { unblurred_glyph_item.to_image(), horizontal_blur_item.to_image())) } + (AllocationKind::TexturePage, TextureInsertOp::Tile(bytes, stretch_size)) => { + let scratch_image_id = self.new_item_id(); + // TODO(pcwalton): Destroy these! + self.allocate(scratch_image_id, + 0, 0, + stretch_size.width, stretch_size.height, + ImageFormat::RGBA8, + true); + let scratch_item = self.get(scratch_image_id); + TextureUpdateOp::Update( + result.uv.x, result.uv.y, + width, height, + TextureUpdateDetails::Tile(bytes, stretch_size, scratch_item.to_image())) + } (AllocationKind::Standalone, TextureInsertOp::Blit(bytes)) => { TextureUpdateOp::Create(self.texture_target_for_standalone_texture(), width, @@ -570,6 +589,10 @@ impl TextureCache { println!("ERROR: Can't blur with a standalone texture yet!"); return } + (AllocationKind::Standalone, TextureInsertOp::Tile(..)) => { + println!("ERROR: Can't blur with a standalone texture yet!"); + return + } }; let update_op = TextureUpdate { @@ -625,6 +648,8 @@ fn texture_create_op(texture_size: u32, levels: u32, format: ImageFormat, mode: pub enum TextureInsertOp { Blit(Vec), Blur(Vec, Size2D, Au), + /// Bytes and stretch size, respectively. + Tile(Vec, Size2D), } trait FitsInside {