<- back to posts

mons (the mountain) - part i: i got sidetracked writing c for a week

2025-02-06 - reading time: 8 minutes

the valley

so i've been working for a while on a large game project and was really starting to get burnt out

it's been my primary focus since graduating from school and progress started slowing to a crawl

it was clear to me that i needed a break from the project to recharge a little bit

so i decided to try and start a project i've been fantasizing about for a long time:

MONS

mons is a monumental undertaking, an excercise in insanity, and my personal code sanctuary

the goal of the project is pretty abstract, i'm just coding whatever i feel like, but doing it with

a couple rules

rule 1: c99

no rust :(

i've been writing rust almost every day for the last 3 years, it's time to get scary

c is like dead simple which is why i think it will be a pretty meditative experience

by meditative i mean mind-numbingly tedious but i'm not in a rush here

i think the absolute worst part of this is the lack of namespaces but i'll live

rule 2: extremely limited dependencies

it's time to get real

if i'm undertaking this journey in masochism voluntary discomfort (only true stoic sigmas get this one) i might as well be learning something while i do it

this would be "no dependencies" but like i kind of want to make some fun stuff at some point

so i have 3 big exceptions

i can use stdlib and posix shit (at my discretion)

i can use "important" libraries

i don't really have a good description for what makes a library "important" but think hardware stuff like opengl/vulkan or xlib

the last thing is that i can use freely available implementations of algorithms i have no chance in hell of understanding, like mikktspace

i just don't see much value in doing that myself

the foot

mons_math

starting out from nothing i decided my first goal would be to spin up a basic 3d graphical application

i decided to start with opengl, i have experience with both it and vulkan and opengl is significantly less work to get started with

before i could even render a basic triangle though, i needed (wanted) a way to represent and manipulate basic linear algebra types such as vectors, matrices, and quaternions

so on the first day i just sat on my couch and typed up a library for exactly that

mons_math/src/
├── mat2.c
├── mat3.c
├── mat4.c
├── quat.c
├── util.c
├── vec2.c
├── vec3.c
└── vec4.c

(i won't get into implementation details too much in this post in the interest of brevity, but all the code is available on my git server)

i found writing the matrix and vector operations super satisfying in spite of the repetition for different dimensionalities

i do wish i had kept in mind that opengl is column-major for matrices, since i wrote mine as row-major like a normal person, but it might honestly be for the best, as it makes using them much more intuitive based on the stuff i learned in math class

once i had this library i was quickly able to spin up a basic 3d app using opengl, along with xlib and glx for windowing and input under x11

first triangle

i had always used glfw for windowing so it was interesting to see how similar it was using xlib (although there were some tricky differences, like windows being created without a depth buffer by default)

auto-embedding shader files

i set up my project to automatically embed shaders in my code which was pretty cool

basically there isn't a c mechanism similar to rust's lovely include_bytes macro, so the best you can really do is generate a constant byte array and store it in a header file

luckily i learned how to use the xxd tool to automatically generate such a header file, and then added a script to automatically embed all files located in my project's embed folder and place generated headers in the corresponding include/embedded location

embed/
└── shaders
    ├── basic.frag.glsl
    └── basic.vert.glsl
include/
├── ...
├── embedded
│   └── shaders
│       ├── basic.frag.glsl.h
│       └── basic.vert.glsl.h
└── ...

then i added it as a fake target in my cmake project and made the main project target depend on it

mons_qoi

not satisfied with a simple solid color, i decided i needed to load images

obviously i wasn't allowed to use an existing loader like stbi

i did take a quick look at the png and jpeg specifications since these are by far and away the most popular texture image formats out there

my lazy ass was not about to write a loader for either of those formats (for now)

for now i decided to look for something easy to implement but like actually respectable (so not ppm), and boy oh boy

enter qoi

this shit descended from the heavens i swear to god

qoi, or the quite ok image format, is extremely simple and extremely fast to encode and decode, and offers pretty decent lossless compression

my decoder is around 200 lines of c i think, which is a bit more than the reference implementation i believe but i tried my best to not refer to it for fun

once i had that i was able to just pop the loaded image data into opengl textures

loaded texture

mons_json

up to this point i was using hardcoded vertex buffers and i wanted to be able to load more complex geometry

i considered using obj for my models but i'm a big fan of gltf so i decided to just jump the gun and go for a gltf loader right off the bat

gltf is stored as json data so i knew if i wanted to have a shot i would need a flexible, dynamic json solution

struct mons_json_value;
union mons_json_entry;

typedef struct mons_json_array {
    struct mons_json_value *values;
    unsigned int len;
} mons_json_array;

typedef union mons_json_value_data {
    char *string;
    float number;
    mons_hashmap object;
    mons_json_array array;
    bool boolean;
    void *null;
} mons_json_value_data;

typedef enum mons_json_value_type {
    MONS_JSON_STRING,
    MONS_JSON_NUMBER,
    MONS_JSON_OBJECT,
    MONS_JSON_ARRAY,
    MONS_JSON_BOOL,
    MONS_JSON_NULL,
} mons_json_value_type;

typedef struct mons_json_value {
    mons_json_value_type type;
    mons_json_value_data data;
} mons_json_value;

i discovered a cool pattern here where a combination of an enum and a union (never actually used one before this project) allowed me to store a variety of types in a mons_json_value while being aware of which type it actually was

mons_hashmap

the performance of my json structures depended on my ability to store key-value pairs in an efficient-to-access way

obvious choice was a hashmap data structure

i just implemented this from memory from my data structures class back in school

typedef struct mons_hashmap_pair {
    char *key;
    void *value;
    struct mons_hashmap_pair *next;
} mons_hashmap_pair;

typedef struct mons_hashmap {
    mons_hashmap_pair **data;
    unsigned int bucket_count;
    unsigned int member_size;
    unsigned int len;
} mons_hashmap;

this hashmap is basically just an array of linked lists that pairs get shoved into based on the hash of the key

for the hash function i just went with the djb2 hash function since it worked well for me in the past

mons_gltf

at long last, i was able to dig into the gltf format

the specification is quite deep and i focused on implementing basic mesh features first, opting to do things like animations, morph targets, scenes, node hierarchies, etc. later

i actually found the process of implementing the loader pretty chill, i especially enjoyed learning how the data was stored in the binary buffers and how to use accessors to read it

after about a day of work i managed to get mesh primitives out of the gltf files and into my in-memory model structure

mesh loaded

(i was using this model for testing)

the textures that came with the model were in jpeg format, which i obviously could not yet load, and the gltf spec doesn't support formats other than jpeg and png

so i said "To hell with the spec!" and switched the image mimetypes to the (illegal) image/qoi and converted the textures using imagemagick anyway

then my textures loaded beautifully

base color texture applied to mesh

at this point i was getting pretty tired lol

i was ready to take a break from my break

but i wanted to leave off in a really satisfying place so i decided to implement normal mapping as one last thing

as i previously mentioned i would use the mikktspace algorithm to do my tangent calculations

normal mapping

and with that i decided i was ready to go back and get some work done on something actually important to me

this whole process took me about a week and was a pretty chill experience

thanks for reading this far, i hope it's not too brutal

- silas