Page 1 of 1

Drawing things efficiently - LÖVE internals question

Posted: Wed Oct 24, 2012 8:42 am
by kikito
There have been several questions about efficiency lately on these forums, and those got me thinking.

I've been wondering whether it would be possible to increase the speed at which things are drawn. I think I've thought of a way, but it'd work only if several assumptions are correct, and it'd involve making a change to LÖVE's boot.lua . And still, it'd be difficult to see any benefit until both options where compared.

** The assumption **

My assumption is that LÖVE "translates" every love.graphics call to a C/C++ OpenGL call. This going around between Lua and C++ takes some time. I've been told that this context-switching takes significant time, but I have no means to attest that.

I tried to browse the LÖVE source code, to verify this assumption, but I'm kindof allergic to C++. I would be grateful if someone with more experience with the source code could confirm this.

** The proposed change **

Instead of switching to C/C++ every time, do that only once, by default when love.draw finishes. love.graphics calls would just "fill up" a pure-Lua table. In other words, this:

Code: Select all

love.graphics.rectangle("fill", 100, 100, 200, 200)
love.graphics.draw(image, 10, 10)
Would be translated into a Lua table like this:

Code: Select all

-- love.graphics.buffer will be most certainly an internal thing, not to be shared with the user.
-- I'm giving it that name just to make it clear that it's a buffer accesible from the love.graphics functions
love.graphics.buffer = {
  { "rectangle", "fill", 100, 100, 200, 200 },
  { "drawable", image, 10, 10 }
}
love.run would have to be slightly modified. It would need a new function, which I have called love.graphics.flush(). It would be used like this:

Code: Select all

-- inside love.run()
if love.graphics then
  love.graphics.clear() -- this empties the buffer
  if love.draw then love.draw() end -- this fills the buffer, but doesn't draw anything
  love.graphics.flush() -- this draws everything stored in the buffer to OpenGL, with a single context switch
end
** Consequences **
  • I must stress this: I'm not sure this would make code any faster. There are many things I don't know. But if my assumptions are correct, and a single context switch + parsing a Lua table is faster than multiple context switches, then I think this might be worth trying.
  • Custom love.run implementations would not work any more. So this should be released on a new version - 0.9.x, I'd say.
  • love.graphics methods called from other places than love.draw (i.e. love.load or love.update) would stop working. I don't see this as a problem, since it is highly discouraged. But there is a difference between discouraging something and making it outright impossible.
Please let me know what you guys think.

Re: Drawing things efficiently - LÖVE internals question

Posted: Wed Oct 24, 2012 9:05 am
by slime
The Lua<->C barrier overhead is a bottleneck, but not the bottleneck, especially when talking about the graphics API. I'm not convinced the potential performance gains would be worth the API complexity, confusion, and any number of unforseen consequences. How do canvases factor into that API? They're off-screen render targets, so they can (and will) be drawn to at any time.

If the goal is to reduce or eliminate the Lua<->C barrier bottleneck, then we should wait until LuaJIT 2.0-final comes out and make LÖVE's API use LuaJIT's FFI wherever possible. :)

Re: Drawing things efficiently - LÖVE internals question

Posted: Wed Oct 24, 2012 9:12 am
by kikito
slime wrote:How do canvases factor into that API? They're off-screen render targets, so they can (and will) be drawn to at any time.
I'm guessing each canvas could have its buffer. But you would have to remember to flush the buffer, which I think could be a pain. Good comment, thanks.
slime wrote:If the goal is to reduce or eliminate the Lua<->C barrier bottleneck, then we should wait until LuaJIT 2.0-final comes out and make LÖVE's API use LuaJIT's FFI wherever possible. :)
Yeah, I forgot about LuaJIT. I might need more coffee.

Re: Drawing things efficiently - LÖVE internals question

Posted: Wed Oct 24, 2012 10:01 am
by bartoleo
I think it could be a "boost"... this 'buffer' of draws... or a concept of 'scene' which is missing in LÖVE

Re: Drawing things efficiently - LÖVE internals question

