C++ constexpr images, Multi-Tracer P. 2

In this blog post, I'll try to use constexpr to generate an image file during compilation. This is going to be a long one, so be free to scroll to a heading in the text that you are most interested in.
For this project I'm using g++9 with c++17. To be more familiar with today's topic, be sure to check out constexpr in the docs.

Making an image in c++

To "make an image in c++", I need to create the image data (pixels), create the metadata of the image (width, height, encoding, etc.) and write it to a file on my disc. But before I begin, I need to choose a file type to store my image.

Picking an image format

There are, of course, numerous options to choose from. More popular ones are jpg, png, bmp, tif and gif. All of them are well-documented, but I have to keep in mind, that the binary data of the file should be created during compilation in the future. So memory allocations and complex functions are out of the way. Most of the formats require a bit too much hassle to set up, so I decided to stick to bmp.

Representing a bmp in code

Although it would be fun to start with creating image data, I wanted to start with creating a bmp in code so that I'm sure there is a reason to do all the other steps. First I took a look at bmp format description on the Wiki and some website I found through browsing StackOverflow.
I decided to use RGBA format (32 bits, 4 bytes) in order to avoid padding every row with unnecessary bytes and to be able to store alpha data in the future. The standard I chose for the info header was BITMAPINFOHEADER, which is 40 bytes in size, supports 4 channels per pixel, compression and color tables. I met some issues with the compiler aligning my struct size, so I had to surround it with pragmas. Here's the complete code, that shows a simple bmp representation:
#pragma pack(push)
#pragma pack(1)
struct pixel
{
	union
	{
		uint32_t color = CC;
		struct { uint8_t a, r, g, b; };
	};
};
struct bmp_fileheader
{
	uint16_t signature;
	uint32_t file_size_bytes;
	uint16_t reserved_1;
	uint16_t reserved_2;
	uint32_t data_start_index;
};
struct bmp_infoheader
{
	uint32_t info_size_bytes;
	uint32_t width;
	uint32_t height;
	uint16_t planes;
	uint16_t bits_per_pixel;
	uint32_t compression;
	uint32_t data_padded_size_bytes;
	uint32_t px_per_meter_hor;
	uint32_t px_per_meter_ver;
	uint32_t color_count;
	uint32_t color_count_important;
};
struct bmp_data
{
	pixel[W*H] data;
};
struct bmp
{
	bmp_fileheader hfile;
	bmp_infoheader hinfo;
	bmp_data hdata;
};
#pragma pack(pop)

Filling the file with data

