Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve numerical robustness in path tiling #401

Merged
merged 7 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions crates/encoding/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
raphlinus marked this conversation as resolved.
Show resolved Hide resolved
pub point1: [f32; 2],
pub y_edge: f32,
pub _padding: u32,
}
Expand Down
78 changes: 78 additions & 0 deletions examples/scenes/src/test_scenes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion examples/with_winit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
134 changes: 63 additions & 71 deletions shader/fine.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32>, local_id: vec2<u32>, result: ptr<function, array<f32, PIXELS_PER_THREAD>>) {
fn fill_path_ms(fill: CmdFill, local_id: vec2<u32>, result: ptr<function, array<f32, PIXELS_PER_THREAD>>) {
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.
Expand All @@ -163,31 +162,18 @@ fn fill_path_ms(fill: CmdFill, wg_id: vec2<u32>, local_id: vec2<u32>, 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 {
Expand Down Expand Up @@ -224,8 +210,9 @@ fn fill_path_ms(fill: CmdFill, wg_id: vec2<u32>, local_id: vec2<u32>, 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);
Expand All @@ -237,6 +224,8 @@ fn fill_path_ms(fill: CmdFill, wg_id: vec2<u32>, local_id: vec2<u32>, 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);
Expand All @@ -257,15 +246,29 @@ fn fill_path_ms(fill: CmdFill, wg_id: vec2<u32>, local_id: vec2<u32>, 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;
raphlinus marked this conversation as resolved.
Show resolved Hide resolved
} 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;
}
Expand Down Expand Up @@ -465,7 +468,7 @@ fn fill_path_ms(fill: CmdFill, wg_id: vec2<u32>, local_id: vec2<u32>, 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);
raphlinus marked this conversation as resolved.
Show resolved Hide resolved
let xored3 = (expected_zero * 0x1010101u) ^ samples3;
let xored3_2 = xored3 | (xored3 >> 1u);
// xored23 contains 2-reductions from words 2 and 3, interleaved
Expand All @@ -492,9 +495,8 @@ fn fill_path_ms(fill: CmdFill, wg_id: vec2<u32>, local_id: vec2<u32>, 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<u32>, local_id: vec2<u32>, result: ptr<function, array<f32, PIXELS_PER_THREAD>>) {
fn fill_path_ms_evenodd(fill: CmdFill, local_id: vec2<u32>, result: ptr<function, array<f32, PIXELS_PER_THREAD>>) {
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 {
Expand All @@ -516,29 +518,18 @@ fn fill_path_ms_evenodd(fill: CmdFill, wg_id: vec2<u32>, local_id: vec2<u32>, 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 {
Expand Down Expand Up @@ -575,8 +566,8 @@ fn fill_path_ms_evenodd(fill: CmdFill, wg_id: vec2<u32>, local_id: vec2<u32>, 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);
Expand Down Expand Up @@ -609,12 +600,11 @@ fn fill_path_ms_evenodd(fill: CmdFill, wg_id: vec2<u32>, local_id: vec2<u32>, 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;
Expand Down Expand Up @@ -811,17 +801,18 @@ fn fill_path(fill: CmdFill, xy: vec2<f32>, result: ptr<function, array<f32, PIXE
for (var i = 0u; i < n_segs; i++) {
let seg_off = fill.seg_data + i;
let segment = segments[seg_off];
let y = segment.origin.y - xy.y;
let y = segment.point0.y - xy.y;
let delta = segment.point1 - segment.point0;
let y0 = clamp(y, 0.0, 1.0);
let y1 = clamp(y + segment.delta.y, 0.0, 1.0);
let y1 = clamp(y + delta.y, 0.0, 1.0);
let dy = y0 - y1;
if dy != 0.0 {
let vec_y_recip = 1.0 / segment.delta.y;
let vec_y_recip = 1.0 / delta.y;
let t0 = (y0 - y) * vec_y_recip;
let t1 = (y1 - y) * vec_y_recip;
let startx = segment.origin.x - xy.x;
let x0 = startx + t0 * segment.delta.x;
let x1 = startx + t1 * segment.delta.x;
let startx = segment.point0.x - xy.x;
let x0 = startx + t0 * delta.x;
let x1 = startx + t1 * delta.x;
let xmin0 = min(x0, x1);
let xmax0 = max(x0, x1);
for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
Expand All @@ -835,7 +826,7 @@ fn fill_path(fill: CmdFill, xy: vec2<f32>, result: ptr<function, array<f32, PIXE
area[i] += a * dy;
}
}
let y_edge = sign(segment.delta.x) * clamp(xy.y - segment.y_edge + 1.0, 0.0, 1.0);
let y_edge = sign(delta.x) * clamp(xy.y - segment.y_edge + 1.0, 0.0, 1.0);
for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
area[i] += y_edge;
}
Expand Down Expand Up @@ -864,6 +855,7 @@ fn main(
) {
let tile_ix = wg_id.y * config.width_in_tiles + wg_id.x;
let xy = vec2(f32(global_id.x * PIXELS_PER_THREAD), f32(global_id.y));
let local_xy = vec2(f32(local_id.x * PIXELS_PER_THREAD), f32(local_id.y));
#ifdef full
var rgba: array<vec4<f32>, PIXELS_PER_THREAD>;
for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
Expand All @@ -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;
}
Expand Down
Loading