lovatt.dev

March 13th, 2019
13/03/19

Hot-Buffered Popcorn

In which we extract the GUI's Buffer logic and investigate how Lua handles modules.


After a slight detour, we're back to our original goal of pulling as much code out of Core.lua as possible. This time it's the logic dealing with buffers.

You might be saying "buff-what?". If you are, then it means my GUI has done a good job of hiding things away where nobody needs to see them.

Reaper makes a set of off-screen "buffers" available for scripts to draw in - 1024 in all. The GUI uses a buffer to put all of the visible layers together before drawing to the main window, each of those layers has a buffer for drawing its elements, and most of those elements have at least one buffer for drawing themselves.

Why Buffers?

Without somewhere off-screen to store what a script has drawn, we would have to redraw every single pixel of every visible element on every script loop. For smaller scripts this is manageable - the earliest versions of my GUI library did it that way - but for more ambitious projects it can start to be a significant drain on a user's CPU.

At best, that might mean choppy graphics. At worst, your script might cause stuttering and glitching in someone's recordings. Or freeze Reaper entirely if there's too much happening at once... not that I've ever managed to do that.

Buffers let us copy and paste existing content instead, a process known in computer graphics as "blitting". When asked to redraw, elements can blit their body and only draw their caption or current value on top, layers can blit the previous copies of their elements unless they specifically need to redraw them, and the window can blit whatever the layers give it to blit.

At worst, it's still far less processing than before. At best, the script does no work at all unless the user turns a knob or clicks a button.

A Place For Everything And Everything In Its Place

Using buffers requires a small amount of logic to keep the process organized. If elements and layers just pick whatever buffer they want and start drawing, it's very likely that some will overlap and start drawing in the same place.

The GUI's Buffer logic acts as a gatekeeper, tracking which buffer numbers have been used, handing out new ones when asked, and noting when buffers are released by a deleted element or layer. This might sound like a lot of work, but it's only 40 lines of very simple code. 40 lines, I should add, that have very little reliance on the rest of the GUI. Hmm...

There's only one real obstacle keeping the buffer logic from being truly independent. When looking for unused buffers to hand out, it does this:

for j = 1023, GUI.z_max + 1, -1 do

GUI.z_max is yet another value maintained by the GUI on each script loop - it tracks the highest z value the GUI saw when sorting through all of the elements, and was previously used for knowing when to stop while updating and drawing the elements. In this function it's used to keep the buffers matching each z from being assigned to anything else, as each z uses that buffer to draw all of its elements.

The problem is... GUI.z_max is gone. We don't need it elsewhere, since the Layer class handles all of the other logic it was needed for, and we don't want our new Buffer module to be dependent on the GUI state.

A question, though: Do z values actually need to use the matching buffer? Wouldn't they work just as well with any other buffers? In fact, couldn't the Layers just request buffers the same way all of the elements have to rather than getting special treatment?

By adding the following to the Layer:new() method:

self.buff = Buffer.get()

and having the layer use it when redrawing, we can get rid of GUI.z_max completely and just loop straight from 1 to 1023.

The Room Of Requirement

There are a few other things taking up space in Core.lua - a lot of space, collectively - that we can pull out with a minimal amount of effort because, like Buffer, they don't really have anything to do with the rest of the GUI code.

After a brief flurry of cutting and pasting we now have modules for: fonts, colors, math stuff, text functions (drawing text shadows and word wrapping), and a few gfx helpers for things like a filled-in roundrect.

With all of these new modules, it's probably worth a brief discussion of how they're all using each other. Lua has a few ways of having one script load another:

loadfile()

My GUI has been using this since whenever I first pulled all of the element classes out of Core.lua. It takes a file path, it loads it, and gives it to you as if it were a function:

loadfile(lib_path .. "Core.lua")()

There's nothing wrong with it, though it's worth noting that if you load the same file multiple times it will open and process the file separately each time. My GUI avoids this internally for the element classes by checking if it already has a class called "Slider" first.

dofile()

This actually just wraps loadfile(), and their behavior only differs in how they respond to errors.

loadstring()

In this case, you have to open the file yourself using Lua's io functions. The content is passed as a string to loadstring, which will take literally anything you give it and try to run it as Lua code. It's usually not a good idea to run raw code like this, though it has its uses.

