diff --git a/src/pixie/blends.nim b/src/pixie/blends.nim index 4a5e49b..18edbd7 100644 --- a/src/pixie/blends.nim +++ b/src/pixie/blends.nim @@ -238,7 +238,7 @@ proc blendScreen*(backdrop, source: ColorRGBX): ColorRGBX {.inline.} = # result = alphaFix(backdrop, source, result) # result = result.toPremultipliedAlpha() -proc blendColorDodge(backdrop, source: ColorRGBX): ColorRGBX = +proc blendColorDodge*(backdrop, source: ColorRGBX): ColorRGBX = let backdrop = backdrop.rgba() source = source.rgba() @@ -341,7 +341,7 @@ proc blendHardLight*(backdrop, source: ColorRGBX): ColorRGBX = result.b = hardLight(backdrop.b, backdrop.a, source.b, source.a) result.a = blendAlpha(backdrop.a, source.a) -proc blendDifference(backdrop, source: ColorRGBX): ColorRGBX = +proc blendDifference*(backdrop, source: ColorRGBX): ColorRGBX = proc blend( backdropColor, backdropAlpha, sourceColor, sourceAlpha: uint8 ): uint8 {.inline.} = @@ -357,7 +357,7 @@ proc blendDifference(backdrop, source: ColorRGBX): ColorRGBX = result.b = blend(backdrop.b, backdrop.a, source.b, source.a) result.a = blendAlpha(backdrop.a, source.a) -proc blendExclusion(backdrop, source: ColorRGBX): ColorRGBX = +proc blendExclusion*(backdrop, source: ColorRGBX): ColorRGBX = proc blend(backdrop, source: uint32): uint8 {.inline.} = let v = (backdrop + source).int32 - ((2 * backdrop * source) div 255).int32 max(0, v).uint8 @@ -366,28 +366,28 @@ proc blendExclusion(backdrop, source: ColorRGBX): ColorRGBX = result.b = blend(backdrop.b.uint32, source.b.uint32) result.a = blendAlpha(backdrop.a, source.a) -proc blendColor(backdrop, source: ColorRGBX): ColorRGBX = +proc blendColor*(backdrop, source: ColorRGBX): ColorRGBX = let backdrop = backdrop.rgba().color source = source.rgba().color blended = SetLum(source, Lum(backdrop)) result = alphaFix(backdrop, source, blended).rgba.rgbx() -proc blendLuminosity(backdrop, source: ColorRGBX): ColorRGBX = +proc blendLuminosity*(backdrop, source: ColorRGBX): ColorRGBX = let backdrop = backdrop.rgba().color source = source.rgba().color blended = SetLum(backdrop, Lum(source)) result = alphaFix(backdrop, source, blended).rgba.rgbx() -proc blendHue(backdrop, source: ColorRGBX): ColorRGBX = +proc blendHue*(backdrop, source: ColorRGBX): ColorRGBX = let backdrop = backdrop.rgba().color source = source.rgba().color blended = SetLum(SetSat(source, Sat(backdrop)), Lum(backdrop)) result = alphaFix(backdrop, source, blended).rgba.rgbx() -proc blendSaturation(backdrop, source: ColorRGBX): ColorRGBX = +proc blendSaturation*(backdrop, source: ColorRGBX): ColorRGBX = let backdrop = backdrop.rgba().color source = source.rgba().color diff --git a/src/pixie/fileformats/svg.nim b/src/pixie/fileformats/svg.nim index 1a5e78a..47c3b6a 100644 --- a/src/pixie/fileformats/svg.nim +++ b/src/pixie/fileformats/svg.nim @@ -550,6 +550,7 @@ proc newImage*(svg: Svg): Image {.raises: [PixieError].} = result = newImage(svg.width, svg.height) try: + var blendMode = OverwriteBlend # Start as overwrite for (path, props) in svg.elements: if props.display and props.opacity > 0: if props.fill != "none": @@ -573,9 +574,12 @@ proc newImage*(svg: Svg): Image {.raises: [PixieError].} = paint = parseHtmlColor(props.fill).rgbx paint.opacity = props.fillOpacity * props.opacity + paint.blendMode = blendMode result.fillPath(path, paint, props.transform, props.fillRule) + blendMode = NormalBlend # Switch to normal when compositing multiple paths + if props.stroke != rgbx(0, 0, 0, 0) and props.strokeWidth > 0: let paint = newPaint(props.stroke) paint.color.a *= (props.opacity * props.strokeOpacity) diff --git a/src/pixie/fileformats/tiff.nim b/src/pixie/fileformats/tiff.nim index 0fcd034..0507d2c 100644 --- a/src/pixie/fileformats/tiff.nim +++ b/src/pixie/fileformats/tiff.nim @@ -7,14 +7,14 @@ const ] knownTags = [ 0x0100.uint16, # ImageWidth - 0x0101, # ImageLength - 0x0102, # BitsPerSample - 0x0103, # Compression - 0x0106, # PhotometricInterpretation - 0x0111, # StripOffsets - 0x0116, # RowsPerStrip - 0x0117, # StripByteCounts - 0x0140, # ColorMap + 0x0101, # ImageLength + 0x0102, # BitsPerSample + 0x0103, # Compression + 0x0106, # PhotometricInterpretation + 0x0111, # StripOffsets + 0x0116, # RowsPerStrip + 0x0117, # StripByteCounts + 0x0140, # ColorMap ] type diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index d1f2637..644eaa1 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -1337,118 +1337,165 @@ proc fillCoverage( blendMode: BlendMode ) = var x = startX - when defined(amd64) and allowSimd: - if blendMode.hasSimdBlender(): - # When supported, SIMD blend as much as possible - let - blenderSimd = blendMode.blenderSimd() - oddMask = mm_set1_epi16(cast[int16](0xff00)) - div255 = mm_set1_epi16(cast[int16](0x8081)) - vec255 = mm_set1_epi32(cast[int32](uint32.high)) - vecZero = mm_setzero_si128() - colorVec = mm_set1_epi32(cast[int32](rgbx)) - for _ in 0 ..< coverages.len div 16: - let - index = image.dataIndex(x, y) - coverageVec = mm_loadu_si128(coverages[x - startX].unsafeAddr) - if mm_movemask_epi8(mm_cmpeq_epi16(coverageVec, vecZero)) != 0xffff: - # If the coverages are not all zero - if mm_movemask_epi8(mm_cmpeq_epi32(coverageVec, vec255)) == 0xffff: - # If the coverages are all 255 - if blendMode == OverwriteBlend: + when allowSimd: + when defined(amd64): + iterator simd( + coverages: seq[uint8], x: var int, startX: int + ): (M128i, bool, bool) = + for _ in 0 ..< coverages.len div 16: + let + coverageVec = mm_loadu_si128(coverages[x - startX].unsafeAddr) + eqZero = mm_cmpeq_epi8(coverageVec, mm_setzero_si128()) + eq255 = mm_cmpeq_epi8(coverageVec, mm_set1_epi8(cast[int8](255))) + allZeroes = mm_movemask_epi8(eqZero) == 0xffff + all255 = mm_movemask_epi8(eq255) == 0xffff + yield (coverageVec, allZeroes, all255) + x += 16 + + proc source(colorVec, coverageVec: M128i): M128i {.inline.} = + let + oddMask = mm_set1_epi16(cast[int16](0xff00)) + div255 = mm_set1_epi16(cast[int16](0x8081)) + + var unpacked = unpackAlphaValues(coverageVec) + unpacked = mm_or_si128(unpacked, mm_srli_epi32(unpacked, 16)) + + var + sourceEven = mm_slli_epi16(colorVec, 8) + sourceOdd = mm_and_si128(colorVec, oddMask) + sourceEven = mm_mulhi_epu16(sourceEven, unpacked) + sourceOdd = mm_mulhi_epu16(sourceOdd, unpacked) + sourceEven = mm_srli_epi16(mm_mulhi_epu16(sourceEven, div255), 7) + sourceOdd = mm_srli_epi16(mm_mulhi_epu16(sourceOdd, div255), 7) + result = mm_or_si128(sourceEven, mm_slli_epi16(sourceOdd, 8)) + + let colorVec = mm_set1_epi32(cast[int32](rgbx)) + + proc source(rgbx: ColorRGBX, coverage: uint8): ColorRGBX {.inline.} = + if coverage > 0: + if coverage == 255: + result = rgbx + else: + result = rgbx( + ((rgbx.r.uint32 * coverage) div 255).uint8, + ((rgbx.g.uint32 * coverage) div 255).uint8, + ((rgbx.b.uint32 * coverage) div 255).uint8, + ((rgbx.a.uint32 * coverage) div 255).uint8 + ) + + case blendMode: + of OverwriteBlend: + when allowSimd: + when defined(amd64): + for (coverageVec, allZeroes, all255) in simd(coverages, x, startX): + if not allZeroes: + if all255: for i in 0 ..< 4: - mm_storeu_si128(image.data[index + i * 4].addr, colorVec) - elif blendMode == NormalBlend: - if rgbx.a == 255: - for i in 0 ..< 4: - mm_storeu_si128(image.data[index + i * 4].addr, colorVec) - else: - for i in 0 ..< 4: - let backdrop = mm_loadu_si128(image.data[index + i * 4].addr) - mm_storeu_si128( - image.data[index + i * 4].addr, - blendNormalSimd(backdrop, colorVec) - ) + mm_storeu_si128(image.unsafe[x + i * 4, y].addr, colorVec) else: - for i in 0 ..< 4: - let backdrop = mm_loadu_si128(image.data[index + i * 4].addr) - mm_storeu_si128( - image.data[index + i * 4].addr, - blenderSimd(backdrop, colorVec) - ) - else: - # Coverages are not all 255 - template useCoverage(blendProc: untyped) = var coverageVec = coverageVec for i in 0 ..< 4: - var unpacked = unpackAlphaValues(coverageVec) - # Shift the coverages from `a` to `g` and `a` for multiplying - unpacked = mm_or_si128(unpacked, mm_srli_epi32(unpacked, 16)) - - var - source = colorVec - sourceEven = mm_slli_epi16(source, 8) - sourceOdd = mm_and_si128(source, oddMask) - - sourceEven = mm_mulhi_epu16(sourceEven, unpacked) - sourceOdd = mm_mulhi_epu16(sourceOdd, unpacked) - - sourceEven = mm_srli_epi16(mm_mulhi_epu16(sourceEven, div255), 7) - sourceOdd = mm_srli_epi16(mm_mulhi_epu16(sourceOdd, div255), 7) - - source = mm_or_si128(sourceEven, mm_slli_epi16(sourceOdd, 8)) - - if blendMode == OverwriteBlend: - mm_storeu_si128(image.data[index + i * 4].addr, source) - else: - let backdrop = mm_loadu_si128(image.data[index + i * 4].addr) - mm_storeu_si128( - image.data[index + i * 4].addr, - blendProc(backdrop, source) - ) - + let source = source(colorVec, coverageVec) + mm_storeu_si128(image.unsafe[x + i * 4, y].addr, source) coverageVec = mm_srli_si128(coverageVec, 4) - if blendMode == NormalBlend: - useCoverage(blendNormalSimd) + for x in x ..< startX + coverages.len: + let coverage = coverages[x - startX] + if coverage != 0: + image.unsafe[x, y] = source(rgbx, coverage) + + of NormalBlend: + when allowSimd: + when defined(amd64): + for (coverageVec, allZeroes, all255) in simd(coverages, x, startX): + if not allZeroes: + if all255 and rgbx.a == 255: + for i in 0 ..< 4: + mm_storeu_si128(image.unsafe[x + i * 4, y].addr, colorVec) else: - useCoverage(blenderSimd) + var coverageVec = coverageVec + for i in 0 ..< 4: + let + backdrop = mm_loadu_si128(image.unsafe[x + i * 4, y].addr) + source = source(colorVec, coverageVec) + mm_storeu_si128( + image.unsafe[x + i * 4, y].addr, + blendNormalSimd(backdrop, source) + ) + coverageVec = mm_srli_si128(coverageVec, 4) - elif blendMode == MaskBlend: - for i in 0 ..< 4: - mm_storeu_si128(image.data[index + i * 4].addr, vecZero) - - x += 16 - - let blender = blendMode.blender() - for x in x ..< startX + coverages.len: - let coverage = coverages[x - startX] - if coverage != 0 or blendMode == ExcludeMaskBlend: - if blendMode == NormalBlend and coverage == 255 and rgbx.a == 255: - # Skip blending + for x in x ..< startX + coverages.len: + let coverage = coverages[x - startX] + if coverage == 255 and rgbx.a == 255: image.unsafe[x, y] = rgbx - continue - - 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 blendMode == OverwriteBlend: - image.unsafe[x, y] = source + elif coverage == 0: + discard else: let backdrop = image.unsafe[x, y] - image.unsafe[x, y] = blender(backdrop, source) - elif blendMode == MaskBlend: - image.unsafe[x, y] = rgbx(0, 0, 0, 0) + image.unsafe[x, y] = blendNormal(backdrop, source(rgbx, coverage)) + + of MaskBlend: + {.linearScanEnd.} + + when allowSimd: + when defined(amd64): + for (coverageVec, allZeroes, all255) in simd(coverages, x, startX): + if not allZeroes: + if all255: + discard + else: + var coverageVec = coverageVec + for i in 0 ..< 4: + let + backdrop = mm_loadu_si128(image.unsafe[x + i * 4, y].addr) + source = source(colorVec, coverageVec) + mm_storeu_si128( + image.unsafe[x + i * 4, y].addr, + blendMaskSimd(backdrop, source) + ) + coverageVec = mm_srli_si128(coverageVec, 4) + else: + for i in 0 ..< 4: + mm_storeu_si128(image.unsafe[x + i * 4, y].addr, mm_setzero_si128()) + + for x in x ..< startX + coverages.len: + let coverage = coverages[x - startX] + if coverage == 0: + image.unsafe[x, y] = rgbx(0, 0, 0, 0) + elif coverage == 255: + discard + else: + let backdrop = image.unsafe[x, y] + image.unsafe[x, y] = blendMask(backdrop, source(rgbx, coverage)) - if blendMode == MaskBlend: image.clearUnsafe(0, y, startX, y) image.clearUnsafe(startX + coverages.len, y, image.width, y) + of SubtractMaskBlend: + for x in x ..< startX + coverages.len: + let coverage = coverages[x - startX] + if coverage == 255 and rgbx.a == 255: + image.unsafe[x, y] = rgbx(0, 0, 0, 0) + elif coverage != 0: + let backdrop = image.unsafe[x, y] + image.unsafe[x, y] = blendSubtractMask(backdrop, source(rgbx, coverage)) + + of ExcludeMaskBlend: + for x in x ..< startX + coverages.len: + let + coverage = coverages[x - startX] + backdrop = image.unsafe[x, y] + image.unsafe[x, y] = blendExcludeMask(backdrop, source(rgbx, coverage)) + + else: + let blender = blendMode.blender() + for x in x ..< startX + coverages.len: + let coverage = coverages[x - startX] + if coverage != 0: + let backdrop = image.unsafe[x, y] + image.unsafe[x, y] = blender(backdrop, source(rgbx, coverage)) + proc fillCoverage( mask: Mask, startX, y: int, @@ -1456,45 +1503,83 @@ proc fillCoverage( blendMode: BlendMode ) = var x = startX - when defined(amd64) and allowSimd: - if blendMode.hasSimdMaskBlender(): - let - maskerSimd = blendMode.maskBlenderSimd() - vecZero = mm_setzero_si128() - for _ in 0 ..< coverages.len div 16: - let - index = mask.dataIndex(x, y) - coverageVec = mm_loadu_si128(coverages[x - startX].unsafeAddr) - if mm_movemask_epi8(mm_cmpeq_epi16(coverageVec, vecZero)) != 0xffff: - # If the coverages are not all zero - if blendMode == OverwriteBlend: - mm_storeu_si128(mask.data[index].addr, coverageVec) - else: - let backdrop = mm_loadu_si128(mask.data[index].addr) + + template simdBlob(blendProc: untyped) = + when allowSimd: + when defined(amd64): + for _ in 0 ..< coverages.len div 16: + let + coveragesVec = mm_loadu_si128(coverages[x - startX].unsafeAddr) + eqZero = mm_cmpeq_epi8(coveragesVec, mm_setzero_si128()) + allZeroes = mm_movemask_epi8(eqZero) == 0xffff + if not allZeroes: + let backdrop = mm_loadu_si128(mask.unsafe[x, y].addr) mm_storeu_si128( - mask.data[index].addr, - maskerSimd(backdrop, coverageVec) + mask.unsafe[x, y].addr, + blendProc(backdrop, coveragesVec) ) - elif blendMode == MaskBlend: - mm_storeu_si128(mask.data[index].addr, vecZero) - x += 16 + x += 16 - let maskBlender = blendMode.maskBlender() - for x in x ..< startX + coverages.len: - let coverage = coverages[x - startX] - if coverage != 0 or blendMode == ExcludeMaskBlend: - if blendMode == OverwriteBlend: - mask.unsafe[x, y] = coverage - else: + template blendBlob(blendProc: untyped) = + for x in x ..< startX + coverages.len: + let coverage = coverages[x - startX] + if coverage != 0: let backdrop = mask.unsafe[x, y] - mask.unsafe[x, y] = maskBlender(backdrop, coverage) - elif blendMode == MaskBlend: - mask.unsafe[x, y] = 0 + mask.unsafe[x, y] = blendProc(backdrop, coverage) + + case blendMode: + of OverwriteBlend: + copyMem( + mask.unsafe[startX, y].addr, + coverages[0].unsafeAddr, + coverages.len + ) + + of NormalBlend: + simdBlob(maskBlendNormalSimd) + blendBlob(maskBlendNormal) + + of MaskBlend: + when allowSimd: + when defined(amd64): + for _ in 0 ..< coverages.len div 16: + let + coveragesVec = mm_loadu_si128(coverages[x - startX].unsafeAddr) + eqZero = mm_cmpeq_epi8(coveragesVec, mm_setzero_si128()) + allZeroes = mm_movemask_epi8(eqZero) == 0xffff + if not allZeroes: + let backdrop = mm_loadu_si128(mask.unsafe[x, y].addr) + mm_storeu_si128( + mask.unsafe[x, y].addr, + maskBlendMaskSimd(backdrop, coveragesVec) + ) + else: + mm_storeu_si128(mask.unsafe[x, y].addr, mm_setzero_si128()) + x += 16 + + for x in x ..< startX + coverages.len: + let coverage = coverages[x - startX] + if coverage != 0: + let backdrop = mask.unsafe[x, y] + mask.unsafe[x, y] = maskBlendMask(backdrop, coverage) + else: + mask.unsafe[x, y] = 0 - if blendMode == MaskBlend: mask.clearUnsafe(0, y, startX, y) mask.clearUnsafe(startX + coverages.len, y, mask.width, y) + of SubtractMaskBlend: + simdBlob(maskBlendSubtractSimd) + blendBlob(maskBlendSubtract) + + of ExcludeMaskBlend: + simdBlob(maskBlendExcludeSimd) + blendBlob(maskBlendExclude) + + else: + let maskBlender = blendMode.maskBlender() + blendBlob(maskBlender) + proc fillHits( image: Image, rgbx: ColorRGBX, @@ -1530,6 +1615,8 @@ proc fillHits( image.unsafe[x, y] = blendNormal(backdrop, rgbx) of MaskBlend: + {.linearScanEnd.} + var filledTo = startX for (start, len) in hits.walkInteger(numHits, windingRule, y, image.width): block: # Clear any gap between this fill and the previous fill @@ -1553,6 +1640,21 @@ proc fillHits( image.clearUnsafe(0, y, startX, y) image.clearUnsafe(filledTo, y, image.width, y) + of SubtractMaskBlend: + for (start, len) in hits.walkInteger(numHits, windingRule, y, image.width): + for x in start ..< start + len: + if rgbx.a == 255: + image.unsafe[x, y] = rgbx(0, 0, 0, 0) + else: + let backdrop = image.unsafe[x, y] + image.unsafe[x, y] = blendSubtractMask(backdrop, rgbx) + + of ExcludeMaskBlend: + for (start, len) in hits.walkInteger(numHits, windingRule, y, image.width): + for x in start ..< start + len: + let backdrop = image.unsafe[x, y] + image.unsafe[x, y] = blendExcludeMask(backdrop, rgbx) + else: let blender = blendMode.blender() for (start, len) in hits.walkInteger(numHits, windingRule, y, image.width): diff --git a/tests/benchmark_blends.nim b/tests/benchmark_blends.nim index d5f1f97..59befb7 100644 --- a/tests/benchmark_blends.nim +++ b/tests/benchmark_blends.nim @@ -1,6 +1,4 @@ -import benchy, chroma, pixie/images, vmath - -include pixie/blends +import benchy, chroma, pixie/blends, pixie/images, vmath let backdrop = newImage(256, 256) diff --git a/tests/benchmark_paths.nim b/tests/benchmark_paths.nim index 2cb55ed..f890b48 100644 --- a/tests/benchmark_paths.nim +++ b/tests/benchmark_paths.nim @@ -47,6 +47,16 @@ block: image.fill(rgbx(255, 255, 255, 255)) image.fillPath(rect, paint) + timeIt "rect Image SubtractMaskBlend": + paint.blendMode = SubtractMaskBlend + image.fill(rgbx(255, 255, 255, 255)) + image.fillPath(rect, paint) + + timeIt "rect Image ExcludeMaskBlend": + paint.blendMode = ExcludeMaskBlend + image.fill(rgbx(255, 255, 255, 255)) + image.fillPath(rect, paint) + timeIt "roundedRect Image OverwriteBlend": paint.blendMode = OverwriteBlend image.fillPath(roundedRect, paint) @@ -60,6 +70,16 @@ block: image.fill(rgbx(255, 255, 255, 255)) image.fillPath(roundedRect, paint) + timeIt "roundedRect Image SubtractMaskBlend": + paint.blendMode = SubtractMaskBlend + image.fill(rgbx(255, 255, 255, 255)) + image.fillPath(roundedRect, paint) + + timeIt "roundedRect Image ExcludeMaskBlend": + paint.blendMode = ExcludeMaskBlend + image.fill(rgbx(255, 255, 255, 255)) + image.fillPath(roundedRect, paint) + block: let mask = newMask(width, height)