Next I moved on to filling the structures with the data I needed. Bitmaps are little-endian, so I had to swap bytes all the time. But hey, it's compile-time, so I'm okay with it. I wrote functions to fill the bmp structure. They have a bit of overhead, but I wanted to represent the structures and procedures closer than your average StackOverflow answer, so I manually filled some additional lines. I also made sure that they can be executed during compile time with the constexpr keyword.
Note: all variables must be initialized in constexpr functions.
constexpr bmp_fileheader bmp_fill_hfile()
{
	bmp_fileheader r = {};
	r.signature = (uint8_t('M') << 8) + uint8_t('B');
	r.file_size_bytes = sizeof(bmp_fileheader) + sizeof(bmp_infoheader) + sizeof(bmp_data);
	r.reserved_2 = r.reserved_1 = 0;
	r.data_start_index = sizeof(bmp_fileheader) + sizeof(bmp_infoheader);
	return r;
}
constexpr bmp_infoheader bmp_fill_hinfo()
{
	bmp_infoheader r = {};
	r.info_size_bytes = sizeof(bmp_infoheader);
	r.width = W;
	r.height = H;
	r.planes = 1;
	r.bits_per_pixel = sizeof(pixel) * 8;
	r.compression = 0;
	r.data_padded_size_bytes = sizeof(bmp_data);
	r.px_per_meter_hor = 2835; //72 DPI × 39.3701 inches per metre yields 2834.6472
	r.px_per_meter_ver = 2835;
	r.color_count = 0;
	r.color_count_important = 0;
	return r;
}
constexpr bmp_data bmp_fill_hdata()
{
	bmp_data res = {};
	for (size_t j = 0; j < H; ++j)
		for (size_t i = 0; i < W; ++i)
		{
			uint8_t a = 0xFF, r = 0, g = 0, b = 0;
			if (i < W / 3) r = (uint8_t)( ( ((float)i) / W * 3.0f) * 255);
			else if (i < 2 * W / 3) g = (uint8_t)( ( ((float)(i - W / 3)) / W * 3.0f) * 255);
			else b = (uint8_t)( ( ((float)(i - 2 * W / 3)) / W * 3.0f) * 255);
			res.data[j * W + i].color = (a << 24) + (r << 16) + (g << 8) + b;
		}
	return res;
}
constexpr bmp bmp_fill()
{
	bmp r = {
		bmp_fill_hfile(),
		bmp_fill_hinfo(),
		bmp_fill_hdata(),
	};
	return r;
}
But I had issues with dumping this binary data to a file (with MSB-LSB conversion). I tried doing in-place swaps using macros, but regardless of what I tried, the result was not correct. I take the blame for writing code that doesn't work, but I just couldn't bring myself to go down to the core of the issue. That is why I wrote a conversion function.
constexpr std::array<uint8_t,sizeof(bmp)> bmp_dump(bmp val)
{
	std::array<uint8_t,sizeof(bmp)> r = {};
	
	r[0] = val.hfile.signature;
	r[1] = val.hfile.signature >> 8;
	r[2] = val.hfile.file_size_bytes;
	r[3] = val.hfile.file_size_bytes >> 8;
	r[4] = val.hfile.file_size_bytes >> 16;
	r[5] = val.hfile.file_size_bytes >> 24;
	r[6] = val.hfile.reserved_1;
	r[7] = val.hfile.reserved_1 >> 8;
	r[8] = val.hfile.reserved_2;
	r[9] = val.hfile.reserved_2 >> 8;
	r[10] = val.hfile.data_start_index;
	r[11] = val.hfile.data_start_index >> 8;
	r[12] = val.hfile.data_start_index >> 16;
	r[13] = val.hfile.data_start_index >> 24;

	r[14+0] = val.hinfo.info_size_bytes;
	r[14+1] = val.hinfo.info_size_bytes >> 8;
	r[14+2] = val.hinfo.info_size_bytes >> 16;
	r[14+3] = val.hinfo.info_size_bytes >> 24;
	r[14+4] = val.hinfo.width;
	r[14+5] = val.hinfo.width >> 8;
	r[14+6] = val.hinfo.width >> 16;
	r[14+7] = val.hinfo.width >> 24;
	r[14+8] = val.hinfo.height;
	r[14+9] = val.hinfo.height >> 8;
	r[14+10] = val.hinfo.height >> 16;
	r[14+11] = val.hinfo.height >> 24;
	r[14+12] = val.hinfo.planes;
	r[14+13] = val.hinfo.planes >> 8;
	r[14+14] = val.hinfo.bits_per_pixel;
	r[14+15] = val.hinfo.bits_per_pixel >> 8;
	r[14+16] = val.hinfo.compression;
	r[14+17] = val.hinfo.compression >> 8;
	r[14+18] = val.hinfo.compression >> 16;
	r[14+19] = val.hinfo.compression >> 24;
	r[14+20] = val.hinfo.data_padded_size_bytes;
	r[14+21] = val.hinfo.data_padded_size_bytes >> 8;
	r[14+22] = val.hinfo.data_padded_size_bytes >> 16;
	r[14+23] = val.hinfo.data_padded_size_bytes >> 24;
	r[14+24] = val.hinfo.px_per_meter_hor;
	r[14+25] = val.hinfo.px_per_meter_hor >> 8;
	r[14+26] = val.hinfo.px_per_meter_hor >> 16;
	r[14+27] = val.hinfo.px_per_meter_hor >> 24;
	r[14+28] = val.hinfo.px_per_meter_ver;
	r[14+29] = val.hinfo.px_per_meter_ver >> 8;
	r[14+30] = val.hinfo.px_per_meter_ver >> 16;
	r[14+31] = val.hinfo.px_per_meter_ver >> 24;
	r[14+32] = val.hinfo.color_count;
	r[14+33] = val.hinfo.color_count >> 8;
	r[14+34] = val.hinfo.color_count >> 16;
	r[14+35] = val.hinfo.color_count >> 24;
	r[14+36] = val.hinfo.color_count_important;
	r[14+37] = val.hinfo.color_count_important >> 8;
	r[14+38] = val.hinfo.color_count_important >> 16;
	r[14+39] = val.hinfo.color_count_important >> 24;

	for (size_t i = 0; i < val.hinfo.data_padded_size_bytes;i += 4)
	{
		r[14+40+i] = val.hdata.data[i / 4].color;
		r[14+40+i + 1] = val.hdata.data[i / 4].color >> 8;
		r[14+40+i + 2] = val.hdata.data[i / 4].color >> 16;
		r[14+40+i + 3] = val.hdata.data[i / 4].color >> 24;
	}

	return r;
}
Note: there are no index out of bounds checks for compile-time array index access, so you can get and set absolutely random data from other places in memory without any crashes or errors.

Creating the file

Now all is left is to call these functions and produce a file. Unfortunately, this can't be done during compilation (obviously the file won't reach the end user). So we have to cheat a bit and do some things during runtime. Here's the code:
int main(int argc, char** argv)
{
	(void)argc;
	(void)argv;
	constexpr auto b = bmp_dump(bmp_fill());
	FILE *f = fopen("test.bmp","wb");
	fwrite(&b,sizeof(b),1,f);
	fclose(f);
	return 0;
}
Everything works now! At least on Linux. It's time for the conclusion.

Conclusion

Why did we do this? What was the point? What did we achieve? First, here's the resulting image:


I was able to create a 4-channel 64x16 pixel BMP image! Now this can be used down the line to save data. I want to emphasize, that BMPs have their image data stored upside-down, so in the future I'll have to flip my image to store it correctly.

Compile-time tricks

What was the point in making it compile-time? Well, I wanted to get my compiler to create a file for me, that I'll then be able to save using my program during runtime and open with an image editor of my choice. I used Wikipedia's example bitmap to check if everything is working correctly:


I thought, that if the file data is being assembled during compilation, it must be put somewhere inside the executable. Although I wasn't successful with the debug executable (looks like a lot of compile-time stuff is ignored there and changed to run-time), I managed to find all of the bitmap data in the release version! There it was, alongside my constant strings and other constant static data!


I was very pleased, that everything worked as expected.
In brief, I was able to make the compiler calculate some arbitrary image data, generate an image file data and put it into the executable during the compilation process. Then dump it into a binary file during runtime.
Why did I want this? I wanted to see, how far can I push the compiler. As the name of the executable suggests, I'll try to make a fully functioning simple raytracer, that will trace an image during compilation. I've already laid the groundwork and I'll do my best to succeed.

Comments