diff --git a/Cargo.lock b/Cargo.lock index f845dcf7..536270b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,6 +172,11 @@ name = "build_const" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "bytemuck" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "byteorder" version = "1.3.2" @@ -1071,6 +1076,18 @@ dependencies = [ "tiff 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "image" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytemuck 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "num-iter 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", + "num-rational 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "inflate" version = "0.4.5" @@ -1605,6 +1622,7 @@ version = "0.1.0" dependencies = [ "arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "image 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "pathfinder_color 0.1.0", "pathfinder_geometry 0.4.0", @@ -2713,6 +2731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" "checksum block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" "checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" +"checksum bytemuck 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "37fa13df2292ecb479ec23aa06f4507928bef07839be9ef15281411076629431" "checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" "checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" "checksum calloop 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7aa2097be53a00de9e8fc349fea6d76221f398f5c4fa550d420669906962d160" @@ -2800,6 +2819,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "023b39be39e3a2da62a94feb433e91e8bcd37676fbc8bea371daf52b7a769a3e" "checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" "checksum image 0.22.4 (registry+https://github.com/rust-lang/crates.io-index)" = "53cb19c4e35102e5c6fb9ade5e0e236c5588424dc171a849af3141bf0b47768a" +"checksum image 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ef4e336ec01a678e7ab692914c641181528e8656451e6252f8f9e33728882eaf" "checksum inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" "checksum instant 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6c346c299e3fe8ef94dc10c2c0253d858a69aac1245157a3bf4125915d528caf" "checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index 8c2da87e..7a4ecea8 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -14,6 +14,7 @@ use pathfinder_color::ColorU; use pathfinder_content::dash::OutlineDash; use pathfinder_content::gradient::Gradient; use pathfinder_content::outline::{ArcDirection, Contour, Outline}; +use pathfinder_content::pattern::Pattern; use pathfinder_content::stroke::{LineCap, LineJoin as StrokeLineJoin}; use pathfinder_content::stroke::{OutlineStrokeToFill, StrokeStyle}; use pathfinder_geometry::line_segment::LineSegment2F; @@ -427,6 +428,7 @@ impl Path2D { pub enum FillStyle { Color(ColorU), Gradient(Gradient), + Pattern(Pattern), } impl FillStyle { @@ -434,6 +436,7 @@ impl FillStyle { match self { FillStyle::Color(color) => Paint::Color(color), FillStyle::Gradient(gradient) => Paint::Gradient(gradient), + FillStyle::Pattern(pattern) => Paint::Pattern(pattern), } } } @@ -456,4 +459,3 @@ pub enum LineJoin { Bevel, Round, } - diff --git a/color/src/lib.rs b/color/src/lib.rs index e19e4109..6025c06e 100644 --- a/color/src/lib.rs +++ b/color/src/lib.rs @@ -10,9 +10,11 @@ use pathfinder_simd::default::F32x4; use std::fmt::{self, Debug, Formatter}; +use std::slice; -// TODO(pcwalton): Maybe this should be a u32? +// TODO(pcwalton): Maybe this should be a u32? Need to be aware of endianness issues if we do that. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +#[repr(C)] pub struct ColorU { pub r: u8, pub g: u8, @@ -51,6 +53,16 @@ impl ColorU { } } + #[inline] + pub fn white() -> ColorU { + ColorU { + r: 255, + g: 255, + b: 255, + a: 255, + } + } + #[inline] pub fn to_f32(&self) -> ColorF { let color = F32x4::new(self.r as f32, self.g as f32, self.b as f32, self.a as f32); @@ -148,3 +160,24 @@ impl Debug for ColorF { ) } } + +#[inline] +pub fn color_slice_to_u8_slice(slice: &[ColorU]) -> &[u8] { + unsafe { + slice::from_raw_parts(slice.as_ptr() as *const u8, slice.len() * 4) + } +} + +#[inline] +pub fn u8_slice_to_color_slice(slice: &[u8]) -> &[ColorU] { + unsafe { + assert_eq!(slice.len() % 4, 0); + slice::from_raw_parts(slice.as_ptr() as *const ColorU, slice.len() / 4) + } +} + +// TODO(pcwalton): Do this without a copy? +#[inline] +pub fn u8_vec_to_color_vec(buffer: Vec) -> Vec { + u8_slice_to_color_slice(&buffer).to_vec() +} diff --git a/content/Cargo.toml b/content/Cargo.toml index 3eecb427..3c8d063b 100644 --- a/content/Cargo.toml +++ b/content/Cargo.toml @@ -10,6 +10,16 @@ bitflags = "1.0" log = "0.4" smallvec = "1.2" +[dependencies.image] +version = "0.23" +default-features = false +features = [] +optional = true + +[features] +default = ["pf-image"] +pf-image = ["image"] + [dependencies.pathfinder_color] path = "../color" diff --git a/content/src/gradient.rs b/content/src/gradient.rs index e7a6098a..6b6dc571 100644 --- a/content/src/gradient.rs +++ b/content/src/gradient.rs @@ -1,4 +1,4 @@ -// pathfinder/geometry/src/gradient.rs +// pathfinder/content/src/gradient.rs // // Copyright © 2020 The Pathfinder Project Developers. // @@ -55,6 +55,7 @@ impl Hash for Gradient { hash_f32(end_radius, state); } } + self.stops.hash(state); fn hash_line_segment(line_segment: LineSegment2F, state: &mut H) where H: Hasher { diff --git a/content/src/lib.rs b/content/src/lib.rs index fa8cc650..25f0b4fa 100644 --- a/content/src/lib.rs +++ b/content/src/lib.rs @@ -22,6 +22,7 @@ pub mod dash; pub mod gradient; pub mod orientation; pub mod outline; +pub mod pattern; pub mod segment; pub mod sorted_vector; pub mod stroke; diff --git a/content/src/pattern.rs b/content/src/pattern.rs new file mode 100644 index 00000000..d60d198c --- /dev/null +++ b/content/src/pattern.rs @@ -0,0 +1,98 @@ +// pathfinder/content/src/pattern.rs +// +// Copyright © 2020 The Pathfinder Project Developers. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Raster image patterns. + +use pathfinder_color::{self as color, ColorU}; +use pathfinder_geometry::vector::Vector2I; +use std::fmt::{self, Debug, Formatter}; + +#[cfg(feature = "pf-image")] +use image::RgbaImage; + +/// A raster image pattern. +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub struct Pattern { + pub image: Image, + pub repeat: Repeat, +} + +/// RGBA, non-premultiplied. +// FIXME(pcwalton): Hash the pixel contents so that we don't have to compare every pixel! +// TODO(pcwalton): Should the pixels be premultiplied? +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct Image { + size: Vector2I, + pixels: Vec, + is_opaque: bool, +} + +bitflags! { + pub struct Repeat: u8 { + const X = 0x01; + const Y = 0x02; + } +} + +impl Pattern { + #[inline] + pub fn new(image: Image, repeat: Repeat) -> Pattern { + Pattern { image, repeat } + } +} + +impl Image { + #[inline] + pub fn new(size: Vector2I, pixels: Vec) -> Image { + assert_eq!(size.x() as usize * size.y() as usize, pixels.len()); + let is_opaque = pixels.iter().all(|pixel| pixel.is_opaque()); + Image { size, pixels, is_opaque } + } + + #[cfg(feature = "pf-image")] + pub fn from_image_buffer(image_buffer: RgbaImage) -> Image { + let (width, height) = image_buffer.dimensions(); + let pixels = color::u8_vec_to_color_vec(image_buffer.into_raw()); + Image::new(Vector2I::new(width as i32, height as i32), pixels) + } + + #[inline] + pub fn size(&self) -> Vector2I { + self.size + } + + #[inline] + pub fn pixels(&self) -> &[ColorU] { + &self.pixels + } + + #[inline] + pub fn is_opaque(&self) -> bool { + self.is_opaque + } + + pub fn set_opacity(&mut self, alpha: f32) { + debug_assert!(alpha >= 0.0 && alpha <= 1.0); + if alpha == 1.0 { + return; + } + + // TODO(pcwalton): Go four pixels at a time with SIMD. + self.pixels.iter_mut().for_each(|pixel| pixel.a = (pixel.a as f32 * alpha).round() as u8); + self.is_opaque = false; + } +} + +impl Debug for Image { + #[inline] + fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { + write!(formatter, "(image {}×{} px)", self.size.x(), self.size.y()) + } +} diff --git a/geometry/src/vector.rs b/geometry/src/vector.rs index a683045e..42bbd213 100644 --- a/geometry/src/vector.rs +++ b/geometry/src/vector.rs @@ -11,6 +11,7 @@ //! A SIMD-optimized point type. use pathfinder_simd::default::{F32x2, F32x4, I32x2}; +use std::hash::{Hash, Hasher}; use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub}; /// 2D points with 32-bit floating point coordinates. @@ -306,6 +307,16 @@ impl PartialEq for Vector2I { } } +impl Eq for Vector2I {} + +impl Hash for Vector2I { + #[inline] + fn hash(&self, state: &mut H) where H: Hasher { + self.x().hash(state); + self.y().hash(state); + } +} + /// 3D points. /// /// The w value in the SIMD vector is always 0.0. diff --git a/renderer/src/allocator.rs b/renderer/src/allocator.rs index bb69b3ae..dcff46d1 100644 --- a/renderer/src/allocator.rs +++ b/renderer/src/allocator.rs @@ -42,7 +42,8 @@ impl TextureAllocator { #[inline] pub fn allocate(&mut self, requested_size: Vector2I) -> Option { - let requested_length = requested_size.x().max(requested_size.y()) as u32; + let requested_length = + (requested_size.x().max(requested_size.y()) as u32).next_power_of_two(); self.root.allocate(Vector2I::default(), self.size, requested_length) } diff --git a/renderer/src/gpu/renderer.rs b/renderer/src/gpu/renderer.rs index 50be4b46..b4d818b0 100644 --- a/renderer/src/gpu/renderer.rs +++ b/renderer/src/gpu/renderer.rs @@ -13,7 +13,7 @@ use crate::gpu::options::{DestFramebuffer, RendererOptions}; use crate::gpu_data::{AlphaTile, FillBatchPrimitive, PaintData, RenderCommand, SolidTileVertex}; use crate::post::DefringingKernel; use crate::tiles::{TILE_HEIGHT, TILE_WIDTH}; -use pathfinder_color::ColorF; +use pathfinder_color::{self as color, ColorF, ColorU}; use pathfinder_geometry::vector::{Vector2I, Vector4F}; use pathfinder_geometry::rect::RectI; use pathfinder_geometry::transform3d::Transform4F; @@ -353,10 +353,11 @@ where fn upload_paint_data(&mut self, paint_data: &PaintData) { // FIXME(pcwalton): This is a hack. We shouldn't be generating paint data at all on the // renderer side. - let (paint_size, paint_texels): (Vector2I, &[u8]); + let (paint_size, paint_texels): (Vector2I, &[ColorU]); + let dummy_paint = [ColorU::white(); 1]; if self.postprocess_options.is_some() { paint_size = Vector2I::splat(1); - paint_texels = &[255; 4]; + paint_texels = &dummy_paint; } else { paint_size = paint_data.size; paint_texels = &paint_data.texels; @@ -370,9 +371,10 @@ where } } + let texels = color::color_slice_to_u8_slice(paint_texels); self.device.upload_to_texture(self.paint_texture.as_ref().unwrap(), RectI::new(Vector2I::default(), paint_size), - TextureDataRef::U8(paint_texels)); + TextureDataRef::U8(texels)); } fn upload_solid_tiles(&mut self, solid_tile_vertices: &[SolidTileVertex]) { diff --git a/renderer/src/gpu_data.rs b/renderer/src/gpu_data.rs index 9c87cf99..67180634 100644 --- a/renderer/src/gpu_data.rs +++ b/renderer/src/gpu_data.rs @@ -12,6 +12,7 @@ use crate::options::BoundingQuad; use crate::tile_map::DenseTileMap; +use pathfinder_color::ColorU; use pathfinder_geometry::line_segment::{LineSegmentU4, LineSegmentU8}; use pathfinder_geometry::rect::RectF; use pathfinder_geometry::vector::Vector2I; @@ -39,7 +40,7 @@ pub enum RenderCommand { #[derive(Clone, Debug)] pub struct PaintData { pub size: Vector2I, - pub texels: Vec, + pub texels: Vec, } #[derive(Clone, Copy, Debug)] diff --git a/renderer/src/paint.rs b/renderer/src/paint.rs index 6ca98e9b..2fce5533 100644 --- a/renderer/src/paint.rs +++ b/renderer/src/paint.rs @@ -14,6 +14,7 @@ use crate::tiles::{TILE_HEIGHT, TILE_WIDTH}; use hashbrown::HashMap; use pathfinder_color::ColorU; use pathfinder_content::gradient::{Gradient, GradientGeometry}; +use pathfinder_content::pattern::Pattern; use pathfinder_geometry::rect::{RectF, RectI}; use pathfinder_geometry::transform2d::{Matrix2x2F, Transform2F}; use pathfinder_geometry::util; @@ -43,6 +44,7 @@ pub struct Palette { pub enum Paint { Color(ColorU), Gradient(Gradient), + Pattern(Pattern), } #[derive(Clone, Copy, PartialEq, Debug)] @@ -59,6 +61,7 @@ impl Debug for Paint { // TODO(pcwalton) write!(formatter, "(gradient)") } + Paint::Pattern(ref pattern) => pattern.fmt(formatter), } } } @@ -87,6 +90,7 @@ impl Paint { Paint::Gradient(ref gradient) => { gradient.stops().iter().all(|stop| stop.color.is_opaque()) } + Paint::Pattern(ref pattern) => pattern.image.is_opaque(), } } @@ -96,6 +100,10 @@ impl Paint { Paint::Gradient(ref gradient) => { gradient.stops().iter().all(|stop| stop.color.is_fully_transparent()) } + Paint::Pattern(_) => { + // TODO(pcwalton): Should we support this? + false + } } } @@ -107,6 +115,7 @@ impl Paint { match *self { Paint::Color(ref mut color) => color.a = (color.a as f32 * alpha).round() as u8, Paint::Gradient(ref mut gradient) => gradient.set_opacity(alpha), + Paint::Pattern(ref mut pattern) => pattern.image.set_opacity(alpha), } } } @@ -147,42 +156,50 @@ impl Palette { pub fn build_paint_info(&self, view_box_size: Vector2I) -> PaintInfo { let mut allocator = TextureAllocator::new(PAINT_TEXTURE_LENGTH); let area = PAINT_TEXTURE_LENGTH as usize * PAINT_TEXTURE_LENGTH as usize; - let (mut texels, mut metadata) = (vec![0; area * 4], vec![]); + let (mut texels, mut metadata) = (vec![ColorU::default(); area], vec![]); let mut solid_color_tile_builder = SolidColorTileBuilder::new(); for paint in &self.paints { - let (texture_location, tex_transform); + let (tex_location, tex_transform); match paint { Paint::Color(color) => { - texture_location = solid_color_tile_builder.allocate(&mut allocator); - let vector = rect_to_inset_uv(texture_location.rect).origin(); + tex_location = solid_color_tile_builder.allocate(&mut allocator); + let vector = rect_to_inset_uv(tex_location.rect).origin(); tex_transform = Transform2F { matrix: Matrix2x2F(F32x4::default()), vector }; - put_pixel(&mut texels, texture_location.rect.origin(), *color); + put_pixel(&mut texels, tex_location.rect.origin(), *color); } Paint::Gradient(ref gradient) => { // TODO(pcwalton): Optimize this: // 1. Use repeating/clamp on the sides. // 2. Choose an optimal size for the gradient that minimizes memory usage while // retaining quality. - texture_location = - allocator.allocate(Vector2I::splat(GRADIENT_TILE_LENGTH as i32)) - .expect("Failed to allocate space for the gradient!"); + tex_location = allocator.allocate(Vector2I::splat(GRADIENT_TILE_LENGTH as i32)) + .expect("Failed to allocate space for the gradient!"); tex_transform = - Transform2F::from_translation(rect_to_uv(texture_location.rect).origin()) * + Transform2F::from_translation(rect_to_uv(tex_location.rect).origin()) * Transform2F::from_scale(Vector2F::splat(GRADIENT_TILE_SCALE) / view_box_size.to_f32()); self.build_paint_info_for_gradient(gradient, - texture_location, + tex_location, &tex_transform, &mut texels); } + Paint::Pattern(ref pattern) => { + tex_location = allocator.allocate(pattern.image.size()) + .expect("Failed to allocate space for the image!"); + tex_transform = + Transform2F::from_translation(rect_to_uv(tex_location.rect).origin()) * + Transform2F::from_uniform_scale(PAINT_TEXTURE_SCALE); + + self.build_paint_info_for_pattern(pattern, tex_location, &mut texels); + } } metadata.push(PaintMetadata { - tex_rect: texture_location.rect, + tex_rect: tex_location.rect, tex_transform, is_opaque: paint.is_opaque(), }); @@ -197,7 +214,7 @@ impl Palette { gradient: &Gradient, texture_location: TextureLocation, tex_transform: &Transform2F, - texels: &mut [u8]) { + texels: &mut [ColorU]) { match *gradient.geometry() { GradientGeometry::Linear(gradient_line) => { // FIXME(pcwalton): Paint transparent if gradient line has zero size, per spec. @@ -219,6 +236,7 @@ impl Palette { } } } + GradientGeometry::Radial { line: gradient_line, start_radius, end_radius } => { // FIXME(pcwalton): Paint transparent if line has zero size and radii are equal, // per spec. @@ -245,6 +263,22 @@ impl Palette { } } } + + fn build_paint_info_for_pattern(&self, + pattern: &Pattern, + texture_location: TextureLocation, + texels: &mut [ColorU]) { + let image_size = pattern.image.size(); + for y in 0..image_size.y() { + let dest_origin = texture_location.rect.origin() + Vector2I::new(0, y); + let dest_start_index = paint_texel_index(dest_origin); + let src_start_index = y as usize * image_size.x() as usize; + let dest_end_index = dest_start_index + image_size.x() as usize; + let src_end_index = src_start_index + image_size.x() as usize; + texels[dest_start_index..dest_end_index].copy_from_slice( + &pattern.image.pixels()[src_start_index..src_end_index]); + } + } } impl PaintMetadata { @@ -256,13 +290,12 @@ impl PaintMetadata { } } -fn put_pixel(texels: &mut [u8], position: Vector2I, color: ColorU) { - let index = (position.y() as usize * PAINT_TEXTURE_LENGTH as usize + - position.x() as usize) * 4; - texels[index + 0] = color.r; - texels[index + 1] = color.g; - texels[index + 2] = color.b; - texels[index + 3] = color.a; +fn paint_texel_index(position: Vector2I) -> usize { + position.y() as usize * PAINT_TEXTURE_LENGTH as usize + position.x() as usize +} + +fn put_pixel(texels: &mut [ColorU], position: Vector2I, color: ColorU) { + texels[paint_texel_index(position)] = color } fn rect_to_uv(rect: RectI) -> RectF { diff --git a/renderer/src/scene.rs b/renderer/src/scene.rs index e438309a..b47d85ba 100644 --- a/renderer/src/scene.rs +++ b/renderer/src/scene.rs @@ -149,6 +149,7 @@ impl Scene { match self.palette.paints[first_paint_id.0 as usize] { Paint::Color(color) => Some(color), Paint::Gradient(_) => None, + Paint::Pattern(_) => None, } }