From 531bf5c2865ba8448b8f96faf6e9245e08dc681d Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Sat, 29 May 2021 15:48:46 -0500 Subject: [PATCH] faster, better --- src/pixie/paths.nim | 186 ++++++++++++++++++++++++-------------------- 1 file changed, 100 insertions(+), 86 deletions(-) diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index 9174b27..bdeb18f 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -604,8 +604,8 @@ proc polygon*(path: var Path, pos: Vec2, size: float32, sides: int) {.inline.} = ## Adds a n-sided regular polygon at (x, y) with the parameter size. path.polygon(pos.x, pos.y, size, sides) -proc commandsToShapes*(path: Path, pixelScale: float32 = 1.0): seq[seq[Vec2]] = - ## Converts SVG-like commands to line segments. +proc commandsToShapes(path: Path, pixelScale: float32 = 1.0): seq[seq[Vec2]] = + ## Converts SVG-like commands to sequences of vectors. var start, at: Vec2 shape: seq[Vec2] @@ -962,6 +962,91 @@ proc commandsToShapes*(path: Path, pixelScale: float32 = 1.0): seq[seq[Vec2]] = if shape.len > 0: result.add(shape) +proc shapesToSegments(shapes: seq[seq[Vec2]]): seq[(Segment, int16)] = + ## Converts the shapes into a set of filtered segments with winding value. + var segmentCount: int + for shape in shapes: + segmentCount += shape.len - 1 + + for shape in shapes: + for segment in shape.segments: + if segment.at.y == segment.to.y: # Skip horizontal + continue + var + segment = segment + winding = 1.int16 + if segment.at.y > segment.to.y: + swap(segment.at, segment.to) + winding = -1 + + result.add((segment, winding)) + +proc computeBounds(segments: seq[(Segment, int16)]): Rect = + ## Compute the bounds of the segments. + var + xMin = float32.high + xMax = float32.low + yMin = float32.high + yMax = float32.low + for (segment, _) in segments: + xMin = min(xMin, min(segment.at.x, segment.to.x)) + xMax = max(xMax, max(segment.at.x, segment.to.x)) + yMin = min(yMin, min(segment.at.y, segment.to.y)) + yMax = max(yMax, max(segment.at.y, segment.to.y)) + + xMin = floor(xMin) + xMax = ceil(xMax) + yMin = floor(yMin) + yMax = ceil(yMax) + + if xMin.isNaN() or xMax.isNaN() or yMin.isNaN() or yMax.isNaN(): + discard + else: + result.x = xMin + result.y = yMin + result.w = xMax - xMin + result.h = yMax - yMin + +proc computeBounds*(path: Path): Rect = + ## Compute the bounds of the path. + path.commandsToShapes().shapesToSegments().computeBounds() + +proc partitionSegments( + segments: seq[(Segment, int16)], top, height: int +): (seq[seq[(Segment, int16)]], uint32, uint32) = + ## Puts segments into the height partitions they intersect with. + ## Returns (partitions, startY, partitionHeight) + let + maxPartitions = max(1, height div 10).uint32 + numPartitions = min(maxPartitions, max(1, segments.len div 10).uint32) + partitionHeight = height.uint32 div numPartitions + + result[0].setLen(numPartitions) + result[1] = top.uint32 + result[2] = partitionHeight + + for (segment, winding) in segments: + if partitionHeight == 0: + result[0][0].add((segment, winding)) + else: + var + atPartition = max(0, segment.at.y - top.float32).uint32 + toPartition = max(0, ceil(segment.to.y - top.float32)).uint32 + atPartition = atPartition div partitionHeight + toPartition = toPartition div partitionHeight + atPartition = clamp(atPartition, 0, result[0].high.uint32) + toPartition = clamp(toPartition, 0, result[0].high.uint32) + for i in atPartition .. toPartition: + result[0][i].add((segment, winding)) + +proc getIndexForY( + partitions: (seq[seq[(Segment, int16)]], uint32, uint32), y: int +): uint32 {.inline.} = + if partitions[2] == 0 or partitions[0].len == 1: + 0.uint32 + else: + min((y.uint32 - partitions[1]) div partitions[2], partitions[0].high.uint32) + proc quickSort(a: var seq[(float32, int16)], inl, inr: int) = ## Sorts in place + faster than standard lib sort. var @@ -983,30 +1068,6 @@ proc quickSort(a: var seq[(float32, int16)], inl, inr: int) = quickSort(a, inl, r) quickSort(a, l, inr) -proc computeBounds(partitions: seq[seq[(Segment, int16)]]): Rect = - ## Compute bounds of a shape segments with windings. - var - xMin = float32.high - xMax = float32.low - yMin = float32.high - yMax = float32.low - for partition in partitions: - for (segment, _) in partition: - xMin = min(xMin, min(segment.at.x, segment.to.x)) - xMax = max(xMax, max(segment.at.x, segment.to.x)) - yMin = min(yMin, min(segment.at.y, segment.to.y)) - yMax = max(yMax, max(segment.at.y, segment.to.y)) - - xMin = floor(xMin) - xMax = ceil(xMax) - yMin = floor(yMin) - yMax = ceil(yMax) - - result.x = xMin - result.y = yMin - result.w = xMax - xMin - result.h = yMax - yMin - proc shouldFill(windingRule: WindingRule, count: int): bool {.inline.} = ## Should we fill based on the current winding rule and count? case windingRule: @@ -1015,49 +1076,12 @@ proc shouldFill(windingRule: WindingRule, count: int): bool {.inline.} = of wrEvenOdd: count mod 2 != 0 -proc partitionSegments( - shapes: seq[seq[Vec2]], height: int -): seq[seq[(Segment, int16)]] = - ## Puts segments into the height partitions they intersect with. - var segmentCount: int - for shape in shapes: - segmentCount += shape.len - 1 - - let - maxPartitions = max(1, height div 10).uint32 - numPartitions = min(maxPartitions, max(1, segmentCount div 10).uint32) - partitionHeight = (height.uint32 div numPartitions) - - result.setLen(numPartitions) - for shape in shapes: - for segment in shape.segments: - if segment.at.y == segment.to.y: # Skip horizontal - continue - var - segment = segment - winding = 1.int16 - if segment.at.y > segment.to.y: - swap(segment.at, segment.to) - winding = -1 - - if partitionHeight == 0: - result[0].add((segment, winding)) - else: - var - atPartition = max(0, segment.at.y).uint32 div partitionHeight - toPartition = max(0, ceil(segment.to.y)).uint32 div partitionHeight - atPartition = clamp(atPartition, 0, result.high.uint32) - toPartition = clamp(toPartition, 0, result.high.uint32) - for i in atPartition .. toPartition: - result[i].add((segment, winding)) - template computeCoverages( coverages: var seq[uint8], hits: var seq[(float32, int16)], size: Vec2, y: int, - partitions: seq[seq[(Segment, int16)]], - partitionHeight: uint32, + partitions: (seq[seq[(Segment, int16)]], uint32, uint32), windingRule: WindingRule ) = const @@ -1066,16 +1090,10 @@ template computeCoverages( offset = 1 / quality.float32 initialOffset = offset / 2 + epsilon - let - partition = - if partitionHeight == 0 or partitions.len == 1: - 0.uint32 - else: - min(y.uint32 div partitionHeight, partitions.high.uint32) - zeroMem(coverages[0].addr, coverages.len) # Do scanlines for this row + let partition = getIndexForY(partitions, y) var yLine = y.float32 + initialOffset - offset numHits: int @@ -1083,7 +1101,7 @@ template computeCoverages( yLine += offset let scanline = line(vec2(0, yLine), vec2(size.x, yLine)) numHits = 0 - for (segment, winding) in partitions[partition]: + for (segment, winding) in partitions[0][partition]: if segment.at.y <= scanline.a.y and segment.to.y >= scanline.a.y: var at: Vec2 if scanline.intersects(segment, at) and segment.to != at: @@ -1150,19 +1168,18 @@ proc fillShapes( windingRule: WindingRule, blendMode: BlendMode ) = - let - rgbx = color.asRgbx() - partitions = partitionSegments(shapes, image.height) - partitionHeight = image.height.uint32 div partitions.len.uint32 - # Figure out the total bounds of all the shapes, # rasterize only within the total bounds let - bounds = computeBounds(partitions) + rgbx = color.asRgbx() + blender = blendMode.blender() + segments = shapes.shapesToSegments() + bounds = computeBounds(segments) startX = max(0, bounds.x.int) startY = max(0, bounds.y.int) stopY = min(image.height, (bounds.y + bounds.h).int) - blender = blendMode.blender() + pathHeight = stopY - startY + partitions = partitionSegments(segments, startY, pathHeight) var coverages = newSeq[uint8](image.width) @@ -1175,7 +1192,6 @@ proc fillShapes( image.wh, y, partitions, - partitionHeight, windingRule ) @@ -1264,17 +1280,16 @@ proc fillShapes( shapes: seq[seq[Vec2]], windingRule: WindingRule ) = - let - partitions = partitionSegments(shapes, mask.height) - partitionHeight = mask.height.uint32 div partitions.len.uint32 - # Figure out the total bounds of all the shapes, # rasterize only within the total bounds let - bounds = computeBounds(partitions) + segments = shapes.shapesToSegments() + bounds = computeBounds(segments) startX = max(0, bounds.x.int) startY = max(0, bounds.y.int) stopY = min(mask.height, (bounds.y + bounds.h).int) + pathHeight = stopY - startY + partitions = partitionSegments(segments, startY, pathHeight) when defined(amd64) and not defined(pixieNoSimd): let maskerSimd = bmNormal.maskerSimd() @@ -1290,7 +1305,6 @@ proc fillShapes( mask.wh, y, partitions, - partitionHeight, windingRule )