faster, better

This commit is contained in:
Ryan Oldenburg 2021-05-29 15:48:46 -05:00
parent fa6fd464bc
commit 531bf5c286

View file

@ -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. ## Adds a n-sided regular polygon at (x, y) with the parameter size.
path.polygon(pos.x, pos.y, size, sides) path.polygon(pos.x, pos.y, size, sides)
proc commandsToShapes*(path: Path, pixelScale: float32 = 1.0): seq[seq[Vec2]] = proc commandsToShapes(path: Path, pixelScale: float32 = 1.0): seq[seq[Vec2]] =
## Converts SVG-like commands to line segments. ## Converts SVG-like commands to sequences of vectors.
var var
start, at: Vec2 start, at: Vec2
shape: seq[Vec2] shape: seq[Vec2]
@ -962,6 +962,91 @@ proc commandsToShapes*(path: Path, pixelScale: float32 = 1.0): seq[seq[Vec2]] =
if shape.len > 0: if shape.len > 0:
result.add(shape) 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) = proc quickSort(a: var seq[(float32, int16)], inl, inr: int) =
## Sorts in place + faster than standard lib sort. ## Sorts in place + faster than standard lib sort.
var var
@ -983,30 +1068,6 @@ proc quickSort(a: var seq[(float32, int16)], inl, inr: int) =
quickSort(a, inl, r) quickSort(a, inl, r)
quickSort(a, l, inr) 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.} = proc shouldFill(windingRule: WindingRule, count: int): bool {.inline.} =
## Should we fill based on the current winding rule and count? ## Should we fill based on the current winding rule and count?
case windingRule: case windingRule:
@ -1015,49 +1076,12 @@ proc shouldFill(windingRule: WindingRule, count: int): bool {.inline.} =
of wrEvenOdd: of wrEvenOdd:
count mod 2 != 0 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( template computeCoverages(
coverages: var seq[uint8], coverages: var seq[uint8],
hits: var seq[(float32, int16)], hits: var seq[(float32, int16)],
size: Vec2, size: Vec2,
y: int, y: int,
partitions: seq[seq[(Segment, int16)]], partitions: (seq[seq[(Segment, int16)]], uint32, uint32),
partitionHeight: uint32,
windingRule: WindingRule windingRule: WindingRule
) = ) =
const const
@ -1066,16 +1090,10 @@ template computeCoverages(
offset = 1 / quality.float32 offset = 1 / quality.float32
initialOffset = offset / 2 + epsilon 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) zeroMem(coverages[0].addr, coverages.len)
# Do scanlines for this row # Do scanlines for this row
let partition = getIndexForY(partitions, y)
var var
yLine = y.float32 + initialOffset - offset yLine = y.float32 + initialOffset - offset
numHits: int numHits: int
@ -1083,7 +1101,7 @@ template computeCoverages(
yLine += offset yLine += offset
let scanline = line(vec2(0, yLine), vec2(size.x, yLine)) let scanline = line(vec2(0, yLine), vec2(size.x, yLine))
numHits = 0 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: if segment.at.y <= scanline.a.y and segment.to.y >= scanline.a.y:
var at: Vec2 var at: Vec2
if scanline.intersects(segment, at) and segment.to != at: if scanline.intersects(segment, at) and segment.to != at:
@ -1150,19 +1168,18 @@ proc fillShapes(
windingRule: WindingRule, windingRule: WindingRule,
blendMode: BlendMode 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, # Figure out the total bounds of all the shapes,
# rasterize only within the total bounds # rasterize only within the total bounds
let let
bounds = computeBounds(partitions) rgbx = color.asRgbx()
blender = blendMode.blender()
segments = shapes.shapesToSegments()
bounds = computeBounds(segments)
startX = max(0, bounds.x.int) startX = max(0, bounds.x.int)
startY = max(0, bounds.y.int) startY = max(0, bounds.y.int)
stopY = min(image.height, (bounds.y + bounds.h).int) stopY = min(image.height, (bounds.y + bounds.h).int)
blender = blendMode.blender() pathHeight = stopY - startY
partitions = partitionSegments(segments, startY, pathHeight)
var var
coverages = newSeq[uint8](image.width) coverages = newSeq[uint8](image.width)
@ -1175,7 +1192,6 @@ proc fillShapes(
image.wh, image.wh,
y, y,
partitions, partitions,
partitionHeight,
windingRule windingRule
) )
@ -1264,17 +1280,16 @@ proc fillShapes(
shapes: seq[seq[Vec2]], shapes: seq[seq[Vec2]],
windingRule: WindingRule 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, # Figure out the total bounds of all the shapes,
# rasterize only within the total bounds # rasterize only within the total bounds
let let
bounds = computeBounds(partitions) segments = shapes.shapesToSegments()
bounds = computeBounds(segments)
startX = max(0, bounds.x.int) startX = max(0, bounds.x.int)
startY = max(0, bounds.y.int) startY = max(0, bounds.y.int)
stopY = min(mask.height, (bounds.y + bounds.h).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): when defined(amd64) and not defined(pixieNoSimd):
let maskerSimd = bmNormal.maskerSimd() let maskerSimd = bmNormal.maskerSimd()
@ -1290,7 +1305,6 @@ proc fillShapes(
mask.wh, mask.wh,
y, y,
partitions, partitions,
partitionHeight,
windingRule windingRule
) )