SafariScope

Winter Devlog 1 : From basic structure to mesh loading


This is a companion blog post to a Stardance devlog. You might want to read it first. Stardance is a huge STEM event for ages 13-18 hosted by Hack Club. Learn more about it here.


Why a new engine?

Every time I want to make a game, I tell myself that I'm just going to write a quick engine and that it shouldn't take too long. What I've found over time is that I a) always take more time than anticipated to work on the engine which gnaws time I could use to work on the game; b) always use the same stack, being raylib and libtcc, leading to what could only be described as perfect engine duplicates.

I've personally grown dissatisfied of raylib over time, both in prototyping and production scenarios, and for multiple reasons; its unprefixed C API clashing with Windows' making it much harder to make networked prototypes, its lack of support for video, its cheatsheet that doesn't detail any of the library's data structures, it really being four libraries hiding as one with only two of them having any documentation on raylib's website, and so on. libtcc has many problems of its own; it, being a C compiler I am forcing into a scripting role1, uses C, C requires type identifiers in places C++ doesn't, and if you're compiling as the player is progressing, you often only get a fatal compilation error later in gameplay, so testing a new feature can be excruciatingly slow, et cetera. Clearly, I need a better way to develop games.

Current progress

What I currently have is a basic engine structure that I can expand upon with drivers. The way Winter works is each platform has a mega-driver (derived from Winter::MegaDrivers::BaseMegaDriver) that holds smaller drivers of given types (derived from Winter::Drivers::Base(TYPE)Driver itself derived from Winter::Drivers::BaseDriver). The current Windows mega-driver is based on SDL3 and uses OpenGL in immediate mode for rendering. My next step should probably be switching to retained mode for performance, but I also want to try my hand at texture parsing first because image format support is a common problem I face with using raylib.

Side note about OpenGL: The original plan for Winter was to use Vulkan, not OpenGL. The problem with that is that the engine would crash when trying to gather properties of a physical device, and that looking up the error would result in nothing. And I was still following a guide from Khronos' website. A fun thing about Git is that I made a commit right after adding SDL3 as a dependency so I hard-reset my master branch to that commit2 and switched to OpenGL because I was not going to fight against a graphics API's cryptic errors on top of its sheer verbosity.

Start-up sequence

At start up, Winter chooses a mega-driver, checks which drivers it currently has, and only runs the routines for the corresponding drivers. Basically, that means that as I'm porting the engine to a new platform, I don't need everything implemented for the game to run. If I have graphics implemented but not audio, or even inputs, that's fine, the engine is going to call just the graphics driver. Admittedly, it leads to the funny idea that you can run a game in Winter without the player being able to interact, but realistically, that won't happen unless you recompile the engine with an empty mega-driver to run that way (i.e.: perhaps a server-side build of the game).

Progress on the OpenGL driver

Just to be clear, this section is about Winter::Drivers::Impl::OpenGL defined in OpenGLDriver.h, not about an operating system driver related to OpenGL.

The OpenGL driver currently implements all functions of Winter::Drivers::BaseGraphicsDriver. Currently, as a test, the entrypoint function in main.cpp pushes an AmbientCG mesh of an apple loaded from an OBJ file onto the graphics driver's mesh storage and the mesh renders properly. There is also a mechanism to prevent IDs from shifting when unloading meshes. The list of meshes is just a vector, so we can manipulate it rather easily. When uploading a mesh to the OpenGL driver, it checks whether or not some IDs have been freed by unloaded meshes following a list of freed IDs. If so, it writes the mesh to the first free ID, otherwise it just pushes it onto the mesh vector. When unloading a mesh from the OpenGL driver, it removes the corresponding mesh's triangles, sets it as disabled, then pushes the ID onto the freed mesh ID list. In code, it means that:

size_t Winter::Drivers::Impl::OpenGL::uploadMesh(Math::Mesh& m)
{
	if (unusedMeshIDs.size() == 0) {
		meshes.push_back(m);
		return meshes.size() - 1;
	}

	else {
		size_t id = unusedMeshIDs[0];
		meshes[id] = m;

		unusedMeshIDs.erase(unusedMeshIDs.begin());

		return id;
	}

	return -1;
}

void Winter::Drivers::Impl::OpenGL::unloadMesh(size_t mID)
{
	meshes[mID].tris.clear();
	meshes[mID].disabled = true;

	unusedMeshIDs.push_back(mID);
}

After that, in another function, it simply draws each triangle from each enabled mesh in immediate mode.

Yes, using immediate mode is bad, but I assure you that it is merely temporary in this context. One of the next steps will be for me to switch to retained mode.

The function that loads OBJ meshes is temporary and makes a lot of assumptions. Currently, the engine's OBJ loader assumes that all faces are triangles, disregards texture coordinates and vertex normals, and doesn't even look at textures. It is soon to be replaced with a proper mesh loading system that both fully supports OBJ and supports other formats.

I want to write the mesh loading system myself because one of the biggest recurring problems raylib has is loading large OBJ meshes crashes the entire program. It uses tinyobjloader for OBJ files, and its usage has been a reoccuring problem with raylib for years. The amount of raylib issues about the program having a segmentation fault loading an OBJ file or OBJ files straight up being broken are multiple, although none have been opened since 2025. The only workaround is using something like glTF3, but that can be really annoying sometimes. I oftentimes worked with DOOM (1993) levels (realistically UDMF, but hey, if horror YouTubers don't care about the difference, why should I) as the basis for my various prototypes' maps, and Ultimate Doom Builder can only export them to OBJ, so I'd have to export them to an OBJ file, then import that OBJ file in Blender, then export that Blender project as a glTF file.

Side note: Winter is not going to simply use a plain 3D model as its map geometry, it is going to have a more optimized map system akin to what id Tech and Source have had. I am simply unsure on how I want to approach it for the time being.

The SDL input driver

Winter supports input drivers. I currently have an input driver that uses SDL for inputs, Winter::Drivers::Impl::SDLInput. The problem with this driver is that it only supports detection of window close requests. This isn't really its fault yet, as that is the only thing the abstract Winter::Drivers::BaseInputDriver supports, but it will need updates in the future.

What is next?

The next steps in the development of the OpenGL driver are:

The next steps in the development of the engine in general are:

If you want to follow Winter's progress, check out the GitHub page for the engine, check out the project's Stardance page, or subscribe to either this blog's Atom or RSS feed.


  1. Don't ask. It's somehow more practical than most scripting solutions I've tested so far.

  2. Yes, Git defaults to main instead of master now. I have no clue why my install still defaults to master. 🤷‍♀️ My friend atlas_core made me notice keeping master as the main branch is kind of a Git for Windows thing.

  3. Yes, it's glTF, not GLTF. That pisses me off too. Take it up with Khronos.