Corona/LUA Runtime - Great optimization potential?

Also, it seems like the lookup table optimizations got factored out of bone positioning… I’m not advocating that lookup tables actually improve performance, but if someone wanted to test here’s a bone.lua that I put together this afternoon that uses one…

local Bone = {} local lookup = {} for i = 0, 359 do lookup[i] = math.sin( math.pi / 180 \* i ) end function Bone.new (data, skeleton, parent) if not data then error("data cannot be nil", 2) end if not skeleton then error("skeleton cannot be nil", 2) end local self = { data = data, skeleton = skeleton, parent = parent, x = 0, y = 0, rotation = 0, rotationIK = 0, scaleX = 1, scaleY = 1, flipX = false, flipY = false, m00 = 0, m01 = 0, worldX = 0, -- a b x m10 = 0, m11 = 0, worldY = 0, -- c d y worldRotation = 0, worldScaleX = 1, worldScaleY = 1, worldFlipX = false, worldFlipY = false, } function self:updateWorldTransform (flipX, flipY) local parent = self.parent if parent then self.worldX = self.x \* parent.m00 + self.y \* parent.m01 + parent.worldX self.worldY = self.x \* parent.m10 + self.y \* parent.m11 + parent.worldY if (self.data.inheritScale) then self.worldScaleX = parent.worldScaleX \* self.scaleX self.worldScaleY = parent.worldScaleY \* self.scaleY else self.worldScaleX = self.scaleX self.worldScaleY = self.scaleY end if (self.data.inheritRotation) then self.worldRotation = parent.worldRotation + self.rotationIK else self.worldRotation = self.rotationIK end self.worldFlipX = parent.worldFlipX ~= self.flipX self.worldFlipY = parent.worldFlipY ~= self.flipY else local skeletonFlipX, skeletonFlipY = self.skeleton.flipX, self.skeleton.flipY if skeletonFlipX then self.worldX = -self.x else self.worldX = self.x end if skeletonFlipY then self.worldY = -self.y else self.worldY = self.y end self.worldScaleX = self.scaleX self.worldScaleY = self.scaleY self.worldRotation = self.rotationIK self.worldFlipX = skeletonFlipX ~= self.flipX self.worldFlipY = skeletonFlipY ~= self.flipY end --local radians = math.rad(self.worldRotation) --local cos = math.cos(radians) --local sin = math.sin(radians) local angle = (self.worldRotation - self.worldRotation % 1) % 360 local cos = lookup[(angle+90) % 360] local sin = lookup[angle] if self.worldFlipX then self.m00 = -cos \* self.worldScaleX self.m01 = sin \* self.worldScaleY else self.m00 = cos \* self.worldScaleX self.m01 = -sin \* self.worldScaleY end if self.worldFlipY then self.m10 = -sin \* self.worldScaleX self.m11 = -cos \* self.worldScaleY else self.m10 = sin \* self.worldScaleX self.m11 = cos \* self.worldScaleY end end function self:setToSetupPose () local data = self.data self.x = data.x self.y = data.y self.rotation = data.rotation self.rotationIK = self.rotation self.scaleX = data.scaleX self.scaleY = data.scaleY self.flipX = data.flipX self.flipY = data.flipY end function self:worldToLocal (worldCoords) local dx = worldCoords[1] - self.worldX local dy = worldCoords[2] - self.worldY local m00 = self.m00 local m10 = self.m10 local m01 = self.m01 local m11 = self.m11 if self.worldFlipX ~= self.worldFlipY then m00 = -m00 m11 = -m11 end local invDet = 1 / (m00 \* m11 - m01 \* m10) worldCoords[1] = dx \* m00 \* invDet - dy \* m01 \* invDet worldCoords[2] = dy \* m11 \* invDet - dx \* m10 \* invDet end function self:localToWorld (localCoords) local localX = localCoords[1] local localY = localCoords[2] localCoords[1] = localX \* self.m00 + localY \* self.m01 + self.worldX localCoords[2] = localX \* self.m10 + localY \* self.m11 + self.worldY end self:setToSetupPose() return self end return Bone

