Mandelbrot set code example, rev 0.1, and a challenge for you Lua experts!

Hi all,

Here’s a first draft rev of the Mandelbrot explorer I coded up yesterday based on a few of your sample code projects. Tapping the screen will zoom the image in around your tap point. You can change a few global parameters to ramp up the resolution at the expense of longer update intervals, and select between a few simple rendering options. You’ll notice that I went a little wild fiddling with variable localization and memory access vs. calculation performance tests. Consider this rev pre-release. I haven’t yet begun to explore issues around closure optimization or loop format testing.

All this has raised a few interesting oddities and questions for me, a Lua novice.

1.) Printing the ElapsedTime return value seems to give odd numbers that don’t make sense when successive calls are bracketed around the rendering and calculation loops. Can anyone explain the proper print format or why there is no interval shown bracketing a long loop that clearly takes several seconds to compute?

2.) When you tap the screen to zoom in, there is a multi-second pause before the touch is registered, longer than the draw rendering delay accounts for. What might be going on here that takes so much time? Garbage collection? Any suggestions on how to manage this?

3.) I noticed that the draw rendering interval increases slowly. Very slowly, but it seems to increase pretty monotomically just repeating the draw cycle. Memory leak? Closures? Any idea what gives?

A few things on my to-do list, but suggestions are welcome here from anyone with more than the remote Lua clue I cling to.

  • solve the ElapsedTime oddities.
  • fiddle with performance tweaks and learn how Lua performance issues differ from, say, c++.
  • add a zoom-out gesture, either two finger, pinch, or double-tap
  • add some more rendering options, including perhaps a smooth escape radius algorithm.

Finally, a challenge for you Lua hotshots. How fast can you make it go? Any optimization suggestions or tweaks? Help a Lua novice brother out!

Source:


______________
--Mandelbrot set image explorer by Phillip Alvelda 
RMode= "Color"
tileNumber = 150
tileSize = WIDTH/tileNumber
tilesize= tileSize

MinRe = -2.0
MaxRe = 2
MinIm = -2.0
MaxIm = 2
Re_factor = (MaxRe-MinRe)/(WIDTH -1)
Im_factor = (MaxIm-MinIm)/(HEIGHT -1)
MaxIterations = 255
UCounter =0
count=0
red={}
green={}
blue ={}
tiles = {}
for y= 1,HEIGHT/tilesize do
    tiles[y] = {}
end

-- Use this function to perform your initial setup
function setup() 
    setInstructionLimit(0)
    InitColorMap()
    ManSetCalc()
end

function InitColorMap()
    for i=0,MaxIterations do
      if RMode == "Iterations" then
                red[i]=255-i*255/MaxIterations
                green[i]=255-i*255/MaxIterations
                blue[i]=255-i*255/MaxIterations
            elseif RMode == "Color" then
                red[i]=(i*8) % 255
                green[i] = (i*9) % 255
                blue[i] = (i * 4) % 255
            elseif RMode == "Binary" then
                if (value % 2) == 0 then
                    red[i]=255
                    green[i]=255
                    blue[i]=255  
                else 
                    red[i]=0
                    green[i]=0
                    blue[i]=0  
                end
            end         
    end    
end



function touched(touch)
  if touch.state == ENDED then --only use the final touch event to scale image window
    dre=MaxRe-MinRe
    dim=MaxIm-MinIm
    x = MinRe + touch.x/WIDTH * dre
    y = MinIm + touch.y/HEIGHT * dim
    print ("touch x=",x,"y=",y)
    
    --set the zoom factor and new window borders
    MinRe = x -  dre / 3
    MaxRe = x + dre / 3
    MinIm = y - dim / 3
    MaxIm = y + dim / 3
    Re_factor = (MaxRe-MinRe)/(WIDTH -1)
    Im_factor = (MaxIm-MinIm)/(HEIGHT -1)
    print "Zoom in, Regen..."
    ManSetCalc()
    print "Done."
  end
end

function ManSetCalc() 
    print("Calc Start",ElapsedTime )    
    local y, x, n, isInside
    local ylim=HEIGHT/tilesize
    local xlim=WIDTH/tilesize
    local ltiles=tiles
    local imf=Im_factor*tilesize
    local ref=Re_factor*tilesize
    local iterations
    for y = 1, ylim do
        c_im = MinIm + y*imf
        
        for x = 1, xlim do
            c_re = MinRe + x*ref
            Z_re = c_re
            Z_im = c_im
            isInside = true
            for n=0, MaxIterations, 1 do
                Z_re2 = Z_re*Z_re
                Z_im2 = Z_im*Z_im
                iterations=n
                if(Z_re2 + Z_im2 > 4)then
                    isInside = false   
                    break
                end
                Z_im = 2*Z_re*Z_im + c_im
                Z_re = Z_re2 - Z_im2 + c_re
            end            
            ltiles[y][x] = iterations                 
        end
    end 
    print("End",ElapsedTime)  
