diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index b0aa3d02..b2191f3f 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -1601,6 +1601,7 @@ proc fillShapes( # rasterize only within the total bounds let rgbx = color.asRgbx() + clearsUncovered = blendMode == MaskBlend segments = shapes.shapesToSegments() bounds = computeBounds(segments).snapToPixels() startX = max(0, bounds.x.int) @@ -1806,7 +1807,8 @@ proc fillShapes( applyOpacity(vecRgbx, area) else: rgbx * area - image.data[dataIndex] = blender(backdrop, source) + if source.a != 0 or clearsUncovered: + image.data[dataIndex] = blender(backdrop, source) block: # Right-side partial coverage let @@ -1844,7 +1846,8 @@ proc fillShapes( applyOpacity(vecRgbx, area) else: rgbx * area - image.data[dataIndex] = blender(backdrop, source) + if source.a != 0 or clearsUncovered: + image.data[dataIndex] = blender(backdrop, source) let fillBegin = leftCoverEnd.clamp(0, image.width) diff --git a/src/pixie/simd/avx2.nim b/src/pixie/simd/avx2.nim index 1800e583..0a959c2e 100644 --- a/src/pixie/simd/avx2.nim +++ b/src/pixie/simd/avx2.nim @@ -477,7 +477,19 @@ proc blendLineCoverageOverwriteAvx2*( mm_srli_si128(coverageHi, 8), ] for j in 0 ..< 4: - mm256_store_si256(line[i].addr, rgbxVec.applyCoverage(coverages[j])) + let + preserve = mm256_cmpeq_epi32( + mm256_cvtepu8_epi32(coverages[j]), vecZero + ) + source = rgbxVec.applyCoverage(coverages[j]) + backdrop = mm256_load_si256(line[i].addr) + mm256_store_si256( + line[i].addr, + mm256_or_si256( + mm256_and_si256(preserve, backdrop), + mm256_andnot_si256(preserve, source) + ) + ) i += 8 for i in i ..< len: diff --git a/src/pixie/simd/neon.nim b/src/pixie/simd/neon.nim index 8455ed0a..fe5def30 100644 --- a/src/pixie/simd/neon.nim +++ b/src/pixie/simd/neon.nim @@ -462,11 +462,20 @@ proc blendLineCoverageOverwriteNeon*( elif mask255 == uint64.high: vst4q_u8(line[i].addr, vecRgbx) else: + let backdrop = vld4q_u8(line[i].addr) var source: uint8x16x4 - source.val[0] = multiplyDiv255(vecRgbx.val[0], coverage) - source.val[1] = multiplyDiv255(vecRgbx.val[1], coverage) - source.val[2] = multiplyDiv255(vecRgbx.val[2], coverage) - source.val[3] = multiplyDiv255(vecRgbx.val[3], coverage) + source.val[0] = vbslq_u8( + eqZero, backdrop.val[0], multiplyDiv255(vecRgbx.val[0], coverage) + ) + source.val[1] = vbslq_u8( + eqZero, backdrop.val[1], multiplyDiv255(vecRgbx.val[1], coverage) + ) + source.val[2] = vbslq_u8( + eqZero, backdrop.val[2], multiplyDiv255(vecRgbx.val[2], coverage) + ) + source.val[3] = vbslq_u8( + eqZero, backdrop.val[3], multiplyDiv255(vecRgbx.val[3], coverage) + ) vst4q_u8(line[i].addr, source) i += 16 diff --git a/src/pixie/simd/sse2.nim b/src/pixie/simd/sse2.nim index 35c7333b..111cc827 100644 --- a/src/pixie/simd/sse2.nim +++ b/src/pixie/simd/sse2.nim @@ -554,10 +554,24 @@ proc blendLineCoverageOverwriteSse2*( mm_store_si128(line[i].addr, rgbxVec) i += 4 else: - var coverage = coverage + var + coverage = coverage + eqZero = eqZero for _ in 0 ..< 4: - mm_store_si128(line[i].addr, rgbxVec.applyCoverage(coverage)) + var preserve = mm_unpacklo_epi8(eqZero, eqZero) + preserve = mm_unpacklo_epi16(preserve, preserve) + let + source = rgbxVec.applyCoverage(coverage) + backdrop = mm_load_si128(line[i].addr) + mm_store_si128( + line[i].addr, + mm_or_si128( + mm_and_si128(preserve, backdrop), + mm_andnot_si128(preserve, source) + ) + ) coverage = mm_srli_si128(coverage, 4) + eqZero = mm_srli_si128(eqZero, 4) i += 4 for i in i ..< len: diff --git a/tests/paths/overwriteCircle.png b/tests/paths/overwriteCircle.png new file mode 100644 index 00000000..803b1dea Binary files /dev/null and b/tests/paths/overwriteCircle.png differ diff --git a/tests/paths/overwriteStar.png b/tests/paths/overwriteStar.png new file mode 100644 index 00000000..9d355956 Binary files /dev/null and b/tests/paths/overwriteStar.png differ diff --git a/tests/test_paths.nim b/tests/test_paths.nim index 29a6f59d..e8ebf5cd 100644 --- a/tests/test_paths.nim +++ b/tests/test_paths.nim @@ -1,4 +1,4 @@ -import chroma, pixie, pixie/fileformats/png, strformat, xrays +import chroma, pixie, std/math, pixie/fileformats/png, strformat, xrays block: let pathStr = """ @@ -760,3 +760,40 @@ block: """ let path = parsePath(pathStr) doAssert path.computeBounds() == rect(0, 0, 0, 100) + +block: + # OverwriteBlend must leave pixels the path does not cover, so on an opaque + # backdrop nothing can end up fully transparent. + let + image = newImage(128, 64) + paint = newPaint(SolidPaint) + path = newPath() + image.fill(rgbx(0, 0, 255, 255)) + paint.color = color(1, 0, 0, 1) + paint.blendMode = OverwriteBlend + path.circle(circle(vec2(64, 32), 40)) + image.fillPath(path, paint) + for c in image.data: + doAssert c.a != 0 + image.xray("tests/paths/overwriteCircle.png") + +block: + # Spikes thin enough that coverage rounds to nothing near their tips. + let + image = newImage(260, 200) + paint = newPaint(SolidPaint) + path = newPath() + image.fill(rgbx(0, 0, 255, 255)) + paint.color = color(1, 0.85, 0.1, 1) + paint.blendMode = OverwriteBlend + for i in 0 ..< 80: + let + a = i.float32 * PI.float32 / 40 + r = if (i and 1) == 0: 42.0 else: 92.0 + pt = vec2(130 + cos(a) * r, 100 + sin(a) * r) + if i == 0: path.moveTo(pt) else: path.lineTo(pt) + path.closePath() + image.fillPath(path, paint) + for c in image.data: + doAssert c.a != 0 + image.xray("tests/paths/overwriteStar.png")