Memory Leaks when Bloom or Gaussian Blur Filters used with Snapshot

If you want to crash your emulator or device with an out of memory error, run the following code, clicking “Click me!” repeatedly until the app crashes.

This code below is loosely based on something I am attempting to do in a real app. I have a score counter inside of a snapshot specifically so that I can apply the bloom filter and get a nice glow effect. In my game, as the score goes up, so does the memory. In my game, I am actually using embossed text but I use standard text below to ensure that the embossed text is not related to the memory issue. To simulate the score doing up in the code below, I added a simple widget button that, when clicked, increments a counter, invalidates the snapshot, and re-applies the filter. (You must re-apply a filter to a snapshot after it’s invalidated or else it won’t take effect.)

Memory is not an issue if the standard blur or brightness filters are used instead. To verify this, change the code in the applyFilter function accordingly. You might also try other filters just to see what they do. I have only tried these four. (See code below.)

The simple red text I used below doesn’t really look special with the bloom filter, but keep in mind that I am just trying to reproduce the memory issue here. A well-used bloom filter looks really cool.

----------------------------------------------------------------------------------------- -- -- main.lua -- ----------------------------------------------------------------------------------------- local widget = require("widget") local function applyFilter(snapshot)     --these filters leak memory!     snapshot.fill.effect = "filter.bloom";     --snapshot.fill.effect = "filter.blurGaussian";     --these filters do NOT leak memory     --snapshot.fill.effect = "filter.blur";     --snapshot.fill.effect = "filter.brightness"; end local counter = 0; local counterTxt = display.newText( '0', display.contentWidth / 2, 20, native.systemFont, 300 ) counterTxt.fill = {1,0,0} local snapshot = display.newSnapshot(display.contentWidth \* 2, 300) snapshot.y = 100 snapshot.group:insert(counterTxt); applyFilter(snapshot); local function handleButtonEvent( event )     if ( "ended" == event.phase ) then         counter = counter + 1;         counterTxt.text = counter;         snapshot:invalidate( )         --must re-apply filter after invalidating snapshot         applyFilter(snapshot);     end end -- Create the widget local memoryKillerBtn = widget.newButton {     id = "memoryKiller",     label = "Click me!",     onEvent = handleButtonEvent } memoryKillerBtn.x = display.contentWidth / 2; memoryKillerBtn.y = 280;

I got to 136 before I stopped clicking. My sim never crashed. How many times should I click before the crash happens?

Hey Alex, on the simulator, it really depends on how much RAM you have on your system. It could take hundreds of clicks if you have, say, 16 GBs of RAM. However, if you are running in the simulator on Windows, instead of waiting for a crash or some other failure, you can open up the the Windows Task Manager and watch the simulator process’s memory usage jump up by about 1-3 MB per click. When I run this code as-is in the CoronaViewer on my iPhone 6 it crashes after about 43 clicks.

I did some more research and found that the memory leak occurs for all multi-pass shaders, period. The two memory leaking filters that I knew about before are both multi-pass shaders. (blurGaussian = blurHorizontal+blurVertical and bloom = blurGaussian+levels+?) I have since tried several filters both independently and as multi-pass shaders and found that every multi-pass shader, regardless of what filters it uses internally, leaks memory. See revised code below.

