From 79cc9da811e0d342cc3501bb9b3b925880fb1510 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 9 Jun 2020 20:35:27 -0700 Subject: [PATCH] Fancy flattening Implement same flattening algorithm as kurbo. --- piet-gpu/shader/path_coarse.comp | 254 +++++++++++++++++++++---------- piet-gpu/shader/path_coarse.spv | Bin 19160 -> 28876 bytes 2 files changed, 170 insertions(+), 84 deletions(-) diff --git a/piet-gpu/shader/path_coarse.comp b/piet-gpu/shader/path_coarse.comp index c7fef8c..b17f3e3 100644 --- a/piet-gpu/shader/path_coarse.comp +++ b/piet-gpu/shader/path_coarse.comp @@ -34,14 +34,65 @@ layout(set = 0, binding = 2) buffer TileBuf { #define SY (1.0 / float(TILE_HEIGHT_PX)) #define ACCURACY 0.25 -#define Q_ACCURACY 0.01 +#define Q_ACCURACY (ACCURACY * 0.1) +#define REM_ACCURACY (ACCURACY - Q_ACCURACY) #define MAX_HYPOT2 (432.0 * Q_ACCURACY * Q_ACCURACY) +vec2 eval_quad(vec2 p0, vec2 p1, vec2 p2, float t) { + float mt = 1.0 - t; + return p0 * (mt * mt) + (p1 * (mt * 2.0) + p2 * t) * t; +} + vec2 eval_cubic(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t) { float mt = 1.0 - t; return p0 * (mt * mt * mt) + (p1 * (mt * mt * 3.0) + (p2 * (mt * 3.0) + p3 * t) * t) * t; } +struct SubdivResult { + float val; + float a0; + float a2; +}; + +/// An approximation to $\int (1 + 4x^2) ^ -0.25 dx$ +/// +/// This is used for flattening curves. +#define D 0.67 +float approx_parabola_integral(float x) { + return x * inversesqrt(sqrt(1.0 - D + (D * D * D * D + 0.25 * x * x))); +} + +/// An approximation to the inverse parabola integral. +#define B 0.39 +float approx_parabola_inv_integral(float x) { + return x * sqrt(1.0 - B + (B * B + 0.25 * x * x)); +} + +SubdivResult estimate_subdiv(vec2 p0, vec2 p1, vec2 p2, float sqrt_tol) { + vec2 d01 = p1 - p0; + vec2 d12 = p2 - p1; + vec2 dd = d01 - d12; + float cross = (p2.x - p0.x) * dd.y - (p2.y - p0.y) * dd.x; + float x0 = (d01.x * dd.x + d01.y * dd.y) / cross; + float x2 = (d12.x * dd.x + d12.y * dd.y) / cross; + float scale = abs(cross / (length(dd) * (x2 - x0))); + + float a0 = approx_parabola_integral(x0); + float a2 = approx_parabola_integral(x2); + float val = 0.0; + if (scale < 1e9) { + float da = abs(a2 - a0); + float sqrt_scale = sqrt(scale); + if (sign(x0) == sign(x2)) { + val = da * sqrt_scale; + } else { + float xmin = sqrt_tol / sqrt_scale; + val = sqrt_tol * da / approx_parabola_integral(xmin); + } + } + return SubdivResult(val, a0, a2); +} + void main() { uint element_ix = gl_GlobalInvocationID.x; PathSegRef ref = PathSegRef(element_ix * PathSeg_size); @@ -78,102 +129,137 @@ void main() { case PathSeg_StrokeCubic: PathStrokeCubic cubic = PathSeg_StrokeCubic_read(ref); // Commented out code is for computing error bound on conversion to quadratics - /* vec2 err_v = 3.0 * (cubic.p2 - cubic.p1) + cubic.p0 - cubic.p3; float err = err_v.x * err_v.x + err_v.y * err_v.y; // The number of quadratics. - uint n = max(uint(ceil(pow(err * (1.0 / MAX_HYPOT2), 1.0 / 6.0))), 1); - */ - // This calculation is based on Sederberg, CAGD Notes section 10.6 - vec2 l = max(abs(cubic.p0 + cubic.p2 - 2 * cubic.p1), abs(cubic.p1 + cubic.p3 - 2 * cubic.p2)); - uint n = max(uint(ceil(sqrt(length(l) * (0.75 / ACCURACY)))), 1); - vec2 p0 = cubic.p0; - float step = 1.0 / float(n); + uint n_quads = max(uint(ceil(pow(err * (1.0 / MAX_HYPOT2), 1.0 / 6.0))), 1); + // Iterate over quadratics and tote up the estimated number of segments. + float val = 0.0; + vec2 qp0 = cubic.p0; + float step = 1.0 / float(n_quads); + for (uint i = 0; i < n_quads; i++) { + float t = float(i + 1) * step; + vec2 qp2 = eval_cubic(cubic.p0, cubic.p1, cubic.p2, cubic.p3, t); + vec2 qp1 = eval_cubic(cubic.p0, cubic.p1, cubic.p2, cubic.p3, t - 0.5 * step); + qp1 = 2.0 * qp1 - 0.5 * (qp0 + qp2); + SubdivResult params = estimate_subdiv(qp0, qp1, qp2, sqrt(REM_ACCURACY)); + val += params.val; + + qp0 = qp2; + } + uint n = max(uint(ceil(val * 0.5 / sqrt(REM_ACCURACY))), 1); + uint path_ix = cubic.path_ix; Path path = Path_read(PathRef(path_ix * Path_size)); ivec4 bbox = ivec4(path.bbox); - for (int i = 0; i < n; i++) { - // TODO: probably need special logic to make sure it's manifold + vec2 p0 = cubic.p0; + qp0 = cubic.p0; + float v_step = val / float(n); + int n_out = 1; + float val_sum = 0.0; + for (uint i = 0; i < n_quads; i++) { float t = float(i + 1) * step; - vec2 p2 = eval_cubic(cubic.p0, cubic.p1, cubic.p2, cubic.p3, t); - /* - vec2 p1 = eval_cubic(cubic.p0, cubic.p1, cubic.p2, cubic.p3, t - 0.5 * step); - p1 = 2.0 * p1 - 0.5 * (p0 + p2); - */ - - xmin = min(p0.x, p2.x) - cubic.stroke.x; - xmax = max(p0.x, p2.x) + cubic.stroke.x; - ymin = min(p0.y, p2.y) - cubic.stroke.y; - ymax = max(p0.y, p2.y) + cubic.stroke.y; - float dx = p2.x - p0.x; - float dy = p2.y - p0.y; - // Set up for per-scanline coverage formula, below. - float invslope = abs(dy) < 1e-9 ? 1e9 : dx / dy; - c = (cubic.stroke.x + abs(invslope) * (0.5 * float(TILE_HEIGHT_PX) + cubic.stroke.y)) * SX; - b = invslope; // Note: assumes square tiles, otherwise scale. - a = (p0.x - (p0.y - 0.5 * float(TILE_HEIGHT_PX)) * b) * SX; - - int x0 = int(floor((xmin) * SX)); - int x1 = int(ceil((xmax) * SX)); - int y0 = int(floor((ymin) * SY)); - int y1 = int(ceil((ymax) * SY)); - - x0 = clamp(x0, bbox.x, bbox.z); - y0 = clamp(y0, bbox.y, bbox.w); - x1 = clamp(x1, bbox.x, bbox.z); - y1 = clamp(y1, bbox.y, bbox.w); - float xc = a + b * float(y0); - int stride = bbox.z - bbox.x; - int base = (y0 - bbox.y) * stride - bbox.x; - // TODO: can be tighter, use c to bound width - uint n_tile_alloc = uint((x1 - x0) * (y1 - y0)); - // Consider using subgroups to aggregate atomic add. - uint tile_offset = atomicAdd(alloc, n_tile_alloc * TileSeg_size); - TileSeg tile_seg; - for (int y = y0; y < y1; y++) { - float tile_y0 = float(y * TILE_HEIGHT_PX); - if (tag == PathSeg_FillCubic && min(p0.y, p2.y) <= tile_y0) { - int xray = max(int(ceil(xc - 0.5 * b)), bbox.x); - if (xray < bbox.z) { - int backdrop = p2.y < p0.y ? 1 : -1; - TileRef tile_ref = Tile_index(path.tiles, uint(base + xray)); - uint tile_el = tile_ref.offset >> 2; - atomicAdd(tile[tile_el + 1], backdrop); - } + vec2 qp2 = eval_cubic(cubic.p0, cubic.p1, cubic.p2, cubic.p3, t); + vec2 qp1 = eval_cubic(cubic.p0, cubic.p1, cubic.p2, cubic.p3, t - 0.5 * step); + qp1 = 2.0 * qp1 - 0.5 * (qp0 + qp2); + SubdivResult params = estimate_subdiv(qp0, qp1, qp2, sqrt(REM_ACCURACY)); + float u0 = approx_parabola_inv_integral(params.a0); + float u2 = approx_parabola_inv_integral(params.a2); + float uscale = 1.0 / (u2 - u0); + float target = float(n_out) * v_step; + while (n_out == n || target < val_sum + params.val) { + vec2 p1; + if (n_out == n) { + p1 = cubic.p3; + } else { + float u = (target - val_sum) / params.val; + float a = mix(params.a0, params.a2, u); + float au = approx_parabola_inv_integral(a); + float t = (au - u0) * uscale; + p1 = eval_quad(qp0, qp1, qp2, t); } - int xx0 = clamp(int(floor(xc - c)), x0, x1); - int xx1 = clamp(int(ceil(xc + c)), x0, x1); - for (int x = xx0; x < xx1; x++) { - float tile_x0 = float(x * TILE_WIDTH_PX); - TileRef tile_ref = Tile_index(path.tiles, uint(base + x)); - uint tile_el = tile_ref.offset >> 2; - uint old = atomicExchange(tile[tile_el], tile_offset); - tile_seg.start = p0; - tile_seg.end = p2; - float y_edge = 0.0; - if (tag == PathSeg_FillCubic) { - y_edge = mix(p0.y, p2.y, (tile_x0 - p0.x) / dx); - if (min(p0.x, p2.x) < tile_x0 && y_edge >= tile_y0 && y_edge < tile_y0 + TILE_HEIGHT_PX) { - if (p0.x > p2.x) { - tile_seg.end = vec2(tile_x0, y_edge); - } else { - tile_seg.start = vec2(tile_x0, y_edge); - } - } else { - y_edge = 1e9; + + // Output line segment + xmin = min(p0.x, p1.x) - cubic.stroke.x; + xmax = max(p0.x, p1.x) + cubic.stroke.x; + ymin = min(p0.y, p1.y) - cubic.stroke.y; + ymax = max(p0.y, p1.y) + cubic.stroke.y; + float dx = p1.x - p0.x; + float dy = p1.y - p0.y; + // Set up for per-scanline coverage formula, below. + float invslope = abs(dy) < 1e-9 ? 1e9 : dx / dy; + c = (cubic.stroke.x + abs(invslope) * (0.5 * float(TILE_HEIGHT_PX) + cubic.stroke.y)) * SX; + b = invslope; // Note: assumes square tiles, otherwise scale. + a = (p0.x - (p0.y - 0.5 * float(TILE_HEIGHT_PX)) * b) * SX; + + int x0 = int(floor((xmin) * SX)); + int x1 = int(ceil((xmax) * SX)); + int y0 = int(floor((ymin) * SY)); + int y1 = int(ceil((ymax) * SY)); + + x0 = clamp(x0, bbox.x, bbox.z); + y0 = clamp(y0, bbox.y, bbox.w); + x1 = clamp(x1, bbox.x, bbox.z); + y1 = clamp(y1, bbox.y, bbox.w); + float xc = a + b * float(y0); + int stride = bbox.z - bbox.x; + int base = (y0 - bbox.y) * stride - bbox.x; + // TODO: can be tighter, use c to bound width + uint n_tile_alloc = uint((x1 - x0) * (y1 - y0)); + // Consider using subgroups to aggregate atomic add. + uint tile_offset = atomicAdd(alloc, n_tile_alloc * TileSeg_size); + TileSeg tile_seg; + for (int y = y0; y < y1; y++) { + float tile_y0 = float(y * TILE_HEIGHT_PX); + if (tag == PathSeg_FillCubic && min(p0.y, p1.y) <= tile_y0) { + int xray = max(int(ceil(xc - 0.5 * b)), bbox.x); + if (xray < bbox.z) { + int backdrop = p1.y < p0.y ? 1 : -1; + TileRef tile_ref = Tile_index(path.tiles, uint(base + xray)); + uint tile_el = tile_ref.offset >> 2; + atomicAdd(tile[tile_el + 1], backdrop); } } - tile_seg.y_edge = y_edge; - tile_seg.next.offset = old; - TileSeg_write(TileSegRef(tile_offset), tile_seg); - tile_offset += TileSeg_size; + int xx0 = clamp(int(floor(xc - c)), x0, x1); + int xx1 = clamp(int(ceil(xc + c)), x0, x1); + for (int x = xx0; x < xx1; x++) { + float tile_x0 = float(x * TILE_WIDTH_PX); + TileRef tile_ref = Tile_index(path.tiles, uint(base + x)); + uint tile_el = tile_ref.offset >> 2; + uint old = atomicExchange(tile[tile_el], tile_offset); + tile_seg.start = p0; + tile_seg.end = p1; + float y_edge = 0.0; + if (tag == PathSeg_FillCubic) { + y_edge = mix(p0.y, p1.y, (tile_x0 - p0.x) / dx); + if (min(p0.x, p1.x) < tile_x0 && y_edge >= tile_y0 && y_edge < tile_y0 + TILE_HEIGHT_PX) { + if (p0.x > p1.x) { + tile_seg.end = vec2(tile_x0, y_edge); + } else { + tile_seg.start = vec2(tile_x0, y_edge); + } + } else { + y_edge = 1e9; + } + } + tile_seg.y_edge = y_edge; + tile_seg.next.offset = old; + TileSeg_write(TileSegRef(tile_offset), tile_seg); + tile_offset += TileSeg_size; + } + xc += b; + base += stride; } - xc += b; - base += stride; - } - p0 = p2; + n_out += 1; + target += v_step; + p0 = p1; + } + val_sum += params.val; + + qp0 = qp2; } + break; } } diff --git a/piet-gpu/shader/path_coarse.spv b/piet-gpu/shader/path_coarse.spv index f91fbb5659da3e95517989794a2546ee7e2dc474..db5bc57af80015b065c9a69d0e04de0844737f37 100644 GIT binary patch literal 28876 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