Skip to content

Fix SIMD OverwriteBlend clearing pixels the path does not cover - #593

Open
Graveflo wants to merge 3 commits into
treeform:masterfrom
Graveflo:coverage-overwrite
Open

Fix SIMD OverwriteBlend clearing pixels the path does not cover#593
Graveflo wants to merge 3 commits into
treeform:masterfrom
Graveflo:coverage-overwrite

Conversation

@Graveflo

Copy link
Copy Markdown

FYI

  • discovered and implemented by AI (GPT-5.6-Sol & Opus 5)

Summary

blendLineCoverageOverwrite is specified by its scalar body to write only
where coverage is non-zero:

for i in 0 ..< len:
  let coverage = coverages[i]
  if coverage != 0:
    line[i] = rgbx * coverage

The SSE2, AVX2 and NEON implementations do not honour that. Where coverage is
zero they write rgbx * 0 — transparent black — over the backdrop.

The vectors this affects are the antialiased edges of the shape being drawn, so
an OverwriteBlend fill or stroke erases whatever was already on the image, in
bands the width of a SIMD vector, wherever it has an edge. Drawing a 40 point
star onto an opaque 400x300 image destroys 15234 pixels it does not cover.

Reproduction

import pixie

let
  paint = newPaint(SolidPaint)
  path = newPath()
paint.color = color(1, 0, 0, 1)
path.circle(circle(vec2(64, 32), 40))

# Coverage: an opaque paint filled onto a transparent image with NormalBlend
# leaves rgbx * coverage, which is what OverwriteBlend should store where
# coverage is non-zero.
let expected = newImage(128, 64)
expected.fillPath(path, paint)

let image = newImage(128, 64)
image.fill(rgbx(0, 0, 255, 255))
paint.blendMode = OverwriteBlend
image.fillPath(path, paint)

var uncovered, wrong: int
for y in 0 ..< 64:
  for x in 0 ..< 128:
    if expected[x, y].a == 0:
      inc uncovered
      if image[x, y] != rgbx(0, 0, 255, 255):
        inc wrong
echo "uncovered: ", uncovered, "  wrongly overwritten: ", wrong

Before

overwrite_demo_star_simd overwrite_demo_stroke_simd overwrite_demo_text_simd

After

overwrite_demo_star_simd overwrite_demo_stroke_simd overwrite_demo_text_simd

After scalar fix

he trapezoid partial-coverage loops in fillShapes wrote
blender(backdrop, source) unconditionally. source is rgbx * area for a
continuous area, so a thin enough sliver rounds it to fully transparent and
OverwriteBlend stores that over the backdrop. fillCoverage cannot hit this
because its coverage is already quantised to 1/255.

The guard matches what fillCoverage's general branch already does — skip zero
coverage for every mode except MaskBlend, which has to clear:

    clearsUncovered = blendMode == MaskBlend
...
                if source.a != 0 or clearsUncovered:
                  image.data[dataIndex] = blender(backdrop, source)

With both commits an OverwriteBlend fill destroys no uncovered pixels at all,
on any build.

overwrite_demo_star_simd

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant