4 Hugues Ross Writes a Devlog: Console Programming 3-Week Project 1 - Texture Loading in DFGame
Hugues Ross

2/11/16

Console Programming 3-Week Project 1 - Texture Loading in DFGame

What is this?

In one of my current classes, students are expected to design and implement coding projects of their choosing, then provide a writeup on their blog or portfolio where they explain the project. This is my first writeup, and you can expect 2 more similar posts later in the year.

As I've mentioned previously, one of my goals for the year is to ditch the DFEngine project and go back to developing games from the ground up. Instead of building on a large, monolithic engine, the goal is to implement lots of helpful code in libraries, which I can use to get started on projects quickly. I gave the library, DFGame, a short mention in a recent Halberd update, but I didn't go particularly in-depth as to what I'm actually doing with the engine. Here, I'll be documenting the work I've done on this engine over the past 3 weeks.

A General Overview of DFGame

Technically speaking DFGame is actually a set of 5 libraries broken up into the following modules:
  • common - The common code that all other modules rely on. Contains various low-level systems such as IO and logging.
  • editor [empty] - The editor-specific backend. Examples of editor-specific backend code include debug drawing, or handling assets that are baked or repackaged in the final exported game.
  • game [empty] - The game-specific backend. Mostly intended for game logic. This is deliberately separated from the frontend code to allow embedding it directly into an editor instance.
  • editor-front - The editor-specific frontend. Written in Vala, and provides helpful widgets and classes for building a graphical GTK3-based editor interface.
  • game-front [empty] - The game-specific frontend. Examples of game-specific frontend code include graphics context creation, or main loop handling.

Note: Why Vala?

I chose to use Vala for all developer front-end code for the same reasons as with Halberd. Writing GTK code (or any UI code, for that matter) in C is a huge pain. Dealing with inherently hierarchical systems like a complex user interface is one of the few most obvious advantages of object-oriented programming as a whole.

I chose Vala because it is fully compatible with C, even more than C++. Vala uses vapi files and gobject-introspection to convert a C#-like object-oriented syntax directly to GObject C, allowing for it to both use and be used in C code. I consider the flexibility that Vala affords me to be extremely valuable.

Using this system of modules gives me a few specific advantages:
  • It physically separates my code, which organizes it and also forces me to decouple distinct sections of my codebase as much as possible.
  • I can write functions that do the same things in different ways for different purposes. For instance, I can write an editor asset loader for my editor that reads in an xml file, then write a game loader for the same asset that loads it from compressed binary.
    • This same principle can be applied to a number of other uses, such as handling logs. If I had to specifically prefix all of these functions with whether they belonged to the editor or the game, then sort through all of them every time they were needed, I'd probably sob into a pillow before going to sleep at night.
  • It allows me to link game logic and rendering into my editor for play-in-editor support, (A necessary feature in any modern engine) without having to perform folder wizardry with multiple main functions.
I've taken the time to write pkg-config files for every module, so linking against DFGame on any *NIX system is a breeze.

The DFGame Texture Loader

In DFGame, textures are stored in a super-simple texture struct:

typedef struct texture
{
    GLuint    handle;
    uint16_t  width;
    uint16_t  height;
} texture;

The handle refers to a texture buffer handled by OpenGL, and is guaranteed by all texture-related functions to be in 32-bit RGBA format.

Note

When I refer to the bit depth of an image, I'm referring to the number of bits per-pixel, not per-channel.

You might notice that I'm using 16-bit unsigned integers to store the texture dimensions. As it turns out, a 32-bit true color image with the maximum dimensions that this struct supports would take up approximately 135 Gigabytes of VRAM. (Also, most OpenGL implementations don't support maximum texture resolutions beyond 4096 anyway)

The Texture Loading Process

Next, I'm going to go over the higher-level part of the texture loader.
The main entry function for the loader is load_resource_to_texture. This function takes in a resource string pair, and returns a newly allocated texture struct containing the image pointed to by the pair. Alternatively, there is also load_resource_to_texture_buffer, which load_resource_to_texture calls. That function gives a nice, 32-bit RGBA buffer for more specialized loaders such as the Halberd tileset loader.

I'm going to go on a quick tangent to explain what a resource string pair is, and how it actually works. If you don't care about that and just want to read about the loaders, feel free to skip to the next heading. The concept of the resource pair goes back to Halberd's development, when I was looking for ways to handle assets in the editor and the game with the same functions. Ultimately, I ended up breaking up file paths into 3 parts: The base path, the resource path, and the filename.

The base path is the path to the actual content directory of the project. In other words, it points to wherever you store all of the game's assets. The base path is kept as a hidden static variable in the C file that handles most resource-related functions. To get the actual filepath to an asset, I have the construct_extended_resource_path function. Most asset loading functions, including the texture loader, use this function to get to the actual file they need. The function simply takes in a resource string pair, then concatenates it with the base path (with some basic error checking, of course). As simple as it is, it gives me a lot of flexibility. I can move the base path all I want, and the engine won't care at all. Furthermore, resource string pairs let me easily separate the actual filename from the path. This is very important for any filename-related functions, because it ensures that I don't accidentally read from/write to the path to the file when messing with the name.

The Loaders

Next, I'll briefly talk about the loaders. They're all based on the lib[type] libraries, but each one is a little different so I think they're worth discussing a little bit.

PNG Loader

My PNG loader is the oldest one that I have, dating back to before this project, and even well before I started Halberd work this summer. Despite that, it's still not a full implementation. Also, because it's so old, it's starting to need some real cleaning up. Notably, there seem to be a few error cases missing still. It works reliably, but could probably benefit the most from another pass.

JPEG Loader

libjpeg is a strange beast. The other libraries were very well documented, but libjpeg contained no API docs at all. From my understanding, this may have been intentional, in order to ensure that users were sufficiently good at working with C to use it. Thankfully, there were some well-commented example applications, so it could've been worse. To distinguish the JPEG test texture, I exported my PNG test with as many artifacts as I could muster (by setting the quality to 5% in the Gimp).

TGA Loader

I started the TGA loader before finishing the JPEG loader, but the end was delayed because of an amusing bug caused by a momentary lapse of judgement. When converting the raw data from a TGA file to the internal standard format that I use, I use the byte-depth to determine where to move bytes around. However, I accidentally passed the bit-depth in instead, resulting in an 8x smaller image! Interestingly, this didn't crash the program. It's definitely worth keeping an eye on memory access, that's for sure.

Tiff Loader

Sadly, the TIFF loader was fairly unremarkable. However, I think it's safe to say that the TIFF and PNG formats are the top two formats that I'm handling, as far as features go. Looking over the documentation surprised me quite a bit, because I don't usually use TIFFs very much. In particular, the ability to store multiple images in a TIFF may be worth revisiting at some point.

Conclusion

So, there you have it. I think this first project went quite well, fitting perfectly into the 3-week slot I carved out for it. My next project is probably going to involve Unix sockets, and be less related to games and more to general software development.

I'll be putting the dfgame project up on my software page soon. Until then, here's a link to the dfgame git repo.
Post a Comment