require()

This differs from loadfile() in two important ways - it doesn't use the file paths we're used to, and it only ever loads a given file once. While the first can be a bit awkward, the latter makes up for it by saving memory and, crucially, maintaining the same local state for that file no matter how many other modules load it.

Consider the Buffer module we just extracted:

With loadfile()

Modules A, B, and C all need to use buffers, so they all load the Buffer module. Module A requests a buffer and gets back 1. Module B requests a buffer and... also gets back 1. There's a separate instance of Buffer for each module, so the entire purpose of a single provider has been defeated.

With require()

Modules A, B, and C all need to use buffers, so they all require the Buffer module. Module A requests a buffer and gets back 1. Module B requests a buffer and gets... 2! There's only one instance of Buffer, which is exactly what we wanted.

I should note that the correct behavior is possible with loadfile() using a technique known as Dependency Injection - in short, the GUI's core would load Buffer and then pass the instance it receives around to any other modules that need it. There are plenty of situations where that structure would be preferable, particularly when testing complex applications, but you can do it just as easily do it with require().

The other notable difference between the two is both a help and hindrance. Whereas loadfile() is very straightforward:

local Buffer = loadfile("modules/buffer.lua")
--> looks for the_current_script_path/modules/buffer.lua

require() looks for modules based on the global variable package.path, which looks like this:

local path = package.path
--> "/usr/local/share/lua/5.3/?.lua;/usr/local/share/lua/5.3/?/init.lua;/usr/local/lib/lua/5.3/?.lua;/usr/local/lib/lua/5.3/?/init.lua;./?.lua;./?/init.lua"

When you require a module it tries each of the file paths listed in that string, filling in the ? with your module name. Without making sure your script's path is included, the following won't work:

local Buffer = require("modules.buffer")
--> None of the paths listed above are able to match .../modules/buffer.lua

In the case of our GUI it's not the current script's path we want to add, but the GUI itself, and there isn't an easy way to find that. When installing the GUI in Reaper users have to run Set Lokasenna_GUI v2 library path, which looks like this:

local info = debug.getinfo(1,'S')
local script_path = info.source:match[[^@?(.*[\/])[^\/]-$]]

reaper.SetExtState("Lokasenna_GUI", "lib_path_v2", script_path, true)
reaper.MB("The library path is now set to:\n" .. script_path, "Lokasenna_GUI v2", 0)

That script is in the same folder as the GUI's core module, so if we have Reaper store it for us, have other scripts read it, and add that to package.path at runtime we'll be all set. Back in Core.lua, we can add:

GUI.lib_path = reaper.GetExtState("Lokasenna_GUI", "lib_path_v2")
if not GUI.lib_path or GUI.lib_path == "" then
    reaper.MB("Couldn't find the Lokasenna_GUI v2 library. Please run 'Set Lokasenna_GUI v2 library path' in your Action List.", "Whoops!", 0)
    return
end

package.path = package.path .. ";" ..
  GUI.lib_path:match("(.*".."/"..")") .. "?.lua"

That last bit just makes sure the path we're putting in doesn't have a filename attached, adds ?.lua for require to search with, and appends it to package.path.

Now when we require("modules.buffer"), we can actually find it. Neat!

You're So Dependable

After all that, back to what we were actually going to look at - how our modules are accessing each other. At the moment our library's folder structure looks like this:

library
    gui
        elements
            Button.lua
            Checklist.lua
            ...
        buffer.lua
        core.lua
        element.lua
        layer.lua

    public
        color.lua
        font.lua
        gfx.lua
        math.lua
        table.lua
        text.lua

    core.lua
    set-library-path.lua

Slider.lua has quite a few dependencies, so the top of the file looks like:

local Buffer = require("gui.buffer")

local Font = require("public.font")
local Color = require("public.color")
local Math = require("public.math")
local GFX = require("public.gfx")
local Text = require("public.text")

local Slider = require("gui.element"):new()

It's a bit verbose, I'll admit, and down the road we might consider alternative solutions, but one thing it does very well is remove most of the module's dependence on Core.lua. Most. Not all, sadly, but we're getting there.


Next up: We continue to abstract and extract for maximum impact.