diff --git a/Cargo.lock b/Cargo.lock index 2964b082..93ee88fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,6 +444,30 @@ dependencies = [ "euclid 0.19.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "pathfinder_renderer" +version = "0.1.0" +dependencies = [ + "byteorder 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "euclid 0.19.4 (registry+https://github.com/rust-lang/crates.io-index)", + "fixedbitset 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "hashbrown 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "pathfinder_geometry 0.3.0", + "quickcheck 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rayon 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pathfinder_svg" +version = "0.1.0" +dependencies = [ + "euclid 0.19.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lyon_path 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pathfinder_geometry 0.3.0", + "pathfinder_renderer 0.1.0", + "usvg 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "phf" version = "0.7.24" @@ -724,6 +748,8 @@ dependencies = [ "lyon_geom 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)", "lyon_path 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "pathfinder_geometry 0.3.0", + "pathfinder_renderer 0.1.0", + "pathfinder_svg 0.1.0", "quickcheck 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", "rayon 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 1e94f7fb..113054d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,8 @@ members = [ "geometry", "gfx-utils", + "renderer", + "svg", "utils/area-lut", "utils/gamma-lut", "utils/tile-svg", diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml new file mode 100644 index 00000000..48bdf37c --- /dev/null +++ b/renderer/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "pathfinder_renderer" +version = "0.1.0" +edition = "2018" +authors = ["Patrick Walton "] + +[dependencies] +byteorder = "1.2" +euclid = "0.19" +fixedbitset = "0.1" +hashbrown = "0.1" +rayon = "1.0" + +[dependencies.pathfinder_geometry] +path = "../geometry" + +[dev-dependencies] +quickcheck = "0.7" diff --git a/renderer/src/builder.rs b/renderer/src/builder.rs new file mode 100644 index 00000000..16a094a7 --- /dev/null +++ b/renderer/src/builder.rs @@ -0,0 +1,119 @@ +// pathfinder/renderer/src/builder.rs +// +// Copyright © 2019 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. + +//! Packs data onto the GPU. + +use crate::gpu_data::{Batch, BuiltObject, FillBatchPrimitive}; +use crate::gpu_data::{MaskTileBatchPrimitive, SolidTileScenePrimitive}; +use crate::scene; +use crate::tiles; +use crate::z_buffer::ZBuffer; +use euclid::Rect; +use std::iter; +use std::u16; + +const MAX_FILLS_PER_BATCH: usize = 0x0002_0000; +const MAX_MASKS_PER_BATCH: u16 = 0xffff; + +pub struct SceneBuilder { + objects: Vec, + z_buffer: ZBuffer, + tile_rect: Rect, + + current_object_index: usize, +} + +impl SceneBuilder { + pub fn new(objects: Vec, z_buffer: ZBuffer, view_box: &Rect) + -> SceneBuilder { + let tile_rect = tiles::round_rect_out_to_tile_bounds(view_box); + SceneBuilder { + objects, + z_buffer, + tile_rect, + current_object_index: 0, + } + } + + pub fn build_solid_tiles(&self) -> Vec { + self.z_buffer + .build_solid_tiles(&self.objects, &self.tile_rect) + } + + pub fn build_batch(&mut self) -> Option { + let mut batch = Batch::new(); + + let mut object_tile_index_to_batch_mask_tile_index = vec![]; + while self.current_object_index < self.objects.len() { + let object = &self.objects[self.current_object_index]; + + if batch.fills.len() + object.fills.len() > MAX_FILLS_PER_BATCH { + break; + } + + object_tile_index_to_batch_mask_tile_index.clear(); + object_tile_index_to_batch_mask_tile_index + .extend(iter::repeat(u16::MAX).take(object.tiles.len())); + + // Copy mask tiles. + for (tile_index, tile) in object.tiles.iter().enumerate() { + // Skip solid tiles, since we handled them above already. + if object.solid_tiles[tile_index] { + continue; + } + + // Cull occluded tiles. + let scene_tile_index = + scene::scene_tile_index(tile.tile_x, tile.tile_y, self.tile_rect); + if !self + .z_buffer + .test(scene_tile_index, self.current_object_index as u32) + { + continue; + } + + // Visible mask tile. + let batch_mask_tile_index = batch.mask_tiles.len() as u16; + if batch_mask_tile_index == MAX_MASKS_PER_BATCH { + break; + } + + object_tile_index_to_batch_mask_tile_index[tile_index] = batch_mask_tile_index; + + batch.mask_tiles.push(MaskTileBatchPrimitive { + tile: *tile, + shader: object.shader, + }); + } + + // Remap and copy fills, culling as necessary. + for fill in &object.fills { + let object_tile_index = object.tile_coords_to_index(fill.tile_x, fill.tile_y); + let mask_tile_index = + object_tile_index_to_batch_mask_tile_index[object_tile_index as usize]; + if mask_tile_index < u16::MAX { + batch.fills.push(FillBatchPrimitive { + px: fill.px, + subpx: fill.subpx, + mask_tile_index, + }); + } + } + + self.current_object_index += 1; + } + + if batch.is_empty() { + None + } else { + Some(batch) + } + } +} diff --git a/renderer/src/gpu_data.rs b/renderer/src/gpu_data.rs new file mode 100644 index 00000000..45738023 --- /dev/null +++ b/renderer/src/gpu_data.rs @@ -0,0 +1,287 @@ +// pathfinder/renderer/src/gpu_data.rs +// +// Copyright © 2019 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. + +//! Packed data ready to be sent to the GPU. + +use crate::paint::{ObjectShader, ShaderId}; +use crate::tiles::{self, TILE_HEIGHT, TILE_WIDTH}; +use euclid::Rect; +use fixedbitset::FixedBitSet; +use pathfinder_geometry::line_segment::{LineSegmentF32, LineSegmentU4, LineSegmentU8}; +use pathfinder_geometry::point::Point2DF32; +use pathfinder_geometry::simd::{F32x4, I32x4}; +use pathfinder_geometry::util; + +#[derive(Debug)] +pub struct BuiltObject { + pub bounds: Rect, + pub tile_rect: Rect, + pub tiles: Vec, + pub fills: Vec, + pub solid_tiles: FixedBitSet, + pub shader: ShaderId, +} + +#[derive(Debug)] +pub struct BuiltScene { + pub view_box: Rect, + pub batches: Vec, + pub solid_tiles: Vec, + pub shaders: Vec, +} + +#[derive(Debug)] +pub struct Batch { + pub fills: Vec, + pub mask_tiles: Vec, +} + +#[derive(Clone, Copy, Debug)] +pub struct FillObjectPrimitive { + pub px: LineSegmentU4, + pub subpx: LineSegmentU8, + pub tile_x: i16, + pub tile_y: i16, +} + +#[derive(Clone, Copy, Debug)] +pub struct TileObjectPrimitive { + pub tile_x: i16, + pub tile_y: i16, + pub backdrop: i16, +} + +#[derive(Clone, Copy, Debug)] +pub struct FillBatchPrimitive { + pub px: LineSegmentU4, + pub subpx: LineSegmentU8, + pub mask_tile_index: u16, +} + +#[derive(Clone, Copy, Debug)] +pub struct SolidTileScenePrimitive { + pub tile_x: i16, + pub tile_y: i16, + pub shader: ShaderId, +} + +#[derive(Clone, Copy, Debug)] +pub struct MaskTileBatchPrimitive { + pub tile: TileObjectPrimitive, + pub shader: ShaderId, +} + +// Utilities for built objects + +impl BuiltObject { + pub fn new(bounds: &Rect, shader: ShaderId) -> BuiltObject { + // Compute the tile rect. + let tile_rect = tiles::round_rect_out_to_tile_bounds(&bounds); + + // Allocate tiles. + let tile_count = tile_rect.size.width as usize * tile_rect.size.height as usize; + let mut tiles = Vec::with_capacity(tile_count); + for y in tile_rect.origin.y..tile_rect.max_y() { + for x in tile_rect.origin.x..tile_rect.max_x() { + tiles.push(TileObjectPrimitive::new(x, y)); + } + } + + let mut solid_tiles = FixedBitSet::with_capacity(tile_count); + solid_tiles.insert_range(..); + + BuiltObject { + bounds: *bounds, + tile_rect, + tiles, + fills: vec![], + solid_tiles, + shader, + } + } + + // TODO(pcwalton): SIMD-ify `tile_x` and `tile_y`. + fn add_fill(&mut self, segment: &LineSegmentF32, tile_x: i16, tile_y: i16) { + //println!("add_fill({:?} ({}, {}))", segment, tile_x, tile_y); + let mut segment = (segment.0 * F32x4::splat(256.0)).to_i32x4(); + + let tile_origin_x = (TILE_WIDTH as i32) * 256 * (tile_x as i32); + let tile_origin_y = (TILE_HEIGHT as i32) * 256 * (tile_y as i32); + let tile_origin = I32x4::new(tile_origin_x, tile_origin_y, tile_origin_x, tile_origin_y); + + segment = segment - tile_origin; + /* + println!("... before min: {} {} {} {}", + segment[0], segment[1], segment[2], segment[3]); + */ + //segment = Sse41::max_epi32(segment, Sse41::setzero_epi32()); + segment = segment.min(I32x4::splat(0x0fff)); + //println!("... after min: {} {} {} {}", segment[0], segment[1], segment[2], segment[3]); + + let shuffle_mask = I32x4::new(0x0c08_0400, 0x0d05_0901, 0, 0); + segment = segment + .as_u8x16() + .shuffle(shuffle_mask.as_u8x16()) + .as_i32x4(); + + let px = LineSegmentU4((segment[1] | (segment[1] >> 12)) as u16); + let subpx = LineSegmentU8(segment[0] as u32); + + let tile_index = self.tile_coords_to_index(tile_x, tile_y); + + /* + // TODO(pcwalton): Cull degenerate fills again. + // Cull degenerate fills. + let (from_px, to_px) = (from.to_u8(), to.to_u8()); + if from_px.x == to_px.x && from_subpx.x == to_subpx.x { + return + } + */ + + self.fills.push(FillObjectPrimitive { + px, + subpx, + tile_x, + tile_y, + }); + self.solid_tiles.set(tile_index as usize, false); + } + + pub fn add_active_fill( + &mut self, + left: f32, + right: f32, + mut winding: i16, + tile_x: i16, + tile_y: i16, + ) { + let tile_origin_y = (i32::from(tile_y) * TILE_HEIGHT as i32) as f32; + let left = Point2DF32::new(left, tile_origin_y); + let right = Point2DF32::new(right, tile_origin_y); + + let segment = if winding < 0 { + LineSegmentF32::new(&left, &right) + } else { + LineSegmentF32::new(&right, &left) + }; + + /* + println!("... emitting active fill {} -> {} winding {} @ tile {}", + left.x(), + right.x(), + winding, + tile_x); + */ + + while winding != 0 { + self.add_fill(&segment, tile_x, tile_y); + if winding < 0 { + winding += 1 + } else { + winding -= 1 + } + } + } + + // TODO(pcwalton): Optimize this better with SIMD! + pub fn generate_fill_primitives_for_line(&mut self, mut segment: LineSegmentF32, tile_y: i16) { + /* + println!("... generate_fill_primitives_for_line(): segment={:?} tile_y={} ({}-{})", + segment, + tile_y, + tile_y as f32 * TILE_HEIGHT as f32, + (tile_y + 1) as f32 * TILE_HEIGHT as f32); + */ + + let winding = segment.from_x() > segment.to_x(); + let (segment_left, segment_right) = if !winding { + (segment.from_x(), segment.to_x()) + } else { + (segment.to_x(), segment.from_x()) + }; + + let segment_tile_left = (f32::floor(segment_left) as i32 / TILE_WIDTH as i32) as i16; + let segment_tile_right = + util::alignup_i32(f32::ceil(segment_right) as i32, TILE_WIDTH as i32) as i16; + + for subsegment_tile_x in segment_tile_left..segment_tile_right { + let (mut fill_from, mut fill_to) = (segment.from(), segment.to()); + let subsegment_tile_right = + ((i32::from(subsegment_tile_x) + 1) * TILE_HEIGHT as i32) as f32; + if subsegment_tile_right < segment_right { + let x = subsegment_tile_right; + let point = Point2DF32::new(x, segment.solve_y_for_x(x)); + if !winding { + fill_to = point; + segment = LineSegmentF32::new(&point, &segment.to()); + } else { + fill_from = point; + segment = LineSegmentF32::new(&segment.from(), &point); + } + } + + let fill_segment = LineSegmentF32::new(&fill_from, &fill_to); + self.add_fill(&fill_segment, subsegment_tile_x, tile_y); + } + } + + // FIXME(pcwalton): Use a `Point2D` instead? + pub fn tile_coords_to_index(&self, tile_x: i16, tile_y: i16) -> u32 { + /*println!("tile_coords_to_index(x={}, y={}, tile_rect={:?})", + tile_x, + tile_y, + self.tile_rect);*/ + (tile_y - self.tile_rect.origin.y) as u32 * self.tile_rect.size.width as u32 + + (tile_x - self.tile_rect.origin.x) as u32 + } + + pub fn get_tile_mut(&mut self, tile_x: i16, tile_y: i16) -> &mut TileObjectPrimitive { + let tile_index = self.tile_coords_to_index(tile_x, tile_y); + &mut self.tiles[tile_index as usize] + } +} + +impl BuiltScene { + #[inline] + pub fn new(view_box: &Rect) -> BuiltScene { + BuiltScene { + view_box: *view_box, + batches: vec![], + solid_tiles: vec![], + shaders: vec![], + } + } +} + +impl Batch { + #[inline] + pub fn new() -> Batch { + Batch { + fills: vec![], + mask_tiles: vec![], + } + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.mask_tiles.is_empty() + } +} + +impl TileObjectPrimitive { + #[inline] + fn new(tile_x: i16, tile_y: i16) -> TileObjectPrimitive { + TileObjectPrimitive { + tile_x, + tile_y, + backdrop: 0, + } + } +} diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs new file mode 100644 index 00000000..009f349b --- /dev/null +++ b/renderer/src/lib.rs @@ -0,0 +1,21 @@ +// pathfinder/renderer/src/lib.rs +// +// Copyright © 2019 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. + +//! The CPU portion of Pathfinder's renderer. + +pub mod builder; +pub mod gpu_data; +pub mod paint; +pub mod scene; +pub mod serialization; +pub mod tiles; +pub mod z_buffer; + +mod sorted_vector; diff --git a/renderer/src/paint.rs b/renderer/src/paint.rs new file mode 100644 index 00000000..41354fda --- /dev/null +++ b/renderer/src/paint.rs @@ -0,0 +1,47 @@ +// pathfinder/renderer/src/paint.rs +// +// Copyright © 2019 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. + +//! How a path is to be filled. + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Paint { + pub color: ColorU, +} + +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct PaintId(pub u16); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] +pub struct ColorU { + pub r: u8, + pub g: u8, + pub b: u8, + pub a: u8, +} + +impl ColorU { + #[inline] + pub fn black() -> ColorU { + ColorU { + r: 0, + g: 0, + b: 0, + a: 255, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ShaderId(pub u16); + +#[derive(Clone, Copy, Debug, Default)] +pub struct ObjectShader { + pub fill_color: ColorU, +} diff --git a/renderer/src/scene.rs b/renderer/src/scene.rs new file mode 100644 index 00000000..894aac21 --- /dev/null +++ b/renderer/src/scene.rs @@ -0,0 +1,132 @@ +// pathfinder/renderer/src/scene.rs +// +// Copyright © 2019 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. + +//! A set of paths to be rendered. + +use crate::gpu_data::BuiltObject; +use crate::paint::{ObjectShader, Paint, PaintId, ShaderId}; +use crate::tiles::Tiler; +use crate::z_buffer::ZBuffer; +use euclid::Rect; +use hashbrown::HashMap; +use pathfinder_geometry::outline::Outline; +use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; + +#[derive(Debug)] +pub struct Scene { + pub objects: Vec, + pub paints: Vec, + pub paint_cache: HashMap, + pub bounds: Rect, + pub view_box: Rect, +} + +impl Scene { + #[inline] + pub fn new() -> Scene { + Scene { + objects: vec![], + paints: vec![], + paint_cache: HashMap::new(), + bounds: Rect::zero(), + view_box: Rect::zero(), + } + } + + #[allow(clippy::trivially_copy_pass_by_ref)] + pub fn push_paint(&mut self, paint: &Paint) -> PaintId { + if let Some(paint_id) = self.paint_cache.get(paint) { + return *paint_id; + } + + let paint_id = PaintId(self.paints.len() as u16); + self.paint_cache.insert(*paint, paint_id); + self.paints.push(*paint); + paint_id + } + + pub fn build_shaders(&self) -> Vec { + self.paints + .iter() + .map(|paint| ObjectShader { + fill_color: paint.color, + }) + .collect() + } + + pub fn build_objects_sequentially(&self, z_buffer: &ZBuffer) -> Vec { + self.objects + .iter() + .enumerate() + .map(|(object_index, object)| { + let mut tiler = Tiler::new( + &object.outline, + &self.view_box, + object_index as u16, + ShaderId(object.paint.0), + z_buffer, + ); + tiler.generate_tiles(); + tiler.built_object + }) + .collect() + } + + pub fn build_objects(&self, z_buffer: &ZBuffer) -> Vec { + self.objects + .par_iter() + .enumerate() + .map(|(object_index, object)| { + let mut tiler = Tiler::new( + &object.outline, + &self.view_box, + object_index as u16, + ShaderId(object.paint.0), + z_buffer, + ); + tiler.generate_tiles(); + tiler.built_object + }) + .collect() + } +} + +#[derive(Debug)] +pub struct PathObject { + outline: Outline, + paint: PaintId, + name: String, + kind: PathObjectKind, +} + +#[derive(Clone, Copy, Debug)] +pub enum PathObjectKind { + Fill, + Stroke, +} + +impl PathObject { + #[inline] + pub fn new(outline: Outline, paint: PaintId, name: String, kind: PathObjectKind) + -> PathObject { + PathObject { + outline, + paint, + name, + kind, + } + } +} + +#[inline] +pub fn scene_tile_index(tile_x: i16, tile_y: i16, tile_rect: Rect) -> u32 { + (tile_y - tile_rect.origin.y) as u32 * tile_rect.size.width as u32 + + (tile_x - tile_rect.origin.x) as u32 +} \ No newline at end of file diff --git a/renderer/src/serialization.rs b/renderer/src/serialization.rs new file mode 100644 index 00000000..d0f9a2f9 --- /dev/null +++ b/renderer/src/serialization.rs @@ -0,0 +1,114 @@ +// pathfinder/renderer/src/serialization.rs +// +// Copyright © 2019 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. + +use byteorder::{LittleEndian, WriteBytesExt}; +use crate::gpu_data::{BuiltScene, FillBatchPrimitive}; +use crate::gpu_data::{MaskTileBatchPrimitive, SolidTileScenePrimitive}; +use crate::paint::ObjectShader; +use std::io::{self, Write}; +use std::mem; + +pub trait RiffSerialize { + fn write(&self, writer: &mut W) -> io::Result<()> where W: Write; +} + +impl RiffSerialize for BuiltScene { + fn write(&self, writer: &mut W) -> io::Result<()> + where + W: Write, + { + writer.write_all(b"RIFF")?; + + let header_size = 4 * 6; + + let solid_tiles_size = self.solid_tiles.len() * mem::size_of::(); + + let batch_sizes: Vec<_> = self + .batches + .iter() + .map(|batch| BatchSizes { + fills: (batch.fills.len() * mem::size_of::()), + mask_tiles: (batch.mask_tiles.len() * mem::size_of::()), + }) + .collect(); + + let total_batch_sizes: usize = batch_sizes.iter().map(|sizes| 8 + sizes.total()).sum(); + + let shaders_size = self.shaders.len() * mem::size_of::(); + + writer.write_u32::( + (4 + 8 + header_size + 8 + solid_tiles_size + 8 + shaders_size + total_batch_sizes) + as u32, + )?; + + writer.write_all(b"PF3S")?; + + writer.write_all(b"head")?; + writer.write_u32::(header_size as u32)?; + writer.write_u32::(FILE_VERSION)?; + writer.write_u32::(self.batches.len() as u32)?; + writer.write_f32::(self.view_box.origin.x)?; + writer.write_f32::(self.view_box.origin.y)?; + writer.write_f32::(self.view_box.size.width)?; + writer.write_f32::(self.view_box.size.height)?; + + writer.write_all(b"shad")?; + writer.write_u32::(shaders_size as u32)?; + for &shader in &self.shaders { + let fill_color = shader.fill_color; + writer.write_all(&[fill_color.r, fill_color.g, fill_color.b, fill_color.a])?; + } + + writer.write_all(b"soli")?; + writer.write_u32::(solid_tiles_size as u32)?; + for &tile_primitive in &self.solid_tiles { + writer.write_i16::(tile_primitive.tile_x)?; + writer.write_i16::(tile_primitive.tile_y)?; + writer.write_u16::(tile_primitive.shader.0)?; + } + + for (batch, sizes) in self.batches.iter().zip(batch_sizes.iter()) { + writer.write_all(b"batc")?; + writer.write_u32::(sizes.total() as u32)?; + + writer.write_all(b"fill")?; + writer.write_u32::(sizes.fills as u32)?; + for fill_primitive in &batch.fills { + writer.write_u16::(fill_primitive.px.0)?; + writer.write_u32::(fill_primitive.subpx.0)?; + writer.write_u16::(fill_primitive.mask_tile_index)?; + } + + writer.write_all(b"mask")?; + writer.write_u32::(sizes.mask_tiles as u32)?; + for &tile_primitive in &batch.mask_tiles { + writer.write_i16::(tile_primitive.tile.tile_x)?; + writer.write_i16::(tile_primitive.tile.tile_y)?; + writer.write_i16::(tile_primitive.tile.backdrop)?; + writer.write_u16::(tile_primitive.shader.0)?; + } + } + + return Ok(()); + + const FILE_VERSION: u32 = 0; + + struct BatchSizes { + fills: usize, + mask_tiles: usize, + } + + impl BatchSizes { + fn total(&self) -> usize { + 8 + self.fills + 8 + self.mask_tiles + } + } + } +} diff --git a/renderer/src/sorted_vector.rs b/renderer/src/sorted_vector.rs new file mode 100644 index 00000000..128839fd --- /dev/null +++ b/renderer/src/sorted_vector.rs @@ -0,0 +1,91 @@ +// pathfinder/renderer/src/sorted_vector.rs +// +// Copyright © 2019 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. + +//! A vector that maintains sorted order with insertion sort. + +#[derive(Clone, Debug)] +pub struct SortedVector +where + T: PartialOrd, +{ + pub array: Vec, +} + +impl SortedVector +where + T: PartialOrd, +{ + #[inline] + pub fn new() -> SortedVector { + SortedVector { array: vec![] } + } + + #[inline] + pub fn push(&mut self, value: T) { + self.array.push(value); + let mut index = self.array.len() - 1; + while index > 0 { + index -= 1; + if self.array[index] <= self.array[index + 1] { + break; + } + self.array.swap(index, index + 1); + } + } + + #[inline] + pub fn peek(&self) -> Option<&T> { + self.array.last() + } + + #[inline] + pub fn pop(&mut self) -> Option { + self.array.pop() + } + + #[inline] + pub fn clear(&mut self) { + self.array.clear() + } + + #[allow(dead_code)] + #[inline] + pub fn is_empty(&self) -> bool { + self.array.is_empty() + } +} + +#[cfg(test)] +mod test { + use crate::sorted_vector::SortedVector; + use quickcheck; + + #[test] + fn test_sorted_vec() { + quickcheck::quickcheck(prop_sorted_vec as fn(Vec) -> bool); + + fn prop_sorted_vec(mut values: Vec) -> bool { + let mut sorted_vec = SortedVector::new(); + for &value in &values { + sorted_vec.push(value) + } + + values.sort(); + let mut results = Vec::with_capacity(values.len()); + while !sorted_vec.is_empty() { + results.push(sorted_vec.pop().unwrap()); + } + results.reverse(); + assert_eq!(&values, &results); + + true + } + } +} \ No newline at end of file diff --git a/renderer/src/tiles.rs b/renderer/src/tiles.rs new file mode 100644 index 00000000..2ea4651f --- /dev/null +++ b/renderer/src/tiles.rs @@ -0,0 +1,467 @@ +// pathfinder/renderer/src/tiles.rs +// +// Copyright © 2019 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. + +use crate::gpu_data::BuiltObject; +use crate::paint::ShaderId; +use crate::sorted_vector::SortedVector; +use crate::z_buffer::ZBuffer; +use euclid::{Point2D, Rect, Size2D}; +use pathfinder_geometry::line_segment::LineSegmentF32; +use pathfinder_geometry::outline::{Contour, Outline, PointIndex}; +use pathfinder_geometry::point::Point2DF32; +use pathfinder_geometry::segment::Segment; +use pathfinder_geometry::util; +use std::cmp::Ordering; +use std::mem; + +// TODO(pcwalton): Make this configurable. +const FLATTENING_TOLERANCE: f32 = 0.1; + +pub const TILE_WIDTH: u32 = 16; +pub const TILE_HEIGHT: u32 = 16; + +pub struct Tiler<'o, 'z> { + outline: &'o Outline, + pub built_object: BuiltObject, + object_index: u16, + z_buffer: &'z ZBuffer, + + point_queue: SortedVector, + active_edges: SortedVector, + old_active_edges: Vec, +} + +impl<'o, 'z> Tiler<'o, 'z> { + #[allow(clippy::or_fun_call)] + pub fn new( + outline: &'o Outline, + view_box: &Rect, + object_index: u16, + shader: ShaderId, + z_buffer: &'z ZBuffer, + ) -> Tiler<'o, 'z> { + let bounds = outline + .bounds() + .intersection(&view_box) + .unwrap_or(Rect::zero()); + let built_object = BuiltObject::new(&bounds, shader); + + Tiler { + outline, + built_object, + object_index, + z_buffer, + + point_queue: SortedVector::new(), + active_edges: SortedVector::new(), + old_active_edges: vec![], + } + } + + pub fn generate_tiles(&mut self) { + // Initialize the point queue. + self.init_point_queue(); + + // Reset active edges. + self.active_edges.clear(); + self.old_active_edges.clear(); + + // Generate strips. + let tile_rect = self.built_object.tile_rect; + for strip_origin_y in tile_rect.origin.y..tile_rect.max_y() { + self.generate_strip(strip_origin_y); + } + + // Cull. + self.cull(); + //println!("{:#?}", self.built_object); + } + + fn generate_strip(&mut self, strip_origin_y: i16) { + // Process old active edges. + self.process_old_active_edges(strip_origin_y); + + // Add new active edges. + let strip_max_y = ((i32::from(strip_origin_y) + 1) * TILE_HEIGHT as i32) as f32; + while let Some(queued_endpoint) = self.point_queue.peek() { + if queued_endpoint.y >= strip_max_y { + break; + } + self.add_new_active_edge(strip_origin_y); + } + } + + fn cull(&self) { + for solid_tile_index in self.built_object.solid_tiles.ones() { + let tile = &self.built_object.tiles[solid_tile_index]; + if tile.backdrop != 0 { + self.z_buffer + .update(tile.tile_x, tile.tile_y, self.object_index); + } + } + } + + fn process_old_active_edges(&mut self, tile_y: i16) { + let mut current_tile_x = self.built_object.tile_rect.origin.x; + let mut current_subtile_x = 0.0; + let mut current_winding = 0; + + debug_assert!(self.old_active_edges.is_empty()); + mem::swap(&mut self.old_active_edges, &mut self.active_edges.array); + + let mut last_segment_x = -9999.0; + + let tile_top = (i32::from(tile_y) * TILE_HEIGHT as i32) as f32; + //println!("---------- tile y {}({}) ----------", tile_y, tile_top); + //println!("old active edges: {:#?}", self.old_active_edges); + + for mut active_edge in self.old_active_edges.drain(..) { + // Determine x-intercept and winding. + let segment_x = active_edge.crossing.x(); + let edge_winding = + if active_edge.segment.baseline.from_y() < active_edge.segment.baseline.to_y() { + 1 + } else { + -1 + }; + + /* + println!("tile Y {}({}): segment_x={} edge_winding={} current_tile_x={} \ + current_subtile_x={} current_winding={}", + tile_y, + tile_top, + segment_x, + edge_winding, + current_tile_x, + current_subtile_x, + current_winding); + println!("... segment={:#?} crossing={:?}", active_edge.segment, active_edge.crossing); + */ + + // FIXME(pcwalton): Remove this debug code! + debug_assert!(segment_x >= last_segment_x); + last_segment_x = segment_x; + + // Do initial subtile fill, if necessary. + let segment_tile_x = (f32::floor(segment_x) as i32 / TILE_WIDTH as i32) as i16; + if current_tile_x < segment_tile_x && current_subtile_x > 0.0 { + let current_x = + (i32::from(current_tile_x) * TILE_WIDTH as i32) as f32 + current_subtile_x; + let tile_right_x = ((i32::from(current_tile_x) + 1) * TILE_WIDTH as i32) as f32; + self.built_object.add_active_fill( + current_x, + tile_right_x, + current_winding, + current_tile_x, + tile_y, + ); + current_tile_x += 1; + current_subtile_x = 0.0; + } + + // Move over to the correct tile, filling in as we go. + while current_tile_x < segment_tile_x { + //println!("... emitting backdrop {} @ tile {}", current_winding, current_tile_x); + self.built_object + .get_tile_mut(current_tile_x, tile_y) + .backdrop = current_winding; + current_tile_x += 1; + current_subtile_x = 0.0; + } + + // Do final subtile fill, if necessary. + debug_assert!(current_tile_x == segment_tile_x); + debug_assert!(current_tile_x < self.built_object.tile_rect.max_x()); + let segment_subtile_x = + segment_x - (i32::from(current_tile_x) * TILE_WIDTH as i32) as f32; + if segment_subtile_x > current_subtile_x { + let current_x = + (i32::from(current_tile_x) * TILE_WIDTH as i32) as f32 + current_subtile_x; + self.built_object.add_active_fill( + current_x, + segment_x, + current_winding, + current_tile_x, + tile_y, + ); + current_subtile_x = segment_subtile_x; + } + + // Update winding. + current_winding += edge_winding; + + // Process the edge. + //println!("about to process existing active edge {:#?}", active_edge); + debug_assert!(f32::abs(active_edge.crossing.y() - tile_top) < 0.1); + active_edge.process(&mut self.built_object, tile_y); + if !active_edge.segment.is_none() { + self.active_edges.push(active_edge); + } + } + + //debug_assert_eq!(current_winding, 0); + } + + fn add_new_active_edge(&mut self, tile_y: i16) { + let outline = &self.outline; + let point_index = self.point_queue.pop().unwrap().point_index; + + let contour = &outline.contours[point_index.contour() as usize]; + + // TODO(pcwalton): Could use a bitset of processed edges… + let prev_endpoint_index = contour.prev_endpoint_index_of(point_index.point()); + let next_endpoint_index = contour.next_endpoint_index_of(point_index.point()); + /* + println!("adding new active edge, tile_y={} point_index={} prev={} next={} pos={:?} \ + prevpos={:?} nextpos={:?}", + tile_y, + point_index.point(), + prev_endpoint_index, + next_endpoint_index, + contour.position_of(point_index.point()), + contour.position_of(prev_endpoint_index), + contour.position_of(next_endpoint_index)); + */ + + if contour.point_is_logically_above(point_index.point(), prev_endpoint_index) { + //println!("... adding prev endpoint"); + process_active_segment( + contour, + prev_endpoint_index, + &mut self.active_edges, + &mut self.built_object, + tile_y, + ); + + self.point_queue.push(QueuedEndpoint { + point_index: PointIndex::new(point_index.contour(), prev_endpoint_index), + y: contour.position_of(prev_endpoint_index).y(), + }); + //println!("... done adding prev endpoint"); + } + + if contour.point_is_logically_above(point_index.point(), next_endpoint_index) { + /* + println!("... adding next endpoint {} -> {}", + point_index.point(), + next_endpoint_index); + */ + process_active_segment( + contour, + point_index.point(), + &mut self.active_edges, + &mut self.built_object, + tile_y, + ); + + self.point_queue.push(QueuedEndpoint { + point_index: PointIndex::new(point_index.contour(), next_endpoint_index), + y: contour.position_of(next_endpoint_index).y(), + }); + //println!("... done adding next endpoint"); + } + } + + fn init_point_queue(&mut self) { + // Find MIN points. + self.point_queue.clear(); + for (contour_index, contour) in self.outline.contours.iter().enumerate() { + let contour_index = contour_index as u32; + let mut cur_endpoint_index = 0; + let mut prev_endpoint_index = contour.prev_endpoint_index_of(cur_endpoint_index); + let mut next_endpoint_index = contour.next_endpoint_index_of(cur_endpoint_index); + loop { + if contour.point_is_logically_above(cur_endpoint_index, prev_endpoint_index) + && contour.point_is_logically_above(cur_endpoint_index, next_endpoint_index) + { + self.point_queue.push(QueuedEndpoint { + point_index: PointIndex::new(contour_index, cur_endpoint_index), + y: contour.position_of(cur_endpoint_index).y(), + }); + } + + if cur_endpoint_index >= next_endpoint_index { + break; + } + + prev_endpoint_index = cur_endpoint_index; + cur_endpoint_index = next_endpoint_index; + next_endpoint_index = contour.next_endpoint_index_of(cur_endpoint_index); + } + } + } +} + +pub fn round_rect_out_to_tile_bounds(rect: &Rect) -> Rect { + let tile_origin = Point2D::new( + (f32::floor(rect.origin.x) as i32 / TILE_WIDTH as i32) as i16, + (f32::floor(rect.origin.y) as i32 / TILE_HEIGHT as i32) as i16, + ); + let tile_extent = Point2D::new( + util::alignup_i32(f32::ceil(rect.max_x()) as i32, TILE_WIDTH as i32) as i16, + util::alignup_i32(f32::ceil(rect.max_y()) as i32, TILE_HEIGHT as i32) as i16, + ); + let tile_size = Size2D::new(tile_extent.x - tile_origin.x, tile_extent.y - tile_origin.y); + Rect::new(tile_origin, tile_size) +} + +fn process_active_segment( + contour: &Contour, + from_endpoint_index: u32, + active_edges: &mut SortedVector, + built_object: &mut BuiltObject, + tile_y: i16, +) { + let mut active_edge = ActiveEdge::from_segment(&contour.segment_after(from_endpoint_index)); + //println!("... process_active_segment({:#?})", active_edge); + active_edge.process(built_object, tile_y); + if !active_edge.segment.is_none() { + active_edges.push(active_edge); + } +} + +// Queued endpoints + +#[derive(PartialEq)] +struct QueuedEndpoint { + point_index: PointIndex, + y: f32, +} + +impl Eq for QueuedEndpoint {} + +impl PartialOrd for QueuedEndpoint { + fn partial_cmp(&self, other: &QueuedEndpoint) -> Option { + // NB: Reversed! + (other.y, other.point_index).partial_cmp(&(self.y, self.point_index)) + } +} + +// Active edges + +#[derive(Clone, PartialEq, Debug)] +struct ActiveEdge { + segment: Segment, + // TODO(pcwalton): Shrink `crossing` down to just one f32? + crossing: Point2DF32, +} + +impl ActiveEdge { + fn from_segment(segment: &Segment) -> ActiveEdge { + let crossing = if segment.baseline.from_y() < segment.baseline.to_y() { + segment.baseline.from() + } else { + segment.baseline.to() + }; + ActiveEdge::from_segment_and_crossing(segment, &crossing) + } + + fn from_segment_and_crossing(segment: &Segment, crossing: &Point2DF32) -> ActiveEdge { + ActiveEdge { + segment: *segment, + crossing: *crossing, + } + } + + fn process(&mut self, built_object: &mut BuiltObject, tile_y: i16) { + let tile_bottom = ((i32::from(tile_y) + 1) * TILE_HEIGHT as i32) as f32; + // println!("process_active_edge({:#?}, tile_y={}({}))", self, tile_y, tile_bottom); + + let mut segment = self.segment; + let winding = segment.baseline.y_winding(); + + if segment.is_line() { + let line_segment = segment.as_line_segment(); + self.segment = match self.process_line_segment(&line_segment, built_object, tile_y) { + Some(lower_part) => Segment::line(&lower_part), + None => Segment::none(), + }; + return; + } + + // TODO(pcwalton): Don't degree elevate! + if !segment.is_cubic() { + segment = segment.to_cubic(); + } + + // If necessary, draw initial line. + if self.crossing.y() < segment.baseline.min_y() { + let first_line_segment = + LineSegmentF32::new(&self.crossing, &segment.baseline.upper_point()) + .orient(winding); + if self + .process_line_segment(&first_line_segment, built_object, tile_y) + .is_some() + { + return; + } + } + + loop { + let rest_segment = match segment + .orient(winding) + .as_cubic_segment() + .flatten_once(FLATTENING_TOLERANCE) + { + None => { + let line_segment = segment.baseline; + self.segment = + match self.process_line_segment(&line_segment, built_object, tile_y) { + Some(ref lower_part) => Segment::line(lower_part), + None => Segment::none(), + }; + return; + } + Some(rest_segment) => rest_segment.orient(winding), + }; + + debug_assert!(segment.baseline.min_y() <= tile_bottom); + + let line_segment = LineSegmentF32::new( + &segment.baseline.upper_point(), + &rest_segment.baseline.upper_point(), + ) + .orient(winding); + if self + .process_line_segment(&line_segment, built_object, tile_y) + .is_some() + { + self.segment = rest_segment; + return; + } + + segment = rest_segment; + } + } + + fn process_line_segment( + &mut self, + line_segment: &LineSegmentF32, + built_object: &mut BuiltObject, + tile_y: i16, + ) -> Option { + let tile_bottom = ((i32::from(tile_y) + 1) * TILE_HEIGHT as i32) as f32; + if line_segment.max_y() <= tile_bottom { + built_object.generate_fill_primitives_for_line(*line_segment, tile_y); + return None; + } + + let (upper_part, lower_part) = line_segment.split_at_y(tile_bottom); + built_object.generate_fill_primitives_for_line(upper_part, tile_y); + self.crossing = lower_part.upper_point(); + Some(lower_part) + } +} + +impl PartialOrd for ActiveEdge { + fn partial_cmp(&self, other: &ActiveEdge) -> Option { + self.crossing.x().partial_cmp(&other.crossing.x()) + } +} diff --git a/renderer/src/z_buffer.rs b/renderer/src/z_buffer.rs new file mode 100644 index 00000000..d90ee17a --- /dev/null +++ b/renderer/src/z_buffer.rs @@ -0,0 +1,82 @@ +// pathfinder/renderer/src/z_buffer.rs +// +// Copyright © 2019 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. + +//! Software occlusion culling. + +use crate::gpu_data::{BuiltObject, SolidTileScenePrimitive}; +use crate::scene; +use crate::tiles; +use euclid::Rect; +use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering}; + +pub struct ZBuffer { + buffer: Vec, + tile_rect: Rect, +} + +impl ZBuffer { + pub fn new(view_box: &Rect) -> ZBuffer { + let tile_rect = tiles::round_rect_out_to_tile_bounds(view_box); + let tile_area = tile_rect.size.width as usize * tile_rect.size.height as usize; + ZBuffer { + buffer: (0..tile_area).map(|_| AtomicUsize::new(0)).collect(), + tile_rect, + } + } + + pub fn test(&self, scene_tile_index: u32, object_index: u32) -> bool { + let existing_depth = self.buffer[scene_tile_index as usize].load(AtomicOrdering::SeqCst); + existing_depth < object_index as usize + 1 + } + + pub fn update(&self, tile_x: i16, tile_y: i16, object_index: u16) { + let scene_tile_index = scene::scene_tile_index(tile_x, tile_y, self.tile_rect) as usize; + let mut old_depth = self.buffer[scene_tile_index].load(AtomicOrdering::SeqCst); + let new_depth = (object_index + 1) as usize; + while old_depth < new_depth { + let prev_depth = self.buffer[scene_tile_index].compare_and_swap( + old_depth, + new_depth, + AtomicOrdering::SeqCst, + ); + if prev_depth == old_depth { + // Successfully written. + return; + } + old_depth = prev_depth; + } + } + + pub fn build_solid_tiles( + &self, + objects: &[BuiltObject], + tile_rect: &Rect, + ) -> Vec { + let mut solid_tiles = vec![]; + for scene_tile_y in 0..tile_rect.size.height { + for scene_tile_x in 0..tile_rect.size.width { + let scene_tile_index = + scene_tile_y as usize * tile_rect.size.width as usize + scene_tile_x as usize; + let depth = self.buffer[scene_tile_index].load(AtomicOrdering::Relaxed); + if depth == 0 { + continue; + } + let object_index = (depth - 1) as usize; + solid_tiles.push(SolidTileScenePrimitive { + tile_x: scene_tile_x + tile_rect.origin.x, + tile_y: scene_tile_y + tile_rect.origin.y, + shader: objects[object_index].shader, + }); + } + } + + solid_tiles + } +} diff --git a/svg/Cargo.toml b/svg/Cargo.toml new file mode 100644 index 00000000..e0bef310 --- /dev/null +++ b/svg/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "pathfinder_svg" +version = "0.1.0" +edition = "2018" +authors = ["Patrick Walton "] + +[dependencies] +euclid = "0.19" +lyon_path = "0.12" +usvg = "0.4" + +[dependencies.pathfinder_geometry] +path = "../geometry" + +[dependencies.pathfinder_renderer] +path = "../renderer" diff --git a/svg/src/lib.rs b/svg/src/lib.rs new file mode 100644 index 00000000..af6736cf --- /dev/null +++ b/svg/src/lib.rs @@ -0,0 +1,256 @@ +// pathfinder/svg/src/lib.rs +// +// Copyright © 2019 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. + +//! Converts a subset of SVG to a Pathfinder scene. + +use euclid::{Point2D, Rect, Size2D}; +use lyon_path::iterator::PathIter; +use pathfinder_geometry::line_segment::LineSegmentF32; +use pathfinder_geometry::monotonic::MonotonicConversionIter; +use pathfinder_geometry::outline::Outline; +use pathfinder_geometry::point::Point2DF32; +use pathfinder_geometry::segment::{PathEventsToSegments, Segment}; +use pathfinder_geometry::segment::{SegmentFlags, SegmentsToPathEvents}; +use pathfinder_geometry::stroke::{StrokeStyle, StrokeToFillIter}; +use pathfinder_geometry::transform::{Transform2DF32, Transform2DF32PathIter}; +use pathfinder_renderer::paint::{ColorU, Paint}; +use pathfinder_renderer::scene::{PathObject, PathObjectKind, Scene}; +use std::mem; +use usvg::{Color as SvgColor, Node, NodeExt, NodeKind, Paint as UsvgPaint}; +use usvg::{PathSegment as UsvgPathSegment, Rect as UsvgRect, Transform as UsvgTransform, Tree}; + +const HAIRLINE_STROKE_WIDTH: f32 = 0.1; + +pub trait SceneExt { + fn from_tree(tree: Tree) -> Self; +} + +impl SceneExt for Scene { + // TODO(pcwalton): Allow a global transform to be set. + fn from_tree(tree: Tree) -> Scene { + let global_transform = Transform2DF32::default(); + + let mut scene = Scene::new(); + + let root = &tree.root(); + match *root.borrow() { + NodeKind::Svg(ref svg) => { + scene.view_box = usvg_rect_to_euclid_rect(&svg.view_box.rect); + for kid in root.children() { + process_node(&mut scene, &kid, &global_transform); + } + } + _ => unreachable!(), + }; + + // FIXME(pcwalton): This is needed to avoid stack exhaustion in debug builds when + // recursively dropping reference counts on very large SVGs. :( + mem::forget(tree); + + scene + } +} + +fn process_node(scene: &mut Scene, node: &Node, transform: &Transform2DF32) { + let node_transform = usvg_transform_to_transform_2d(&node.transform()); + let transform = transform.pre_mul(&node_transform); + + match *node.borrow() { + NodeKind::Group(_) => { + for kid in node.children() { + process_node(scene, &kid, &transform) + } + } + NodeKind::Path(ref path) => { + if let Some(ref fill) = path.fill { + let style = scene.push_paint(&Paint::from_svg_paint(&fill.paint)); + + let path = UsvgPathToSegments::new(path.segments.iter().cloned()); + let path = Transform2DF32PathIter::new(path, &transform); + let path = MonotonicConversionIter::new(path); + let outline = Outline::from_segments(path); + + scene.bounds = scene.bounds.union(outline.bounds()); + scene.objects.push(PathObject::new( + outline, + style, + node.id().to_string(), + PathObjectKind::Fill, + )); + } + + if let Some(ref stroke) = path.stroke { + let style = scene.push_paint(&Paint::from_svg_paint(&stroke.paint)); + let stroke_width = + f32::max(stroke.width.value() as f32, HAIRLINE_STROKE_WIDTH); + + let path = UsvgPathToSegments::new(path.segments.iter().cloned()); + let path = SegmentsToPathEvents::new(path); + let path = PathIter::new(path); + let path = StrokeToFillIter::new(path, StrokeStyle::new(stroke_width)); + let path = PathEventsToSegments::new(path); + let path = Transform2DF32PathIter::new(path, &transform); + let path = MonotonicConversionIter::new(path); + let outline = Outline::from_segments(path); + + scene.bounds = scene.bounds.union(outline.bounds()); + scene.objects.push(PathObject::new( + outline, + style, + node.id().to_string(), + PathObjectKind::Stroke, + )); + } + } + _ => { + // TODO(pcwalton): Handle these by punting to WebRender. + } + } +} + +trait PaintExt { + fn from_svg_paint(svg_paint: &UsvgPaint) -> Self; +} + +impl PaintExt for Paint { + #[inline] + fn from_svg_paint(svg_paint: &UsvgPaint) -> Paint { + Paint { + color: match *svg_paint { + UsvgPaint::Color(color) => ColorU::from_svg_color(color), + UsvgPaint::Link(_) => { + // TODO(pcwalton) + ColorU::black() + } + } + } + } +} + +fn usvg_rect_to_euclid_rect(rect: &UsvgRect) -> Rect { + Rect::new( + Point2D::new(rect.x, rect.y), + Size2D::new(rect.width, rect.height), + ) + .to_f32() +} + +fn usvg_transform_to_transform_2d(transform: &UsvgTransform) -> Transform2DF32 { + Transform2DF32::row_major( + transform.a as f32, + transform.b as f32, + transform.c as f32, + transform.d as f32, + transform.e as f32, + transform.f as f32, + ) +} + +struct UsvgPathToSegments +where + I: Iterator, +{ + iter: I, + first_subpath_point: Point2DF32, + last_subpath_point: Point2DF32, + just_moved: bool, +} + +impl UsvgPathToSegments +where + I: Iterator, +{ + fn new(iter: I) -> UsvgPathToSegments { + UsvgPathToSegments { + iter, + first_subpath_point: Point2DF32::default(), + last_subpath_point: Point2DF32::default(), + just_moved: false, + } + } +} + +impl Iterator for UsvgPathToSegments +where + I: Iterator, +{ + type Item = Segment; + + fn next(&mut self) -> Option { + match self.iter.next()? { + UsvgPathSegment::MoveTo { x, y } => { + let to = Point2DF32::new(x as f32, y as f32); + self.first_subpath_point = to; + self.last_subpath_point = to; + self.just_moved = true; + self.next() + } + UsvgPathSegment::LineTo { x, y } => { + let to = Point2DF32::new(x as f32, y as f32); + let mut segment = + Segment::line(&LineSegmentF32::new(&self.last_subpath_point, &to)); + if self.just_moved { + segment.flags.insert(SegmentFlags::FIRST_IN_SUBPATH); + } + self.last_subpath_point = to; + self.just_moved = false; + Some(segment) + } + UsvgPathSegment::CurveTo { + x1, + y1, + x2, + y2, + x, + y, + } => { + let ctrl0 = Point2DF32::new(x1 as f32, y1 as f32); + let ctrl1 = Point2DF32::new(x2 as f32, y2 as f32); + let to = Point2DF32::new(x as f32, y as f32); + let mut segment = Segment::cubic( + &LineSegmentF32::new(&self.last_subpath_point, &to), + &LineSegmentF32::new(&ctrl0, &ctrl1), + ); + if self.just_moved { + segment.flags.insert(SegmentFlags::FIRST_IN_SUBPATH); + } + self.last_subpath_point = to; + self.just_moved = false; + Some(segment) + } + UsvgPathSegment::ClosePath => { + let mut segment = Segment::line(&LineSegmentF32::new( + &self.last_subpath_point, + &self.first_subpath_point, + )); + segment.flags.insert(SegmentFlags::CLOSES_SUBPATH); + self.just_moved = false; + self.last_subpath_point = self.first_subpath_point; + Some(segment) + } + } + } +} + +trait ColorUExt { + fn from_svg_color(svg_color: SvgColor) -> Self; +} + +impl ColorUExt for ColorU { + #[inline] + fn from_svg_color(svg_color: SvgColor) -> ColorU { + ColorU { + r: svg_color.red, + g: svg_color.green, + b: svg_color.blue, + a: 255, + } + } +} diff --git a/utils/tile-svg/Cargo.toml b/utils/tile-svg/Cargo.toml index 28b27e61..38fec614 100644 --- a/utils/tile-svg/Cargo.toml +++ b/utils/tile-svg/Cargo.toml @@ -24,6 +24,12 @@ usvg = "0.4" [dependencies.pathfinder_geometry] path = "../../geometry" +[dependencies.pathfinder_renderer] +path = "../../renderer" + +[dependencies.pathfinder_svg] +path = "../../svg" + [dev-dependencies] quickcheck = "0.7" rand = "0.5" diff --git a/utils/tile-svg/src/main.rs b/utils/tile-svg/src/main.rs index 1a5e08bf..ceef5f83 100644 --- a/utils/tile-svg/src/main.rs +++ b/utils/tile-svg/src/main.rs @@ -15,53 +15,24 @@ extern crate quickcheck; #[cfg(test)] extern crate rand; -use byteorder::{LittleEndian, WriteBytesExt}; use clap::{App, Arg}; -use euclid::{Point2D, Rect, Size2D}; -use fixedbitset::FixedBitSet; -use hashbrown::HashMap; use jemallocator; -use lyon_path::iterator::PathIter; -use pathfinder_geometry::line_segment::{LineSegmentF32, LineSegmentU4, LineSegmentU8}; -use pathfinder_geometry::monotonic::MonotonicConversionIter; -use pathfinder_geometry::outline::{Contour, Outline, PointIndex}; -use pathfinder_geometry::point::Point2DF32; -use pathfinder_geometry::segment::{PathEventsToSegments, Segment}; -use pathfinder_geometry::segment::{SegmentFlags, SegmentsToPathEvents}; -use pathfinder_geometry::simd::{F32x4, I32x4}; -use pathfinder_geometry::stroke::{StrokeStyle, StrokeToFillIter}; -use pathfinder_geometry::transform::{Transform2DF32, Transform2DF32PathIter}; -use pathfinder_geometry::util; -use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; +use pathfinder_renderer::builder::SceneBuilder; +use pathfinder_renderer::gpu_data::BuiltScene; +use pathfinder_renderer::scene::Scene; +use pathfinder_renderer::serialization::RiffSerialize; +use pathfinder_renderer::z_buffer::ZBuffer; +use pathfinder_svg::SceneExt; use rayon::ThreadPoolBuilder; -use std::cmp::Ordering; use std::fs::File; -use std::io::{self, BufWriter, Write}; -use std::iter; -use std::mem; +use std::io::BufWriter; use std::path::PathBuf; -use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering}; use std::time::{Duration, Instant}; -use std::u16; -use std::u32; -use svgtypes::Color as SvgColor; -use usvg::{Node, NodeExt, NodeKind, Options as UsvgOptions, Paint as UsvgPaint}; -use usvg::{PathSegment as UsvgPathSegment, Rect as UsvgRect, Transform as UsvgTransform, Tree}; +use usvg::{Options as UsvgOptions, Tree}; #[global_allocator] static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; -// TODO(pcwalton): Make this configurable. -const SCALE_FACTOR: f32 = 1.0; - -// TODO(pcwalton): Make this configurable. -const FLATTENING_TOLERANCE: f32 = 0.1; - -const HAIRLINE_STROKE_WIDTH: f32 = 0.1; - -const MAX_FILLS_PER_BATCH: usize = 0x0002_0000; -const MAX_MASKS_PER_BATCH: u16 = 0xffff; - fn main() { let matches = App::new("tile-svg") .arg( @@ -177,1404 +148,3 @@ fn main() { fn duration_to_ms(duration: &Duration) -> f64 { duration.as_secs() as f64 * 1000.0 + f64::from(duration.subsec_micros()) / 1000.0 } - -#[derive(Debug)] -struct Scene { - objects: Vec, - paints: Vec, - paint_cache: HashMap, - bounds: Rect, - view_box: Rect, -} - -#[derive(Debug)] -struct PathObject { - outline: Outline, - paint: PaintId, - name: String, - kind: PathObjectKind, -} - -#[derive(Clone, Copy, Debug)] -pub enum PathObjectKind { - Fill, - Stroke, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -struct Paint { - color: ColorU, -} - -#[derive(Clone, Copy, PartialEq, Debug)] -struct PaintId(u16); - -impl Scene { - fn new() -> Scene { - Scene { - objects: vec![], - paints: vec![], - paint_cache: HashMap::new(), - bounds: Rect::zero(), - view_box: Rect::zero(), - } - } - - fn from_tree(tree: Tree) -> Scene { - let global_transform = Transform2DF32::from_scale(&Point2DF32::splat(SCALE_FACTOR)); - - let mut scene = Scene::new(); - - let root = &tree.root(); - match *root.borrow() { - NodeKind::Svg(ref svg) => { - scene.view_box = usvg_rect_to_euclid_rect(&svg.view_box.rect); - for kid in root.children() { - process_node(&mut scene, &kid, &global_transform); - } - } - _ => unreachable!(), - }; - - // FIXME(pcwalton): This is needed to avoid stack exhaustion in debug builds when - // recursively dropping reference counts on very large SVGs. :( - mem::forget(tree); - - return scene; - - fn process_node(scene: &mut Scene, node: &Node, transform: &Transform2DF32) { - let node_transform = usvg_transform_to_transform_2d(&node.transform()); - let transform = transform.pre_mul(&node_transform); - - match *node.borrow() { - NodeKind::Group(_) => { - for kid in node.children() { - process_node(scene, &kid, &transform) - } - } - NodeKind::Path(ref path) => { - if let Some(ref fill) = path.fill { - let style = scene.push_paint(&Paint::from_svg_paint(&fill.paint)); - - let path = UsvgPathToSegments::new(path.segments.iter().cloned()); - let path = Transform2DF32PathIter::new(path, &transform); - let path = MonotonicConversionIter::new(path); - let outline = Outline::from_segments(path); - - scene.bounds = scene.bounds.union(outline.bounds()); - scene.objects.push(PathObject::new( - outline, - style, - node.id().to_string(), - PathObjectKind::Fill, - )); - } - - if let Some(ref stroke) = path.stroke { - let style = scene.push_paint(&Paint::from_svg_paint(&stroke.paint)); - let stroke_width = - f32::max(stroke.width.value() as f32, HAIRLINE_STROKE_WIDTH); - - let path = UsvgPathToSegments::new(path.segments.iter().cloned()); - let path = SegmentsToPathEvents::new(path); - let path = PathIter::new(path); - let path = StrokeToFillIter::new(path, StrokeStyle::new(stroke_width)); - let path = PathEventsToSegments::new(path); - let path = Transform2DF32PathIter::new(path, &transform); - let path = MonotonicConversionIter::new(path); - let outline = Outline::from_segments(path); - - scene.bounds = scene.bounds.union(outline.bounds()); - scene.objects.push(PathObject::new( - outline, - style, - node.id().to_string(), - PathObjectKind::Stroke, - )); - } - } - _ => { - // TODO(pcwalton): Handle these by punting to WebRender. - } - } - } - } - - #[allow(clippy::trivially_copy_pass_by_ref)] - fn push_paint(&mut self, paint: &Paint) -> PaintId { - if let Some(paint_id) = self.paint_cache.get(paint) { - return *paint_id; - } - - let paint_id = PaintId(self.paints.len() as u16); - self.paint_cache.insert(*paint, paint_id); - self.paints.push(*paint); - paint_id - } - - fn build_shaders(&self) -> Vec { - self.paints - .iter() - .map(|paint| ObjectShader { - fill_color: paint.color, - }) - .collect() - } - - fn build_objects_sequentially(&self, z_buffer: &ZBuffer) -> Vec { - self.objects - .iter() - .enumerate() - .map(|(object_index, object)| { - let mut tiler = Tiler::new( - &object.outline, - &self.view_box, - object_index as u16, - ShaderId(object.paint.0), - z_buffer, - ); - tiler.generate_tiles(); - tiler.built_object - }) - .collect() - } - - fn build_objects(&self, z_buffer: &ZBuffer) -> Vec { - self.objects - .par_iter() - .enumerate() - .map(|(object_index, object)| { - let mut tiler = Tiler::new( - &object.outline, - &self.view_box, - object_index as u16, - ShaderId(object.paint.0), - z_buffer, - ); - tiler.generate_tiles(); - tiler.built_object - }) - .collect() - } -} - -impl PathObject { - fn new(outline: Outline, paint: PaintId, name: String, kind: PathObjectKind) -> PathObject { - PathObject { - outline, - paint, - name, - kind, - } - } -} - -// Tiling - -const TILE_WIDTH: u32 = 16; -const TILE_HEIGHT: u32 = 16; - -struct Tiler<'o, 'z> { - outline: &'o Outline, - built_object: BuiltObject, - object_index: u16, - z_buffer: &'z ZBuffer, - - point_queue: SortedVector, - active_edges: SortedVector, - old_active_edges: Vec, -} - -impl<'o, 'z> Tiler<'o, 'z> { - #[allow(clippy::or_fun_call)] - fn new( - outline: &'o Outline, - view_box: &Rect, - object_index: u16, - shader: ShaderId, - z_buffer: &'z ZBuffer, - ) -> Tiler<'o, 'z> { - let bounds = outline - .bounds() - .intersection(&view_box) - .unwrap_or(Rect::zero()); - let built_object = BuiltObject::new(&bounds, shader); - - Tiler { - outline, - built_object, - object_index, - z_buffer, - - point_queue: SortedVector::new(), - active_edges: SortedVector::new(), - old_active_edges: vec![], - } - } - - fn generate_tiles(&mut self) { - // Initialize the point queue. - self.init_point_queue(); - - // Reset active edges. - self.active_edges.clear(); - self.old_active_edges.clear(); - - // Generate strips. - let tile_rect = self.built_object.tile_rect; - for strip_origin_y in tile_rect.origin.y..tile_rect.max_y() { - self.generate_strip(strip_origin_y); - } - - // Cull. - self.cull(); - //println!("{:#?}", self.built_object); - } - - fn generate_strip(&mut self, strip_origin_y: i16) { - // Process old active edges. - self.process_old_active_edges(strip_origin_y); - - // Add new active edges. - let strip_max_y = ((i32::from(strip_origin_y) + 1) * TILE_HEIGHT as i32) as f32; - while let Some(queued_endpoint) = self.point_queue.peek() { - if queued_endpoint.y >= strip_max_y { - break; - } - self.add_new_active_edge(strip_origin_y); - } - } - - fn cull(&self) { - for solid_tile_index in self.built_object.solid_tiles.ones() { - let tile = &self.built_object.tiles[solid_tile_index]; - if tile.backdrop != 0 { - self.z_buffer - .update(tile.tile_x, tile.tile_y, self.object_index); - } - } - } - - fn process_old_active_edges(&mut self, tile_y: i16) { - let mut current_tile_x = self.built_object.tile_rect.origin.x; - let mut current_subtile_x = 0.0; - let mut current_winding = 0; - - debug_assert!(self.old_active_edges.is_empty()); - mem::swap(&mut self.old_active_edges, &mut self.active_edges.array); - - let mut last_segment_x = -9999.0; - - let tile_top = (i32::from(tile_y) * TILE_HEIGHT as i32) as f32; - //println!("---------- tile y {}({}) ----------", tile_y, tile_top); - //println!("old active edges: {:#?}", self.old_active_edges); - - for mut active_edge in self.old_active_edges.drain(..) { - // Determine x-intercept and winding. - let segment_x = active_edge.crossing.x(); - let edge_winding = - if active_edge.segment.baseline.from_y() < active_edge.segment.baseline.to_y() { - 1 - } else { - -1 - }; - - /* - println!("tile Y {}({}): segment_x={} edge_winding={} current_tile_x={} \ - current_subtile_x={} current_winding={}", - tile_y, - tile_top, - segment_x, - edge_winding, - current_tile_x, - current_subtile_x, - current_winding); - println!("... segment={:#?} crossing={:?}", active_edge.segment, active_edge.crossing); - */ - - // FIXME(pcwalton): Remove this debug code! - debug_assert!(segment_x >= last_segment_x); - last_segment_x = segment_x; - - // Do initial subtile fill, if necessary. - let segment_tile_x = (f32::floor(segment_x) as i32 / TILE_WIDTH as i32) as i16; - if current_tile_x < segment_tile_x && current_subtile_x > 0.0 { - let current_x = - (i32::from(current_tile_x) * TILE_WIDTH as i32) as f32 + current_subtile_x; - let tile_right_x = ((i32::from(current_tile_x) + 1) * TILE_WIDTH as i32) as f32; - self.built_object.add_active_fill( - current_x, - tile_right_x, - current_winding, - current_tile_x, - tile_y, - ); - current_tile_x += 1; - current_subtile_x = 0.0; - } - - // Move over to the correct tile, filling in as we go. - while current_tile_x < segment_tile_x { - //println!("... emitting backdrop {} @ tile {}", current_winding, current_tile_x); - self.built_object - .get_tile_mut(current_tile_x, tile_y) - .backdrop = current_winding; - current_tile_x += 1; - current_subtile_x = 0.0; - } - - // Do final subtile fill, if necessary. - debug_assert!(current_tile_x == segment_tile_x); - debug_assert!(current_tile_x < self.built_object.tile_rect.max_x()); - let segment_subtile_x = - segment_x - (i32::from(current_tile_x) * TILE_WIDTH as i32) as f32; - if segment_subtile_x > current_subtile_x { - let current_x = - (i32::from(current_tile_x) * TILE_WIDTH as i32) as f32 + current_subtile_x; - self.built_object.add_active_fill( - current_x, - segment_x, - current_winding, - current_tile_x, - tile_y, - ); - current_subtile_x = segment_subtile_x; - } - - // Update winding. - current_winding += edge_winding; - - // Process the edge. - //println!("about to process existing active edge {:#?}", active_edge); - debug_assert!(f32::abs(active_edge.crossing.y() - tile_top) < 0.1); - active_edge.process(&mut self.built_object, tile_y); - if !active_edge.segment.is_none() { - self.active_edges.push(active_edge); - } - } - - //debug_assert_eq!(current_winding, 0); - } - - fn add_new_active_edge(&mut self, tile_y: i16) { - let outline = &self.outline; - let point_index = self.point_queue.pop().unwrap().point_index; - - let contour = &outline.contours[point_index.contour() as usize]; - - // TODO(pcwalton): Could use a bitset of processed edges… - let prev_endpoint_index = contour.prev_endpoint_index_of(point_index.point()); - let next_endpoint_index = contour.next_endpoint_index_of(point_index.point()); - /* - println!("adding new active edge, tile_y={} point_index={} prev={} next={} pos={:?} \ - prevpos={:?} nextpos={:?}", - tile_y, - point_index.point(), - prev_endpoint_index, - next_endpoint_index, - contour.position_of(point_index.point()), - contour.position_of(prev_endpoint_index), - contour.position_of(next_endpoint_index)); - */ - - if contour.point_is_logically_above(point_index.point(), prev_endpoint_index) { - //println!("... adding prev endpoint"); - process_active_segment( - contour, - prev_endpoint_index, - &mut self.active_edges, - &mut self.built_object, - tile_y, - ); - - self.point_queue.push(QueuedEndpoint { - point_index: PointIndex::new(point_index.contour(), prev_endpoint_index), - y: contour.position_of(prev_endpoint_index).y(), - }); - //println!("... done adding prev endpoint"); - } - - if contour.point_is_logically_above(point_index.point(), next_endpoint_index) { - /* - println!("... adding next endpoint {} -> {}", - point_index.point(), - next_endpoint_index); - */ - process_active_segment( - contour, - point_index.point(), - &mut self.active_edges, - &mut self.built_object, - tile_y, - ); - - self.point_queue.push(QueuedEndpoint { - point_index: PointIndex::new(point_index.contour(), next_endpoint_index), - y: contour.position_of(next_endpoint_index).y(), - }); - //println!("... done adding next endpoint"); - } - } - - fn init_point_queue(&mut self) { - // Find MIN points. - self.point_queue.clear(); - for (contour_index, contour) in self.outline.contours.iter().enumerate() { - let contour_index = contour_index as u32; - let mut cur_endpoint_index = 0; - let mut prev_endpoint_index = contour.prev_endpoint_index_of(cur_endpoint_index); - let mut next_endpoint_index = contour.next_endpoint_index_of(cur_endpoint_index); - loop { - if contour.point_is_logically_above(cur_endpoint_index, prev_endpoint_index) - && contour.point_is_logically_above(cur_endpoint_index, next_endpoint_index) - { - self.point_queue.push(QueuedEndpoint { - point_index: PointIndex::new(contour_index, cur_endpoint_index), - y: contour.position_of(cur_endpoint_index).y(), - }); - } - - if cur_endpoint_index >= next_endpoint_index { - break; - } - - prev_endpoint_index = cur_endpoint_index; - cur_endpoint_index = next_endpoint_index; - next_endpoint_index = contour.next_endpoint_index_of(cur_endpoint_index); - } - } - } -} - -fn process_active_segment( - contour: &Contour, - from_endpoint_index: u32, - active_edges: &mut SortedVector, - built_object: &mut BuiltObject, - tile_y: i16, -) { - let mut active_edge = ActiveEdge::from_segment(&contour.segment_after(from_endpoint_index)); - //println!("... process_active_segment({:#?})", active_edge); - active_edge.process(built_object, tile_y); - if !active_edge.segment.is_none() { - active_edges.push(active_edge); - } -} - -// Scene construction - -impl BuiltScene { - fn new(view_box: &Rect) -> BuiltScene { - BuiltScene { - view_box: *view_box, - batches: vec![], - solid_tiles: vec![], - shaders: vec![], - } - } -} - -fn scene_tile_index(tile_x: i16, tile_y: i16, tile_rect: Rect) -> u32 { - (tile_y - tile_rect.origin.y) as u32 * tile_rect.size.width as u32 - + (tile_x - tile_rect.origin.x) as u32 -} - -struct SceneBuilder { - objects: Vec, - z_buffer: ZBuffer, - tile_rect: Rect, - - current_object_index: usize, -} - -impl SceneBuilder { - fn new(objects: Vec, z_buffer: ZBuffer, view_box: &Rect) -> SceneBuilder { - let tile_rect = round_rect_out_to_tile_bounds(view_box); - SceneBuilder { - objects, - z_buffer, - tile_rect, - current_object_index: 0, - } - } - - fn build_solid_tiles(&self) -> Vec { - self.z_buffer - .build_solid_tiles(&self.objects, &self.tile_rect) - } - - fn build_batch(&mut self) -> Option { - let mut batch = Batch::new(); - - let mut object_tile_index_to_batch_mask_tile_index = vec![]; - while self.current_object_index < self.objects.len() { - let object = &self.objects[self.current_object_index]; - - if batch.fills.len() + object.fills.len() > MAX_FILLS_PER_BATCH { - break; - } - - object_tile_index_to_batch_mask_tile_index.clear(); - object_tile_index_to_batch_mask_tile_index - .extend(iter::repeat(u16::MAX).take(object.tiles.len())); - - // Copy mask tiles. - for (tile_index, tile) in object.tiles.iter().enumerate() { - // Skip solid tiles, since we handled them above already. - if object.solid_tiles[tile_index] { - continue; - } - - // Cull occluded tiles. - let scene_tile_index = scene_tile_index(tile.tile_x, tile.tile_y, self.tile_rect); - if !self - .z_buffer - .test(scene_tile_index, self.current_object_index as u32) - { - continue; - } - - // Visible mask tile. - let batch_mask_tile_index = batch.mask_tiles.len() as u16; - if batch_mask_tile_index == MAX_MASKS_PER_BATCH { - break; - } - - object_tile_index_to_batch_mask_tile_index[tile_index] = batch_mask_tile_index; - - batch.mask_tiles.push(MaskTileBatchPrimitive { - tile: *tile, - shader: object.shader, - }); - } - - // Remap and copy fills, culling as necessary. - for fill in &object.fills { - let object_tile_index = object.tile_coords_to_index(fill.tile_x, fill.tile_y); - let mask_tile_index = - object_tile_index_to_batch_mask_tile_index[object_tile_index as usize]; - if mask_tile_index < u16::MAX { - batch.fills.push(FillBatchPrimitive { - px: fill.px, - subpx: fill.subpx, - mask_tile_index, - }); - } - } - - self.current_object_index += 1; - } - - if batch.is_empty() { - None - } else { - Some(batch) - } - } -} - -// Culling - -struct ZBuffer { - buffer: Vec, - tile_rect: Rect, -} - -impl ZBuffer { - fn new(view_box: &Rect) -> ZBuffer { - let tile_rect = round_rect_out_to_tile_bounds(view_box); - let tile_area = tile_rect.size.width as usize * tile_rect.size.height as usize; - ZBuffer { - buffer: (0..tile_area).map(|_| AtomicUsize::new(0)).collect(), - tile_rect, - } - } - - fn test(&self, scene_tile_index: u32, object_index: u32) -> bool { - let existing_depth = self.buffer[scene_tile_index as usize].load(AtomicOrdering::SeqCst); - existing_depth < object_index as usize + 1 - } - - fn update(&self, tile_x: i16, tile_y: i16, object_index: u16) { - let scene_tile_index = scene_tile_index(tile_x, tile_y, self.tile_rect) as usize; - let mut old_depth = self.buffer[scene_tile_index].load(AtomicOrdering::SeqCst); - let new_depth = (object_index + 1) as usize; - while old_depth < new_depth { - let prev_depth = self.buffer[scene_tile_index].compare_and_swap( - old_depth, - new_depth, - AtomicOrdering::SeqCst, - ); - if prev_depth == old_depth { - // Successfully written. - return; - } - old_depth = prev_depth; - } - } - - fn build_solid_tiles( - &self, - objects: &[BuiltObject], - tile_rect: &Rect, - ) -> Vec { - let mut solid_tiles = vec![]; - for scene_tile_y in 0..tile_rect.size.height { - for scene_tile_x in 0..tile_rect.size.width { - let scene_tile_index = - scene_tile_y as usize * tile_rect.size.width as usize + scene_tile_x as usize; - let depth = self.buffer[scene_tile_index].load(AtomicOrdering::Relaxed); - if depth == 0 { - continue; - } - let object_index = (depth - 1) as usize; - solid_tiles.push(SolidTileScenePrimitive { - tile_x: scene_tile_x + tile_rect.origin.x, - tile_y: scene_tile_y + tile_rect.origin.y, - shader: objects[object_index].shader, - }); - } - } - - solid_tiles - } -} - -// Primitives - -#[derive(Debug)] -struct BuiltObject { - bounds: Rect, - tile_rect: Rect, - tiles: Vec, - fills: Vec, - solid_tiles: FixedBitSet, - shader: ShaderId, -} - -#[derive(Debug)] -struct BuiltScene { - view_box: Rect, - batches: Vec, - solid_tiles: Vec, - shaders: Vec, -} - -#[derive(Debug)] -struct Batch { - fills: Vec, - mask_tiles: Vec, -} - -#[derive(Clone, Copy, Debug)] -struct FillObjectPrimitive { - px: LineSegmentU4, - subpx: LineSegmentU8, - tile_x: i16, - tile_y: i16, -} - -#[derive(Clone, Copy, Debug)] -struct TileObjectPrimitive { - tile_x: i16, - tile_y: i16, - backdrop: i16, -} - -#[derive(Clone, Copy, Debug)] -struct FillBatchPrimitive { - px: LineSegmentU4, - subpx: LineSegmentU8, - mask_tile_index: u16, -} - -#[derive(Clone, Copy, Debug)] -struct SolidTileScenePrimitive { - tile_x: i16, - tile_y: i16, - shader: ShaderId, -} - -#[derive(Clone, Copy, Debug)] -struct MaskTileBatchPrimitive { - tile: TileObjectPrimitive, - shader: ShaderId, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -struct ShaderId(pub u16); - -#[derive(Clone, Copy, Debug, Default)] -struct ObjectShader { - fill_color: ColorU, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] -struct ColorU { - r: u8, - g: u8, - b: u8, - a: u8, -} - -// Utilities for built objects - -impl BuiltObject { - fn new(bounds: &Rect, shader: ShaderId) -> BuiltObject { - // Compute the tile rect. - let tile_rect = round_rect_out_to_tile_bounds(&bounds); - - // Allocate tiles. - let tile_count = tile_rect.size.width as usize * tile_rect.size.height as usize; - let mut tiles = Vec::with_capacity(tile_count); - for y in tile_rect.origin.y..tile_rect.max_y() { - for x in tile_rect.origin.x..tile_rect.max_x() { - tiles.push(TileObjectPrimitive::new(x, y)); - } - } - - let mut solid_tiles = FixedBitSet::with_capacity(tile_count); - solid_tiles.insert_range(..); - - BuiltObject { - bounds: *bounds, - tile_rect, - tiles, - fills: vec![], - solid_tiles, - shader, - } - } - - // TODO(pcwalton): SIMD-ify `tile_x` and `tile_y`. - fn add_fill(&mut self, segment: &LineSegmentF32, tile_x: i16, tile_y: i16) { - //println!("add_fill({:?} ({}, {}))", segment, tile_x, tile_y); - let mut segment = (segment.0 * F32x4::splat(256.0)).to_i32x4(); - - let tile_origin_x = (TILE_WIDTH as i32) * 256 * (tile_x as i32); - let tile_origin_y = (TILE_HEIGHT as i32) * 256 * (tile_y as i32); - let tile_origin = I32x4::new(tile_origin_x, tile_origin_y, tile_origin_x, tile_origin_y); - - segment = segment - tile_origin; - /* - println!("... before min: {} {} {} {}", - segment[0], segment[1], segment[2], segment[3]); - */ - //segment = Sse41::max_epi32(segment, Sse41::setzero_epi32()); - segment = segment.min(I32x4::splat(0x0fff)); - //println!("... after min: {} {} {} {}", segment[0], segment[1], segment[2], segment[3]); - - let shuffle_mask = I32x4::new(0x0c08_0400, 0x0d05_0901, 0, 0); - segment = segment - .as_u8x16() - .shuffle(shuffle_mask.as_u8x16()) - .as_i32x4(); - - let px = LineSegmentU4((segment[1] | (segment[1] >> 12)) as u16); - let subpx = LineSegmentU8(segment[0] as u32); - - let tile_index = self.tile_coords_to_index(tile_x, tile_y); - - /* - // TODO(pcwalton): Cull degenerate fills again. - // Cull degenerate fills. - let (from_px, to_px) = (from.to_u8(), to.to_u8()); - if from_px.x == to_px.x && from_subpx.x == to_subpx.x { - return - } - */ - - self.fills.push(FillObjectPrimitive { - px, - subpx, - tile_x, - tile_y, - }); - self.solid_tiles.set(tile_index as usize, false); - } - - fn add_active_fill( - &mut self, - left: f32, - right: f32, - mut winding: i16, - tile_x: i16, - tile_y: i16, - ) { - let tile_origin_y = (i32::from(tile_y) * TILE_HEIGHT as i32) as f32; - let left = Point2DF32::new(left, tile_origin_y); - let right = Point2DF32::new(right, tile_origin_y); - - let segment = if winding < 0 { - LineSegmentF32::new(&left, &right) - } else { - LineSegmentF32::new(&right, &left) - }; - - /* - println!("... emitting active fill {} -> {} winding {} @ tile {}", - left.x(), - right.x(), - winding, - tile_x); - */ - - while winding != 0 { - self.add_fill(&segment, tile_x, tile_y); - if winding < 0 { - winding += 1 - } else { - winding -= 1 - } - } - } - - // TODO(pcwalton): Optimize this better with SIMD! - fn generate_fill_primitives_for_line(&mut self, mut segment: LineSegmentF32, tile_y: i16) { - /* - println!("... generate_fill_primitives_for_line(): segment={:?} tile_y={} ({}-{})", - segment, - tile_y, - tile_y as f32 * TILE_HEIGHT as f32, - (tile_y + 1) as f32 * TILE_HEIGHT as f32); - */ - - let winding = segment.from_x() > segment.to_x(); - let (segment_left, segment_right) = if !winding { - (segment.from_x(), segment.to_x()) - } else { - (segment.to_x(), segment.from_x()) - }; - - let segment_tile_left = (f32::floor(segment_left) as i32 / TILE_WIDTH as i32) as i16; - let segment_tile_right = - util::alignup_i32(f32::ceil(segment_right) as i32, TILE_WIDTH as i32) as i16; - - for subsegment_tile_x in segment_tile_left..segment_tile_right { - let (mut fill_from, mut fill_to) = (segment.from(), segment.to()); - let subsegment_tile_right = - ((i32::from(subsegment_tile_x) + 1) * TILE_HEIGHT as i32) as f32; - if subsegment_tile_right < segment_right { - let x = subsegment_tile_right; - let point = Point2DF32::new(x, segment.solve_y_for_x(x)); - if !winding { - fill_to = point; - segment = LineSegmentF32::new(&point, &segment.to()); - } else { - fill_from = point; - segment = LineSegmentF32::new(&segment.from(), &point); - } - } - - let fill_segment = LineSegmentF32::new(&fill_from, &fill_to); - self.add_fill(&fill_segment, subsegment_tile_x, tile_y); - } - } - - // FIXME(pcwalton): Use a `Point2D` instead? - fn tile_coords_to_index(&self, tile_x: i16, tile_y: i16) -> u32 { - /*println!("tile_coords_to_index(x={}, y={}, tile_rect={:?})", - tile_x, - tile_y, - self.tile_rect);*/ - (tile_y - self.tile_rect.origin.y) as u32 * self.tile_rect.size.width as u32 - + (tile_x - self.tile_rect.origin.x) as u32 - } - - fn get_tile_mut(&mut self, tile_x: i16, tile_y: i16) -> &mut TileObjectPrimitive { - let tile_index = self.tile_coords_to_index(tile_x, tile_y); - &mut self.tiles[tile_index as usize] - } -} - -impl Paint { - fn from_svg_paint(svg_paint: &UsvgPaint) -> Paint { - Paint { - color: match *svg_paint { - UsvgPaint::Color(color) => ColorU::from_svg_color(color), - UsvgPaint::Link(_) => { - // TODO(pcwalton) - ColorU::black() - } - }, - } - } -} - -// Scene serialization - -impl BuiltScene { - fn write(&self, writer: &mut W) -> io::Result<()> - where - W: Write, - { - writer.write_all(b"RIFF")?; - - let header_size = 4 * 6; - - let solid_tiles_size = self.solid_tiles.len() * mem::size_of::(); - - let batch_sizes: Vec<_> = self - .batches - .iter() - .map(|batch| BatchSizes { - fills: (batch.fills.len() * mem::size_of::()), - mask_tiles: (batch.mask_tiles.len() * mem::size_of::()), - }) - .collect(); - - let total_batch_sizes: usize = batch_sizes.iter().map(|sizes| 8 + sizes.total()).sum(); - - let shaders_size = self.shaders.len() * mem::size_of::(); - - writer.write_u32::( - (4 + 8 + header_size + 8 + solid_tiles_size + 8 + shaders_size + total_batch_sizes) - as u32, - )?; - - writer.write_all(b"PF3S")?; - - writer.write_all(b"head")?; - writer.write_u32::(header_size as u32)?; - writer.write_u32::(FILE_VERSION)?; - writer.write_u32::(self.batches.len() as u32)?; - writer.write_f32::(self.view_box.origin.x)?; - writer.write_f32::(self.view_box.origin.y)?; - writer.write_f32::(self.view_box.size.width)?; - writer.write_f32::(self.view_box.size.height)?; - - writer.write_all(b"shad")?; - writer.write_u32::(shaders_size as u32)?; - for &shader in &self.shaders { - let fill_color = shader.fill_color; - writer.write_all(&[fill_color.r, fill_color.g, fill_color.b, fill_color.a])?; - } - - writer.write_all(b"soli")?; - writer.write_u32::(solid_tiles_size as u32)?; - for &tile_primitive in &self.solid_tiles { - writer.write_i16::(tile_primitive.tile_x)?; - writer.write_i16::(tile_primitive.tile_y)?; - writer.write_u16::(tile_primitive.shader.0)?; - } - - for (batch, sizes) in self.batches.iter().zip(batch_sizes.iter()) { - writer.write_all(b"batc")?; - writer.write_u32::(sizes.total() as u32)?; - - writer.write_all(b"fill")?; - writer.write_u32::(sizes.fills as u32)?; - for fill_primitive in &batch.fills { - writer.write_u16::(fill_primitive.px.0)?; - writer.write_u32::(fill_primitive.subpx.0)?; - writer.write_u16::(fill_primitive.mask_tile_index)?; - } - - writer.write_all(b"mask")?; - writer.write_u32::(sizes.mask_tiles as u32)?; - for &tile_primitive in &batch.mask_tiles { - writer.write_i16::(tile_primitive.tile.tile_x)?; - writer.write_i16::(tile_primitive.tile.tile_y)?; - writer.write_i16::(tile_primitive.tile.backdrop)?; - writer.write_u16::(tile_primitive.shader.0)?; - } - } - - return Ok(()); - - const FILE_VERSION: u32 = 0; - - struct BatchSizes { - fills: usize, - mask_tiles: usize, - } - - impl BatchSizes { - fn total(&self) -> usize { - 8 + self.fills + 8 + self.mask_tiles - } - } - } -} - -impl Batch { - fn new() -> Batch { - Batch { - fills: vec![], - mask_tiles: vec![], - } - } - - fn is_empty(&self) -> bool { - self.mask_tiles.is_empty() - } -} - -impl TileObjectPrimitive { - fn new(tile_x: i16, tile_y: i16) -> TileObjectPrimitive { - TileObjectPrimitive { - tile_x, - tile_y, - backdrop: 0, - } - } -} - -impl ColorU { - fn black() -> ColorU { - ColorU { - r: 0, - g: 0, - b: 0, - a: 255, - } - } - - fn from_svg_color(svg_color: SvgColor) -> ColorU { - ColorU { - r: svg_color.red, - g: svg_color.green, - b: svg_color.blue, - a: 255, - } - } -} - -// Tile geometry utilities - -fn round_rect_out_to_tile_bounds(rect: &Rect) -> Rect { - let tile_origin = Point2D::new( - (f32::floor(rect.origin.x) as i32 / TILE_WIDTH as i32) as i16, - (f32::floor(rect.origin.y) as i32 / TILE_HEIGHT as i32) as i16, - ); - let tile_extent = Point2D::new( - util::alignup_i32(f32::ceil(rect.max_x()) as i32, TILE_WIDTH as i32) as i16, - util::alignup_i32(f32::ceil(rect.max_y()) as i32, TILE_HEIGHT as i32) as i16, - ); - let tile_size = Size2D::new(tile_extent.x - tile_origin.x, tile_extent.y - tile_origin.y); - Rect::new(tile_origin, tile_size) -} - -// USVG stuff - -fn usvg_rect_to_euclid_rect(rect: &UsvgRect) -> Rect { - Rect::new( - Point2D::new(rect.x, rect.y), - Size2D::new(rect.width, rect.height), - ) - .to_f32() -} - -fn usvg_transform_to_transform_2d(transform: &UsvgTransform) -> Transform2DF32 { - Transform2DF32::row_major( - transform.a as f32, - transform.b as f32, - transform.c as f32, - transform.d as f32, - transform.e as f32, - transform.f as f32, - ) -} - -struct UsvgPathToSegments -where - I: Iterator, -{ - iter: I, - first_subpath_point: Point2DF32, - last_subpath_point: Point2DF32, - just_moved: bool, -} - -impl UsvgPathToSegments -where - I: Iterator, -{ - fn new(iter: I) -> UsvgPathToSegments { - UsvgPathToSegments { - iter, - first_subpath_point: Point2DF32::default(), - last_subpath_point: Point2DF32::default(), - just_moved: false, - } - } -} - -impl Iterator for UsvgPathToSegments -where - I: Iterator, -{ - type Item = Segment; - - fn next(&mut self) -> Option { - match self.iter.next()? { - UsvgPathSegment::MoveTo { x, y } => { - let to = Point2DF32::new(x as f32, y as f32); - self.first_subpath_point = to; - self.last_subpath_point = to; - self.just_moved = true; - self.next() - } - UsvgPathSegment::LineTo { x, y } => { - let to = Point2DF32::new(x as f32, y as f32); - let mut segment = - Segment::line(&LineSegmentF32::new(&self.last_subpath_point, &to)); - if self.just_moved { - segment.flags.insert(SegmentFlags::FIRST_IN_SUBPATH); - } - self.last_subpath_point = to; - self.just_moved = false; - Some(segment) - } - UsvgPathSegment::CurveTo { - x1, - y1, - x2, - y2, - x, - y, - } => { - let ctrl0 = Point2DF32::new(x1 as f32, y1 as f32); - let ctrl1 = Point2DF32::new(x2 as f32, y2 as f32); - let to = Point2DF32::new(x as f32, y as f32); - let mut segment = Segment::cubic( - &LineSegmentF32::new(&self.last_subpath_point, &to), - &LineSegmentF32::new(&ctrl0, &ctrl1), - ); - if self.just_moved { - segment.flags.insert(SegmentFlags::FIRST_IN_SUBPATH); - } - self.last_subpath_point = to; - self.just_moved = false; - Some(segment) - } - UsvgPathSegment::ClosePath => { - let mut segment = Segment::line(&LineSegmentF32::new( - &self.last_subpath_point, - &self.first_subpath_point, - )); - segment.flags.insert(SegmentFlags::CLOSES_SUBPATH); - self.just_moved = false; - self.last_subpath_point = self.first_subpath_point; - Some(segment) - } - } - } -} - -// Monotonic conversion utilities - -// SortedVector - -#[derive(Clone, Debug)] -pub struct SortedVector -where - T: PartialOrd, -{ - array: Vec, -} - -impl SortedVector -where - T: PartialOrd, -{ - fn new() -> SortedVector { - SortedVector { array: vec![] } - } - - fn push(&mut self, value: T) { - self.array.push(value); - let mut index = self.array.len() - 1; - while index > 0 { - index -= 1; - if self.array[index] <= self.array[index + 1] { - break; - } - self.array.swap(index, index + 1); - } - } - - fn peek(&self) -> Option<&T> { - self.array.last() - } - fn pop(&mut self) -> Option { - self.array.pop() - } - fn clear(&mut self) { - self.array.clear() - } - - #[allow(dead_code)] - fn is_empty(&self) -> bool { - self.array.is_empty() - } -} - -// Queued endpoints - -#[derive(PartialEq)] -struct QueuedEndpoint { - point_index: PointIndex, - y: f32, -} - -impl Eq for QueuedEndpoint {} - -impl PartialOrd for QueuedEndpoint { - fn partial_cmp(&self, other: &QueuedEndpoint) -> Option { - // NB: Reversed! - (other.y, other.point_index).partial_cmp(&(self.y, self.point_index)) - } -} - -// Active edges - -#[derive(Clone, PartialEq, Debug)] -struct ActiveEdge { - segment: Segment, - // TODO(pcwalton): Shrink `crossing` down to just one f32? - crossing: Point2DF32, -} - -impl ActiveEdge { - fn from_segment(segment: &Segment) -> ActiveEdge { - let crossing = if segment.baseline.from_y() < segment.baseline.to_y() { - segment.baseline.from() - } else { - segment.baseline.to() - }; - ActiveEdge::from_segment_and_crossing(segment, &crossing) - } - - fn from_segment_and_crossing(segment: &Segment, crossing: &Point2DF32) -> ActiveEdge { - ActiveEdge { - segment: *segment, - crossing: *crossing, - } - } - - fn process(&mut self, built_object: &mut BuiltObject, tile_y: i16) { - let tile_bottom = ((i32::from(tile_y) + 1) * TILE_HEIGHT as i32) as f32; - // println!("process_active_edge({:#?}, tile_y={}({}))", self, tile_y, tile_bottom); - - let mut segment = self.segment; - let winding = segment.baseline.y_winding(); - - if segment.is_line() { - let line_segment = segment.as_line_segment(); - self.segment = match self.process_line_segment(&line_segment, built_object, tile_y) { - Some(lower_part) => Segment::line(&lower_part), - None => Segment::none(), - }; - return; - } - - // TODO(pcwalton): Don't degree elevate! - if !segment.is_cubic() { - segment = segment.to_cubic(); - } - - // If necessary, draw initial line. - if self.crossing.y() < segment.baseline.min_y() { - let first_line_segment = - LineSegmentF32::new(&self.crossing, &segment.baseline.upper_point()) - .orient(winding); - if self - .process_line_segment(&first_line_segment, built_object, tile_y) - .is_some() - { - return; - } - } - - loop { - let rest_segment = match segment - .orient(winding) - .as_cubic_segment() - .flatten_once(FLATTENING_TOLERANCE) - { - None => { - let line_segment = segment.baseline; - self.segment = - match self.process_line_segment(&line_segment, built_object, tile_y) { - Some(ref lower_part) => Segment::line(lower_part), - None => Segment::none(), - }; - return; - } - Some(rest_segment) => rest_segment.orient(winding), - }; - - debug_assert!(segment.baseline.min_y() <= tile_bottom); - - let line_segment = LineSegmentF32::new( - &segment.baseline.upper_point(), - &rest_segment.baseline.upper_point(), - ) - .orient(winding); - if self - .process_line_segment(&line_segment, built_object, tile_y) - .is_some() - { - self.segment = rest_segment; - return; - } - - segment = rest_segment; - } - } - - fn process_line_segment( - &mut self, - line_segment: &LineSegmentF32, - built_object: &mut BuiltObject, - tile_y: i16, - ) -> Option { - let tile_bottom = ((i32::from(tile_y) + 1) * TILE_HEIGHT as i32) as f32; - if line_segment.max_y() <= tile_bottom { - built_object.generate_fill_primitives_for_line(*line_segment, tile_y); - return None; - } - - let (upper_part, lower_part) = line_segment.split_at_y(tile_bottom); - built_object.generate_fill_primitives_for_line(upper_part, tile_y); - self.crossing = lower_part.upper_point(); - Some(lower_part) - } -} - -impl PartialOrd for ActiveEdge { - fn partial_cmp(&self, other: &ActiveEdge) -> Option { - self.crossing.x().partial_cmp(&other.crossing.x()) - } -} - -// Testing - -#[cfg(test)] -mod test { - use crate::SortedVector; - use quickcheck; - - #[test] - fn test_sorted_vec() { - quickcheck::quickcheck(prop_sorted_vec as fn(Vec) -> bool); - - fn prop_sorted_vec(mut values: Vec) -> bool { - let mut sorted_vec = SortedVector::new(); - for &value in &values { - sorted_vec.push(value) - } - - values.sort(); - let mut results = Vec::with_capacity(values.len()); - while !sorted_vec.is_empty() { - results.push(sorted_vec.pop().unwrap()); - } - results.reverse(); - assert_eq!(&values, &results); - - true - } - } -}