Written by Robin on 2/16/2025
I generally enjoy C++ as a programming language, and I generally find myself lately having to make an active effort to not sound so negative. This article and it's contents will seemingly actively reject both of those statements. Just a fair warning. We're going for frustrated XKCD-style here, not CGPGray explanation. Now, with that out of the way, let's get into details
I recently started programming a game engine, and as a result I've found myself having to do one of the hardest things a programmer could be asked to do: to make something generic. This is because you have to find ways to coerce C++ (and it's compilers) to let you store either multiple variable types in one place (pointer), or switch out large codebases easily.
I'm personally not a fan of the former. It's a style used by a lot of dynamic coding languages out there, and languages like Lua, Python, and even GDScript use it to great effect to make their languages significantly easier to learn and even (as much as programming can be) beginner-friendly! I don't like it because it makes the CPU run slower. There's just a lot more to do and a lot more to manage. If I (really) want to store multiple data types in one place, I can probably just get away with some generic pointer bullshit (void *) and recasting to get the bits back into an easily-accesible structure. But I haven't found myself needing to do that yet. And that is explicitly because of the ladder option I discussed in the last paragraph (switching out large codebases easily). Let's dive into that.
The general way that most game engines (at least as far as I can tell) switch out different backends (essentially libraries that manage rendering/window management etc.) is by (arguably) abusing one of the simpler-to-understand features of the C ecosystem. That would be headers. Headers (generally .h or .hpp) are used at the top of .c or .cpp documents in order to import code. The specific action that imports said code is called include. They point to the relative filename of the header file and basically tell the compiler(?) to copy-paste all the text in that header file into our main code file before actually compiling anything. This can lead to some weird errors with some compilers where, generally, they can do a good job telling you what error came from which file, but I have found that if there's an error at the end of a file (for example, a missing semicolon) the compiler seems to "carry it over" to the main document and report the error there. Very weird stuff. Either way, somewhat easy to diagnose.
Tangents aside (is that the same thing twice?), the nice thing is it means you can literally switch out the text "raylib" with "citro2d" and it will carry all your code over seamlessly(ish) to the new backend (Switching between these different files is where the term "implementation files" comes from, as we're switching out different code implementations for different backends). I'm particularly excited about this because raylib is a library for PC and citro2d is a library for the 3DS. I would love to get some cross-platform action working hopefully. Before I can do that, though, I need to implement
The bread and butter of the modern videogame landscape. Good old-fashioned plain texture rendering. That's how you get your sprites, that's how you get your backgrounds, that's how you get your tiles. Generally, OpenGL (currently the most popular graphics library for computers, soon to be succeeded by Vulkan) makes this easy by making textures rendered onscreen functionally the same as textures rendered in a 3D environment. It all just depends on the particular rendering setting whether or not the 3D perspective is applied. They're still points in a 3D space. Engines like (I believe) Godot and Unity are able to show this when they show rotated perspective views of 2D games in-editor. Sadly, this seems to be a bit more complicated on the 3DS, so I've decided to table 3DS dev for now to focus on just getting the main Windows part up-and-running. Farewell, 3DS. I hope I will see you again soon.
So how does raylib structure it's texture system? Welllllll,, I'm always a bit hesitant to look at raylib as an exclusive guiding example when building my engine, though I will admit it's definitely my main experience with low-level code-bases. Well, it (in it's simplest form) uses two functions, and a struct. The struct choice is a result of being a library made in C instead of C++. Not a big deal, but I figured that both functions could be simplified down into a nice, pretty C++-style class. Apparently, I was mistaken.
Not only did I have issues with just loading the texture in the constructor (not raylib's fault, errors were all C++ (or maybe the compiler?)) were so numorous that I decided to drop it for a more strongly-typed style of texture loading instead, in hopes of making the compiler calm down. I made loading it's own function in my engine. Suddenly, it worked fine! Great! Now, let's just add the unloader to the destructor of the texture class and we'll have some amount of simplicity. Little did I know this would reveal the true error of my ways.
A lot of bugs occur in game development as a result of a mismanagment of code order. Generally this is helped by breaking things down (the step function/draw function system is very nice for this) into smaller functions, but sometimes, when you're developing a game engine for example, you will have bugs within the actual game event order itself. If what I'm saying doesn't make sense, don't worry. I think things will make more sense as we go along.
So. Imagine you're me. You've just finished setting up your genius (if somewhat compromised) design of a class for textures and are excited and ready to get it running in the game engine. And then.
Nothing. The window is blank. You go to check the console and...
INFO: TEXTURE: [ID 1] Default texture unloaded successfully
INFO: TEXTURE: [ID 1] Default texture unloaded successfully
INFO: TEXTURE: [ID 1] Default texture unloaded successfully
INFO: TEXTURE: [ID 1] Default texture unloaded successfully
INFO: TEXTURE: [ID 1] Default texture unloaded successfully
INFO: TEXTURE: [ID 1] Default texture unloaded successfully
INFO: TEXTURE: [ID 1] Default texture unloaded successfully
Well that's not good.
So. Let's take a step back and just try to make a solid description of what we think our current issue is. Our texture, which is declared on a global scale, and as such shouldn't have it's destructor fire off until the game is closing, is triggering EVERY FRAME. Now, the question is,
Welcome to the wonderful world of programming problem-solving. I say this because, while it's admittedly somewhat obtuse to explain and understand, once you do get it it's ABSOLUTELY one of the best and (in my opinion) most fun tools you can have in your arsenal. For the sake of not making this article go on forever and being somewhat managable for me (and my somewhat achy hands), I don't think I'll go too much into the actual debugging process today, though it wasn't really that interesting. The gist is that I made the code pause every time the destructor was called and then checked what was calling that destructor. Who was the culprit?
YES IT WAS SOMETHING STUPID AND HARD TO EXPLAIN!!!! The rDrawTexture function. In order to understand why, we need to talk about the two main ways to transfer variables between functions, classes, etc. (NOTE: There is technically a third way to "pass variables" but it's generally used to modify variables rather than actually use them, so I'm putting it in it's own little category for the sake of this discussion.
The first type is Passing by Value. Essentially what this does is it creates a new variable within the function itself that is separate from the one being passed in, allowing you to modify it freely within the function without messing up random variables.
The second type is Passing by Pointer. This simply passes the function a pointer, which is a memory address, essentially telling it where the variable is stored in RAM. This way, you don't have to create separate variables, copy all the data from one to the next, and then let the garbage collector pick them up. Generally this is fast enough that I do let it happen, but sometimes I'd argue it's more efficient to just pass a pointer than all of the data itself.
Now that all of that is explained, it's time to finally explain...
r.
draw.
texture.
rDrawTexture.
The parameters?
rDrawTexture(rTexture texture, rVector2 position, rColor tint);
You'll notice the texture is being distinctly passed not as a pointer but directly as a value. The whole texture.
This creates a texture variable within the scope of the function. Variables are cleaned up by the garbage collector when they go out of scope. When the function ends, the variable is removed from memory but not before then the destructor is called.
Today's lesson?
The destructor is always called.
And since raylib seems to use some kind of point global memory for it's textures, calling unload once does it for every other object using that texture. At this point I decided culling textures by scope is kind of a weird way to manage textures anyway so I opted to just chunk rUnloadTexture into its own function and vowed (not really) to create a separate texture manager within the engine later. (Probably over the next few days as of writing this, but we'll see. I'm deeply unemployed lol)
This leaves one last note. As I mentioned earlier, passing by pointer is very nice when you don't want to copy all that data separately into the function's scope and just want it to be able to read it easily. Textures are not small. Textures tend to not play nice. Now, that being said I imagine that raylib probably isn't storing the entire texture data itself in the struct, probably just some OpenGL context to the texture in the GPU's memory, but still. This might not even be a limitation of raylib, but just OpenGL itself. It's hard to know what's really happening when a lot of the code you're using is based off systems from years ago or graphics formats that seem to both be staying everpresent and constantly becoming out of date. But. Just to be safe, I updated the function.
rDrawTexture(rTexture* texture, rVector2 position, rColor tint);
I generally enjoy C++ as a programming language, and I generally find myself lately having to make an active effort to not sound so negative. As such, I would like to finish up today by saying that this article may really sound like I'm complaining at the work of hundreds of people far smarter than me, but really it's just me trying to point out the incongruities of our structures while standing on the shoulders of giants (Am I getting too poetic? Maybe I've just been thinking about "Gendering Teddy" too much lately lol). Plugging into these systems elegantly and providing a simple-to-use end experience is the fun part of it. You get your own say. You get to do it the way you want to do it. At the end of the day even something as objective as computing can still be subjective. And I think that's really cool.
Thank you for reading :3 ❤️