From c3c6df020ddab15ea816b3a38486931f7eea9a2d Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Fri, 27 Oct 2023 15:00:20 -0700 Subject: [PATCH 1/6] Encode fill rule in "draw flags" and fully abandon "linewidth" * Pipelines downstream of flatten (draw_leaf, coarse) now extract the fill rule using a "draw flags" field. Flatten still writes this to the path bounding-box structure, which gets propagated to the draw info list, and eventually translates to the fill rule written to the PTCL. * draw_leaf no longer deals with transforming the linewidth for strokes. This was a leftover from the previous architecture and the logic is no longer needed. * bbox_clear used to contain a duplicate PathBbox data structure. It now uses the one from shader/shared/bbox.wgsl This continues the work outlined in #303 --- crates/encoding/src/draw.rs | 5 +++++ crates/encoding/src/lib.rs | 2 +- crates/encoding/src/path.rs | 4 ++-- shader/bbox_clear.wgsl | 10 +--------- shader/coarse.wgsl | 22 +++++++++++----------- shader/draw_leaf.wgsl | 17 ++++++----------- shader/flatten.wgsl | 19 ++++++++++--------- shader/shared/bbox.wgsl | 6 +++++- shader/shared/drawtag.wgsl | 5 +++++ src/cpu_shader/coarse.rs | 5 ++++- src/cpu_shader/draw_leaf.rs | 10 +++++----- src/cpu_shader/flatten.rs | 10 ++++------ 12 files changed, 59 insertions(+), 56 deletions(-) diff --git a/crates/encoding/src/draw.rs b/crates/encoding/src/draw.rs index 8e8b1ba07..f760ca1c9 100644 --- a/crates/encoding/src/draw.rs +++ b/crates/encoding/src/draw.rs @@ -41,6 +41,11 @@ impl DrawTag { } } +/// The first word of each draw info stream entry contains the flags. This is not part of the +/// draw object stream but gets used after the draw objects get reduced on the GPU. +/// 0 represents a non-zero fill. 1 represents an even-odd fill. +pub const DRAW_INFO_FLAGS_FILL_RULE_BIT: u32 = 1; + /// Draw object bounding box. #[derive(Copy, Clone, Pod, Zeroable, Debug, Default)] #[repr(C)] diff --git a/crates/encoding/src/lib.rs b/crates/encoding/src/lib.rs index 798bf9890..9b61cc5bf 100644 --- a/crates/encoding/src/lib.rs +++ b/crates/encoding/src/lib.rs @@ -29,7 +29,7 @@ pub use config::{ }; pub use draw::{ DrawBbox, DrawBeginClip, DrawColor, DrawImage, DrawLinearGradient, DrawMonoid, - DrawRadialGradient, DrawTag, + DrawRadialGradient, DrawTag, DRAW_INFO_FLAGS_FILL_RULE_BIT, }; pub use encoding::{Encoding, StreamOffsets}; pub use math::Transform; diff --git a/crates/encoding/src/path.rs b/crates/encoding/src/path.rs index f586f8773..83e537042 100644 --- a/crates/encoding/src/path.rs +++ b/crates/encoding/src/path.rs @@ -380,8 +380,8 @@ pub struct PathBbox { pub x1: i32, /// Maximum y value. pub y1: i32, - /// Line width. - pub linewidth: f32, + /// Style flags + pub draw_flags: u32, /// Index into the transform stream. pub trans_ix: u32, } diff --git a/shader/bbox_clear.wgsl b/shader/bbox_clear.wgsl index fe8ccebbd..b2b9f93b3 100644 --- a/shader/bbox_clear.wgsl +++ b/shader/bbox_clear.wgsl @@ -1,19 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense #import config +#import bbox @group(0) @binding(0) var config: Config; -struct PathBbox { - x0: i32, - y0: i32, - x1: i32, - y1: i32, - linewidth: f32, - trans_ix: u32, -} - @group(0) @binding(1) var path_bboxes: array; diff --git a/shader/coarse.wgsl b/shader/coarse.wgsl index 0635443fe..44f30ae81 100644 --- a/shader/coarse.wgsl +++ b/shader/coarse.wgsl @@ -82,8 +82,8 @@ fn alloc_cmd(size: u32) { } } -fn write_path(tile: Tile, tile_ix: u32, linewidth: f32) -> bool { - let even_odd = linewidth < -1.0; +fn write_path(tile: Tile, tile_ix: u32, draw_flags: u32) -> bool { + let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u; // We overload the "segments" field to store both count (written by // path_count stage) and segment allocation (used by path_tiling and // fine). @@ -350,16 +350,16 @@ fn main( switch drawtag { // DRAWTAG_FILL_COLOR case 0x44u: { - let linewidth = bitcast(info_bin_data[di]); - if write_path(tile, tile_ix, linewidth) { + let draw_flags = info_bin_data[di]; + if write_path(tile, tile_ix, draw_flags) { let rgba_color = scene[dd]; write_color(CmdColor(rgba_color)); } } // DRAWTAG_FILL_LIN_GRADIENT case 0x114u: { - let linewidth = bitcast(info_bin_data[di]); - if write_path(tile, tile_ix, linewidth) { + let draw_flags = info_bin_data[di]; + if write_path(tile, tile_ix, draw_flags) { let index = scene[dd]; let info_offset = di + 1u; write_grad(CMD_LIN_GRAD, index, info_offset); @@ -367,8 +367,8 @@ fn main( } // DRAWTAG_FILL_RAD_GRADIENT case 0x29cu: { - let linewidth = bitcast(info_bin_data[di]); - if write_path(tile, tile_ix, linewidth) { + let draw_flags = info_bin_data[di]; + if write_path(tile, tile_ix, draw_flags) { let index = scene[dd]; let info_offset = di + 1u; write_grad(CMD_RAD_GRAD, index, info_offset); @@ -376,8 +376,8 @@ fn main( } // DRAWTAG_FILL_IMAGE case 0x248u: { - let linewidth = bitcast(info_bin_data[di]); - if write_path(tile, tile_ix, linewidth) { + let draw_flags = info_bin_data[di]; + if write_path(tile, tile_ix, draw_flags) { write_image(di + 1u); } } @@ -395,7 +395,7 @@ fn main( // DRAWTAG_END_CLIP case 0x21u: { clip_depth -= 1u; - write_path(tile, tile_ix, -1.0); + write_path(tile, tile_ix, 0u); let blend = scene[dd]; let alpha = bitcast(scene[dd + 1u]); write_end_clip(CmdEndClip(blend, alpha)); diff --git a/shader/draw_leaf.wgsl b/shader/draw_leaf.wgsl index 827825974..920d6714b 100644 --- a/shader/draw_leaf.wgsl +++ b/shader/draw_leaf.wgsl @@ -109,25 +109,20 @@ fn main( // let y1 = f32(bbox.y1); // let bbox_f = vec4(x0, y0, x1, y1); var transform = Transform(); - var linewidth = bbox.linewidth; - if linewidth >= 0.0 || tag_word == DRAWTAG_FILL_LIN_GRADIENT || tag_word == DRAWTAG_FILL_RAD_GRADIENT || + let draw_flags = bbox.draw_flags; + if tag_word == DRAWTAG_FILL_LIN_GRADIENT || tag_word == DRAWTAG_FILL_RAD_GRADIENT || tag_word == DRAWTAG_FILL_IMAGE { transform = read_transform(config.transform_base, bbox.trans_ix); } - if linewidth >= 0.0 { - // Note: doesn't deal with anisotropic case - let matrx = transform.matrx; - linewidth *= sqrt(abs(matrx.x * matrx.w - matrx.y * matrx.z)); - } switch tag_word { // DRAWTAG_FILL_COLOR case 0x44u: { - info[di] = bitcast(linewidth); + info[di] = draw_flags; } // DRAWTAG_FILL_LIN_GRADIENT case 0x114u: { - info[di] = bitcast(linewidth); + info[di] = draw_flags; var p0 = bitcast>(vec2(scene[dd + 1u], scene[dd + 2u])); var p1 = bitcast>(vec2(scene[dd + 3u], scene[dd + 4u])); p0 = transform_apply(transform, p0); @@ -146,7 +141,7 @@ fn main( // on the algorithm at // This epsilon matches what Skia uses let GRADIENT_EPSILON = 1.0 / f32(1 << 12u); - info[di] = bitcast(linewidth); + info[di] = draw_flags; var p0 = bitcast>(vec2(scene[dd + 1u], scene[dd + 2u])); var p1 = bitcast>(vec2(scene[dd + 3u], scene[dd + 4u])); var r0 = bitcast(scene[dd + 5u]); @@ -226,7 +221,7 @@ fn main( } // DRAWTAG_FILL_IMAGE case 0x248u: { - info[di] = bitcast(linewidth); + info[di] = draw_flags; let inv = transform_inverse(transform); info[di + 1u] = bitcast(inv.matrx.x); info[di + 2u] = bitcast(inv.matrx.y); diff --git a/shader/flatten.wgsl b/shader/flatten.wgsl index a5d328f09..e9f516eaa 100644 --- a/shader/flatten.wgsl +++ b/shader/flatten.wgsl @@ -3,6 +3,7 @@ // Flatten curves to lines #import config +#import drawtag #import pathtag #import segment #import cubic @@ -22,7 +23,7 @@ struct AtomicPathBbox { y0: atomic, x1: atomic, y1: atomic, - linewidth: f32, + draw_flags: u32, trans_ix: u32, } @@ -221,12 +222,9 @@ fn main( let style_flags = scene[config.style_base + tm.style_ix]; // TODO: We assume all paths are fills at the moment. This is where we will extract the stroke // vs fill state using STYLE_FLAGS_STYLE_BIT. - // TODO: The downstream pipelines still use the old floating-point linewidth/fill encoding scheme - // This will change to represent the fill rule as a single bit inside the bounding box and draw - // info data structures. - let linewidth = select(-2.0, -1.0, (style_flags & STYLE_FLAGS_FILL_BIT) == 0u); + let draw_flags = select(DRAW_INFO_FLAGS_FILL_RULE_BIT, 0u, (style_flags & STYLE_FLAGS_FILL_BIT) == 0u); if (tag_byte & PATH_TAG_PATH) != 0u { - (*out).linewidth = linewidth; + (*out).draw_flags = draw_flags; (*out).trans_ix = tm.trans_ix; } // Decode path data @@ -277,15 +275,18 @@ fn main( } } var stroke = vec2(0.0, 0.0); - if linewidth >= 0.0 { + let is_stroke = (style_flags & STYLE_FLAGS_STYLE_BIT) != 0u; + /* + // TODO: the stroke handling here is dead code for now + if is_stroke { // See https://www.iquilezles.org/www/articles/ellipses/ellipses.htm // This is the correct bounding box, but we're not handling rendering // in the isotropic case, so it may mismatch. stroke = 0.5 * linewidth * vec2(length(transform.mat.xz), length(transform.mat.yw)); bbox += vec4(-stroke, stroke); } - let flags = u32(linewidth >= 0.0); - flatten_cubic(Cubic(p0, p1, p2, p3, stroke, tm.path_ix, flags)); + */ + flatten_cubic(Cubic(p0, p1, p2, p3, stroke, tm.path_ix, u32(is_stroke))); // Update bounding box using atomics only. Computing a monoid is a // potential future optimization. if bbox.z > bbox.x || bbox.w > bbox.y { diff --git a/shader/shared/bbox.wgsl b/shader/shared/bbox.wgsl index 714602d9d..a3e23040a 100644 --- a/shader/shared/bbox.wgsl +++ b/shader/shared/bbox.wgsl @@ -4,12 +4,16 @@ // but contains a link to the active transform, mostly for gradients. // Coordinates are integer pixels (for the convenience of atomic update) // but will probably become fixed-point fractions for rectangles. +// +// TODO: This also carries a `draw_flags` field that contains information that gets propagated to +// the draw info stream. This is currently only used for the fill rule. If the other bits remain +// unused we could possibly pack this into some other field, such as the the MSB of `trans_ix`. struct PathBbox { x0: i32, y0: i32, x1: i32, y1: i32, - linewidth: f32, + draw_flags: u32, trans_ix: u32, } diff --git a/shader/shared/drawtag.wgsl b/shader/shared/drawtag.wgsl index 0b8ae41e7..231657f52 100644 --- a/shader/shared/drawtag.wgsl +++ b/shader/shared/drawtag.wgsl @@ -23,6 +23,11 @@ let DRAWTAG_FILL_IMAGE = 0x248u; let DRAWTAG_BEGIN_CLIP = 0x9u; let DRAWTAG_END_CLIP = 0x21u; +/// The first word of each draw info stream entry contains the flags. This is not a part of the +/// draw object stream but get used after the draw objects have been reduced on the GPU. +/// 0 represents a non-zero fill. 1 represents an even-odd fill. +let DRAW_INFO_FLAGS_FILL_RULE_BIT = 1u; + fn draw_monoid_identity() -> DrawMonoid { return DrawMonoid(); } diff --git a/src/cpu_shader/coarse.rs b/src/cpu_shader/coarse.rs index 390df7f74..70304ad46 100644 --- a/src/cpu_shader/coarse.rs +++ b/src/cpu_shader/coarse.rs @@ -241,7 +241,10 @@ fn coarse_main( let n_segs = tile.segment_count_or_ix; let include_tile = n_segs != 0 || (tile.backdrop == 0) == is_clip || is_blend; if include_tile { - // TODO: get drawinfo (linewidth for fills) + // TODO: The first word of the info buffer (`info[di]`) contains flags that + // indicate the even-odd vs non-zero fill rule. Read that here and pass it + // to `write_path` so it gets propagated to the PTCL (see + // `PathBbox::FLAGS_FILL_STYLE_BIT` for its interpretation). match DrawTag(drawtag) { DrawTag::COLOR => { tile_state.write_path(config, bump, ptcl, tile); diff --git a/src/cpu_shader/draw_leaf.rs b/src/cpu_shader/draw_leaf.rs index 0aa779e5c..a422f6a27 100644 --- a/src/cpu_shader/draw_leaf.rs +++ b/src/cpu_shader/draw_leaf.rs @@ -41,13 +41,13 @@ fn draw_leaf_main( { let bbox = path_bbox[m.path_ix as usize]; let transform = Transform::read(config.layout.transform_base, bbox.trans_ix, scene); - let linewidth = bbox.linewidth; + let draw_flags = bbox.draw_flags; match tag_word { DrawTag::COLOR => { - info[di] = f32::to_bits(linewidth); + info[di] = draw_flags; } DrawTag::LINEAR_GRADIENT => { - info[di] = f32::to_bits(linewidth); + info[di] = draw_flags; let p0 = Vec2::new( f32::from_bits(scene[dd as usize + 1]), f32::from_bits(scene[dd as usize + 2]), @@ -67,7 +67,7 @@ fn draw_leaf_main( info[di + 3] = f32::to_bits(line_c); } DrawTag::RADIAL_GRADIENT => { - info[di] = f32::to_bits(linewidth); + info[di] = draw_flags; let p0 = Vec2::new( f32::from_bits(scene[dd as usize + 1]), f32::from_bits(scene[dd as usize + 2]), @@ -108,7 +108,7 @@ fn draw_leaf_main( info[di + 19] = f32::to_bits(roff); } DrawTag::IMAGE => { - info[di] = f32::to_bits(linewidth); + info[di] = draw_flags; let z = transform.0; let inv_det = (z[0] * z[3] - z[1] * z[2]).recip(); let inv_mat = [ diff --git a/src/cpu_shader/flatten.rs b/src/cpu_shader/flatten.rs index 150aec174..bbf9e49ef 100644 --- a/src/cpu_shader/flatten.rs +++ b/src/cpu_shader/flatten.rs @@ -6,6 +6,7 @@ use crate::cpu_dispatch::CpuBinding; use super::util::{Transform, Vec2}; use vello_encoding::{ BumpAllocators, ConfigUniform, LineSoup, Monoid, PathBbox, PathMonoid, Style, + DRAW_INFO_FLAGS_FILL_RULE_BIT, }; fn to_minus_one_quarter(x: f32) -> f32 { @@ -218,13 +219,10 @@ fn flatten_main( let out = &mut path_bboxes[tm.path_ix as usize]; // TODO: We assume all paths are fills at the moment. This is where we will extract the // stroke vs fill state using STYLE_FLAGS_STYLE_BIT. - // TODO: The downstream pipelines still use the old floating-point linewidth/fill - // encoding scheme. This will change to represent the fill rule as a single bit inside - // the bounding box and draw info data structures. - out.linewidth = if (style_flags & Style::FLAGS_FILL_BIT) == 0 { - -1.0 + out.draw_flags = if (style_flags & Style::FLAGS_FILL_BIT) == 0 { + 0 } else { - -2.0 + DRAW_INFO_FLAGS_FILL_RULE_BIT }; out.trans_ix = tm.trans_ix; } From 64b8946ae018304023cd51690120d13fd8570bb3 Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Fri, 27 Oct 2023 15:14:16 -0700 Subject: [PATCH 2/6] [cpu_shaders] Support even-odd fill rule in coarse Wired up the even-odd fill-rule to the CPU version of coarse. --- src/cpu_shader/coarse.rs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/cpu_shader/coarse.rs b/src/cpu_shader/coarse.rs index 70304ad46..445530dff 100644 --- a/src/cpu_shader/coarse.rs +++ b/src/cpu_shader/coarse.rs @@ -1,7 +1,10 @@ // Copyright 2023 The Vello authors // SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense -use vello_encoding::{BinHeader, BumpAllocators, ConfigUniform, DrawMonoid, DrawTag, Path, Tile}; +use vello_encoding::{ + BinHeader, BumpAllocators, ConfigUniform, DrawMonoid, DrawTag, Path, Tile, + DRAW_INFO_FLAGS_FILL_RULE_BIT, +}; use crate::cpu_dispatch::CpuBinding; @@ -63,6 +66,7 @@ impl TileState { bump: &mut BumpAllocators, ptcl: &mut [u32], tile: &mut Tile, + draw_flags: u32, ) { let n_segs = tile.segment_count_or_ix; if n_segs != 0 { @@ -71,7 +75,7 @@ impl TileState { bump.segments += n_segs; self.alloc_cmd(4, config, bump, ptcl); self.write(ptcl, 0, CMD_FILL); - let even_odd = false; // TODO + let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0; let size_and_rule = (n_segs << 1) | (even_odd as u32); self.write(ptcl, 1, size_and_rule); self.write(ptcl, 2, seg_ix); @@ -241,22 +245,21 @@ fn coarse_main( let n_segs = tile.segment_count_or_ix; let include_tile = n_segs != 0 || (tile.backdrop == 0) == is_clip || is_blend; if include_tile { - // TODO: The first word of the info buffer (`info[di]`) contains flags that - // indicate the even-odd vs non-zero fill rule. Read that here and pass it - // to `write_path` so it gets propagated to the PTCL (see - // `PathBbox::FLAGS_FILL_STYLE_BIT` for its interpretation). match DrawTag(drawtag) { DrawTag::COLOR => { - tile_state.write_path(config, bump, ptcl, tile); + let draw_flags = info_bin_data[di as usize]; + tile_state.write_path(config, bump, ptcl, tile, draw_flags); let rgba_color = scene[dd as usize]; tile_state.write_color(config, bump, ptcl, rgba_color); } DrawTag::IMAGE => { - tile_state.write_path(config, bump, ptcl, tile); + let draw_flags = info_bin_data[di as usize]; + tile_state.write_path(config, bump, ptcl, tile, draw_flags); tile_state.write_image(config, bump, ptcl, di + 1); } DrawTag::LINEAR_GRADIENT => { - tile_state.write_path(config, bump, ptcl, tile); + let draw_flags = info_bin_data[di as usize]; + tile_state.write_path(config, bump, ptcl, tile, draw_flags); let index = scene[dd as usize]; tile_state.write_grad( config, @@ -268,7 +271,8 @@ fn coarse_main( ); } DrawTag::RADIAL_GRADIENT => { - tile_state.write_path(config, bump, ptcl, tile); + let draw_flags = info_bin_data[di as usize]; + tile_state.write_path(config, bump, ptcl, tile, draw_flags); let index = scene[dd as usize]; tile_state.write_grad( config, @@ -290,7 +294,7 @@ fn coarse_main( } DrawTag::END_CLIP => { clip_depth -= 1; - tile_state.write_path(config, bump, ptcl, tile); + tile_state.write_path(config, bump, ptcl, tile, 0); let blend = scene[dd as usize]; let alpha = f32::from_bits(scene[dd as usize + 1]); tile_state.write_end_clip(config, bump, ptcl, blend, alpha); From d17dd705d7e405e1cf39c9ead4f677f374d7fde7 Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Fri, 27 Oct 2023 23:05:50 -0700 Subject: [PATCH 3/6] [cpu_shaders} Correctly write PTCL commands for even-odd fill The CPU version of the coarse stage was missing logic to correctly handle the even-odd fill rule. This change copies over the existing logic from the GPU version with some added documentation. --- shader/coarse.wgsl | 13 ++++++- src/cpu_shader/coarse.rs | 73 +++++++++++++++++++++++++--------------- 2 files changed, 58 insertions(+), 28 deletions(-) diff --git a/shader/coarse.wgsl b/shader/coarse.wgsl index 44f30ae81..e7c93e0a9 100644 --- a/shader/coarse.wgsl +++ b/shader/coarse.wgsl @@ -82,6 +82,7 @@ fn alloc_cmd(size: u32) { } } +// Returns true if this path should paint the tile. fn write_path(tile: Tile, tile_ix: u32, draw_flags: u32) -> bool { let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u; // We overload the "segments" field to store both count (written by @@ -100,6 +101,15 @@ fn write_path(tile: Tile, tile_ix: u32, draw_flags: u32) -> bool { ptcl[cmd_offset + 3u] = u32(fill.backdrop); cmd_offset += 4u; } else { + // If no line segments cross this tile then the tile is completely inside a + // subregion of the path. If the rule is non-zero, the tile should get completely + // painted based on the draw tag (either a solid fill, gradient fill, or an image fill; + // writing CMD_SOLID below ensures that the pixel area coverage is 100%). + // + // If the rule is even-odd then this path shouldn't paint the tile at all if its + // winding number is even. We check this by simply looking at the backdrop, which + // contains the winding number of the top-left corner of the tile (and in this case, it + // applies to the whole tile). if even_odd && (abs(tile.backdrop) & 1) == 0 { return false; } @@ -395,7 +405,8 @@ fn main( // DRAWTAG_END_CLIP case 0x21u: { clip_depth -= 1u; - write_path(tile, tile_ix, 0u); + // A clip shape is always a non-zero fill (draw_flags=0). + write_path(tile, tile_ix, /*draw_flags=*/0u); let blend = scene[dd]; let alpha = bitcast(scene[dd + 1u]); write_end_clip(CmdEndClip(blend, alpha)); diff --git a/src/cpu_shader/coarse.rs b/src/cpu_shader/coarse.rs index 445530dff..a2d4d47da 100644 --- a/src/cpu_shader/coarse.rs +++ b/src/cpu_shader/coarse.rs @@ -60,6 +60,7 @@ impl TileState { ptcl[(self.cmd_offset + offset) as usize] = value; } + // Returns true if this path should paint the tile. fn write_path( &mut self, config: &ConfigUniform, @@ -67,7 +68,8 @@ impl TileState { ptcl: &mut [u32], tile: &mut Tile, draw_flags: u32, - ) { + ) -> bool { + let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0; let n_segs = tile.segment_count_or_ix; if n_segs != 0 { let seg_ix = bump.segments; @@ -75,17 +77,29 @@ impl TileState { bump.segments += n_segs; self.alloc_cmd(4, config, bump, ptcl); self.write(ptcl, 0, CMD_FILL); - let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0; let size_and_rule = (n_segs << 1) | (even_odd as u32); self.write(ptcl, 1, size_and_rule); self.write(ptcl, 2, seg_ix); self.write(ptcl, 3, tile.backdrop as u32); self.cmd_offset += 4; } else { + // If no line segments cross this tile then the tile is completely inside a + // subregion of the path. If the rule is non-zero, the tile should get completely + // painted based on the draw tag (either a solid fill, gradient fill, or an image fill; + // writing CMD_SOLID below ensures that the pixel area coverage is 100%). + // + // If the rule is even-odd then this path shouldn't paint the tile at all if its + // winding number is even. We check this by simply looking at the backdrop, which + // contains the winding number of the top-left corner of the tile (and in this case, it + // applies to the whole tile). + if even_odd && (tile.backdrop.abs() & 1) == 0 { + return false; + } self.alloc_cmd(1, config, bump, ptcl); self.write(ptcl, 0, CMD_SOLID); self.cmd_offset += 1; } + true } fn write_color( @@ -248,40 +262,44 @@ fn coarse_main( match DrawTag(drawtag) { DrawTag::COLOR => { let draw_flags = info_bin_data[di as usize]; - tile_state.write_path(config, bump, ptcl, tile, draw_flags); - let rgba_color = scene[dd as usize]; - tile_state.write_color(config, bump, ptcl, rgba_color); + if tile_state.write_path(config, bump, ptcl, tile, draw_flags) { + let rgba_color = scene[dd as usize]; + tile_state.write_color(config, bump, ptcl, rgba_color); + } } DrawTag::IMAGE => { let draw_flags = info_bin_data[di as usize]; - tile_state.write_path(config, bump, ptcl, tile, draw_flags); - tile_state.write_image(config, bump, ptcl, di + 1); + if tile_state.write_path(config, bump, ptcl, tile, draw_flags) { + tile_state.write_image(config, bump, ptcl, di + 1); + } } DrawTag::LINEAR_GRADIENT => { let draw_flags = info_bin_data[di as usize]; - tile_state.write_path(config, bump, ptcl, tile, draw_flags); - let index = scene[dd as usize]; - tile_state.write_grad( - config, - bump, - ptcl, - CMD_LIN_GRAD, - index, - di + 1, - ); + if tile_state.write_path(config, bump, ptcl, tile, draw_flags) { + let index = scene[dd as usize]; + tile_state.write_grad( + config, + bump, + ptcl, + CMD_LIN_GRAD, + index, + di + 1, + ); + } } DrawTag::RADIAL_GRADIENT => { let draw_flags = info_bin_data[di as usize]; - tile_state.write_path(config, bump, ptcl, tile, draw_flags); - let index = scene[dd as usize]; - tile_state.write_grad( - config, - bump, - ptcl, - CMD_RAD_GRAD, - index, - di + 1, - ); + if tile_state.write_path(config, bump, ptcl, tile, draw_flags) { + let index = scene[dd as usize]; + tile_state.write_grad( + config, + bump, + ptcl, + CMD_RAD_GRAD, + index, + di + 1, + ); + } } DrawTag::BEGIN_CLIP => { if tile.segment_count_or_ix == 0 && tile.backdrop == 0 { @@ -294,6 +312,7 @@ fn coarse_main( } DrawTag::END_CLIP => { clip_depth -= 1; + // A clip shape is always a non-zero fill (draw_flags=0). tile_state.write_path(config, bump, ptcl, tile, 0); let blend = scene[dd as usize]; let alpha = f32::from_bits(scene[dd as usize + 1]); From 161a31cc3897715efa6adcaae71f3c537a7a281d Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Fri, 27 Oct 2023 23:09:11 -0700 Subject: [PATCH 4/6] Add fill-rule tests with blends Added more draws to the fill_types test scene where different fills overlap and blend with each other. This is mostly to validate the even-odd fill logic in the `coarse` stage. --- examples/scenes/src/test_scenes.rs | 40 ++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index 89606caf4..75678d80f 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -324,7 +324,43 @@ fn fill_types(sb: &mut SceneBuilder, params: &mut SceneParams) { sb.fill( rule.0, Affine::translate((0., 10.)) * t, - Color::BLACK, + Color::YELLOW, + None, + &rule.2, + ); + } + + // Draw blends + let t = Affine::translate((700., 0.)) * t; + for (i, rule) in rules.iter().enumerate() { + let t = Affine::translate(((i % 2) as f64 * 306., (i / 2) as f64 * 340.)) * t; + params.text.add(sb, None, 24., None, t, rule.1); + let t = Affine::translate((0., 5.)) * t * scale; + sb.fill( + Fill::NonZero, + t, + &Brush::Solid(Color::rgb8(128, 128, 128)), + None, + &rect, + ); + sb.fill( + rule.0, + Affine::translate((0., 10.)) * t, + Color::YELLOW, + None, + &rule.2, + ); + sb.fill( + rule.0, + Affine::translate((0., 10.)) * t * Affine::rotate(0.06), + Color::rgba(0., 1., 0.7, 0.6), + None, + &rule.2, + ); + sb.fill( + rule.0, + Affine::translate((0., 10.)) * t * Affine::rotate(-0.06), + Color::rgba(0.9, 0.7, 0.5, 0.6), None, &rule.2, ); @@ -373,7 +409,7 @@ fn longpathdash(cap: Cap) -> impl FnMut(&mut SceneBuilder, &mut SceneParams) { sb.stroke( &Stroke::new(1.0).with_caps(cap).with_dashes(0.0, [1.0, 1.0]), Affine::translate((50.0, 50.0)), - Color::rgb8(255, 255, 0), + Color::YELLOW, None, &path, ); From 7a849140d1cd1c86ed8a4b79cbb15a5216b9e31a Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Tue, 31 Oct 2023 01:16:29 -0700 Subject: [PATCH 5/6] [coarse] Remove branch conditionals in even-odd handling Moved the per-drawobject tile discard logic near the existing tile inclusion code in the main loop. This avoids the branch conditionals that were present in each draw tag case branch. --- shader/coarse.wgsl | 69 ++++++++++++++---------------- src/cpu_shader/coarse.rs | 91 ++++++++++++++++++---------------------- 2 files changed, 72 insertions(+), 88 deletions(-) diff --git a/shader/coarse.wgsl b/shader/coarse.wgsl index e7c93e0a9..c23d088c1 100644 --- a/shader/coarse.wgsl +++ b/shader/coarse.wgsl @@ -82,9 +82,7 @@ fn alloc_cmd(size: u32) { } } -// Returns true if this path should paint the tile. -fn write_path(tile: Tile, tile_ix: u32, draw_flags: u32) -> bool { - let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u; +fn write_path(tile: Tile, tile_ix: u32, draw_flags: u32) { // We overload the "segments" field to store both count (written by // path_count stage) and segment allocation (used by path_tiling and // fine). @@ -94,6 +92,7 @@ fn write_path(tile: Tile, tile_ix: u32, draw_flags: u32) -> bool { tiles[tile_ix].segment_count_or_ix = ~seg_ix; alloc_cmd(4u); ptcl[cmd_offset] = CMD_FILL; + let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u; let size_and_rule = (n_segs << 1u) | u32(even_odd); let fill = CmdFill(size_and_rule, seg_ix, tile.backdrop); ptcl[cmd_offset + 1u] = fill.size_and_rule; @@ -101,23 +100,10 @@ fn write_path(tile: Tile, tile_ix: u32, draw_flags: u32) -> bool { ptcl[cmd_offset + 3u] = u32(fill.backdrop); cmd_offset += 4u; } else { - // If no line segments cross this tile then the tile is completely inside a - // subregion of the path. If the rule is non-zero, the tile should get completely - // painted based on the draw tag (either a solid fill, gradient fill, or an image fill; - // writing CMD_SOLID below ensures that the pixel area coverage is 100%). - // - // If the rule is even-odd then this path shouldn't paint the tile at all if its - // winding number is even. We check this by simply looking at the backdrop, which - // contains the winding number of the top-left corner of the tile (and in this case, it - // applies to the whole tile). - if even_odd && (abs(tile.backdrop) & 1) == 0 { - return false; - } alloc_cmd(1u); ptcl[cmd_offset] = CMD_SOLID; cmd_offset += 1u; } - return true; } fn write_color(color: CmdColor) { @@ -320,7 +306,20 @@ fn main( let blend = scene[dd]; is_blend = blend != BLEND_CLIP; } - let include_tile = tile.segment_count_or_ix != 0u || (tile.backdrop == 0) == is_clip || is_blend; + + let di = draw_monoids[drawobj_ix].info_offset; + let draw_flags = info_bin_data[di]; + let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u; + let n_segs = tile.segment_count_or_ix; + + // If this draw object represents an even-odd fill and we know that no line segment + // crosses this tile and then this draw object should not contribute to the tile if its + // backdrop (i.e. the winding number of its top-left corner) is even. + // + // NOTE: A clip should never get encoded with an even-odd fill. + let even_odd_discard = n_segs == 0u && even_odd && (abs(tile.backdrop) & 1) == 0; + let include_tile = !even_odd_discard + && (n_segs != 0u || (tile.backdrop == 0) == is_clip || is_blend); if include_tile { let el_slice = el_ix / 32u; let el_mask = 1u << (el_ix & 31u); @@ -346,6 +345,7 @@ fn main( continue; } } + let el_ix = slice_ix * 32u + firstTrailingBit(bitmap); drawobj_ix = sh_drawobj_ix[el_ix]; // clear LSB of bitmap, using bit magic @@ -354,42 +354,35 @@ fn main( let dm = draw_monoids[drawobj_ix]; let dd = config.drawdata_base + dm.scene_offset; let di = dm.info_offset; + let draw_flags = info_bin_data[di]; if clip_zero_depth == 0u { let tile_ix = sh_tile_base[el_ix] + sh_tile_stride[el_ix] * tile_y + tile_x; let tile = tiles[tile_ix]; switch drawtag { // DRAWTAG_FILL_COLOR case 0x44u: { - let draw_flags = info_bin_data[di]; - if write_path(tile, tile_ix, draw_flags) { - let rgba_color = scene[dd]; - write_color(CmdColor(rgba_color)); - } + write_path(tile, tile_ix, draw_flags); + let rgba_color = scene[dd]; + write_color(CmdColor(rgba_color)); } // DRAWTAG_FILL_LIN_GRADIENT case 0x114u: { - let draw_flags = info_bin_data[di]; - if write_path(tile, tile_ix, draw_flags) { - let index = scene[dd]; - let info_offset = di + 1u; - write_grad(CMD_LIN_GRAD, index, info_offset); - } + write_path(tile, tile_ix, draw_flags); + let index = scene[dd]; + let info_offset = di + 1u; + write_grad(CMD_LIN_GRAD, index, info_offset); } // DRAWTAG_FILL_RAD_GRADIENT case 0x29cu: { - let draw_flags = info_bin_data[di]; - if write_path(tile, tile_ix, draw_flags) { - let index = scene[dd]; - let info_offset = di + 1u; - write_grad(CMD_RAD_GRAD, index, info_offset); - } + write_path(tile, tile_ix, draw_flags); + let index = scene[dd]; + let info_offset = di + 1u; + write_grad(CMD_RAD_GRAD, index, info_offset); } // DRAWTAG_FILL_IMAGE case 0x248u: { - let draw_flags = info_bin_data[di]; - if write_path(tile, tile_ix, draw_flags) { - write_image(di + 1u); - } + write_path(tile, tile_ix, draw_flags); + write_image(di + 1u); } // DRAWTAG_BEGIN_CLIP case 0x9u: { diff --git a/src/cpu_shader/coarse.rs b/src/cpu_shader/coarse.rs index a2d4d47da..54a62e4f5 100644 --- a/src/cpu_shader/coarse.rs +++ b/src/cpu_shader/coarse.rs @@ -60,7 +60,6 @@ impl TileState { ptcl[(self.cmd_offset + offset) as usize] = value; } - // Returns true if this path should paint the tile. fn write_path( &mut self, config: &ConfigUniform, @@ -68,8 +67,7 @@ impl TileState { ptcl: &mut [u32], tile: &mut Tile, draw_flags: u32, - ) -> bool { - let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0; + ) { let n_segs = tile.segment_count_or_ix; if n_segs != 0 { let seg_ix = bump.segments; @@ -77,29 +75,17 @@ impl TileState { bump.segments += n_segs; self.alloc_cmd(4, config, bump, ptcl); self.write(ptcl, 0, CMD_FILL); + let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0; let size_and_rule = (n_segs << 1) | (even_odd as u32); self.write(ptcl, 1, size_and_rule); self.write(ptcl, 2, seg_ix); self.write(ptcl, 3, tile.backdrop as u32); self.cmd_offset += 4; } else { - // If no line segments cross this tile then the tile is completely inside a - // subregion of the path. If the rule is non-zero, the tile should get completely - // painted based on the draw tag (either a solid fill, gradient fill, or an image fill; - // writing CMD_SOLID below ensures that the pixel area coverage is 100%). - // - // If the rule is even-odd then this path shouldn't paint the tile at all if its - // winding number is even. We check this by simply looking at the backdrop, which - // contains the winding number of the top-left corner of the tile (and in this case, it - // applies to the whole tile). - if even_odd && (tile.backdrop.abs() & 1) == 0 { - return false; - } self.alloc_cmd(1, config, bump, ptcl); self.write(ptcl, 0, CMD_SOLID); self.cmd_offset += 1; } - true } fn write_color( @@ -256,50 +242,55 @@ fn coarse_main( let blend = scene[dd as usize]; is_blend = blend != BLEND_CLIP; } + + let draw_flags = info_bin_data[di as usize]; + let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0; let n_segs = tile.segment_count_or_ix; - let include_tile = n_segs != 0 || (tile.backdrop == 0) == is_clip || is_blend; + + // If this draw object represents an even-odd fill and we know that no line segment + // crosses this tile and then this draw object should not contribute to the tile if its + // backdrop (i.e. the winding number of its top-left corner) is even. + // + // NOTE: A clip should never get encoded with an even-odd fill. + assert!(!even_odd || !is_clip); + let even_odd_discard = + n_segs == 0 && even_odd && (tile.backdrop.abs() & 1) == 0; + let include_tile = !even_odd_discard + && (n_segs != 0 || (tile.backdrop == 0) == is_clip || is_blend); if include_tile { match DrawTag(drawtag) { DrawTag::COLOR => { - let draw_flags = info_bin_data[di as usize]; - if tile_state.write_path(config, bump, ptcl, tile, draw_flags) { - let rgba_color = scene[dd as usize]; - tile_state.write_color(config, bump, ptcl, rgba_color); - } + tile_state.write_path(config, bump, ptcl, tile, draw_flags); + let rgba_color = scene[dd as usize]; + tile_state.write_color(config, bump, ptcl, rgba_color); } DrawTag::IMAGE => { - let draw_flags = info_bin_data[di as usize]; - if tile_state.write_path(config, bump, ptcl, tile, draw_flags) { - tile_state.write_image(config, bump, ptcl, di + 1); - } + tile_state.write_path(config, bump, ptcl, tile, draw_flags); + tile_state.write_image(config, bump, ptcl, di + 1); } DrawTag::LINEAR_GRADIENT => { - let draw_flags = info_bin_data[di as usize]; - if tile_state.write_path(config, bump, ptcl, tile, draw_flags) { - let index = scene[dd as usize]; - tile_state.write_grad( - config, - bump, - ptcl, - CMD_LIN_GRAD, - index, - di + 1, - ); - } + tile_state.write_path(config, bump, ptcl, tile, draw_flags); + let index = scene[dd as usize]; + tile_state.write_grad( + config, + bump, + ptcl, + CMD_LIN_GRAD, + index, + di + 1, + ); } DrawTag::RADIAL_GRADIENT => { - let draw_flags = info_bin_data[di as usize]; - if tile_state.write_path(config, bump, ptcl, tile, draw_flags) { - let index = scene[dd as usize]; - tile_state.write_grad( - config, - bump, - ptcl, - CMD_RAD_GRAD, - index, - di + 1, - ); - } + tile_state.write_path(config, bump, ptcl, tile, draw_flags); + let index = scene[dd as usize]; + tile_state.write_grad( + config, + bump, + ptcl, + CMD_RAD_GRAD, + index, + di + 1, + ); } DrawTag::BEGIN_CLIP => { if tile.segment_count_or_ix == 0 && tile.backdrop == 0 { From 4d24ce382fa102dd324b658ebfbbb87aae3bfd83 Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Tue, 31 Oct 2023 14:54:05 -0700 Subject: [PATCH 6/6] Incorporate review feedback wrt even-odd handling in coarse --- shader/coarse.wgsl | 7 ++----- src/cpu_shader/coarse.rs | 13 ++++++------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/shader/coarse.wgsl b/shader/coarse.wgsl index c23d088c1..fcf543f43 100644 --- a/shader/coarse.wgsl +++ b/shader/coarse.wgsl @@ -315,11 +315,8 @@ fn main( // If this draw object represents an even-odd fill and we know that no line segment // crosses this tile and then this draw object should not contribute to the tile if its // backdrop (i.e. the winding number of its top-left corner) is even. - // - // NOTE: A clip should never get encoded with an even-odd fill. - let even_odd_discard = n_segs == 0u && even_odd && (abs(tile.backdrop) & 1) == 0; - let include_tile = !even_odd_discard - && (n_segs != 0u || (tile.backdrop == 0) == is_clip || is_blend); + let backdrop_clear = select(tile.backdrop, abs(tile.backdrop) & 1, even_odd) == 0; + let include_tile = n_segs != 0u || (backdrop_clear == is_clip) || is_blend; if include_tile { let el_slice = el_ix / 32u; let el_mask = 1u << (el_ix & 31u); diff --git a/src/cpu_shader/coarse.rs b/src/cpu_shader/coarse.rs index 54a62e4f5..ce9481da7 100644 --- a/src/cpu_shader/coarse.rs +++ b/src/cpu_shader/coarse.rs @@ -250,13 +250,12 @@ fn coarse_main( // If this draw object represents an even-odd fill and we know that no line segment // crosses this tile and then this draw object should not contribute to the tile if its // backdrop (i.e. the winding number of its top-left corner) is even. - // - // NOTE: A clip should never get encoded with an even-odd fill. - assert!(!even_odd || !is_clip); - let even_odd_discard = - n_segs == 0 && even_odd && (tile.backdrop.abs() & 1) == 0; - let include_tile = !even_odd_discard - && (n_segs != 0 || (tile.backdrop == 0) == is_clip || is_blend); + let backdrop_clear = if even_odd { + tile.backdrop.abs() & 1 + } else { + tile.backdrop + } == 0; + let include_tile = n_segs != 0 || (backdrop_clear == is_clip) || is_blend; if include_tile { match DrawTag(drawtag) { DrawTag::COLOR => {