end

function draw()
    print("Draw interval=",DeltaTime)
    count = count + 1
    noSmooth()
    background(0, 0, 0, 255)
    scale(tilesize,tilesize)
    local lred=red
    local lgreen=green
    local lblue=blue
    local ltiles=tiles
    local x,y,column, value
    local lrect=rect
    local lfill=fill
    local lipairs=ipairs
    for y,column in lipairs(ltiles) do    
          for x,value in lipairs(column) do
           lfill(lred[value],lgreen[value],lblue[value],255)
            --fill((value*8) % 255, (value*9) % 255,(value*4) % 255,255)
            lrect(x, y, 1, 1)
          end   
    end
end

Fah. It lost the line breaks. Let me fiddle a bit to see how best to post.

Okay this brings up an annoyance. You can’t copy from the Codify edit window and paste into the forum comments without losing the line breaks. Suggestions?

Okay look here:

git://gist.github.com/1408257.git

< code > < / code >

@Ipda41001: it’s actually < pre > < / pre > for large multi-line blocks, < code > appears to be for in-line elements.

I’ve edited your post to add the formatting tags, @alvelda

< code > tag:
testing code over multiple lines

< pre > tag:


testing pre
over multiple lines

Also, @alvelda, we have an update coming with an image class that you can render into. This will allow you to pre-render your Mandelbrot then simply draw the image every frame (much cheaper). When a zoom happens you can respond immediately and then re-render using a co-routine. Unfortunately we have no idea when Apple will allow it through.

Fantastic! Thanks Simeon. Would you be interested in having a beta user? Would you be comfortable/willing to register my iPad’s id for dev access to the pre-release code? I’d love to fiddle and help test…(and get early access!). :wink:

I’m guessing some of the things your seeing is print()

  1. printing is like an extra draw
  2. filling up the print buffer slows programs in the current version

Try watch() when pratical

Yes, we’d like to do a wider beta test. We are just unsure how to handle beta discussion. I don’t think I would want it to happen on this forum, as it might cause confusion to new users to see discussion of unavailable features.

@Ipda41001 is right, print() really slows things down. I’ll look at culling the buffer past a certain number of lines (100 or so).

Great. Happy to help. Maybe a private beta users’ wiki somewhere? Postbox runs a nice forum where they just maintain separate conversations for beta features. Some confusion occasionally arises, and is generally managed by moving new threads to the conversations where they belong.

Otherwise I’d be happy to start via email. Just use p_alvelda@yahoo.com for me.

Okay, here’s another rev with watches instead of prints. No notable changes in behavior. So I don’t think the print statements are the major part of the odd behavior, particlarly with ElapsedTime.

How and when is watch() updated?

-P



RMode= "Color"
tileNumber = 200
tileSize = WIDTH/tileNumber
tilesize= tileSize
deltatime=0
ManCalcStart=0
ManCalcEnd=0
ManCalcTime=0
gx,gy=0
MinRe = -2.0
MaxRe = 2
MinIm = -2.0
MaxIm = 2
Re_factor = (MaxRe-MinRe)/(WIDTH -1)
Im_factor = (MaxIm-MinIm)/(HEIGHT -1)
MaxIterations = 255
UCounter =0
count=0
red={}
green={}
blue ={}
tiles = {}
for y= 1,HEIGHT/tilesize do
    tiles[y] = {}
end

-- Use this function to perform your initial setup
function setup() 
    setInstructionLimit(0)
    watch("deltatime")
    watch("ManCalcStart")
    watch("ManCalcEnd")
    watch("ManCalcTime")
    watch("gx")
    watch("gy")
    InitColorMap()
    ManSetCalc()
end

function InitColorMap()
    for i=0,MaxIterations do
      if RMode == "Iterations" then
                red[i]=255-i*255/MaxIterations
                green[i]=255-i*255/MaxIterations
                blue[i]=255-i*255/MaxIterations
            elseif RMode == "Color" then
                red[i]=(i*8) % 255
                green[i] = (i*9) % 255
                blue[i] = (i * 4) % 255
            elseif RMode == "Binary" then
                if (value % 2) == 0 then
                    red[i]=255
                    green[i]=255
                    blue[i]=255  
                else 
                    red[i]=0
                    green[i]=0
                    blue[i]=0  
                end
            end         
    end    