----------------------------------------------------------------------------------------- -- -- main.lua -- ----------------------------------------------------------------------------------------- local widget = require("widget") local function defineCustomShader1()     local kernel = {}     kernel.language = "glsl"     kernel.category = "filter"     kernel.name = "test1"     kernel.graph =     {        nodes = {           pass1 = { effect="filter.blurHorizontal", input1="paint1" },           pass2 = { effect="filter.blurVertical", input1="pass1" },        },        output = "pass2",     }     graphics.defineEffect(kernel) end local function defineCustomShader2()     local kernel = {}     kernel.language = "glsl"     kernel.category = "filter"     kernel.name = "test2"     kernel.graph =     {        nodes = {           pass1 = { effect="filter.bulge", input1="paint1" },           pass2 = { effect="filter.dissolve", input1="pass1" },        },        output = "pass2",     }     graphics.defineEffect(kernel) end local function applyFilter(snapshot)     --these filters leak memory!     --snapshot.fill.effect = "filter.bloom";     --snapshot.fill.effect = "filter.blurGaussian";     snapshot.fill.effect = "filter.custom.test1";     --snapshot.fill.effect = "filter.custom.test2";     --these filters do NOT leak memory     --snapshot.fill.effect = "filter.blur";     --snapshot.fill.effect = "filter.brightness";     --snapshot.fill.effect = "filter.blurHorizontal";     --snapshot.fill.effect = "filter.blurVertical";     --snapshot.fill.effect = "filter.bulge";     --snapshot.fill.effect = "filter.dissolve"; end defineCustomShader1() defineCustomShader2() local counter = 0; local counterTxt = display.newText( '0', display.contentWidth / 2, 20, native.systemFont, 300 ) counterTxt.fill = {1,0,0} local snapshot = display.newSnapshot(display.contentWidth \* 2, 300) snapshot.y = 100 snapshot.group:insert(counterTxt); applyFilter(snapshot); local function handleButtonEvent( event )     if ( "ended" == event.phase ) then         counter = counter + 1;         counterTxt.text = counter;         snapshot:invalidate( )         --must re-apply filter after invalidating snapshot         applyFilter(snapshot);     end end -- Create the widget local memoryKillerBtn = widget.newButton {     id = "memoryKiller",     label = "Keep clicking me!",     onEvent = handleButtonEvent } memoryKillerBtn.x = display.contentWidth / 2; memoryKillerBtn.y = 280;

If you haven’t already done so, it looks like you basically have the makings of a bug report there.

Thanks StarCrunch. I’ve posted a bug report (Case 43997). :slight_smile:

For those that are interested. I have found a work-around that is sufficient for me. I was able to change my code such that I didn’t need the bloom filter and I have created a single-pass gaussian blur filter that is fast enough for my needs. Note that using a single pass blur is much slower than the dual-pass approach used by the Corona’s gaussian blur. Unfortunately, it seems it’s the dual-pass filters that cause the memory issues.

In any case, the code is below. I didn’t bother trying to implement parameter-passing, so in order to customize the blur, try tweaking the values of sigma and mSize in the shader kernel fragment. Just be sure that mSize is an odd number.

My code is based off the code I found here: https://www.shadertoy.com/view/XdfGDH. The original code did not support alphas by the way. My code does.

