treeform / pixie

Full-featured 2d graphics library for Nim.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Program crashes when calling ctx.stroke() even when wrapped in try

kwhitefoot opened this issue · comments

I'm using pixie.Context to experiment with some semi-random drawing.
I build strokeStyle and path and then execute ctx.stroke. The program crashes even if I wrap the ctx.stroke call in a try.
I daresay that the strokeStyle is the culprit but it should surely not kill the program.

the snippet of code looks like this:

        echo "GStroke"
        echo "StrokeStyle: ", ctx.strokeStyle.repr
        try:
          ctx.stroke()
        except:
          echo getCurrentExceptionMsg()
          raise

And this is the result:

GStroke
StrokeStyle: ref 0x7fb0d3461590 --> [kind = AngularGradientPaint,
blendMode = NormalBlend,
opacity = 0.2196078449487686,
color = [r = 0.2156862765550613,
g = 0.0,
b = 0.007843137718737125,
a = 0.2196078449487686],
image = ref 0x7fb0d345a050 --> [width = 1,
height = 1,
data = 0x7fb0d345a080@[[r = 0,
g = 0,
b = 0,
a = 0]]],
imageMat = [arr = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]],
gradientHandlePositions = 0x7fb0d345e5d0@[[arr = [0.0, 0.0]], [arr = [0.0, 0.0]], [arr = [0.0, 0.0]]],
gradientStops = 0x7fb0d345e450@[[color = [r = 0.0,
g = 0.0,
b = 0.0,
a = 0.0],
position = 0.0]]]
Killed
Error: execution of an external program failed: ...

I can try to create a stand alone example if necessary.

Edit: I've hardcoded the paint kind to SolidPaint so presumably it is not the strokeStyle that is the problem after all, perhaps it is the path.

If you can give us a stand alone example we can run that would be great.

When I run this, nothing crashes:

import pixie

let image = newImage(200, 200)
image.fill(rgba(255, 255, 255, 255))

let ctx = newContext(image)

echo "GStroke"
echo "StrokeStyle: ", ctx.strokeStyle.repr
try:
  ctx.stroke()
except:
  echo getCurrentExceptionMsg()
  raise

image.writeFile("crashStroke.png")

Please provide a better example and we will fix it.

Here is even a better example that replicates your stroke style, still no crash:

import pixie

let image = newImage(200, 200)
image.fill(rgba(255, 255, 255, 255))

let ctx = newContext(image)

ctx.beginPath()
ctx.moveTo(75, 50)
ctx.lineTo(100, 75)
ctx.lineTo(100, 25)

ctx.lineWidth = 10

ctx.strokeStyle = Paint(
  kind: AngularGradientPaint,
  blendMode: NormalBlend,
  opacity: 0.2196078449487686,
  color: color(0.2156862765550613, 0.0, 0.007843137718737125, 0.2196078449487686),
  image: newImage(1, 1),
  imageMat: mat3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
  gradientHandlePositions: @[vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 0.0)],
  gradientStops: @[ColorStop(color: color(0.0, 0.0, 0.0, 0.0), position: 0.0)]
)

echo "GStroke"
echo "StrokeStyle: ", ctx.strokeStyle.repr
try:
  ctx.stroke()
except:
  echo getCurrentExceptionMsg()
  raise