@no2games - Good stuff, very condensed library. I like the way you’ve handled the events.

Only thing I can offer at first glance is skeleton:createImage is not returned dynamically resized images. If you need to, you could use:

return display.newImageRect(options.imageDir … “/” … attachment.name … “.png”, attachment.width, attachment.height)

Thanks.

I didn’t realize the attachment passed the width and height. Nice.

I’m happy to commit this code to gitHub and let everyone have at improvements. I think an “unstable” fork/branch of Spine’s LUA Runtimes would also be an interesting idea too. I’ve got a few things I wouldn’t mind changing, and I’m sure having them in GitHub will make it easier for them to merge into official releases.

Anyway, between the three of us (@no2games, @kilopop and @torbenratzlaff) I’m sure we have a fairly substantial community update.

Yeah, sounds good. My Spine library is a little more specific to our book app framework. But you could take a look through and see how it is doing anything you might be able to use.

Currently though there is the issue that calling the animation function repeatedly results in memory increases and framerate drops. This could be same for your skeleton:play no2game?. If you pass your onEvent to a function which repeatedly calls :play do you have resource issues eventually?

I’ll package up the library and post here with an example. I would greatly appreciate any feedback as to what might be causing this. Without knowing exactly how Spine holds data, I originally thought it has to do with some of Spine’s global functions.

I don’t have any issues with multiple calls to skeleton:play(). I make sure a new animation uses the same enterFrame() loop and it seems to be fine.

I only seem to have resource issues with lots of spine animations loaded. It doesn’t matter if it is the same spine or a bunch of different ones AND it seems that most of the time is spent in the enterFrame() loop doing the update(), apply() and worldTransform(). You can see in the wrapper that I check to see if a spine is off screen and basically short circuit the enterFrame() and that helps a bunch, so my assumption is that’s where things are getting bogged down.

My hope is that there’s some optimization to be done in those routines that can deliver some extra perfomance.

Anyway, I’d love to take a peek at your stuff. I haven’t even started on adding easy slot swapping (which i will need to do) and maybe there’s some things I can steal :slight_smile:

Yeah I didn’t appear to have issues with multiple calls until the calls were happening more frequently such as every 2 seconds. Certainly everything is multiplied with multiple skeletons.

Also there is no resource drain if you setup a test condition that plays animations within onComplete e.g.

state.onComplete = function (trackIndex, loop)
    state:addAnimationByName( data )
end

The drain only occurs when you call the animation function.

Anyway, here’s my Spine library, SpineTrigger, collected with an example project:

https://dl.orangedox.com/fwtujtJVvpG2Ey8npr/spineTrigger.zip

This one creates 6 dragons and sets their animations off. When they each trigger onComplete, they call a function which changes the name of the animation and sends it back into the animation function.

There is a performance function which displays framerate and luaMem. You’ll see this quickly degrade from 60fps down to 1 fps. The memory continues to balloon. So something is compounding somewhere each time the animation function is called. I would have blamed the global nature of Spine but it might be something more or less straight forward…

Steal away. I’ll steel myself for the feedback :slight_smile:

I have not enough time to share my SpineWrapper with you, as it fullfills the very specific needs of my project.

But I’d like to share my optimized corona spine runtime with you:

http://torben-ratzlaff.de/stuff/spine%20remastered%20vs001.zip

Just call the spine librabry with require(“spine.spine”)

@kilopop

Concerning the memory issues, it seems to be the fault of your wrapper.

If you call

state:setAnimation(...)

directly inside the “dragon” function the main lua, (just add the need parameters to the event)

there is no memory leak and the framerate stays at 60.

I believe the reason is the “updateState” function.

As calling timers and Runtimes in a quasi recursive function is not a good idea in most cases.

Hope I could spare some time in the future to optimize further.

@torbenratzlaff  Thanks for sharing. I’ll have a look at this today. I’m assuming you would be okay with sharing these updates on gitHub somewhere?

