Breakout Musings
This is a messy document about some of the issues and thoughts I had while working on breakout in Elixir.
Application Separation
I initially tried to move the resource manager to a separate application in the supervision tree, but the message passing slowdown was too significant. I think this was largely because I was trying to get the resource information (textures/shaders) in the render loop, which needs to be pretty fast, as it’s executed every few milliseconds. This is a pretty obviously bad place to try to separate applications, but you live in learn.
I still think there are ways to structure things as separate
applications to good effect, but I’m not sure exactly how to do that
yet. The main issue is having a single rendering context, I think.
This can actually be worked around with wxErlang, via set_env/1
and get_env/0
,
I think, but I’m not sure how useful it would be due to the message
passing latency. I did, however, have some potential success with
Task
s.
Tasks
My only test here was with matrix multiplication, which is a pretty easy thing to split up into tasks. I’ve previously done some tests with this kind of task-spread-out in my ((also) very bad) ray tracer in Elixir. This kind of math is easily parallelizable. My profiling showed that it definitely sped up matrix multiplication calls, but the new biggest bottleneck was message passing. It didn’t seem to slow things down, but also didn’t really speed things up. I think it’s a reasonable place to spread things out, though.
State Management
This is maybe the biggest issue I had, although that’s mostly self-inflicted. I wanted to start with a mostly straight-forward port from the C++ source, with the intention of “Elixir-izing” it after. Hopefully I’ll make a post about that in the near future.
Updating deeply nested state, especially when it’s in deeply nested branching logic, is not fun. I’m curious to see how managing state with ETS, for example, would improve things - both in terms of code quality and potentially performance.
Guarding is good
@icefoxen on a graphmath issue pointed out that guards on function definitions seem to help with performance. They noticed Wings3D does this, and the results are pretty significant. In my testing with a 4x4 matrix multiplication function, where the matrix is, implemented as a four-element tuple each containing four-element tuples, I found the following:
- Guarding all of the values for one matrix is ideal
- Guarding one value doesn’t make a difference
- It’s not linear - the biggest impact is when adding a guard to the final few values in the matrix
- It doesn’t seem to matter which matrix is guarded against
- Guarding both matrices doesn’t improve things more
- I wonder if that would change with more complex computations
The speed improvement is nearly 1ns per call (roughly 1.2us vs 0.3us). When doing ~60k calls per second to the matrix multiplication function, that’s the difference between that function taking ~15% of processing time vs ~4%. Pretty impressive!
I should guard more. It feels slightly awkward to write
@spec
s and simple “is_type” guards, but it seems worth
it.
Silent failure
One of the main issues I ran into was figuring out silent failures. This is pretty vague and general, but there were a lot of situations where I felt like I should’ve had some kind of louder error, but the application would just crash.
Huh, wait, I really should’ve gotten some messages..
Oops
While writing this I realized I must be doing something wrong. Turns out, I had this:
@impl :wx_object
def terminate(_reason, state) do
Supervisor.stop(Breakout.Supervisor)
{:shutdown, state}
end
Which, in retrospect, obviously just eats any errors that come up. I just wrote it once at some point and didn’t think about it. It definitely made figuring out most errors much more difficult.
Ain’t that just the way.
Image Parsing
This isn’t super relevant, but implementing a (partial) PNG parser was very pleasant. In another project, I’ve written a 3D model loader, which was also nice to write. These types of things are definitely a joy to write in Elixir (or Erlang), and I wish I had more of a reason to publish packages for blob parsing in general. If you have any requests, let me know!
The Future
One of the goals of this project is to see what Elixir can do in the video game space without the use of NIFs. I’m not really sure why - everyone has said it’s a bad idea. And really, it probably is a bad idea. I’m restricted to wxWidgets and OpenGL. It makes a lot of things more difficult or impossible. I have to recreate existing things. The list goes on.
However, this has been pretty positive, I think. Elixir isn’t the best at number crunching by itself, but it’s better than I thought. I’m excited to keep trying things out, and seeing what can be done with Elixir. At some point I’ll need to use NIFs and other packages, but this has been a good experience overall.
Some things I plan on working with next:
- ECSx
- I’ve already played with it some, but I want to implement a game using ECSx with OpenGL rendering that I can share.
- Membrane
- I didn’t include audio in the game because it would require something like Membrane, and I wanted to keep things dependency-free for now.
- Nx
- My testing of maths functionality in plain Elixir is promising, but I want to spend more time seeing if Nx can help with game development math. Wings3D uses OpenCL, which probably makes more sense, but maybe I can make an OpenCL backend for Nx or something.
- Other rendering pipelines
- OpenGL is available for free thanks to
:gl
from Erlang/OTP, but it would be nice to be able to use Metal, Vulkan, and DirectX for rendering.
- OpenGL is available for free thanks to
- Distribution
- At some level, this project isn’t complete until I can distribute a game, ideally via something like Steam. I’ve had some success with burrito and bakeware in testing distributable executables, but more needs to be done.