From 3a8227d025ab9938b6c4bc3e318a40c36dcf2d63 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 9 Jun 2020 14:56:05 -0700 Subject: [PATCH] Non-load balanced coarse path raster This is a bit of a revert of the load-balanced ("more parallel") coarse path rasterizer, but includes fills and also uses atomicExchange. I'm doing it this way because it should be considerably easier to do flattening in this structure, even though there will be some performance regression. --- piet-gpu/shader/path_coarse.comp | 131 ++++++++----------------------- piet-gpu/shader/path_coarse.spv | Bin 20828 -> 15920 bytes 2 files changed, 33 insertions(+), 98 deletions(-) diff --git a/piet-gpu/shader/path_coarse.comp b/piet-gpu/shader/path_coarse.comp index f587079..693082e 100644 --- a/piet-gpu/shader/path_coarse.comp +++ b/piet-gpu/shader/path_coarse.comp @@ -33,24 +33,7 @@ layout(set = 0, binding = 2) buffer TileBuf { #define SX (1.0 / float(TILE_WIDTH_PX)) #define SY (1.0 / float(TILE_HEIGHT_PX)) -shared uint sh_tile_count[COARSE_WG]; -shared uint sh_width[COARSE_WG]; -shared uint sh_draw_width[COARSE_WG]; -shared uint sh_tag[COARSE_WG]; -shared vec2 sh_p0[COARSE_WG]; -shared vec2 sh_p1[COARSE_WG]; -shared int sh_x0[COARSE_WG]; -shared int sh_bbox_x1[COARSE_WG]; -shared int sh_y0[COARSE_WG]; -shared float sh_a[COARSE_WG]; -shared float sh_b[COARSE_WG]; -shared float sh_c[COARSE_WG]; -shared uint sh_base[COARSE_WG]; -shared uint sh_stride[COARSE_WG]; -shared uint sh_alloc_start; - void main() { - uint th_ix = gl_LocalInvocationID.x; uint element_ix = gl_GlobalInvocationID.x; PathSegRef ref = PathSegRef(element_ix * PathSeg_size); @@ -58,32 +41,27 @@ void main() { if (element_ix < n_pathseg) { tag = PathSeg_tag(ref); } - sh_tag[th_ix] = tag; // Setup for coverage algorithm. float a, b, c; // Bounding box of element in pixel coordinates. float xmin, xmax, ymin, ymax; PathStrokeLine line; + float dx; switch (tag) { case PathSeg_FillLine: case PathSeg_StrokeLine: line = PathSeg_StrokeLine_read(ref); - sh_p0[th_ix] = line.p0; - sh_p1[th_ix] = line.p1; xmin = min(line.p0.x, line.p1.x) - line.stroke.x; xmax = max(line.p0.x, line.p1.x) + line.stroke.x; ymin = min(line.p0.y, line.p1.y) - line.stroke.y; ymax = max(line.p0.y, line.p1.y) + line.stroke.y; - float dx = line.p1.x - line.p0.x; + dx = line.p1.x - line.p0.x; float dy = line.p1.y - line.p0.y; // Set up for per-scanline coverage formula, below. float invslope = abs(dy) < 1e-9 ? 1e9 : dx / dy; c = (line.stroke.x + abs(invslope) * (0.5 * float(TILE_HEIGHT_PX) + line.stroke.y)) * SX; b = invslope; // Note: assumes square tiles, otherwise scale. a = (line.p0.x - (line.p0.y - 0.5 * float(TILE_HEIGHT_PX)) * b) * SX; - sh_a[th_ix] = a; - sh_b[th_ix] = b; - sh_c[th_ix] = c; break; } int x0 = int(floor((xmin) * SX)); @@ -98,96 +76,53 @@ void main() { y0 = clamp(y0, bbox.y, bbox.w); x1 = clamp(x1, bbox.x, bbox.z); y1 = clamp(y1, bbox.y, bbox.w); - sh_x0[th_ix] = x0; - sh_bbox_x1[th_ix] = bbox.z; - // TODO: can get rid of this (fold into base), with care (also need to update `a`) - sh_y0[th_ix] = y0; + float t = a + b * float(y0); int stride = bbox.z - bbox.x; - sh_stride[th_ix] = stride; - sh_base[th_ix] = path.tiles.offset - (bbox.y * stride + bbox.x) * Tile_size; - uint width = uint(x1 - x0); - sh_width[th_ix] = width; - uint draw_width = min(width, uint(1.0 + ceil(2.0 * c))); - if (draw_width == 0 && bbox.x == 0 && bbox.z > 0) { - // Create opportunity to draw backdrop for segments to the left of viewport. - // Note: predicate can be strengthened to exclude segments that don't cross - // a horizontal tile boundary. - draw_width = 1; - } - sh_draw_width[th_ix] = draw_width; - uint tile_count = draw_width * uint(y1 - y0); - - sh_tile_count[th_ix] = tile_count; - for (uint i = 0; i < LG_COARSE_WG; i++) { - barrier(); - if (th_ix >= (1 << i)) { - tile_count += sh_tile_count[th_ix - (1 << i)]; - } - barrier(); - sh_tile_count[th_ix] = tile_count; - } - if (th_ix == COARSE_WG - 1) { - sh_alloc_start = atomicAdd(alloc, tile_count * TileSeg_size); - } - barrier(); - uint alloc_start = sh_alloc_start; - uint total_tile_count = sh_tile_count[COARSE_WG - 1]; - - for (uint ix = th_ix; ix < total_tile_count; ix += COARSE_WG) { - // Binary search to find element - uint el_ix = 0; - for (uint i = 0; i < LG_COARSE_WG; i++) { - uint probe = el_ix + ((COARSE_WG / 2) >> i); - if (ix >= sh_tile_count[probe - 1]) { - el_ix = probe; + 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_FillLine && min(line.p0.y, line.p1.y) <= tile_y0) { + int xray = max(int(ceil(t - 0.5 * b)), bbox.x); + if (xray < bbox.z) { + int backdrop = line.p1.y < line.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); } } - uint seq_ix = ix - (el_ix > 0 ? sh_tile_count[el_ix - 1] : 0); - uint draw_width = sh_draw_width[el_ix]; - int x0 = sh_x0[el_ix]; - int x1 = x0 + int(sh_width[el_ix]); - int dx = int(seq_ix % draw_width); - uint y = sh_y0[el_ix] + seq_ix / draw_width; - float b = sh_b[el_ix]; - float t = sh_a[el_ix] + b * float(y); - float c = sh_c[el_ix]; int xx0 = clamp(int(floor(t - c)), x0, x1); int xx1 = clamp(int(ceil(t + c)), x0, x1); - int x = xx0 + dx; - vec2 tile_xy = vec2(x * TILE_WIDTH_PX, y * TILE_HEIGHT_PX); - vec2 p0 = sh_p0[el_ix]; - vec2 p1 = sh_p1[el_ix]; - uint tile_el = (sh_base[el_ix] + uint(y * sh_stride[el_ix] + x) * Tile_size) >> 2; - if (sh_tag[el_ix] == PathSeg_FillLine && dx == 0 && min(p0.y, p1.y) <= tile_xy.y) { - int xray = max(int(ceil(t - 0.5 * b)), x0); - if (xray < sh_bbox_x1[el_ix]) { - int backdrop = p1.y < p0.y ? 1 : -1; - atomicAdd(tile[tile_el + 1 + 2 * (xray - x)], backdrop); - } - } - - if (x < xx1) { - uint tile_offset = alloc_start + ix * TileSeg_size; + 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); - TileSeg tile_seg; + tile_seg.start = line.p0; + tile_seg.end = line.p1; float y_edge = 0.0; - if (sh_tag[el_ix] == PathSeg_FillLine) { - y_edge = mix(p0.y, p1.y, (tile_xy.x - p0.x) / (p1.x - p0.x)); - if (min(p0.x, p1.x) < tile_xy.x && y_edge >= tile_xy.y && y_edge < tile_xy.y + TILE_HEIGHT_PX) { - if (p0.x > p1.x) { - p1 = vec2(tile_xy.x, y_edge); + if (tag == PathSeg_FillLine) { + y_edge = mix(line.p0.y, line.p1.y, (tile_x0 - line.p0.x) / dx); + if (min(line.p0.x, line.p1.x) < tile_x0 && y_edge >= tile_y0 && y_edge < tile_y0 + TILE_HEIGHT_PX) { + if (line.p0.x > line.p1.x) { + tile_seg.end = vec2(tile_x0, y_edge); } else { - p0 = vec2(tile_xy.x, y_edge); + tile_seg.start = vec2(tile_x0, y_edge); } } else { y_edge = 1e9; } } - tile_seg.start = p0; - tile_seg.end = p1; tile_seg.y_edge = y_edge; tile_seg.next.offset = old; TileSeg_write(TileSegRef(tile_offset), tile_seg); + tile_offset += TileSeg_size; } + t += b; + base += stride; } } diff --git a/piet-gpu/shader/path_coarse.spv b/piet-gpu/shader/path_coarse.spv index a4db461ccbb1ba2d62df7cf8951015d7a3d2256d..8c61a4bb6f791b50e9567561def3cd6431197839 100644 GIT binary patch literal 15920 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 literal 20828 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