Elixir OpenGL Details

In my previous post, I mentioned I’d go over some of the details. For the most part, it’s “normal” stuff for working with wxWidgets, OpenGL, Elixir, and Erlang. But there are a few things that weren’t obvious to me, so I’d like to go over some of the code in more detail.

I’ll just reiterate that I’m not an expert in any of these technologies individually, so I may be getting things wrong. Please let me know what can be improved on or corrected.

Overview

I’m going to go over each piece briefly, and note some of the issues or oddities about them. This isn’t a deep dive into how to work with any of these technologies individually, and you would be better off reading about them from their respective docs and resources. This is mostly just about integrating them to make a (somewhat) coherent program.

wxErlang

Working with wxErlang is mostly just normal wxWidgets stuff. The main things to point out are how OpenGL attributes work and handling events.

OpenGL Attributes

The first thing that’s awkward about getting an OpenGL Core Profile context is that you have to specify attributes in an old style, giving an attribList:

gl_attrib = [attribList: [
  :wx_const.wx_gl_core_profile,
  :wx_const.wx_gl_major_version, 3,
  :wx_const.wx_gl_minor_version, 3,
  :wx_const.wx_gl_doublebuffer,
  0
]]

You can find the list of flags available in the wxWidgets docs. The main thing to keep in mind is that some of these flags expect a value to follow, and some don’t. The docs explain which do and which don’t, so it’s not too bad. You also need to ensure that the list is null-terminated. This will come up again with some of the OpenGL code, and it’s easy to forget.

wxErlang Events

Events in wxErlang are somewhat “Erlang-ized”. The docs for wxEvtHandler cover this in some detail, but for a quick explanation, you can use messages or callback functions. For messages, you just give an event type, which is an atom. The atoms come from event handler types, so for something like this:

:wxWindow.connect(frame, :close_window)

:close_window is from wxCloseEventType. It’s kind of awkward to figure out which atom you want for which event. I haven’t figured out how to figure it out other than looking in the docs. I’m importing all the records defined by wx in the WxRecords module, but this doesn’t bring *EventType type stuff over. If anyone knows how to improve this, please let me know. I could define them all myself, but if there’s an easier way that would be nice.

OpenGL

OpenGL code from Erlang’s gl module is often the same as normal OpenGL stuff, but there are a number of small differences and non-obvious issues when using Elixir.

Shaders

The first thing is making sure shaders are null-terminated, just like how the attribList list must be. This is easy to forget, but not too bad. The more confusing part is this:

vertex_source = '...'
vertex_shader = :gl.createShader(:gl_const.gl_vertex_shader)
:gl.shaderSource(vertex_shader, [vertex_souce]) # huh?
:gl.compileShader(vertex_shader)

One of the issues with this is that, if you’re familiar with OpenGL, you may know that glShaderSource takes four arguments. In OpenGL, you can pass multiple shader strings. For simple uses, the call often looks like this:

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);

In gl, this is the only way to assign source text to shaders. As in, count is always 1, and length is always NULL. It still needs to be in a list, though, and must be length 1.

Bits

When passing position vectors to OpenGL, we’re passing three vec3s:

vertices = [
  -0.5, -0.5, 0.0,
   0.5, -0.5, 0.0,
   0.0,  0.5, 0.0,
] |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::float-native-size(32)>> end)

But what’s with that messy Enum.reduce? Well, we need the data to be in a bitstring, with single precision floats, with native endianness. (It doesn’t actually need to be single precision, but it should be.) Bit strings in Elixir are big endian (network order) by default. Which usually makes sense, but not for OpenGL, unfortunately. The native and size modifiers make that easy to correct for, but it is still a bit awkward.

Later on, we see see this:

:gl.vertexAttribPointer(0, 3, :gl_const.gl_float, :gl_const.gl_false, 3 * byte_size(<<0.0::float-size(32)>>), 0)

This is not much more awkward than normal OpenGL when manually specifying strides, but the byte_size(<<0.0::float-size(32)>>) bit is not as nice as sizeof(float). You can shave a few bytes off the code to minimize that, but I like that it’s explicit. In an actual project, you wouldn’t need to do that everywhere, so it’s not a big deal.

Elixir

I don’t think there is too many weird Elixir things, really. The weirdest is probably just dealing with Erlang records via the WxRecords module, but it’s not that odd, I just haven’t seen records used all that much in Elixir. It’s only a few lines of code, and easy to understand and mess around with, I think.

There’s also the whole “interacting with Erlang/C APIs” aspect that sometimes feels not very “Elixiry”, but it’s not too bad.

Erlang

The main thing that’s unfortunate is dealing with all the Erlang macros. The solution of wrapping them in Erlang functions isn’t too bad, but I don’t love it. It feels not very “Elixir-y”, not very “OpenGL-y”, and not very “Erlang-y”.

I was hopeful that I could use Quaff, but it breaks with the macros defined in wx, because they’re not defined “in order”. For example:

-define(wxBK_ALIGN_MASK, (?wxBK_TOP bor ?wxBK_BOTTOM bor ?wxBK_LEFT bor ?wxBK_RIGHT)).
-define(wxBK_RIGHT, 128).
-define(wxBK_LEFT, 64).
-define(wxBK_BOTTOM, 32).
-define(wxBK_TOP, 16).

Quaff doesn’t know what to do with that because ?wxBK_TOP isn’t defined when it tries to read ?wxBK_ALIGN_MASK. A patched version, or a different library that doesn’t have this problem, would be great. I haven’t bothered yet.

Strings

Strings/chardata/charlists/binaries are crucial to keep an eye on when interacting with Erlang modules from Elixir. We use charlists here everywhere.

This is normal, but easy to mix up. Luckily, it blows up loudly so it’s easy to notice and fix.

Conclusion

That’s pretty much it for now. I have some more complex projects that I’m working on, and some initial feelings about things like matrix/vector math (eh), debugging (nightmare), abstractions (Shader module, for example), iterating (extremely nice, since I can just recompile modules from iex and the next draw loop will be updated), games (ECSx), publishing (bakeware/burrito), etc. that I plan on writing more about.

Overall, my initial findings are very positive. Hopefully this might attract some talent from people that actually know what they’re doing with any of these technologies, because I think a world where some real video games could be made with Elixir. Which would be pretty cool!