image.writeFile("crashStroke.png")```

I added code to my program to make it write the nim statements that were being executed. So here is a relatively short program that illustrates the problem. It never returns from ctx.StrokePolygon. I'm not sure exactly why it fails on that instead of at ctx.Stroke now but it's probably because I commented out some code in a vain attempt to isolate the problem.

import pixie

let image = newImage(200, 200)
image.fill(rgba(255, 255, 255, 255))

let ctx = newContext(image)
ctx.strokeStyle = "#FF5C00"
ctx.translate(100, 100)

echo "StrokeStyle: ", ctx.strokeStyle.repr

ctx.lineWidth = 10

let
  start = vec2(25, 25)
  stop = vec2(175, 175)

echo "ctx lw: ", ctx.lineWidth
ctx.strokeSegment(segment(start, stop))

ctx.restore()
ctx.clearRect(0.0, 0.0, 0.0, 0.0)
ctx.clearRect(0.0, 0.0, 0.0, 0.0)
ctx.rotate(0.0)
ctx.save()
#ctx.fillStyle = paint
ctx.strokeRoundedRect(rect(vec2(0.0, 0.0), vec2(0.0, 0.0)), 0.0, 0.0, 0.0, 0.0)
ctx.strokeRect(rect(vec2(0.0, 0.0), vec2(0.0, 0.0)))
ctx.setLineDash(@[0.0.float32])
ctx.resetTransform()
ctx.strokeRoundedRect(rect(vec2(0.0, 0.0), vec2(0.0, 0.0)), 0.0, 0.0, 0.0, 0.0)
ctx.rotate(0.0)
ctx.globalAlpha = 0.0
ctx.clearRect(0.0, 0.0, 0.0, 0.0)
#ctx.strokeStyle = paint
ctx.roundedRect(rect(vec2(0.0, 0.0), vec2(0.0, 0.0)), 0.0,0.0,0.0,0.0)
ctx.strokeRoundedRect(rect(vec2(0.0, 0.0), vec2(0.0, 0.0)), 0.0, 0.0, 0.0, 0.0)
#ctx.strokeStyle = paint
ctx.clearRect(0.0, 0.0, 0.0, 0.0)
ctx.rotate(0.0)
#ctx.strokeStyle = paint
#ctx.fillStyle = paint
#ctx.fillStyle = paint
ctx.lineCap = RoundCap
echo "poly with 3 sides, size=50, in centre"
ctx.strokePolygon(vec2(100.0, 100.0), 50.0, 3)
echo "poly with 3 sides, size=10, in centre"
ctx.strokePolygon(vec2(100.0, 100.0), 10.0, 3)
echo "poly with 3 sides, size=10"
ctx.strokePolygon(vec2(0.0, 0.0), 10.0, 3)
echo "poly with 3 sides, size=0"
ctx.strokePolygon(vec2(0.0, 0.0), 0.0, 3)
echo "poly with 0 sides, size=10"
ctx.strokePolygon(vec2(0.0, 0.0), 10.0, 0)
echo "poly with 0 sides, size=0"
ctx.strokePolygon(vec2(0.0, 0.0), 0.0, 0)

image.writeFile("line.png")

It is the operating system that is killing the program. It reached 99% CPU, 69.6% memory before htop and everything else stopped responding.
I thought for a moment that it was the fact that the strokePolygon call has zero length sides so I added the calls with non-zero size, sides, and centre, all fail.
I'll try to pare down the number of statements but I thought I should post it as is while I try because you might see straight away what is just mysterious to me.

Thanks! I see the bug now.

That was hardly a simple example here is the same crash, but with all not needed lines removed:

import pixie

let image = newImage(200, 200)
let ctx = newContext(image)
ctx.setLineDash(@[0.0.float32])
ctx.strokePolygon(vec2(100.0, 100.0), 50.0, 3)

The problem is setLineDash to 0, it causes an infinite loop in the dash generating code. It probably should raise an exception or simply draw nothing...

Fixed, now it will throw an exception when this happens. Should be in the next release, you can get it from source or just don't set line dash to 0.

Got it.

import pixie

let image = newImage(200, 200)
image.fill(rgba(255, 255, 255, 255))

let ctx = newContext(image)

ctx.setLineDash(@[0.0.float32])    <---- This is the offending statement.  
ctx.strokePolygon(vec2(100.0, 100.0), 50.0, 3)

image.writeFile("line.png")

if the argument tosetLineDash contains only zeroes then strokePolygon never completes and uses all available memory.

I think it must be in the pixie/paths.nim strokePaths proc that the infinite loops occurs:


    for i in 1 ..< shape.len:
      let
        pos = shape[i]
        prevPos = shape[i - 1]

      if dashes.len > 0:
        var distance = dist(prevPos, pos)
        let dir = dir(pos, prevPos)
        var currPos = prevPos
        block dashLoop:
          while true:
            for i, d in dashes:
              if i mod 2 == 0:
                let d = min(distance, d)
                shapeStroke.add(makeRect(currPos, currPos + dir * d))
              currPos += dir * d
              distance -= d
              if distance <= 0:  <--- when the dashes are all zeroes this condition will never be true (I think)
                break dashLoop
      else:
        shapeStroke.add(makeRect(prevPos, pos))

I can easily check for this condition in my code but perhaps pixie should too.
And lastly, thanks for creating pixie!

It probably doesn't matter but it seems to me that your fix is stricter than necessary.
Throwing an exception if any dash is zero is efficient but at the moment a lineDash array that sums to greater than zero is legal even if some of it's constituent values are zero or even negative. At least this works at the moment.

import pixie

let image = newImage(200, 200)
image.fill(rgba(255, 255, 255, 255))

let ctx = newContext(image)
ctx.strokeStyle = "#FF5C00"

ctx.lineWidth = 10

ctx.setLineDash(@[-2.0.float32, 10.0.float32, 0.0.float32, 0.0.float32])
ctx.strokePolygon(vec2(100.0, 100.0), 70.0, 3)