diff --git a/crates/encoding/src/path.rs b/crates/encoding/src/path.rs index 83e537042..fb97f2d73 100644 --- a/crates/encoding/src/path.rs +++ b/crates/encoding/src/path.rs @@ -207,8 +207,9 @@ pub struct SegmentCount { #[derive(Clone, Copy, Debug, Zeroable, Pod, Default)] #[repr(C)] pub struct PathSegment { - pub origin: [f32; 2], - pub delta: [f32; 2], + // Points are relative to tile origin + pub point0: [f32; 2], + pub point1: [f32; 2], pub y_edge: f32, pub _padding: u32, } diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index 75678d80f..8fcf9541c 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -44,6 +44,7 @@ pub fn test_scenes() -> SceneSet { scene!(blend_grid), scene!(conflation_artifacts), scene!(labyrinth), + scene!(robust_paths), scene!(base_color_test: animated), scene!(clip_test: animated), scene!(longpathdash(Cap::Butt), "longpathdash (butt caps)", false), @@ -1119,6 +1120,83 @@ fn labyrinth(sb: &mut SceneBuilder, _: &mut SceneParams) { ) } +fn robust_paths(sb: &mut SceneBuilder, _: &mut SceneParams) { + let mut path = BezPath::new(); + path.move_to((16.0, 16.0)); + path.line_to((32.0, 16.0)); + path.line_to((32.0, 32.0)); + path.line_to((16.0, 32.0)); + path.close_path(); + path.move_to((48.0, 18.0)); + path.line_to((64.0, 23.0)); + path.line_to((64.0, 33.0)); + path.line_to((48.0, 38.0)); + path.close_path(); + path.move_to((80.0, 18.0)); + path.line_to((82.0, 16.0)); + path.line_to((94.0, 16.0)); + path.line_to((96.0, 18.0)); + path.line_to((96.0, 30.0)); + path.line_to((94.0, 32.0)); + path.line_to((82.0, 32.0)); + path.line_to((80.0, 30.0)); + path.close_path(); + path.move_to((112.0, 16.0)); + path.line_to((128.0, 16.0)); + path.line_to((128.0, 32.0)); + path.close_path(); + path.move_to((144.0, 16.0)); + path.line_to((160.0, 32.0)); + path.line_to((144.0, 32.0)); + path.close_path(); + path.move_to((168.0, 8.0)); + path.line_to((184.0, 8.0)); + path.line_to((184.0, 24.0)); + path.close_path(); + path.move_to((200.0, 8.0)); + path.line_to((216.0, 24.0)); + path.line_to((200.0, 24.0)); + path.close_path(); + path.move_to((241.0, 17.5)); + path.line_to((255.0, 17.5)); + path.line_to((255.0, 19.5)); + path.line_to((241.0, 19.5)); + path.close_path(); + path.move_to((241.0, 22.5)); + path.line_to((256.0, 22.5)); + path.line_to((256.0, 24.5)); + path.line_to((241.0, 24.5)); + path.close_path(); + sb.fill(Fill::NonZero, Affine::IDENTITY, Color::YELLOW, None, &path); + sb.fill( + Fill::EvenOdd, + Affine::translate((300.0, 0.0)), + Color::LIME, + None, + &path, + ); + + path.move_to((8.0, 4.0)); + path.line_to((8.0, 40.0)); + path.line_to((260.0, 40.0)); + path.line_to((260.0, 4.0)); + path.close_path(); + sb.fill( + Fill::NonZero, + Affine::translate((0.0, 100.0)), + Color::YELLOW, + None, + &path, + ); + sb.fill( + Fill::EvenOdd, + Affine::translate((300.0, 100.0)), + Color::LIME, + None, + &path, + ); +} + fn base_color_test(sb: &mut SceneBuilder, params: &mut SceneParams) { // Cycle through the hue value every 5 seconds (t % 5) * 360/5 let color = Color::hlc((params.time % 5.0) * 72.0, 80.0, 80.0); diff --git a/examples/with_winit/Cargo.toml b/examples/with_winit/Cargo.toml index 8b4028a8d..4d3d2e72c 100644 --- a/examples/with_winit/Cargo.toml +++ b/examples/with_winit/Cargo.toml @@ -46,5 +46,7 @@ android_logger = "0.13.0" console_error_panic_hook = "0.1.7" console_log = "1" wasm-bindgen-futures = "0.4.33" -web-sys = { version = "0.3.60", features = [ "HtmlCollection", "Text" ] } +# Note: pinning the exact dep here because 0.3.65 broke semver. Update this +# when revving wgpu. +web-sys = { version = "=0.3.64", features = [ "HtmlCollection", "Text" ] } getrandom = { version = "0.2.10", features = ["js"] } diff --git a/shader/fine.wgsl b/shader/fine.wgsl index 5d537f6a5..80c349487 100644 --- a/shader/fine.wgsl +++ b/shader/fine.wgsl @@ -130,16 +130,15 @@ let ROBUST_EPSILON: f32 = 2e-7; // is invited to study the even-odd case first, as there only one bit is // needed to represent a winding number parity, thus there is a lot less // bit shifting, and less shuffling altogether. -fn fill_path_ms(fill: CmdFill, wg_id: vec2, local_id: vec2, result: ptr>) { +fn fill_path_ms(fill: CmdFill, local_id: vec2, result: ptr>) { let even_odd = (fill.size_and_rule & 1u) != 0u; // This isn't a divergent branch because the fill parameters are workgroup uniform, // provably so because the ptcl buffer is bound read-only. if even_odd { - fill_path_ms_evenodd(fill, wg_id, local_id, result); + fill_path_ms_evenodd(fill, local_id, result); return; } let n_segs = fill.size_and_rule >> 1u; - let tile_origin = vec2(f32(wg_id.x) * f32(TILE_HEIGHT), f32(wg_id.y) * f32(TILE_WIDTH)); let th_ix = local_id.y * (TILE_WIDTH / PIXELS_PER_THREAD) + local_id.x; // Initialize winding number arrays to a winding number of 0, which is 0x80 in an // 8 bit biased signed integer encoding. @@ -163,31 +162,18 @@ fn fill_path_ms(fill: CmdFill, wg_id: vec2, local_id: vec2, result: pt // TODO: might save a register rewriting this in terms of limit if th_ix < slice_size { let segment = segments[seg_off]; - // Note: coords relative to tile origin probably a good idea in coarse path, - // especially as f16 would work. But keeping existing scheme for compatibility. - let xy0 = segment.origin - tile_origin; - let xy1 = xy0 + segment.delta; + let xy0 = segment.point0; + let xy1 = segment.point1; var y_edge_f = f32(TILE_HEIGHT); var delta = select(-1, 1, xy1.x <= xy0.x); - if xy0.x == 0.0 && xy1.x == 0.0 { - if xy0.y == 0.0 { - y_edge_f = 0.0; - } else if xy1.y == 0.0 { - y_edge_f = 0.0; - delta = -delta; - } - } else { - if xy0.x == 0.0 { - if xy0.y != 0.0 { - y_edge_f = xy0.y; - } - } else if xy1.x == 0.0 && xy1.y != 0.0 { - y_edge_f = xy1.y; - } - // discard horizontal lines aligned to pixel grid - if !(xy0.y == xy1.y && xy0.y == floor(xy0.y)) { - count = span(xy0.x, xy1.x) + span(xy0.y, xy1.y) - 1u; - } + if xy0.x == 0.0 { + y_edge_f = xy0.y; + } else if xy1.x == 0.0 { + y_edge_f = xy1.y; + } + // discard horizontal lines aligned to pixel grid + if !(xy0.y == xy1.y && xy0.y == floor(xy0.y)) { + count = span(xy0.x, xy1.x) + span(xy0.y, xy1.y) - 1u; } let y_edge = u32(ceil(y_edge_f)); if y_edge < TILE_HEIGHT { @@ -224,8 +210,9 @@ fn fill_path_ms(fill: CmdFill, wg_id: vec2, local_id: vec2, result: pt let sub_ix = i - select(0u, sh_count[el_ix - 1u], el_ix > 0u); let seg_off = fill.seg_data + batch * WG_SIZE + el_ix; let segment = segments[seg_off]; - let xy0_in = segment.origin - tile_origin; - let xy1_in = xy0_in + segment.delta; + // Coordinates are relative to tile origin + let xy0_in = segment.point0; + let xy1_in = segment.point1; let is_down = xy1_in.y >= xy0_in.y; let xy0 = select(xy1_in, xy0_in, is_down); let xy1 = select(xy0_in, xy1_in, is_down); @@ -237,6 +224,8 @@ fn fill_path_ms(fill: CmdFill, wg_id: vec2, local_id: vec2, result: pt let dy = xy1.y - xy0.y; let idxdy = 1.0 / (dx + dy); var a = dx * idxdy; + // is_positive_slope is true for \ and | slopes, false for /. For + // horizontal lines, it follows the original data. let is_positive_slope = xy1.x >= xy0.x; let x_sign = select(-1.0, 1.0, is_positive_slope); let xt0 = floor(xy0.x * x_sign); @@ -257,15 +246,29 @@ fn fill_path_ms(fill: CmdFill, wg_id: vec2, local_id: vec2, result: pt let z = floor(zf); let x = x0i + i32(x_sign * z); let y = i32(y0i) + i32(sub_ix) - i32(z); + // is_delta captures whether the line crosses the top edge of this + // pixel. If so, then a delta is added to `sh_winding`, followed by + // a prefix sum, so that a winding number delta is applied to all + // pixels to the right of this one. var is_delta: bool; - // We need to adjust winding number if slope is positive and there - // is a crossing at the left edge of the pixel. + // is_bump captures whether x0 crosses the left edge of this pixel. var is_bump = false; let zp = floor(a * f32(sub_ix - 1u) + b); if sub_ix == 0u { - is_delta = y0i == xy0.y && y0i != xy1.y; - is_bump = xy0.x == 0.0; + // The first (top-most) pixel in the line. It is considered to be + // a line crossing when it touches the top of the pixel. + // + // Note: horizontal lines aligned to the pixel grid have already + // been discarded. + is_delta = y0i == xy0.y; + // The pixel is counted as a left edge crossing only at the left + // edge of the tile (and when it is not the top left corner, + // using logic analogous to tiling). + is_bump = xy0.x == 0.0 && y0i != xy0.y; } else { + // Pixels other than the first are a crossing at the top or on + // the side, based on the conservative line rasterization. When + // positive slope, the crossing is on the left. is_delta = z == zp; is_bump = is_positive_slope && !is_delta; } @@ -465,7 +468,7 @@ fn fill_path_ms(fill: CmdFill, wg_id: vec2, local_id: vec2, result: pt // bits 4 * k + 2 and 4 * k + 3 contain 4-reductions let xored01_4 = xored01 | (xored01 * 4u); let xored2 = (expected_zero * 0x1010101u) ^ samples2; - let xored2_2 = xored0 | (xored0 * 2u); + let xored2_2 = xored2 | (xored2 * 2u); let xored3 = (expected_zero * 0x1010101u) ^ samples3; let xored3_2 = xored3 | (xored3 >> 1u); // xored23 contains 2-reductions from words 2 and 3, interleaved @@ -492,9 +495,8 @@ fn fill_path_ms(fill: CmdFill, wg_id: vec2, local_id: vec2, result: pt // as both have the same effect on winding number. // // TODO: factor some logic out to reduce code duplication. -fn fill_path_ms_evenodd(fill: CmdFill, wg_id: vec2, local_id: vec2, result: ptr>) { +fn fill_path_ms_evenodd(fill: CmdFill, local_id: vec2, result: ptr>) { let n_segs = fill.size_and_rule >> 1u; - let tile_origin = vec2(f32(wg_id.x) * f32(TILE_HEIGHT), f32(wg_id.y) * f32(TILE_WIDTH)); let th_ix = local_id.y * (TILE_WIDTH / PIXELS_PER_THREAD) + local_id.x; if th_ix < TILE_HEIGHT { if th_ix == 0u { @@ -516,29 +518,18 @@ fn fill_path_ms_evenodd(fill: CmdFill, wg_id: vec2, local_id: vec2, re // TODO: might save a register rewriting this in terms of limit if th_ix < slice_size { let segment = segments[seg_off]; - // Note: coords relative to tile origin probably a good idea in coarse path, - // especially as f16 would work. But keeping existing scheme for compatibility. - let xy0 = segment.origin - tile_origin; - let xy1 = xy0 + segment.delta; + // Coordinates are relative to tile origin + let xy0 = segment.point0; + let xy1 = segment.point1; var y_edge_f = f32(TILE_HEIGHT); - if xy0.x == 0.0 && xy1.x == 0.0 { - if xy0.y == 0.0 { - y_edge_f = 0.0; - } else if xy1.y == 0.0 { - y_edge_f = 0.0; - } - } else { - if xy0.x == 0.0 { - if xy0.y != 0.0 { - y_edge_f = xy0.y; - } - } else if xy1.x == 0.0 && xy1.y != 0.0 { - y_edge_f = xy1.y; - } - // discard horizontal lines aligned to pixel grid - if !(xy0.y == xy1.y && xy0.y == floor(xy0.y)) { - count = span(xy0.x, xy1.x) + span(xy0.y, xy1.y) - 1u; - } + if xy0.x == 0.0 { + y_edge_f = xy0.y; + } else if xy1.x == 0.0 { + y_edge_f = xy1.y; + } + // discard horizontal lines aligned to pixel grid + if !(xy0.y == xy1.y && xy0.y == floor(xy0.y)) { + count = span(xy0.x, xy1.x) + span(xy0.y, xy1.y) - 1u; } let y_edge = u32(ceil(y_edge_f)); if y_edge < TILE_HEIGHT { @@ -575,8 +566,8 @@ fn fill_path_ms_evenodd(fill: CmdFill, wg_id: vec2, local_id: vec2, re let sub_ix = i - select(0u, sh_count[el_ix - 1u], el_ix > 0u); let seg_off = fill.seg_data + batch * WG_SIZE + el_ix; let segment = segments[seg_off]; - let xy0_in = segment.origin - tile_origin; - let xy1_in = xy0_in + segment.delta; + let xy0_in = segment.point0; + let xy1_in = segment.point1; let is_down = xy1_in.y >= xy0_in.y; let xy0 = select(xy1_in, xy0_in, is_down); let xy1 = select(xy0_in, xy1_in, is_down); @@ -609,12 +600,11 @@ fn fill_path_ms_evenodd(fill: CmdFill, wg_id: vec2, local_id: vec2, re let x = x0i + i32(x_sign * z); let y = i32(y0i) + i32(sub_ix) - i32(z); var is_delta: bool; - // We need to adjust winding number if slope is positive and there - // is a crossing at the left edge of the pixel. + // See comments in nonzero case. var is_bump = false; let zp = floor(a * f32(sub_ix - 1u) + b); if sub_ix == 0u { - is_delta = y0i == xy0.y && y0i != xy1.y; + is_delta = y0i == xy0.y; is_bump = xy0.x == 0.0; } else { is_delta = z == zp; @@ -811,17 +801,18 @@ fn fill_path(fill: CmdFill, xy: vec2, result: ptr, result: ptr, PIXELS_PER_THREAD>; for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { @@ -886,9 +878,9 @@ fn main( case 1u: { let fill = read_fill(cmd_ix); #ifdef msaa - fill_path_ms(fill, wg_id.xy, local_id.xy, &area); + fill_path_ms(fill, local_id.xy, &area); #else - fill_path(fill, xy, &area); + fill_path(fill, local_xy, &area); #endif cmd_ix += 4u; } diff --git a/shader/path_tiling.wgsl b/shader/path_tiling.wgsl index abfa4475d..625b326f3 100644 --- a/shader/path_tiling.wgsl +++ b/shader/path_tiling.wgsl @@ -120,19 +120,53 @@ fn main( xy1 = vec2(x_clip, yt); } } - // See comments in CPU version of shader var y_edge = 1e9; - if xy0.x == tile_xy.x && xy1.x != tile_xy.x && xy0.y != tile_xy.y { - y_edge = xy0.y; - } else if xy1.x == tile_xy.x && xy1.y != tile_xy.y { - y_edge = xy1.y; + // Apply numerical robustness logic + var p0 = xy0 - tile_xy; + var p1 = xy1 - tile_xy; + // When we move to f16, this will be f16::MIN_POSITIVE + let EPSILON = 1e-6; + if p0.x == 0.0 { + if p1.x == 0.0 { + p0.x = EPSILON; + if p0.y == 0.0 { + // Entire tile + p1.x = EPSILON; + p1.y = f32(TILE_HEIGHT); + } else { + // Make segment disappear + p1.x = 2.0 * EPSILON; + p1.y = p0.y; + } + } else if p0.y == 0.0 { + p0.x = EPSILON; + } else { + y_edge = p0.y; + } + } else if p1.x == 0.0 { + if p1.y == 0.0 { + p1.x = EPSILON; + } else { + y_edge = p1.y; + } + } + // Hacky approach to numerical robustness in fine. + // This just makes sure there are no vertical lines aligned to + // the pixel grid internal to the tile. It's faster to do this + // logic here rather than in fine, but at some point we might + // rework it. + if p0.x == floor(p0.x) && p0.x != 0.0 { + p0.x -= EPSILON; + } + if p1.x == floor(p1.x) && p1.x != 0.0 { + p1.x -= EPSILON; } if !is_down { - let tmp = xy0; - xy0 = xy1; - xy1 = tmp; + let tmp = p0; + p0 = p1; + p1 = tmp; } - let segment = Segment(xy0, xy1 - xy0, y_edge); + let segment = Segment(p0, p1, y_edge); segments[seg_start + seg_within_slice] = segment; } } diff --git a/shader/shared/segment.wgsl b/shader/shared/segment.wgsl index 9fe134478..03a68e4c9 100644 --- a/shader/shared/segment.wgsl +++ b/shader/shared/segment.wgsl @@ -2,8 +2,9 @@ // Segments laid out for contiguous storage struct Segment { - origin: vec2, - delta: vec2, + // Points are relative to tile origin + point0: vec2, + point1: vec2, y_edge: f32, } diff --git a/src/cpu_shader/fine.rs b/src/cpu_shader/fine.rs index c64c87627..dc3203959 100644 --- a/src/cpu_shader/fine.rs +++ b/src/cpu_shader/fine.rs @@ -58,20 +58,24 @@ fn fill_path(area: &mut [f32], segments: &[PathSegment], fill: &CmdFill, x_tile: *a = backdrop_f; } for segment in &segments[fill.seg_data as usize..][..n_segs as usize] { + let delta = [ + segment.point1[0] - segment.point0[0], + segment.point1[1] - segment.point0[1], + ]; for yi in 0..TILE_HEIGHT { - let y = segment.origin[1] - (y_tile + yi as f32); + let y = segment.point0[1] - (y_tile + yi as f32); let y0 = y.clamp(0.0, 1.0); - let y1 = (y + segment.delta[1]).clamp(0.0, 1.0); + let y1 = (y + delta[1]).clamp(0.0, 1.0); let dy = y0 - y1; - let y_edge = segment.delta[0].signum() - * (y_tile + yi as f32 - segment.y_edge + 1.0).clamp(0.0, 1.0); + let y_edge = + delta[0].signum() * (y_tile + yi as f32 - segment.y_edge + 1.0).clamp(0.0, 1.0); if dy != 0.0 { - let vec_y_recip = segment.delta[1].recip(); + let vec_y_recip = delta[1].recip(); let t0 = (y0 - y) * vec_y_recip; let t1 = (y1 - y) * vec_y_recip; - let startx = segment.origin[0] - x_tile; - let x0 = startx + t0 * segment.delta[0]; - let x1 = startx + t1 * segment.delta[0]; + let startx = segment.point0[0] - x_tile; + let x0 = startx + t0 * delta[0]; + let x1 = startx + t1 * delta[0]; let xmin0 = x0.min(x1); let xmax0 = x0.max(x1); for i in 0..TILE_WIDTH { diff --git a/src/cpu_shader/path_tiling.rs b/src/cpu_shader/path_tiling.rs index 6b8a9413e..25dd6a4e7 100644 --- a/src/cpu_shader/path_tiling.rs +++ b/src/cpu_shader/path_tiling.rs @@ -115,28 +115,54 @@ fn path_tiling_main( xy1 = Vec2::new(x_clip, yt); } } + let mut y_edge = 1e9; + // Apply numerical robustness logic + let mut p0 = xy0 - tile_xy; + let mut p1 = xy1 - tile_xy; + const EPSILON: f32 = 1e-6; + if p0.x == 0.0 { + if p1.x == 0.0 { + p0.x = EPSILON; + if p0.y == 0.0 { + // Entire tile + p1.x = EPSILON; + p1.y = TILE_HEIGHT as f32; + } else { + // Make segment disappear + p1.x = 2.0 * EPSILON; + p1.y = p0.y; + } + } else if p0.y == 0.0 { + p0.x = EPSILON; + } else { + y_edge = p0.y; + } + } else if p1.x == 0.0 { + if p1.y == 0.0 { + p1.x = EPSILON; + } else { + y_edge = p1.y; + } + } + if p0.x == p0.x.floor() && p0.x != 0.0 { + p0.x -= EPSILON; + } + if p1.x == p1.x.floor() && p1.x != 0.0 { + p1.x -= EPSILON; + } if !is_down { - (xy0, xy1) = (xy1, xy0); + (p0, p1) = (p1, p0); } - // TODO (part of move to 8 byte encoding for segments): don't store y_edge at all, - // resolve this in fine. - let y_edge = if xy0.x == tile_xy.x && xy1.x != tile_xy.x && xy0.y != tile_xy.y { - xy0.y - } else if xy1.x == tile_xy.x && xy1.y != tile_xy.y { - xy1.y - } else { - 1e9 - }; let segment = PathSegment { - origin: xy0.to_array(), - delta: (xy1 - xy0).to_array(), + point0: p0.to_array(), + point1: p1.to_array(), y_edge, _padding: Default::default(), }; - assert!(xy0.x >= tile_xy.x && xy0.x <= tile_xy1.x); - assert!(xy0.y >= tile_xy.y && xy0.y <= tile_xy1.y); - assert!(xy1.x >= tile_xy.x && xy1.x <= tile_xy1.x); - assert!(xy1.y >= tile_xy.y && xy1.y <= tile_xy1.y); + assert!(p0.x >= 0.0 && p0.x <= TILE_WIDTH as f32); + assert!(p0.y >= 0.0 && p0.y <= TILE_HEIGHT as f32); + assert!(p1.x >= 0.0 && p1.x <= TILE_WIDTH as f32); + assert!(p1.y >= 0.0 && p1.y <= TILE_HEIGHT as f32); segments[(seg_start + seg_within_slice) as usize] = segment; } }