Skip to content
This repository was archived by the owner on May 31, 2020. It is now read-only.

Ruby, OpenGL, and Memory

Noel Cower edited this page Feb 12, 2014 · 7 revisions

So, it turns out memory is a tricky topic in Ruby -- it's basically not fun. Which is counter to a lot of coding in Ruby, but if you're using OpenGL in Ruby, you're going to end up doing some amount of manual memory management. As such, I'll go over two options for memory management here as they pertain to use with opengl-core.

opengl-core makes two assumptions:

  1. You already know or have the facilities to learn the OpenGL API.
  2. You know how to work with memory, either via Fiddle or some other gem that works with Fiddle.

Its second assumption's usually incorrect. Not a lot of people use Fiddle even though it's part of the Ruby stdlib (MRI's, anyway). Most people ended up using ruby-ffi, but that's dead now, so look where it got them? A whole lot of code that's going to rot. So, take that, people who told me I should use ruby-ffi instead of Fiddle. Anyway, let's start with Fiddle.

For this entire talk, all sizes and memory offsets are measured in bytes.

Fiddle

Fiddle's a fairly simple libffi wrapper library, which means it deals in more than just memory. That it actually provides tools for working with pointers is probably completely incidental and just in there because a lot of C APIs expect you to be able to use pointers.

So, let's go over memory allocation really quickly:

require 'fiddle' # Hereinafter, this line is implied.

# Allocate a 1024-byte block, get a pointer in return.
a_pointer = Fiddle.malloc(1024)             # => 140660413834240

The pointer returned is a number -- just the address of the memory. Ruby's garbage collector isn't managing this memory, it's there until you explicitly free it. In other words, it's not safe, and you can't really do anything with just the address anyway. Still, it's important to free it:

Fiddle.free(a_pointer)                      # => nil

This isn't really the most convenient way to work with memory in Fiddle and, in fact, it's as low-level as you can possibly get here. So, instead, use Fiddle::Pointer, which is just a simple wrapper around pointers. I can't think of a good way to segue into this code snippet, so here it is:

another_pointer = Fiddle::Pointer.malloc(1024)
  # => #<Fiddle::Pointer:... ptr=... size=1024 free=0x00000000000000>

This gives you a Pointer object to work with, which is a bit more convenient. That said, by default, Fiddle pointers have no free function attached (note: free=0x0). You can assign one, but the assumption is that the function it's getting is a Fiddle::Function, and I'm not covering that here -- that way lies bindings and stuff, and you can figure it out at your leisure. So, by default, this memory is not actually freed when the GC collects it.

Attaching a free function isn't particularly nice because it means getting a Fiddle function, so you can still just call Fiddle.free on the Pointer's address and discard it, as in the example that follows. Keep in mind, however, that this doesn't take care of another that's still got that pointer:

Fiddle.free(another_pointer.to_i)           # => nil

Anyway, the Pointer object gives you two ways to work with the memory you've allocated, though not a particularly useful way. First, you can pull an arbitrary unsigned 8-bit integer at an offset, or you can pull a sequence of integers as a string (the integers making up the characters of the string). So, quick example, pretending I never deallocated another_pointer:

# Assign some byte values
another_pointer[0] = 77                     # => 77
another_pointer[1] = 101                    # => 101
another_pointer[2] = 97                     # => 97
another_pointer[3] = 116                    # => 116

# Pull them out as a string
another_pointer[0, 4]                       # => "Meat"

# Note: ranges do not work for extracting a string.

So, in theory, you could use Array.pack and String.unpack to stuff things into a block of memory, but that's not really a great use of resources. Instead, let's talk about snow-data now.

snow-data

The snow-data README covers the structure bits, so I'll just go over memory management with it and leave it at that.

First off, allocating memory with snow-data is functionally similar to using Fiddle:

require 'snow-data'

a_pointer = Snow::Memory[1024] # Same as Snow::Memory.malloc(1024)
  # => <Snow::Memory:0x003fd5a4f11fa8 *0x007fab4913ae08:1024:8>

There are two important points when working with raw memory in snow-data vs. Fiddle:

  1. The Memory object allocated is garbage-collected if you allocated it using malloc. If you wrap a pointer using Snow::Memory.new or Snow::Memory.wrap,

  2. The Memory object itself provides a large number of methods for working with numeric types and, if you define them, structural types as well.

So, let's say I want to stick a 64-bit float into offset 0 of the memory I just allocated? In Fiddle, which I'll go back to briefly now, I'd do this:

SANAKAN = 1.23456789
packed_sanakan = [SANAKAN].pack('E')

# pretend a_pointer is a Fiddle::Pointer here
a_pointer[0, packed_sanakan.bytesize + 1] = packed_sanakan

You don't really want to write a lot of code like that. In fact, you don't even really want to write functions to wrap code like that, because although packing and unpacking is reasonably fast for general usage, doing this at 60 frames per second isn't reasonable. Instead, I should really just go the direct route. Back in snow-data:

KILLY = 9.87654321

a_pointer.set_double(0, KILLY) # put KILLY at offset 0
puts a_pointer.get_double(0)   # dereference a double at offset 0

# > 9.87654321

Simpler, right? Probably. It's admittedly not pretty, but snow-data doesn't claim to be pretty, it just claims to do exactly what it was built for: working with memory and any number of contraints that comes with.

Now, when I'm done with the pointer, I can just let the GC collect it. However, if you don't need it, why keep it around?

# Because the other code might have the pointer around still, it can
# check if the pointer's been nullified:
a_pointer.null?   # => false

# Free the pointer and nullify the address held by the Memory object:
a_pointer.free!

# The object's still, but at least you know if the pointer's null:
a_pointer.null?   # => true

That's a brief intro to memory management in Ruby. Now let's talk about OpenGL really quickly.

OpenGL in Ruby

This pertains specifically to opengl-core, again, so what I say here may not apply to other OpenGL bindings. If you're using those bindings, you're probably in the wrong place.

Modern OpenGL depends on your ability to load data into buffers, texture objects, and so on, as well as memory to allocate GL objects. For this entire section, when I say object, I mean GL objects -- Ruby objects are different and no longer relevant, so forget about them. If this confuses you, stop acting like there's only one type of object. The world's huge, Ruby is small, you are small, and OpenGL is cool.

Assuming you do not use any of the soon-to-be-deprecated auxiliary tools in opengl-core, you really only have the default GL API to work with. This means you're dealing with pointers, so what can you do?

Well, first, let's allocate a buffer. I'll show two ways to do this: once using strings and once using snow-data. There's no point mentioning Fiddle anymore since it's sitting underneath both approaches.

# Assume the next two statements are implied going forward.
require 'opengl-core'

include Gl

# Assume somewhere in here is context creation and so on. If you need
# a playground for running code snippets, however, use this:
# 

buffer0 = "0000"
glGenBuffers(1, buffer0)
buffer0 = *buffer0.unpack('L')

buffer1 = Snow::Memory.malloc(4)
# Note the use of .address here -- it's needed to give GL the pointer.
glGenBuffers(1, buffer1.address)
# You could just as easily let the GC collect this, but I'll free it one go
buffer1 = buffer1.get_uint32_t(0).tap { buffer1.free! }

Both of these work, though I prefer the latter because it avoids an unpack, array allocation, and string parsing. The problem is that it's also verbose. This is mainly because we're working with a raw block of memory, and it goes without saying that it's not going to be the prettiest. So, I'll define a quick Snow::CStruct to make it a little easier:

# Define a GLObject type with a single field: name. There are other things
# to say about alignment and whatnot with structures, but this is only
# really relevant for unusually-sized and compound structures. This isn't
# one of those.
GLObject = Snow::CStruct.new { uint32_t :name }

buffer = GLObject.new
  # => <GLObject:0x003fdb4d6c9144 *0x007fb69d1b4d08:4:4>
glGenBuffers(1, buffer.address)

# And later, free the buffer
glDeleteBuffers(1, buffer.address)
buffer.free! # <-- Optional because of the GC

And that's creating and deleting a buffer object. If I want an array of buffer objects, though, that's easy too:

# Allocate an array of GLObjects
buffers = GLObject[12]
  # => <GLObject::Array:0x003fdb4cd2a390 *0x007fb69d41ba88:48:4>
glGenBuffers(buffers.length, buffers.address)

# Get a specific buffer
buffer_three = buffers[2]
  # => <GLObject:0x003fdb4d0406ec *0x007fb69d41ba90:4:8>
buffer_three.name # => 3

# Iterate over all buffers
buffers.each do |buf|
    glBindBuffer(GL_ARRAY_BUFFER, buf.name)
    # ...
end

# Free an array of buffers
glDeleteBuffers(buffers.length, buffers.address)
buffers.free!

So, pretty easy. Each element of the array is contiguous in memory, so it's safe to pass the array address to glGenBuffers and such. You can do this for anything in opengl-core that takes a pointer. Of course, the types of structure you'll need to use varies from function to function, so just be aware of the types in use.

In addition, for vertex data, matrices, and so on, you can also use [snow-math], a gem for handling 3D math in OpenGL efficiently. It has the upside to doing math in Ruby (MRI) because almost all of it is implemented in C, so it avoids the overhead of having everything run through the Ruby interpreter. For other Ruby implementations, it may be entirely unnecessary to use C bindings for 3D math.

Ultimately, that's really all there is to it. If you use snow-data, you get easy garbage collection, C-like structures, and getter/setter methods for a wide variety of common numeric types. Any further CStruct defined will also get its own getter and setter, as well.

Possibly more to come on this later depending on what still needs to be addressed according to the biomass.

Clone this wiki locally