From 5f99324e45847d0aef6006709fe031c0385c8925 Mon Sep 17 00:00:00 2001
From: Ryan Oldenburg <ryan@guzba.com>
Date: Mon, 29 Nov 2021 01:48:51 -0600
Subject: [PATCH] redo how paths detects and skips aa when possible

---
 src/pixie/paths.nim | 79 +++++++++++++++++++++++++--------------------
 1 file changed, 44 insertions(+), 35 deletions(-)

diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim
index 8fd2d96..6921896 100644
--- a/src/pixie/paths.nim
+++ b/src/pixie/paths.nim
@@ -36,12 +36,16 @@ type
   SomePath* = Path | string
 
   PartitionEntry = object
-    atY, toY: float32
+    segment: Segment
     m, b: float32
     winding: int16
 
+  Partition = object
+    entries: seq[PartitionEntry]
+    requiresAntiAliasing: bool
+
   Partitioning = object
-    partitions: seq[seq[PartitionEntry]]
+    partitions: seq[Partition]
     startY, partitionHeight: uint32
 
 const
@@ -1036,20 +1040,6 @@ proc shapesToSegments(shapes: seq[seq[Vec2]]): seq[(Segment, int16)] =
 
       result.add((segment, winding))
 
-proc requiresAntiAliasing(segments: seq[(Segment, int16)]): bool =
-  ## Returns true if the fill requires antialiasing.
-
-  template hasFractional(v: float32): bool =
-    v - trunc(v) != 0
-
-  for i, (segment, _) in segments:
-    if segment.at.x != segment.to.x or
-      segment.at.x.hasFractional() or # at.x and to.x are the same
-      segment.at.y.hasFractional() or
-      segment.to.y.hasFractional():
-      # AA is required if all segments are not vertical or have fractional > 0
-      return true
-
 proc transform(shapes: var seq[seq[Vec2]], transform: Mat3) =
   if transform != mat3():
     for shape in shapes.mitems:
