diff --git a/experiments/benchmark_cairo_draw.nim b/experiments/benchmark_cairo_draw.nim index 0538d0c..c2f6cdf 100644 --- a/experiments/benchmark_cairo_draw.nim +++ b/experiments/benchmark_cairo_draw.nim @@ -1,4 +1,41 @@ -import benchy, cairo, pixie +import benchy, cairo, pixie, pixie/blends + +when defined(amd64) and not defined(pixieNoSimd): + import nimsimd/sse2 + +when defined(release): + {.push checks: off.} + +proc drawBasic(backdrop, source: Image) = + let sourceIsOpaque = source.isOpaque() + + for y in 0 ..< min(backdrop.height, source.height): + if sourceIsOpaque: + copyMem( + backdrop.data[backdrop.dataIndex(0, y)].addr, + source.data[source.dataIndex(0, y)].addr, + min(backdrop.width, source.width) * 4 + ) + else: + var x: int + when defined(amd64) and not defined(pixieNoSimd): + let vec255 = mm_set1_epi32(cast[int32](uint32.high)) + for _ in 0 ..< min(backdrop.width, source.width) div 4: + let sourceVec = mm_loadu_si128(source.data[source.dataIndex(x, y)].addr) + if mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, mm_setzero_si128())) != 0xffff: + if (mm_movemask_epi8(mm_cmpeq_epi8(sourceVec, vec255)) and 0x8888) == 0x8888: + mm_storeu_si128(backdrop.data[backdrop.dataIndex(x, y)].addr, sourceVec) + else: + let backdropVec = mm_loadu_si128(backdrop.data[backdrop.dataIndex(x, y)].addr) + mm_storeu_si128( + backdrop.data[backdrop.dataIndex(x, y)].addr, + blendNormalInlineSimd(backdropVec, sourceVec) + ) + x += 4 + # No scalar for now + +when defined(release): + {.pop.} block: let @@ -7,7 +44,13 @@ block: tmp = imageSurfaceCreate(FORMAT_ARGB32, 1568, 940) ctx = tmp.create() - timeIt "cairo draw basic": + timeIt "cairo draw normal": + # ctx.setSourceRgba(0.5, 0.5, 0.5, 1) + # let operator = ctx.getOperator() + # ctx.setOperator(OperatorSource) + # ctx.paint() + # ctx.setOperator(operator) + ctx.setSource(backdrop, 0, 0) ctx.paint() ctx.setSource(source, 0, 0) @@ -22,15 +65,71 @@ block: source = readImage("tests/fileformats/svg/masters/Ghostscript_Tiger.png") tmp = newImage(1568, 940) - timeIt "isOneColor": - doAssert not backdrop.isOneColor() - - timeIt "pixie draw basic": + timeIt "pixie draw normal": + # tmp.fill(rgbx(127, 127, 127, 255)) tmp.draw(backdrop) tmp.draw(source) # tmp.writeFile("tmp2.png") +block: + let + backdrop = readImage("tests/fileformats/svg/masters/dragon2.png") + source = readImage("tests/fileformats/svg/masters/Ghostscript_Tiger.png") + tmp = newImage(1568, 940) + + timeIt "pixie draw overwrite": + # tmp.fill(rgbx(127, 127, 127, 255)) + tmp.draw(backdrop, blendMode = bmOverwrite) + tmp.draw(source) + + # tmp.writeFile("tmp2.png") + +block: + let + backdrop = readImage("tests/fileformats/svg/masters/dragon2.png") + source = readImage("tests/fileformats/svg/masters/Ghostscript_Tiger.png") + tmp = newImage(1568, 940) + + timeIt "pixie draw basic": + # tmp.fill(rgbx(127, 127, 127, 255)) + tmp.drawBasic(backdrop) + tmp.drawBasic(source) + + # tmp.writeFile("tmp2.png") + +block: + let + backdrop = imageSurfaceCreateFromPng("tests/fileformats/svg/masters/dragon2.png") + source = imageSurfaceCreateFromPng("tests/fileformats/svg/masters/Ghostscript_Tiger.png") + tmp = imageSurfaceCreate(FORMAT_ARGB32, 1568, 940) + ctx = tmp.create() + + timeIt "cairo draw mask": + ctx.setSourceRgba(1, 1, 1, 1) + let operator = ctx.getOperator() + ctx.setOperator(OperatorSource) + ctx.paint() + ctx.setOperator(operator) + + ctx.setSource(backdrop, 0, 0) + ctx.mask(source, 0, 0) + tmp.flush() + + # echo tmp.writeToPng("tmp_masked.png") + +block: + let + backdrop = readImage("tests/fileformats/svg/masters/dragon2.png") + source = readImage("tests/fileformats/svg/masters/Ghostscript_Tiger.png") + tmp = newImage(1568, 940) + + timeIt "pixie draw mask": + tmp.draw(backdrop) + tmp.draw(source, blendMode = bmMask) + + # tmp.writeFile("tmp_masked2.png") + block: let backdrop = imageSurfaceCreateFromPng("tests/fileformats/svg/masters/dragon2.png") diff --git a/src/pixie.nim b/src/pixie.nim index 00b9190..a9fac34 100644 --- a/src/pixie.nim +++ b/src/pixie.nim @@ -1,10 +1,9 @@ -import bumpy, chroma, flatty/binny, os, pixie/blends, pixie/common, - pixie/contexts, pixie/fileformats/bmp, pixie/fileformats/gif, - pixie/fileformats/jpg, pixie/fileformats/png, pixie/fileformats/svg, - pixie/fonts, pixie/images, pixie/masks, pixie/paints, pixie/paths, strutils, vmath +import bumpy, chroma, flatty/binny, os, pixie/common, pixie/contexts, + pixie/fileformats/bmp, pixie/fileformats/gif, pixie/fileformats/jpg, + pixie/fileformats/png, pixie/fileformats/svg, pixie/fonts, pixie/images, + pixie/masks, pixie/paints, pixie/paths, strutils, vmath -export blends, bumpy, chroma, common, contexts, fonts, images, masks, paints, - paths, vmath +export bumpy, chroma, common, contexts, fonts, images, masks, paints, paths, vmath type FileFormat* = enum diff --git a/src/pixie/blends.nim b/src/pixie/blends.nim index 371b50f..b9f148b 100644 --- a/src/pixie/blends.nim +++ b/src/pixie/blends.nim @@ -9,31 +9,6 @@ when defined(amd64) and not defined(pixieNoSimd): # See https://www.khronos.org/registry/OpenGL/extensions/KHR/KHR_blend_equation_advanced.txt type - BlendMode* = enum - bmNormal - bmDarken - bmMultiply - # bmLinearBurn - bmColorBurn - bmLighten - bmScreen - # bmLinearDodge - bmColorDodge - bmOverlay - bmSoftLight - bmHardLight - bmDifference - bmExclusion - bmHue - bmSaturation - bmColor - bmLuminosity - - bmMask ## Special blend mode that is used for masking - bmOverwrite ## Special blend mode that just copies pixels - bmSubtractMask ## Inverse mask - bmExcludeMask - Blender* = proc(backdrop, source: ColorRGBX): ColorRGBX {.gcsafe, raises: [].} ## Function signature returned by blender. Masker* = proc(backdrop, source: uint8): uint8 {.gcsafe, raises: [].} @@ -165,7 +140,7 @@ proc SetSat(C: Color, s: float32): Color {.inline.} = if satC > 0: result = (C - min([C.r, C.g, C.b])) * s / satC -proc blendNormal(backdrop, source: ColorRGBX): ColorRGBX = +proc blendNormal*(backdrop, source: ColorRGBX): ColorRGBX = if backdrop.a == 0 or source.a == 255: return source if source.a == 0: @@ -419,7 +394,7 @@ proc blendSaturation(backdrop, source: ColorRGBX): ColorRGBX = blended = SetLum(SetSat(backdrop, Sat(source)), Lum(backdrop)) result = alphaFix(backdrop, source, blended).rgba.rgbx() -proc blendMask(backdrop, source: ColorRGBX): ColorRGBX = +proc blendMask*(backdrop, source: ColorRGBX): ColorRGBX = let k = source.a.uint32 result.r = ((backdrop.r * k) div 255).uint8 result.g = ((backdrop.g * k) div 255).uint8 @@ -477,10 +452,13 @@ proc maskNormal(backdrop, source: uint8): uint8 = ## Blending masks blendAlpha(backdrop, source) -proc maskMask(backdrop, source: uint8): uint8 = +proc maskMaskInline*(backdrop, source: uint8): uint8 {.inline.} = ## Masking masks ((backdrop.uint32 * source) div 255).uint8 +proc maskMask(backdrop, source: uint8): uint8 = + maskMaskInline(backdrop, source) + proc maskSubtract(backdrop, source: uint8): uint8 = ((backdrop.uint32 * (255 - source)) div 255).uint8 diff --git a/src/pixie/common.nim b/src/pixie/common.nim index 4e11656..a3f7eea 100644 --- a/src/pixie/common.nim +++ b/src/pixie/common.nim @@ -3,6 +3,31 @@ import bumpy, chroma, vmath type PixieError* = object of ValueError ## Raised if an operation fails. + BlendMode* = enum + bmNormal + bmDarken + bmMultiply + # bmLinearBurn + bmColorBurn + bmLighten + bmScreen + # bmLinearDodge + bmColorDodge + bmOverlay + bmSoftLight + bmHardLight + bmDifference + bmExclusion + bmHue + bmSaturation + bmColor + bmLuminosity + + bmMask ## Special blend mode that is used for masking + bmOverwrite ## Special blend mode that just copies pixels + bmSubtractMask ## Inverse mask + bmExcludeMask + proc mix*(a, b: uint8, t: float32): uint8 {.inline, raises: [].} = ## Linearly interpolate between a and b using t. let t = round(t * 255).uint32 diff --git a/src/pixie/contexts.nim b/src/pixie/contexts.nim index 133d57c..268558c 100644 --- a/src/pixie/contexts.nim +++ b/src/pixie/contexts.nim @@ -1,5 +1,5 @@ -import bumpy, chroma, pixie/blends, pixie/common, pixie/fonts, pixie/images, - pixie/masks, pixie/paints, pixie/paths, tables, vmath +import bumpy, chroma, pixie/common, pixie/fonts, pixie/images, pixie/masks, + pixie/paints, pixie/paths, tables, vmath ## This file provides a Nim version of the Canvas 2D API commonly used on the ## web. The goal is to make picking up Pixie easy for developers familiar with diff --git a/src/pixie/images.nim b/src/pixie/images.nim index a863331..cb8e1eb 100644 --- a/src/pixie/images.nim +++ b/src/pixie/images.nim @@ -148,7 +148,7 @@ proc isOneColor*(image: Image): bool {.raises: [].} = values1 = mm_loadu_si128(image.data[i + 4].addr) mask0 = mm_movemask_epi8(mm_cmpeq_epi8(values0, colorVec)) mask1 = mm_movemask_epi8(mm_cmpeq_epi8(values1, colorVec)) - if mask0 != uint16.high.int or mask1 != uint16.high.int: + if mask0 != 0xffff or mask1 != 0xffff: return false i += 8 @@ -162,7 +162,7 @@ proc isTransparent*(image: Image): bool {.raises: [].} = var i: int when defined(amd64) and not defined(pixieNoSimd): - let zeroVec = mm_setzero_si128() + let vecZero = mm_setzero_si128() for _ in 0 ..< image.data.len div 16: let values0 = mm_loadu_si128(image.data[i + 0].addr) @@ -172,8 +172,7 @@ proc isTransparent*(image: Image): bool {.raises: [].} = values01 = mm_or_si128(values0, values1) values23 = mm_or_si128(values2, values3) values = mm_or_si128(values01, values23) - mask = mm_movemask_epi8(mm_cmpeq_epi8(values, zeroVec)) - if mask != uint16.high.int: + if mm_movemask_epi8(mm_cmpeq_epi8(values, vecZero)) != 0xffff: return false i += 16 @@ -181,6 +180,31 @@ proc isTransparent*(image: Image): bool {.raises: [].} = if image.data[j].a != 0: return false +proc isOpaque*(image: Image): bool {.raises: [].} = + result = true + + var i: int + when defined(amd64) and not defined(pixieNoSimd): + let + vec255 = mm_set1_epi32(cast[int32](uint32.high)) + colorMask = mm_set1_epi32(cast[int32]([255.uint8, 255, 255, 0])) + for _ in 0 ..< image.data.len div 16: + let + values0 = mm_loadu_si128(image.data[i + 0].addr) + values1 = mm_loadu_si128(image.data[i + 4].addr) + values2 = mm_loadu_si128(image.data[i + 8].addr) + values3 = mm_loadu_si128(image.data[i + 12].addr) + values01 = mm_and_si128(values0, values1) + values23 = mm_and_si128(values2, values3) + values = mm_or_si128(mm_and_si128(values01, values23), colorMask) + if mm_movemask_epi8(mm_cmpeq_epi8(values, vec255)) != 0xffff: + return false + i += 16 + + for j in i ..< image.data.len: + if image.data[j].a != 255: + return false + proc flipHorizontal*(image: Image) {.raises: [].} = ## Flips the image around the Y axis. let w = image.width div 2 @@ -636,7 +660,7 @@ proc getRgbaSmooth*( topMix proc drawCorrect( - a, b: Image | Mask, mat = mat3(), tiled = false, blendMode = bmNormal + a, b: Image | Mask, transform = mat3(), tiled = false, blendMode = bmNormal ) {.raises: [PixieError].} = ## Draws one image onto another using matrix with color blending. @@ -646,11 +670,11 @@ proc drawCorrect( let masker = blendMode.masker() var - matInv = mat.inverse() + inverseTransform = transform.inverse() # Compute movement vectors - p = matInv * vec2(0 + h, 0 + h) - dx = matInv * vec2(1 + h, 0 + h) - p - dy = matInv * vec2(0 + h, 1 + h) - p + p = inverseTransform * vec2(0 + h, 0 + h) + dx = inverseTransform * vec2(1 + h, 0 + h) - p + dy = inverseTransform * vec2(0 + h, 1 + h) - p filterBy2 = max(dx.length, dy.length) b = b @@ -660,7 +684,7 @@ proc drawCorrect( dx /= 2 dy /= 2 filterBy2 /= 2 - matInv = scale(vec2(1/2, 1/2)) * matInv + inverseTransform = scale(vec2(1/2, 1/2)) * inverseTransform while filterBy2 <= 0.5: b = b.magnifyBy2() @@ -668,12 +692,12 @@ proc drawCorrect( dx *= 2 dy *= 2 filterBy2 *= 2 - matInv = scale(vec2(2, 2)) * matInv + inverseTransform = scale(vec2(2, 2)) * inverseTransform for y in 0 ..< a.height: for x in 0 ..< a.width: let - samplePos = matInv * vec2(x.float32 + h, y.float32 + h) + samplePos = inverseTransform * vec2(x.float32 + h, y.float32 + h) xFloat = samplePos.x - h yFloat = samplePos.y - h @@ -697,14 +721,14 @@ proc drawCorrect( a.setValueUnsafe(x, y, masker(backdrop, sample)) proc drawUber( - a, b: Image | Mask, mat = mat3(), blendMode = bmNormal + a, b: Image | Mask, transform = mat3(), blendMode: BlendMode ) {.raises: [PixieError].} = let corners = [ - mat * vec2(0, 0), - mat * vec2(b.width.float32, 0), - mat * vec2(b.width.float32, b.height.float32), - mat * vec2(0, b.height.float32) + transform * vec2(0, 0), + transform * vec2(b.width.float32, 0), + transform * vec2(b.width.float32, b.height.float32), + transform * vec2(0, b.height.float32) ] perimeter = [ segment(corners[0], corners[1]), @@ -714,11 +738,11 @@ proc drawUber( ] var - matInv = mat.inverse() + inverseTransform = transform.inverse() # Compute movement vectors - p = matInv * vec2(0 + h, 0 + h) - dx = matInv * vec2(1 + h, 0 + h) - p - dy = matInv * vec2(0 + h, 1 + h) - p + p = inverseTransform * vec2(0 + h, 0 + h) + dx = inverseTransform * vec2(1 + h, 0 + h) - p + dy = inverseTransform * vec2(0 + h, 1 + h) - p filterBy2 = max(dx.length, dy.length) b = b @@ -739,29 +763,28 @@ proc drawUber( let smooth = not( dx.length == 1.0 and dy.length == 1.0 and - mat[2, 0].fractional == 0.0 and - mat[2, 1].fractional == 0.0 + transform[2, 0].fractional == 0.0 and + transform[2, 1].fractional == 0.0 ) + # Determine where we should start and stop drawing in the y dimension + var + yMin = a.height + yMax = 0 + for segment in perimeter: + yMin = min(yMin, segment.at.y.floor.int) + yMax = max(yMax, segment.at.y.ceil.int) + yMin = yMin.clamp(0, a.height) + yMax = yMax.clamp(0, a.height) + when type(a) is Image: let blender = blendMode.blender() else: # a is a Mask let masker = blendMode.masker() - # Determine where we should start and stop drawing in the y dimension - var yMin, yMax: int if blendMode == bmMask: - yMin = 0 - yMax = a.height - else: - yMin = a.height - yMax = 0 - for segment in perimeter: - yMin = min(yMin, segment.at.y.floor.int) - yMax = max(yMax, segment.at.y.ceil.int) - - yMin = yMin.clamp(0, a.height) - yMax = yMax.clamp(0, a.height) + if yMin > 0: + zeroMem(a.data[0].addr, 4 * yMin * a.width) for y in yMin ..< yMax: # Determine where we should start and stop drawing in the x dimension @@ -889,34 +912,106 @@ proc drawUber( clamp(srcPos.y, 0, b.height.float32) ) - for x in x ..< xMax: - let samplePos = ivec2((srcPos.x - h).int32, (srcPos.y - h).int32) - - when type(a) is Image: - let backdrop = a.unsafe[x, y] - when type(b) is Image: - let - sample = b.unsafe[samplePos.x, samplePos.y] - blended = blender(backdrop, sample) - else: # b is a Mask - let - sample = b.unsafe[samplePos.x, samplePos.y] - blended = blender(backdrop, rgbx(0, 0, 0, sample)) - a.unsafe[x, y] = blended - else: # a is a Mask - let backdrop = a.unsafe[x, y] - when type(b) is Image: - let sample = b.unsafe[samplePos.x, samplePos.y].a - else: # b is a Mask - let sample = b.unsafe[samplePos.x, samplePos.y] - a.unsafe[x, y] = masker(backdrop, sample) - - srcPos += dx + case blendMode: + of bmOverwrite: + for x in x ..< xMax: + let samplePos = ivec2((srcPos.x - h).int32, (srcPos.y - h).int32) + when type(a) is Image: + when type(b) is Image: + let source = b.unsafe[samplePos.x, samplePos.y] + else: # b is a Mask + let source = rgbx(0, 0, 0, b.unsafe[samplePos.x, samplePos.y]) + if source.a > 0: + a.unsafe[x, y] = source + else: # a is a Mask + when type(b) is Image: + let source = b.unsafe[samplePos.x, samplePos.y].a + else: # b is a Mask + let source = b.unsafe[samplePos.x, samplePos.y] + if source > 0: + a.unsafe[x, y] = source + srcPos += dx + of bmNormal: + for x in x ..< xMax: + let samplePos = ivec2((srcPos.x - h).int32, (srcPos.y - h).int32) + when type(a) is Image: + when type(b) is Image: + let source = b.unsafe[samplePos.x, samplePos.y] + else: # b is a Mask + let source = rgbx(0, 0, 0, b.unsafe[samplePos.x, samplePos.y]) + if source.a > 0: + if source.a == 255: + a.unsafe[x, y] = source + else: + let backdrop = a.unsafe[x, y] + a.unsafe[x, y] = blendNormal(backdrop, source) + else: # a is a Mask + when type(b) is Image: + let source = b.unsafe[samplePos.x, samplePos.y].a + else: # b is a Mask + let source = b.unsafe[samplePos.x, samplePos.y] + if source > 0: + if source == 255: + a.unsafe[x, y] = source + else: + let backdrop = a.unsafe[x, y] + a.unsafe[x, y] = blendAlpha(backdrop, source) + srcPos += dx + of bmMask: + for x in x ..< xMax: + let samplePos = ivec2((srcPos.x - h).int32, (srcPos.y - h).int32) + when type(a) is Image: + when type(b) is Image: + let source = b.unsafe[samplePos.x, samplePos.y] + else: # b is a Mask + let source = rgbx(0, 0, 0, b.unsafe[samplePos.x, samplePos.y]) + if source.a == 0: + a.unsafe[x, y] = rgbx(0, 0, 0, 0) + elif source.a != 255: + let backdrop = a.unsafe[x, y] + a.unsafe[x, y] = blendMask(backdrop, source) + else: # a is a Mask + when type(b) is Image: + let source = b.unsafe[samplePos.x, samplePos.y].a + else: # b is a Mask + let source = b.unsafe[samplePos.x, samplePos.y] + if source == 0: + a.unsafe[x, y] = 0 + elif source != 255: + let backdrop = a.unsafe[x, y] + a.unsafe[x, y] = maskMaskInline(backdrop, source) + srcPos += dx + else: + for x in x ..< xMax: + let samplePos = ivec2((srcPos.x - h).int32, (srcPos.y - h).int32) + when type(a) is Image: + let backdrop = a.unsafe[x, y] + when type(b) is Image: + let + sample = b.unsafe[samplePos.x, samplePos.y] + blended = blender(backdrop, sample) + else: # b is a Mask + let + sample = b.unsafe[samplePos.x, samplePos.y] + blended = blender(backdrop, rgbx(0, 0, 0, sample)) + a.unsafe[x, y] = blended + else: # a is a Mask + let backdrop = a.unsafe[x, y] + when type(b) is Image: + let sample = b.unsafe[samplePos.x, samplePos.y].a + else: # b is a Mask + let sample = b.unsafe[samplePos.x, samplePos.y] + a.unsafe[x, y] = masker(backdrop, sample) + srcPos += dx if blendMode == bmMask: if a.width - xMax > 0: zeroMem(a.data[a.dataIndex(xMax, y)].addr, 4 * (a.width - xMax)) + if blendMode == bmMask: + if a.height - yMax > 0: + zeroMem(a.data[a.dataIndex(0, yMax)].addr, 4 * a.width * (a.height - yMax)) + proc draw*( a, b: Image, transform = mat3(), blendMode = bmNormal ) {.inline, raises: [PixieError].} = diff --git a/src/pixie/paints.nim b/src/pixie/paints.nim index ba81df6..6e2c247 100644 --- a/src/pixie/paints.nim +++ b/src/pixie/paints.nim @@ -1,4 +1,4 @@ -import blends, chroma, common, images, vmath +import chroma, common, images, vmath when defined(amd64) and not defined(pixieNoSimd): import nimsimd/sse2 diff --git a/tests/benchmark_images.nim b/tests/benchmark_images.nim index 728397b..7be2761 100644 --- a/tests/benchmark_images.nim +++ b/tests/benchmark_images.nim @@ -25,6 +25,10 @@ image.fill(rgba(0, 0, 0, 0)) timeIt "isTransparent": doAssert image.isTransparent() +image.fill(rgba(255, 255, 255, 255)) +timeIt "isOpaque": + doAssert image.isOpaque() + reset() timeIt "subImage":