Nasty crash when using BlurFilter on iOS in Legacy

Help! I discovered that my new app is crashing on iPhones. As far as I can tell, the crash seems to be happening deep inside Legacy NME code, and is tied to the Blur filter (when I remove all the filters, no crash). What’s really weird is that it’s only iOS, and only on the device – I wasn’t able to replicate in the simulator. See the following short program for an example – it consistently crashes on my iPad within 5-30 seconds.

package;

import openfl.display.Sprite;
import openfl.text.TextField;
import openfl.filters.BlurFilter;
import openfl.events.Event;
import openfl.text.Font;
import openfl.text.TextFormat;

@:font( "Assets/YanoneKaffeesatz-Light.ttf" ) class FYanone extends Font {}

class Main extends Sprite {
    private var tf : Array<TextField>;

    public function new () {

        super ();

         Font.registerFont( FYanone );

         var fmt = new TextFormat(
             "Yanone Kaffeesatz Light", 200, 0x000000 );

        tf = new Array<TextField>();

        for( i in 0...20 ) {
            var t = new TextField();
            t.defaultTextFormat = fmt;
            t.text = "Weird Bug";
            addChild( t );
            t.x = 100;
            t.y = 100;
            t.width = 1000;
            t.height = 200;

            t.filters = [new BlurFilter( 10, 10, openfl.filters.BitmapFilterQuality.LOW )];
            tf.push( t );
        }

        addEventListener( Event.ENTER_FRAME, onEnter );
    }

    private function onEnter( ev )
    {
        for( i in 0...20 ) {
            var a = Math.random() * 5000 - 2000;
            var b = Math.random() * 5000 - 2000;

            tf[i].x = a;
            tf[i].y = b;
        }
    }
}

For reference, here’s the top part of the stack trace in Xcode:

#0	0x0009ea4c in nme::BlurRow(nme::ARGB const*, int, int, int, nme::ARGB*, int, int, int, int) ()
#1	0x000a03ae in void nme::BlurFilter::DoApply<nme::ARGB>(nme::Surface const*, nme::Surface*, nme::Point2D<int>, nme::Point2D<int>, int) const ()
#2	0x0009ebae in nme::BlurFilter::Apply(nme::Surface const*, nme::Surface*, nme::Point2D<int>, nme::Point2D<int>, int) const ()
#3	0x000a004a in nme::FilterBitmap(nme::QuickVec<nme::Filter*, 16> const&, nme::Surface*, nme::TRect<int> const&, nme::TRect<int> const&, bool, nme::Point2D<int>) ()
#4	0x000947c4 in nme::DisplayObjectContainer::Render(nme::RenderTarget const&, nme::RenderState const&) ()
#5	0x00094150 in nme::DisplayObjectContainer::Render(nme::RenderTarget const&, nme::RenderState const&) ()
#6	0x00094150 in nme::DisplayObjectContainer::Render(nme::RenderTarget const&, nme::RenderState const&) ()
#7	0x0009609a in nme::Stage::RenderStage() ()
#8	0x000a3ffe in nme_render_stage(_value*) ()

Any thoughts on what might be going wrong, and how to work around it? Obviously I could just not use blurring, and I’ll do that if no other fix is possible. (If you’re going to test this, substitute in your favourite font as an alternative.)

Thanks.

What if you blur, then bitmapData draw? That would ensure the software renderer runs only once

Sorry, you’ll have to unpack that just a bit for me. Do I not add the TextField as a child of the sprite, instead blitting its pixels onto the parent sprite manually? Or do you have something else in mind? Can you describe the change in terms of the sample program I posted here?

I mean, do your TextField + filter, but then create a BitmapData large enough, and use bitmapData.draw to commit the TextField render to a BitmapData. Remove the TextField from the stage. Now you have a static bitmap that has the effect drawn

Ah, OK, thanks. I thought of doing that. The downside is that before I removed the blur from my app, I had an Actuate effect where the blur filter width would decrease smoothly from some positive number to zero to reveal the name of the level. That’s a bit less feasible with blurs baked into bitmaps. Of course, if I get a chance I’ll still try this out just to convince myself that it avoids the bug.

The other thing that I thought could help (depending on how blur is implemented) would be to turn off the visibility of any blurred TextFields that have been translated off screen, so that they’re skipped during redraw. Would that help, by any chance? I don’t understand the internals well enough to know for sure.

Note that I tried setting cacheAsBitmap to true for the blurred TextFields, but they’re still being drawn every frame. Shouldn’t that prevent redraws, at least in theory?

Try blurring the TextField to your maximum value, commit it a BitmapData, add it over the original (non blurred) TextField, and fade out the blurred copy. That might provide some of the effect you want?

No, unfortunately the center of the bitmap will fade out too quickly. For this effect, you’d want to keep the center pixels at full alpha until everything else had faded out.

On the other hand, a ColorMatrixFilter might do the trick, assuming that it’s ok to return alpha values below 0. Here’s how I’d do it:

  1. Create a TextField with the full-size blur filter.
  2. Draw it onto a Bitmap.
  3. Get rid of the text field.
  4. Create a new ColorMatrixFilter.
  5. Save a reference to your filter and to the filter’s matrix property.
  6. Apply the filter to your Bitmap and add it to the stage.
  7. Each frame, add a small amount to matrix[18] and subtract the same amount from matrix[19]. The amount you add/subtract may need to increase each frame.
  8. If you’re compiling for Flash, set colorMatrixFilter.matrix = matrix; to notify Flash of your changes.

The filter will multiply each pixel’s alpha by matrix[18], and add matrix[19]. Also, matrix[18] starts at 1, and matrix[19] starts at 0. So let’s say you’ve added/subtracted 0.5 so far.

0   * 1.5 - 0.5 == -0.5
0.2 * 1.5 - 0.5 == -0.2
0.4 * 1.5 - 0.5 == 0.1
0.6 * 1.5 - 0.5 == 0.4
0.8 * 1.5 - 0.5 == 0.7
1   * 1.5 - 0.5 == 1

As you can see, the smaller values decrease faster, and the larger values barely decreased at all. Assuming negative values are interpreted as 0, this means the outer edges of your bitmap will disappear much sooner than the center pixels.

The 100% alpha pixels that made up the original text field will never disappear, because the formula always returns 1 for an input of 1.

That’s a very cool idea, thanks. I might try it. I suppose it depends on how negative alpha values are treated, which may even be platform independent.

Is there no hope of identifying the underlying problem with BlurFilter? And does this mean that cacheAsBitmap doesn’t work the way I expect it to?

Well, trying once, only, it helps eliminate possibilities. The legacy renderer might try software rendering over and over, where bitmapData draw would force it to happen once only

Incidentally, looking through that code, I just observed that I was using a single static Array<openfl.filters.BitmapFilter> object and assigning it to all the TextFields, since I wanted them all filtered in the same way. Is that potentially the source of the problem? Or is it perfectly safe to have multiple Sprites use the same BlurFilter instance?

Might be worth trying both ways? It’s possible that could make legacy angry