local function defineCustomBlur() local kernel = {} kernel.language = "glsl" kernel.category = "filter" kernel.name = "blur" kernel.fragment = [[#ifdef GL\_ES precision mediump float; #endif P\_DEFAULT float normpdf(P\_DEFAULT float x, P\_DEFAULT float sigma) { return 0.39894\*exp(-0.5\*x\*x/(sigma\*sigma))/sigma; } P\_COLOR vec4 FragmentKernel( P\_UV vec2 fragCoord ) { //declare stuff P\_DEFAULT const int mSize = 15; //must be odd number P\_DEFAULT const int kSize = (mSize-1)/2; P\_DEFAULT float kernel[mSize]; P\_COLOR vec4 final\_colour = vec4(0.0); //create the 1-D kernel P\_DEFAULT float sigma = 70.0; P\_DEFAULT float Z = 0.0; for (P\_DEFAULT int j = 0; j \<= kSize; ++j) { kernel[kSize+j] = kernel[kSize-j] = normpdf(float(j), sigma); } //get the normalization factor (as the gaussian has been clamped) for (P\_DEFAULT int j = 0; j \< mSize; ++j) { Z += kernel[j]; } //read out the texels for (P\_DEFAULT int i=-kSize; i \<= kSize; ++i) { for (P\_DEFAULT int j=-kSize; j \<= kSize; ++j) { final\_colour += kernel[kSize+j]\*kernel[kSize+i]\*texture2D(CoronaSampler0, fragCoord.xy+vec2(float(i) \* CoronaTexelSize.x,float(j) \* CoronaTexelSize.y)); } } return CoronaColorScale(vec4(final\_colour/(Z\*Z))); } ]] graphics.defineEffect(kernel) end; defineCustomBlur(); --to use blur, simply call object.fill.effect = "filter.custom.blur"

Sadly, I was wrong! :frowning: My work-around shader does not work on iOS, or at least not on my test device. If someone knows how to make this shader work correctly or if Corona developers could fix the bug I reported that would be greatly appreciated!

Woo hoo! I haven’t been notified of the fix but it’s obvious that the Corona devs are working on this issue and may have even totally fixed it! At this exact moment, this code is working just fine without memory issues! I really hope this is a permanent fix!

I got to 136 before I stopped clicking. My sim never crashed. How many times should I click before the crash happens?

Hey Alex, on the simulator, it really depends on how much RAM you have on your system. It could take hundreds of clicks if you have, say, 16 GBs of RAM. However, if you are running in the simulator on Windows, instead of waiting for a crash or some other failure, you can open up the the Windows Task Manager and watch the simulator process’s memory usage jump up by about 1-3 MB per click. When I run this code as-is in the CoronaViewer on my iPhone 6 it crashes after about 43 clicks.

I did some more research and found that the memory leak occurs for all multi-pass shaders, period. The two memory leaking filters that I knew about before are both multi-pass shaders. (blurGaussian = blurHorizontal+blurVertical and bloom = blurGaussian+levels+?) I have since tried several filters both independently and as multi-pass shaders and found that every multi-pass shader, regardless of what filters it uses internally, leaks memory. See revised code below.

----------------------------------------------------------------------------------------- -- -- main.lua -- ----------------------------------------------------------------------------------------- local widget = require("widget") local function defineCustomShader1() &nbsp;&nbsp; &nbsp;local kernel = {} &nbsp;&nbsp; &nbsp;kernel.language = "glsl" &nbsp;&nbsp; &nbsp;kernel.category = "filter" &nbsp;&nbsp; &nbsp;kernel.name = "test1" &nbsp;&nbsp; &nbsp;kernel.graph = &nbsp;&nbsp; &nbsp;{ &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; nodes = { &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; pass1 = { effect="filter.blurHorizontal", input1="paint1" }, &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; pass2 = { effect="filter.blurVertical", input1="pass1" }, &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; }, &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; output = "pass2", &nbsp;&nbsp; &nbsp;} &nbsp;&nbsp; &nbsp;graphics.defineEffect(kernel) end local function defineCustomShader2() &nbsp;&nbsp; &nbsp;local kernel = {} &nbsp;&nbsp; &nbsp;kernel.language = "glsl" &nbsp;&nbsp; &nbsp;kernel.category = "filter" &nbsp;&nbsp; &nbsp;kernel.name = "test2" &nbsp;&nbsp; &nbsp;kernel.graph = &nbsp;&nbsp; &nbsp;{ &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; nodes = { &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; pass1 = { effect="filter.bulge", input1="paint1" }, &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; pass2 = { effect="filter.dissolve", input1="pass1" }, &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; }, &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; output = "pass2", &nbsp;&nbsp; &nbsp;} &nbsp;&nbsp; &nbsp;graphics.defineEffect(kernel) end local function applyFilter(snapshot) &nbsp;&nbsp; &nbsp;--these filters leak memory! &nbsp;&nbsp; &nbsp;--snapshot.fill.effect = "filter.bloom"; &nbsp;&nbsp; &nbsp;--snapshot.fill.effect = "filter.blurGaussian"; &nbsp;&nbsp; &nbsp;snapshot.fill.effect = "filter.custom.test1"; &nbsp;&nbsp; &nbsp;--snapshot.fill.effect = "filter.custom.test2"; &nbsp;&nbsp; &nbsp;--these filters do NOT leak memory &nbsp;&nbsp; &nbsp;--snapshot.fill.effect = "filter.blur"; &nbsp;&nbsp; &nbsp;--snapshot.fill.effect = "filter.brightness"; &nbsp;&nbsp; &nbsp;--snapshot.fill.effect = "filter.blurHorizontal"; &nbsp;&nbsp; &nbsp;--snapshot.fill.effect = "filter.blurVertical"; &nbsp;&nbsp; &nbsp;--snapshot.fill.effect = "filter.bulge"; &nbsp;&nbsp; &nbsp;--snapshot.fill.effect = "filter.dissolve"; end defineCustomShader1() defineCustomShader2() local counter = 0; local counterTxt = display.newText( '0', display.contentWidth / 2, 20, native.systemFont, 300 ) counterTxt.fill = {1,0,0} local snapshot = display.newSnapshot(display.contentWidth \* 2, 300) snapshot.y = 100 snapshot.group:insert(counterTxt); applyFilter(snapshot); local function handleButtonEvent( event ) &nbsp;&nbsp; &nbsp;if ( "ended" == event.phase ) then &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;counter = counter + 1; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;counterTxt.text = counter; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;snapshot:invalidate( ) &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;--must re-apply filter after invalidating snapshot &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;applyFilter(snapshot); &nbsp;&nbsp; &nbsp;end end -- Create the widget local memoryKillerBtn = widget.newButton { &nbsp;&nbsp; &nbsp;id = "memoryKiller", &nbsp;&nbsp; &nbsp;label = "Keep clicking me!", &nbsp;&nbsp; &nbsp;onEvent = handleButtonEvent } memoryKillerBtn.x = display.contentWidth / 2; memoryKillerBtn.y = 280;

If you haven’t already done so, it looks like you basically have the makings of a bug report there.

Thanks StarCrunch. I’ve posted a bug report (Case 43997). :slight_smile:

For those that are interested. I have found a work-around that is sufficient for me. I was able to change my code such that I didn’t need the bloom filter and I have created a single-pass gaussian blur filter that is fast enough for my needs. Note that using a single pass blur is much slower than the dual-pass approach used by the Corona’s gaussian blur. Unfortunately, it seems it’s the dual-pass filters that cause the memory issues.

In any case, the code is below. I didn’t bother trying to implement parameter-passing, so in order to customize the blur, try tweaking the values of sigma and mSize in the shader kernel fragment. Just be sure that mSize is an odd number.

My code is based off the code I found here: https://www.shadertoy.com/view/XdfGDH. The original code did not support alphas by the way. My code does.

local function defineCustomBlur() local kernel = {} kernel.language = "glsl" kernel.category = "filter" kernel.name = "blur" kernel.fragment = [[#ifdef GL\_ES precision mediump float; #endif P\_DEFAULT float normpdf(P\_DEFAULT float x, P\_DEFAULT float sigma) { return 0.39894\*exp(-0.5\*x\*x/(sigma\*sigma))/sigma; } P\_COLOR vec4 FragmentKernel( P\_UV vec2 fragCoord ) { //declare stuff P\_DEFAULT const int mSize = 15; //must be odd number P\_DEFAULT const int kSize = (mSize-1)/2; P\_DEFAULT float kernel[mSize]; P\_COLOR vec4 final\_colour = vec4(0.0); //create the 1-D kernel P\_DEFAULT float sigma = 70.0; P\_DEFAULT float Z = 0.0; for (P\_DEFAULT int j = 0; j \<= kSize; ++j) { kernel[kSize+j] = kernel[kSize-j] = normpdf(float(j), sigma); } //get the normalization factor (as the gaussian has been clamped) for (P\_DEFAULT int j = 0; j \< mSize; ++j) { Z += kernel[j]; } //read out the texels for (P\_DEFAULT int i=-kSize; i \<= kSize; ++i) { for (P\_DEFAULT int j=-kSize; j \<= kSize; ++j) { final\_colour += kernel[kSize+j]\*kernel[kSize+i]\*texture2D(CoronaSampler0, fragCoord.xy+vec2(float(i) \* CoronaTexelSize.x,float(j) \* CoronaTexelSize.y)); } } return CoronaColorScale(vec4(final\_colour/(Z\*Z))); } ]] graphics.defineEffect(kernel) end; defineCustomBlur(); --to use blur, simply call object.fill.effect = "filter.custom.blur"

Sadly, I was wrong! :frowning: My work-around shader does not work on iOS, or at least not on my test device. If someone knows how to make this shader work correctly or if Corona developers could fix the bug I reported that would be greatly appreciated!

Woo hoo! I haven’t been notified of the fix but it’s obvious that the Corona devs are working on this issue and may have even totally fixed it! At this exact moment, this code is working just fine without memory issues! I really hope this is a permanent fix!