The svson.xyz blog

Mythos Engine Update 12-06

Published:
Tags: gamedev c++ libpng
1750 words
9 min read

Decided to start writing up some of my work on my game engine. I don’t have a lot of media from my previous game engine projects, so I’ll give this a shot.

Since this is the first post in the Mythos series, then I’ll give a bit of background. It’s going to be a JRPG-ish 2D tilemap engine. Haven’t got a precise plan for what to do with the engine, so mostly just implementing things I find fun at the moment. First check-in was May 23th where it was just windowing code, then on May 30th I integrated RmlUi into the engine. Today is the third time working on it.

The name (Mythos) came to like I’ve always named my game engines — just pick a random word related to a loose idea of the game vision :). t. author of the “cardgame” engine, which turned from a simple card game engine to a 2D RPG engine, wholly written in C11 and rendered in software :). It wasn’t very good though and a huge mess of code underneath, as it was my first ever engine that actually resembled a game.

The windowing and input is handled by GLFW and the rendering performed using OpenGL 3.2. As already mentioned, the GUI library used is RmlUi (a still updated fork of libRocket). The engine is written in C++17.

Okay, onto the progress now.

Originally set out to implement tilemap rendering today, but ended up doing a bunch of (mostly) unrelated things.

Today I implemented:

  • PNG loading using libpng,
  • Filesystem abstraction using PhysFS,
  • Custom cursor support,
  • Bunch of refactoring.

The filesystem abstraction and refactoring were pretty straightforward, so not going to go into them too deep.

Not much to show right now :)

PNG loading

Oh, man.

I was not expecting PNG loading to turn out such a difficult task. The problem was, that my filesystem abstraction on top of PhysFS isn’t really set up to deal with C++ file streams, so I couldn’t use most of the C++ image loading libraries. All of my file loading use-cases to this point have been to read the entirety of files, so I never saw a reason to implement C++ file streams (and probably/hopefully won’t have one). There is a C-style API for glue code with RmlUi, but I didn’t really want to use that either1.

Cimg doesn’t support loading from memory (at least I couldn’t find a way to do it), DevIL looked to contain a state machine (?) and that didn’t sound nice to my tastes.

I don’t need dozens of formats anyways, so I wrote my own. It’s not like decompressing a chunk of PNG data into pixel data is that compilicated, right? :)

Turns out libpng isn’t that well set up to decompress from memory either… (at least I couldn’t find a way to easily point it to a chunk of memory).

In the libpng documentation I was greeted by a huge wall of text (which turned out to be a very useful and nicely written source, once I got past the initial fear). The example was also very wordy (codey?), as it had multiple examples in one file, but the documentation contained the same code nicely laid out and commented on.

First issue was getting libpng to read from memory. I did find one blog post from 2009 in which a person faced (and solved!) almost the same problem. He wished to read from a custom stream, but required end result is still bypassing the libpng C IO interface in both of our problems. So I had the confirmation that it’s possible.

I’m not going to go too much into it here, as the original blog post goes into it, but to read from memory I ended up using the following read function:

static void png_read_fn(png_structp png_ptr, png_bytep dst, png_size_t sz)
{
    png_voidp io_ptr = png_get_io_ptr(png_ptr);
    if (io_ptr == nullptr) {
        // I prefer C style logging :)
        LOG_ERR("io_ptr nullptr\n");
        return;
    }

    auto ** bytes = reinterpret_cast<unsigned char**>(io_ptr);
    std::memcpy(dst, *bytes, sz);
    *bytes += sz;
}

Since I had already read all of the file, but libpng still wanted to play the reading game with me, I passed a pointer to a pointer to the start of my read memory block in the user IO pointer (with an offset past the PNG header).

Then every time the read function got called I copied the wanted amount of bytes into the libpng buffer and incremented my pointer by the bytes “read” count.

// compressed_img is a std::unique_ptr<unsigned char[]>
unsigned char * png_data_ptr = compressed_img.get() + png_sig_sz;
png_set_read_fn(png_ptr, &png_data_ptr, png_read_fn);

I did using consider fmemopen(), but if this engine goes anywhere then I’d have to implement it on Windows, as it’s missing, and deal with Win32 API :).

Oh, man, that still wasn’t the end of it.

I did get my files correctly loaded… as long as they were RGBA files.

Hmmmm