Posted: Wed Oct 24, 2012 11:25 am
by T-Bone
Does Löve need the performance boost? I mean, it's a framework for 2D games. Even my crappy netbook runs pretty much all Löve games over 100 FPS. There are several things that could be improved in new releases of Löve, and performance doesn't feel like a high priority problem. But I could be wrong. Especially if targeting Android and other portable platforms become a large focus, I guess the extra performance could be a significant improvement.

Re: Drawing things efficiently - LÖVE internals question

Posted: Wed Oct 24, 2012 1:26 pm
by vrld
kikito wrote:** The assumption **

My assumption is that LÖVE "translates" every love.graphics call to a C/C++ OpenGL call. This going around between Lua and C++ takes some time. I've been told that this context-switching takes significant time, but I have no means to attest that.
It's actually not that simple, since LÖVE does a lot of abstractions and boilerplate in the background. One call to a love.graphics function usually results in a lot more OpenGL calls. For example, look at the C++ code invoked on love.graphics.setCanvas(), love.graphics.print(), love.graphics.draw(image, ...) (ignoring computation of the transformation) and spritebatch:set(). Of course there are also functions that produce a single opengl call (e.g. love.graphics.setColor() and love.graphics.setPixelEffect()), but in general it's a 1-to-n relation.
kikito wrote:** The proposed change **

Instead of switching to C/C++ every time, do that only once, by default when love.draw finishes. love.graphics calls would just "fill up" a pure-Lua table.
I am not sure that this would help, as we'd have to associate functions with the tags (e.g. "rectangle" -> Graphics::rectangle()). This either means having a huge, unmaintainable switch statement or using a function registry. Since C++ is statically typed and the parameter types and even count vary with each function, we'd have to either pull some serious stunts to call the function or rely on Lua (which does these stunts very elegantly). So in the end we'd have to choose between writing unmaintainable code or still crossing the Lua-C++ barrier, only in a more complicated way than we do now. (Disclaimer: I might be missing something.)


Unrelated to the above:
Crossing the Application <-> OpenGL barrier is also costly, as most of the time that means sending stuff to the graphics card. Given prior knowledge, some functions (e.g. drawing images) could be marginally faster. Unfortunately, we don't have this knowledge. :(

Edit and slightly more related:
Bartbes recently did some profiling. I don't exactly remember the outcome though... :roll:

Re: Drawing things efficiently - LÖVE internals question

Posted: Wed Oct 24, 2012 2:11 pm
by kikito
vrld wrote:(lots of insight)
Thank you for your insight. I see what you mean.
vrld wrote:I am not sure that this would help, as we'd have to associate functions with the tags (e.g. "rectangle" -> Graphics::rectangle()). This either means having a huge, unmaintainable switch statement or using a function registry
Isn't that how things are done in C++ anyway? I'm not trolling. I used to be good at C++, but that was long time ago.

I remember that when you needed something more dynamic than a generic type, you had to use macros. That's just How Things Were, if-you-didn't-like-it-you-could-go-play-with-your-dynamic-toy-languages-you-crybaby.

Hmm... fascinating. Where did that came from? :D

Re: Drawing things efficiently - LÖVE internals question

Posted: Wed Oct 24, 2012 7:38 pm
by qaisjp
nevertheless i like this buffer array...

Re: Drawing things efficiently - LÖVE internals question

Posted: Mon Nov 05, 2012 8:39 pm
by Desty
slime wrote:If the goal is to reduce or eliminate the Lua<->C barrier bottleneck, then we should wait until LuaJIT 2.0-final comes out and make LÖVE's API use LuaJIT's FFI wherever possible. :)
Looks like that will be possible soon - 2.0 RC1 came out last week. I noticed this:
http://repo.or.cz/w/luajit-2.0.git/commitdiff/1d5c2ce4e295562daddfe6ce8e470749f0d42542 wrote: a message polling function might not run Lua callbacks right away and the call gets JIT-compiled. If it later happens to call back into Lua, you'll get a VM PANIC with the message "bad callback". Then you'll need to manually turn off JIT-compilation with jit.off() for the surrounding Lua function that invokes such a message polling function (or similar).
As long as it's possible to work around that, awesome, I'd love to see an official LuaJIT-powered LÖVE.