From 1395abaff9f9e43cacf5ae6de1b151b023c0eb8d Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Thu, 3 Jun 2021 03:48:11 -0500 Subject: [PATCH] anti-aliasing optimization --- experiments/benchmark_cairo.nim | 84 +++++++++++++++++++++++---------- src/pixie/paths.nim | 84 ++++++++++++++++++--------------- 2 files changed, 104 insertions(+), 64 deletions(-) diff --git a/experiments/benchmark_cairo.nim b/experiments/benchmark_cairo.nim index f4f45b7..02c0677 100644 --- a/experiments/benchmark_cairo.nim +++ b/experiments/benchmark_cairo.nim @@ -1,35 +1,67 @@ import benchy, cairo, chroma, math, pixie -var - surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000) - ctx = surface.create() +block: + var + surface = imageSurfaceCreate(FORMAT_ARGB32, 1920, 1080) + ctx = surface.create() -ctx.setSourceRgba(0, 0, 0, 1) -ctx.fill() -ctx.setSourceRgba(0, 0, 1, 1) + ctx.setSourceRgba(0, 0, 1, 1) -timeIt "cairo": - ctx.newPath() - ctx.moveTo(0, 0) - ctx.lineTo(500, 0) - ctx.lineTo(500, 500) - ctx.lineTo(0, 500) - ctx.closePath() - ctx.fill() + timeIt "cairo1": + ctx.newPath() + ctx.moveTo(0, 0) + ctx.lineTo(1920, 0) + ctx.lineTo(1920, 1080) + ctx.lineTo(0, 1080) + ctx.closePath() + ctx.fill() + surface.flush() + + # discard surface.writeToPng("cairo1.png") + + var a = newImage(1920, 1080) + a.fill(rgba(255, 255, 255, 255)) + + timeIt "pixie1": + var p: pixie.Path + p.moveTo(0, 0) + p.lineTo(1920, 0) + p.lineTo(1920, 1080) + p.lineTo(0, 1080) + p.closePath() + a.fillPath(p, rgba(0, 0, 255, 255)) + + # a.writeFile("pixie1.png") + +block: + var + surface = imageSurfaceCreate(FORMAT_ARGB32, 1920, 1080) + ctx = surface.create() + + ctx.setSourceRgba(0, 0, 1, 1) + + timeIt "cairo2": + ctx.newPath() + ctx.moveTo(500, 240) + ctx.lineTo(1500, 240) + ctx.lineTo(1920, 600) + ctx.lineTo(0, 600) + ctx.closePath() + ctx.fill() surface.flush() -# discard surface.writeToPng("cairo.png") + # discard surface.writeToPng("cairo2.png") -var a = newImage(1000, 1000) -a.fill(rgba(0, 0, 0, 255)) + var a = newImage(1920, 1080) + a.fill(rgba(255, 255, 255, 255)) -timeIt "pixie": - var p: pixie.Path - p.moveTo(0, 0) - p.lineTo(500, 0) - p.lineTo(500, 500) - p.lineTo(0, 500) - p.closePath() - a.fillPath(p, rgba(0, 0, 255, 255)) + timeIt "pixie2": + var p: pixie.Path + p.moveTo(500, 240) + p.lineTo(1500, 240) + p.lineTo(1920, 600) + p.lineTo(0, 600) + p.closePath() + a.fillPath(p, rgba(0, 0, 255, 255)) -# a.writeFile("pixie.png") + # a.writeFile("pixie2.png") diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index 573ac68..c79ab33 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -1,5 +1,5 @@ import blends, bumpy, chroma, common, images, masks, paints, pixie/internal, - strutils, vmath + strutils, system/memory, vmath when defined(amd64) and not defined(pixieNoSimd): import nimsimd/sse2 @@ -990,6 +990,21 @@ 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 in 0 ..< segments.len: # For arc + let segment = segments[i][0] + 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 computePixelBounds(segments: seq[(Segment, int16)]): Rect = ## Compute the bounds of the segments. var @@ -1100,11 +1115,12 @@ proc computeCoverages( hits: var seq[(float32, int16)], size: Vec2, y: int, + aa: bool, partitioning: Partitioning, windingRule: WindingRule ) {.inline.} = - const - quality = 5 # Must divide 255 cleanly (1, 3, 5, 15, 17, 51, 85) + 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.float32 initialOffset = offset / 2 + epsilon @@ -1174,15 +1190,18 @@ proc computeCoverages( let fillLen = at.int - fillStart if fillLen > 0: var i = fillStart - when defined(amd64) and not defined(pixieNoSimd): - let vSampleCoverage = mm_set1_epi8(cast[int8](sampleCoverage)) - for j in countup(i, fillStart + fillLen - 16, 16): - var coverage = mm_loadu_si128(coverages[j].addr) - coverage = mm_add_epi8(coverage, vSampleCoverage) - mm_storeu_si128(coverages[j].addr, coverage) - i += 16 - for j in i ..< fillStart + fillLen: - coverages[j] += sampleCoverage + if aa: + when defined(amd64) and not defined(pixieNoSimd): + let vSampleCoverage = mm_set1_epi8(cast[int8](sampleCoverage)) + for j in countup(i, fillStart + fillLen - 16, 16): + var coverage = mm_loadu_si128(coverages[j].addr) + coverage = mm_add_epi8(coverage, vSampleCoverage) + mm_storeu_si128(coverages[j].addr, coverage) + i += 16 + for j in i ..< fillStart + fillLen: + coverages[j] += sampleCoverage + else: + nimSetMem(coverages[fillStart].addr, sampleCoverage.cint, fillLen) prevAt = at @@ -1205,6 +1224,7 @@ proc fillShapes( rgbx = color.asRgbx() blender = blendMode.blender() segments = shapes.shapesToSegments() + aa = segments.requiresAntiAliasing() bounds = computePixelBounds(segments) startX = max(0, bounds.x.int) startY = max(0, bounds.y.int) @@ -1221,6 +1241,7 @@ proc fillShapes( hits, image.wh, y, + aa, partitioning, windingRule ) @@ -1247,7 +1268,7 @@ proc fillShapes( # If the coverages are not all zero if mm_movemask_epi8(mm_cmpeq_epi32(coverage, first32)) == 0xffff: # Coverages are all 255 - if rgbx.a == 255 and blendMode == bmNormal: + if blendMode == bmNormal and rgbx.a == 255: mm_storeu_si128(image.data[index].addr, vColor) else: let backdrop = mm_loadu_si128(image.data[index].addr) @@ -1282,25 +1303,18 @@ proc fillShapes( x += 4 while x < image.width: - if x + 8 <= coverages.len: - let peeked = cast[ptr uint64](coverages[x].addr)[] - if peeked == 0: - x += 8 - continue - let coverage = coverages[x] if coverage != 0: - var source = rgbx - if coverage != 255: - source.r = ((source.r.uint32 * coverage) div 255).uint8 - source.g = ((source.g.uint32 * coverage) div 255).uint8 - source.b = ((source.b.uint32 * coverage) div 255).uint8 - source.a = ((source.a.uint32 * coverage) div 255).uint8 - - if source.a == 255 and blendMode == bmNormal: + if blendMode == bmNormal and coverage == 255 and rgbx.a == 255: # Skip blending - image.setRgbaUnsafe(x, y, source) + image.setRgbaUnsafe(x, y, rgbx) else: + var source = rgbx + if coverage != 255: + source.r = ((source.r.uint32 * coverage) div 255).uint8 + source.g = ((source.g.uint32 * coverage) div 255).uint8 + source.b = ((source.b.uint32 * coverage) div 255).uint8 + source.a = ((source.a.uint32 * coverage) div 255).uint8 let backdrop = image.getRgbaUnsafe(x, y) image.setRgbaUnsafe(x, y, blender(backdrop, source)) inc x @@ -1314,6 +1328,7 @@ proc fillShapes( # rasterize only within the total bounds let segments = shapes.shapesToSegments() + aa = segments.requiresAntiAliasing() bounds = computePixelBounds(segments) startX = max(0, bounds.x.int) startY = max(0, bounds.y.int) @@ -1334,6 +1349,7 @@ proc fillShapes( hits, mask.wh, y, + aa, partitioning, windingRule ) @@ -1356,18 +1372,10 @@ proc fillShapes( x += 16 while x < mask.width: - if x + 8 <= coverages.len: - let peeked = cast[ptr uint64](coverages[x].addr)[] - if peeked == 0: - x += 8 - continue - let coverage = coverages[x] if coverage != 0: - let - backdrop = mask.getValueUnsafe(x, y) - blended = blendAlpha(backdrop, coverage) - mask.setValueUnsafe(x, y, blended) + let backdrop = mask.getValueUnsafe(x, y) + mask.setValueUnsafe(x, y, blendAlpha(backdrop, coverage)) inc x proc miterLimitToAngle*(limit: float32): float32 =