Everything went wrong after loading RGB or grayscale images. It seemed to contain the correct data, just badly scaled. I double checked all of my coordinate code, all correct. Stepped through the thing in a debugger countless times, never saw anything suspicious. Hooked up RenderDoc and it looks almost like I had a mipmapped image, but that shouldn’t happen (and doesn’t happen with RGBA). Compared my code against the one in the blog post, couldn’t see anything I had written functionally different.

I didn't order mipmapping

Finally, in my search for answers, I stumbled upon the manual for libpng 1.2.5. A paragraph stood out to me…

The first pass will return an image 1/8 as wide as the entire image (every 8th column starting in column 0) and 1/8 as high as the original (every 8th row starting in row 0), the second will be 1/8 as wide (starting in column 4) and 1/8 as high (also starting in row 0).
The third pass will be 1/4 as wide (every 4th pixel starting in column 0) and 1/8 as high (every 8th row starting in row 4), and the fourth pass will be 1/4 as wide and 1/4 as high (every 4th column starting in column 2, and every 4th row starting in row 0).
The fifth pass will return an image 1/2 as wide, and 1/4 as high (starting at column 0 and row 2), while the sixth pass will be 1/2 as wide and 1/2 as high as the original (starting in column 1 and row 0).
The seventh and final pass will be as wide as the original, and 1/2 as high, containing all of the odd numbered scanlines.
Phew!

So… image 1/2 as wide, 1/4 as wide and 1/8 as wide.. seems similar to what I’m seeing. And sure enough, when I edited and saved the image I left “interlacing” ticked.

The solution? Run the image decoder in a loop for the amount of times returned by png_set_interlace_handling().

Phew indeed. Would’ve been a bit easier to solve if I had read the documentation more carefully :).

Custom cursors

Since I wasn’t feeling too artistic, then I used some cursors from a KDE cursor pack as my game cursors, at least for the time being. Picked the Mocu cursor pack, since it looks pretty nice.

First step was converting them from the X11 cursor format to a format the engine can load. That was pretty simple, as there’s a tool for that: xcur2png. Seeing as I was already working on PNG support (or could’ve used ImageMagick to convert them to other formats), it’ll work perfectly.

I thought it’d be real neat to support multiple DPI-s, so I postfixed the cursors according to their DPI level (so _96.png, _128.png and so forth). Then during loading time I can check if we have an exact match or find a nearest match and use that.

I hard-coded a list of some cursors to load during start-up, which then get loaded from image and stored as GLFWcursor*-s in a look-up table (LUT) and selected based on signals from RmlUi.

RmlUi has the functionality to emit cursor change events (it’ll call a function with the cursor name text, ex. “edit” when hovering a line edit box), which I then check against the LUT and then use GLFW to swap out the cursors.

A thing I had never thought about was that all cursors don’t point to the same place. For example, the “edit” cursor can have a different offset than the regular pointer cursor. It does feel pretty “off” when the offsets are off, so I implemented that functionality as well.

Since I was already loading (converted) X11 cursors and remembered noticing some .conf files in the directory, I took a look at those. Sure enough they contain offset (or “hotspot”) information. I wrote a dead-simple parser for that specific tab-delimited table format and loaded those files. I did deviate from the X11 cursor config in the “size” column, which I replaced with the DPI value. Maybe I’ll roll that back if I have to edit a lot of X11 cursor configs ;).

One of the config files looks like this:

#size	xhot	yhot	Path to PNG image	delay(unused in Mythos)
96	17	17	text_96.png	50
128	23	23	text_128.png	50
164	29	29	text_164.png	50
192	34	34	text_192.png	50
228	46	46	text_228.png	50

Code size

I think it’d be interesting to see how much the codebase bloats with each update, so I’ll try to remember to keep posting these :-D.

I’m not too avid of a commenter. I mostly comment “gotchas” and “TODO”-s in the code, as I’ve found those to be the most useful to me, compared to function descriptions, which I barely read and which can take up a lot of time to keep up to date in my hectic development process.

-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
C++                             15            346            185           1298
C/C++ Header                    11            214            247            569
-------------------------------------------------------------------------------
SUM:                            26            560            432           1867
-------------------------------------------------------------------------------

  1. As I see it, all of the files read by the engine are coming from unmodified game data archives, not user supplied sources, so they should be correct files. Even if one of the files fails, then most likely the game couldn’t work either (what’s the engine supposed to do if map tiles don’t load? Player won’t probably enjoy playing the game like that). There is validation, but I’m not too focused on trying to work around file load errors and most of the time just throw an execption and die. ↩︎

Next part: Mythos Engine Update 19-06, and on writing tools

Series outline