@@ -1086,8 +1076,7 @@ proc computeBounds*(
   computeBounds(shapes.shapesToSegments())
 
 proc initPartitionEntry(segment: Segment, winding: int16): PartitionEntry =
-  result.atY = segment.at.y
-  result.toY = segment.to.y
+  result.segment = segment
   result.winding = winding
   let d = segment.at.x - segment.to.x
   if d == 0:
@@ -1096,6 +1085,20 @@ proc initPartitionEntry(segment: Segment, winding: int16): PartitionEntry =
     result.m = (segment.at.y - segment.to.y) / d
     result.b = segment.at.y - result.m * segment.at.x
 
+proc requiresAntiAliasing(entries: var seq[PartitionEntry]): bool =
+  ## Returns true if the fill requires antialiasing.
+
+  template hasFractional(v: float32): bool =
+    v - trunc(v) != 0
+
+  for entry in entries:
+    if entry.segment.at.x != entry.segment.to.x or
+      entry.segment.at.x.hasFractional() or # at.x and to.x are the same
+      entry.segment.at.y.hasFractional() or
+      entry.segment.to.y.hasFractional():
+      # AA is required if all segments are not vertical or have fractional > 0
+      return true
+
 proc partitionSegments(
   segments: seq[(Segment, int16)], top, height: int
 ): Partitioning =
@@ -1111,7 +1114,7 @@ proc partitionSegments(
   for (segment, winding) in segments:
     let entry = initPartitionEntry(segment, winding)
     if result.partitionHeight == 0:
-      result.partitions[0].add(entry)
+      result.partitions[0].entries.add(entry)
     else:
       var
         atPartition = max(0, segment.at.y - result.startY.float32).uint32
@@ -1121,7 +1124,11 @@ proc partitionSegments(
       atPartition = clamp(atPartition, 0, result.partitions.high.uint32)
       toPartition = clamp(toPartition, 0, result.partitions.high.uint32)
       for i in atPartition .. toPartition:
-        result.partitions[i].add(entry)
+        result.partitions[i].entries.add(entry)
+
+  for partition in result.partitions.mitems:
+    partition.requiresAntiAliasing =
+      requiresAntiAliasing(partition.entries)
 
 proc getIndexForY(partitioning: Partitioning, y: int): uint32 {.inline.} =
   if partitioning.partitions.len == 1:
@@ -1134,7 +1141,7 @@ proc getIndexForY(partitioning: Partitioning, y: int): uint32 {.inline.} =
 
 proc maxEntryCount(partitioning: Partitioning): int =
   for i in 0 ..< partitioning.partitions.len:
-    result = max(result, partitioning.partitions[i].len)
+    result = max(result, partitioning.partitions[i].entries.len)
 
 proc sortHits(hits: var seq[(float32, int16)], inl, inr: int) =
   ## Quicksort + insertion sort, in-place and faster than standard lib sort.
@@ -1217,32 +1224,34 @@ proc computeCoverage(
   coverages: var seq[uint8],
   hits: var seq[(float32, int16)],
   numHits: var int,
+  aa: var bool,
   width: float32,
   y, startX: int,
-  aa: bool,
   partitioning: Partitioning,
   windingRule: WindingRule
 ) {.inline.} =
+  let
+    partitionIndex = partitioning.getIndexForY(y)
+    partitionEntryCount = partitioning.partitions[partitionIndex].entries.len
+
+  aa = partitioning.partitions[partitionIndex].requiresAntiAliasing
+
+  if aa: # Coverage is only used for anti-aliasing
+    zeroMem(coverages[0].addr, coverages.len)
+
   let
     quality = if aa: 5 else: 1 # Must divide 255 cleanly (1, 3, 5, 15, 17, 51, 85)
     sampleCoverage = (255 div quality).uint8
     offset = 1 / quality.float64
     initialOffset = offset / 2 + epsilon
 
-  if aa: # Coverage is only used for anti-aliasing
-    zeroMem(coverages[0].addr, coverages.len)
-
-  # Do scanlines for this row
-  let
-    partitionIndex = partitioning.getIndexForY(y)
-    partitionEntryCount = partitioning.partitions[partitionIndex].len
   var yLine = y.float64 + initialOffset - offset
   for m in 0 ..< quality:
     yLine += offset
     numHits = 0
     for i in 0 ..< partitionEntryCount: # Perf
-      let entry = partitioning.partitions[partitionIndex][i].unsafeAddr # Perf
-      if entry.atY <= yLine and entry.toY >= yLine:
+      let entry = partitioning.partitions[partitionIndex].entries[i].unsafeAddr # Perf
+      if entry.segment.at.y <= yLine and entry.segment.to.y >= yLine:
         let x =
           if entry.m == 0:
             entry.b
@@ -1555,7 +1564,6 @@ proc fillShapes(
   # rasterize only within the total bounds
   let
     segments = shapes.shapesToSegments()
-    aa = segments.requiresAntiAliasing()
     bounds = computeBounds(segments).snapToPixels()
     startX = max(0, bounds.x.int)
     startY = max(0, bounds.y.int)
@@ -1566,16 +1574,17 @@ proc fillShapes(
     coverages = newSeq[uint8](bounds.w.int)
     hits = newSeq[(float32, int16)](partitioning.maxEntryCount)
     numHits: int
+    aa: bool
 
   for y in startY ..< pathHeight:
     computeCoverage(
       coverages,
       hits,
       numHits,
+      aa,
       mask.width.float32,
       y,
       startX,
-      aa,
       partitioning,
       windingRule
     )
@@ -2370,7 +2379,6 @@ else:
     let
       rgbx = color.asRgbx()
       segments = shapes.shapesToSegments()
-      aa = segments.requiresAntiAliasing()
       bounds = computeBounds(segments).snapToPixels()
       startX = max(0, bounds.x.int)
       startY = max(0, bounds.y.int)
@@ -2381,16 +2389,17 @@ else:
       coverages = newSeq[uint8](bounds.w.int)
       hits = newSeq[(float32, int16)](partitioning.maxEntryCount)
       numHits: int
+      aa: bool
 
     for y in startY ..< pathHeight:
       computeCoverage(
         coverages,
         hits,
         numHits,
+        aa,
         image.width.float32,
         y,
         startX,
-        aa,
         partitioning,
         windingRule
       )