end



function touched(touch)
  if touch.state == ENDED then --only use the final touch event to scale image window
    dre=MaxRe-MinRe
    dim=MaxIm-MinIm
    gx = MinRe + touch.x/WIDTH * dre
    gy = MinIm + touch.y/HEIGHT * dim
    
    --set the zoom factor and new window borders
    MinRe = gx -  dre / 3
    MaxRe = gx + dre / 3
    MinIm = gy - dim / 3
    MaxIm = gy + dim / 3
    Re_factor = (MaxRe-MinRe)/(WIDTH -1)
    Im_factor = (MaxIm-MinIm)/(HEIGHT -1)
    ManSetCalc()
  end
end

function ManSetCalc() 
    ManCalcStart= ElapsedTime    
    local y, x, n, isInside
    local ylim=HEIGHT/tilesize
    local xlim=WIDTH/tilesize
    local ltiles=tiles
    local imf=Im_factor*tilesize
    local ref=Re_factor*tilesize
    local iterations
    for y = 1, ylim do
        c_im = MinIm + y*imf
        
        for x = 1, xlim do
            c_re = MinRe + x*ref
            Z_re = c_re
            Z_im = c_im
            isInside = true
            for n=0, MaxIterations, 1 do
                Z_re2 = Z_re*Z_re
                Z_im2 = Z_im*Z_im
                iterations=n
                if(Z_re2 + Z_im2 > 4)then
                    isInside = false   
                    break
                end
                Z_im = 2*Z_re*Z_im + c_im
                Z_re = Z_re2 - Z_im2 + c_re
            end            
            ltiles[y][x] = iterations                 
        end
    end 
    ManCalcEnd = ElapsedTime
    ManCalcTime = ManCalcEnd - ManCalcStart  
end

function draw()
    deltatime=DeltaTime
    count = count + 1
    noSmooth()
    background(0, 0, 0, 255)
    scale(tilesize,tilesize)
    local lred=red
    local lgreen=green
    local lblue=blue
    local ltiles=tiles
    local x,y,column, value
    local lrect=rect
    local lfill=fill
    local lipairs=ipairs
    for y,column in lipairs(ltiles) do    
          for x,value in lipairs(column) do
           lfill(lred[value],lgreen[value],lblue[value],255)
            --fill((value*8) % 255, (value*9) % 255,(value*4) % 255,255)
            lrect(x, y, 1, 1)
          end   
    end
end


watch is updated every frame with the value of the variable being watched.

Posting a screenshot

Mandelbrot

Regarding your performance concerns, @alvelda, I just profiled Codea with your Mandelbrot project running and there are no memory leaks during the execution of your script - it stays using a constant amount of memory. However I did find a memory leak caused by exiting and re-entering the viewer, which could potentially cause slow-downs the more it’s done. This is fixed now.

Cool. What/where was the fix? And how did you run the profile? in Xcode?

Any suggestions where to begin on code optimizations? Loops? Closures?

-P

Lua code actually interprets pretty fast, even on iPad 1. The biggest issue seems to be rendering speed, which is something we’re trying to improve by adjusting our shaders.

For example, the biggest speedup in your Mandelbrot project is probably caused by calling noSmooth().

I guess what you can optimise is the user experience, by focusing on how to minimise the delay between interaction and response, not just through optimising code, but through delaying heavy operations until later. There’s not much room for this at the moment in Codea, once stuff like the image class and retained backing modes are available there will be more flexibility.

I have some ideas about how to interleave the set calculation with the draw cycle to lower the apparently stalled intervals.

But it seems to me like there are still some unexplained delays. The one that is most puzzling to me is when the image is static in the tile table array, and draw is being called every 1.3 seconds (apparently the time it takes to draw all the tiles, and go through the execution loop callbacks, etc…) and you touch the screen. Once you do that, there is something like a 2-3 second delay before the the touch event is registered and the recalculation of the display window and mandelbrot set values even starts.

What is going on then? Just completing the draw cycle would take less time. It can’t take a second to manage the touch event callback, as other apps work just fine.

My suspicion is that the tile table loop structure might be causing a massive number of closures that need to be cleaned up, and something like garbage collection and closure management is causing the stall…

Any other ideas of what could be causing the extreme latency on the touch event handling? It might point us towards other issues.