@kilopop, yep, what @torbenratzlaff said… You are creating a new infinite timer every time you call updateState() and only removing it once… I did a quick hack to get it working…

 if not self.removeEL then \_gal.timerStash[name] = timer.performWithDelay( 1, updateState, -1 ) self.removeEL = updateState --allow instance to keep reference to this for pause/ resume purposes --Runtime:addEventListener("enterFrame", updateState) print("Added Timer") end

Replace what you have around line 380 with this, and a new timer will only get created when removeEL doesn’t exist. FYI, I don’t know if I’m a fan of the timers so I’d go back to your enterFrame events. :slight_smile:

I use one enterframe per spine animation, but I’ve thought about optimizing the wrapper to have a single enterFrame() event so things like frame delta only need to be calculated once. That may help your performance too.

@no2games

Yeah, sure I’m ok with sharing it on github.

But I would like to do that myself in the next couple of days.

Sounds good. I’ll hold on to my revisions and submit them when you post.

Thanks!

Hey guys - many thanks for taking a look at the memory issues of my module. @no2games your amendment works a treat. It wasn’t enough that the timer was referred to by the same name on each recursion so that’s where I went amiss.

@torbenratzlaff - I’ll test run your localised version of spine as soon as i get the chance. Have you seen about getting it accepted into the official runtimes? I would suggest contacting Esoteric directly through their forums. Otherwise as soon as those runtimes are updated, all your ammendments will need to be made again…

Hey guys,

sorry it took me so long to upload to github.

Here’s the link of the fork.

https://github.com/torbenratzlaff/spine-runtimes

Would be great if you contribute your changes/feedback there :slight_smile:

Looks good. I’m on vacation for a couple weeks, but I’ll try to get my wrapper updated for this and add the pre-calculated sin/cos/rad functions as soon as I can. Nice work.

Would be great for the performance to implement the lookup table.

But I wonder, if slow rotation would look a bit strange, because you round the angle to the next integer.

Soo, anything new on this front?

for what it’s worth…, just be sure to benchmark any proposed trig lookup on an actual device, cuz it often doesn’t pay off.  math.cos (et al) are native calls, whereas a table lookup needs:  conversion of the angle to index, a proper modulo handling negative values, and finally the table indexing itself.  and all that interpreted indexing code often works out to be slower than just calling math.cos directly!  so you may lose accuracy AND performance.

I did update Bone.lua to use a single lookup table, but I agree since Corona itself is capped at 30 or 60FPS unless you have performance issues on a device there’s no reason to implement it. And you do see “clicking” into angles with slow rotations of big objects.

Here’s a snippet of the lines I added. It’s pretty easy to comment/uncomment to see it both ways.

I really haven’t had a chance to evaluate your fork of the spine runtimes, because I’m working on a game release right now and we’re trying to lock things down and I don’t want to introduce any new code. I will take a peek the second things slow down.

local Bone = {} local lookup = {} for i = 0, 359 do lookup[i] = math.sin( math.pi / 180 \* i ) end function Bone.new (data, skeleton, parent) if not data then error("data cannot be nil", 2) end if not skeleton then error("skeleton cannot be nil", 2) end local self = { data = data, -- continued -- self.worldScaleY = self.scaleY self.worldRotation = self.rotationIK self.worldFlipX = skeletonFlipX ~= self.flipX self.worldFlipY = skeletonFlipY ~= self.flipY end --local radians = math.rad(self.worldRotation) --local cos = math.cos(radians) --local sin = math.sin(radians) local angle = (self.worldRotation - self.worldRotation % 1) % 360 local cos = lookup[(angle+90) % 360] local sin = lookup[angle]

Searched the web for some validation and found a few (not Corona) test that other developers shared.

The conclusion seems to be, that it depends which framework you use and how it handles the math calls.

But the difference is so small in comparison to other optimizations (e.g. localising) that it isn’t worth the effort and downsides to use a lookup table instead of a localized math function.

@no2games

did you upload your spine wrapper to github ? 
i need for a good spine wrapper 

Regards 
Jens

@blomjens1 Not pushed it github yet, but it’s pretty much the code I posted on the first page of this thread…