Monday, September 21, 2026

Designing My Engine's Narrative Format

So! Here you are. You're almost done making an enormous adventure game full of dialogue and descriptions. It's finally time to hand your writing off to an editor, then translators, then voice actors!

...Sorry, what's that? You say that all your game's text is scattered across the code?

It'll take you literally months just to siphon it into legible documents?

You're applying all revisions, translations, etc. by hand?

You're worried you'll miss a few spots?

Repetitive strain injuries?

Oh my!

Introducing... "Resistance"

That's the kind of doomsday scenario I want to avoid in my games. So one day I thought, why not just design that possibility away? I looked into my options and decided I should make my own system from scratch. I'll walk you through how it went.

To start, imagine a gamedev-adjacent person sitting down at a workspace to interact with a video game's text. What types of workflows could they be immersed in?

This is all I could come up with:

  • Writing/Editing
  • Localizing/Translating
  • Voice Acting
  • Integrating into the Game

Can you think of any other operating modes? No seriously, I'm probably missing something.

For me it's crucial to frame my engineering in terms of RESISTANCE. In this case, we'd want certain tasks to be easy or automatic (lower resistance), while others we'd make difficult or impossible (higher resistance).

Within those four workflows above, what specific types of tasks would you wish were easy or hard?

The opposite of "resistance" is "flow",
but "flow" could also mean "flow state",
so I flow toward "resistance"
as I'm resistant to "flow".

Let's take Translating as an example. It should be...
1.) easy for the translator to understand the context surrounding each line, and...
2.) difficult for that person to accidentally skip translating a line.

Pause! With those two goals alone, is a possible User Interface already forming in your head? 

And so on. Now, what happens if a major plot hole is discovered... after the story was already translated?

Oof, well, send your corrected text off to be re-translated... Ok, again with a few more fixes... Now it's been a few months. Wait, did we hear back from everyone? Which localizations were updated?

Ideally, we'd just know! It should be (nearly) impossible for the dev team to be unaware of any mismatches between languages.


Anyway I played around with this framing of resistance in these workflows for a while, thinking about all the specific tasks people would have to do, despite having never done most of them myself...

And here's what I-

BUT FIRST

My system needs a good name! :D

From a thesaurus: Plot, Story, Dialogue, Script, Manuscript, Log, Book, Lines, Episode, Moment, Lexicon, Tome, Codex, Scroll, Event...

Some of these are promising. "Yo, look at this moment file." Slick.

But then I found the clear winner:

LORE.

It fits everything! Dialogue, stories, history, information... It's perfect. It's like the game we are making will someday be an ancient artifact set in stone, as if every file begins with, "Let it be known that in the year 20XX..." "...saving the game was called 'Save Game'."

So there it is. NetMission Lore or NMLore or just Lore files. For a while I've used *.nmlore extensions, but sometimes I use *.txt since that's easier to share with others. Doesn't matter.

(Future editing note: NME's users, including Team SCU, have been using this name since January 2025. "Lore" terms can be found inside all published *.exe files since then. Another organization announced a game-related product called Lore in June 2026. I do not wish to create, nor do I anticipate, any confusion between these two Lores.)

Back on Track

Anyway, here's the amazing system I came up with!

First, you just type into a plain-text file on your computer. Type whatever you want!

Hello, World!
This is some text.
Blah blah blah.

Then attach this file to a game object, like a Sign Post.

As a result, that game object can now access our message with a single function: readLore().

That's it!

...

Oh, you expected more?

Fancy Schmancy

There's more of course, but hold your horses.

Sure, instead of plain text, I could have built a UI tool. An advanced all-in-one story management suite with internal schemas and external integrations.

The thing is, that's risky. Every single task might start out impossible until I eventually add the corresponding features someday. That seems like a curse.

Meanwhile in the dead-simple system I proposed above, everything is already possible in theory, even if extremely frustrating. After all, games are free to process text on their own using code! Same goes for any external software managing these files.

Therefore our focus can be on making desired tasks easier from this paltry starting point. What would be best for the workflows?

I really want you to be able to feeeel the resistance. Where is it? Which steps irk you? What have you been avoiding in life? Why??


 

Who's Asking?

Many games will want to know who is saying each line. Let's allow optional tags like so:

Monster: Hello, I want to pass.
Wizard: You don't have permission to pass.

To support these tags, as the game code extracts our Lore line by line, the engine conveniently provides both fields to use at each step: speaker ("Monster") and text ("Hello, I want to pass.").

There are storage and performance benefits to handling this common pattern directly in the narrative format. It also encourages conformity across games and saves developers some effort.

Congrats! We've successfully lowered those forms of resistance.

Caution

And yet, with that simple feature we've already added new resistance. Check this out. One day, we place a new Sign Post with helpful directions:

North: The Deep Mines
South: The Community College

The player approaches it and... whoops. The game has no idea who these "North" and "South" characters are. That's right, we've introduced unexpected colon interference. (Gross!)

You... shall not... pass... that stool.

Luckily, there's a traditional workaround you might already know, involving this symbol: \. In common practice, that slash creates a special "escape" for whatever immediately follows. So,  North\: The Deep Mines indicates that our colon is just a colon, not meant to be speaking. (Silent but deadly?)

Still, these nuanced concepts increase the workload, as people must implement, debug, document, discover, learn, and think about them. And for any games that don't use dialogue this way, all of this becomes bloat.

In the end... is this feature worth it? I said yes for NMLore, but it's honestly a fair question!

Bonus question: Should I allow spaces in the speaker names?
There's arguably a correct answer.

As you can see, resistance may involve a mess of competing layers. Sometimes you can improve a design by simply patching out one pain point at a time, but if that's all you do you might be missing the bigger picture. That's why I originally imagined many workflows together.


Okay, enough spinning in circles. Here's what actually sets NetMission Lore apart:

Localization

This is an approach I haven't seen before. To translate a Lore file to a new language, copy that file somewhere the engine can recognize, then insert the new target language using >>> markers like this:

That's when she realized something important.
    >>> Fue entonces cuando se dio cuenta de algo importante.
The library is in the city.
    >>> La biblioteca está en la ciudad.

It's not much, but... there's one detail in this design that's incredibly important: the continued existence of the source language.

"Hold on, Troid. Doesn't that mean you're duplicating the exact same data across all these different language files? That feels wrong."

True, there are concerns. For one, I'm relying on the fact that text data is relatively small, usually less than 1MB uncompressed for a full novel.

"No, what I mean is if you edit the original text in any file, the other files won't match anymore. It's sloppy and bug-prone. In your own terms, it's highly resistant to change!"

Ah, but high resistance isn't necessarily bad! Do you remember that hypothetical scenario where we struggled to keep track of which languages were updated? Well, by comparing files, NetMission will now alert us with exactly which spots are out-of-date! Manually dealing with each one... is a good policy to enforce here! Granted, there's additional nuance in making sure the alert doesn't incentivize the wrong workarounds in the heat of the moment, but hopefully you get the idea.

As a nice bonus, with the context all there, you'll never have to open multiple documents side-by-side to see the before-and-after.


The library is in the city.

There's one more localization feature worth sharing: you can use [[[ and ]]] to group lines together.

[[[
  That's when she realized something important.
  The library is in the city.
]]] >>> [[[
  Aquí hay una frase extra para aclarar que el sujeto es la mujer.
  En fin, fue entonces cuando se dio cuenta de algo importante.
  La biblioteca está en la ciudad.
]]]

This lets the localization team completely upend and reorder the entire scene, if that's what it takes to convey the equivalent meaning. Language and culture are so uniquely expressive that I want to avoid artificial limitations, like forcing strict sentence-to-sentence pairings.


As a final note, if translating directly with plain-text symbols like that would be too cumbersome or prohibitive, there's nothing stopping someone from building a GUI over this format to keep it clean on your screen.

In fact, I don't see why you couldn't apply a similar localization method on top of any other plain-text story/narration format! Even as a basic preprocessor layer, this particular balance of resistance levels should shine through.

Then again... I've never actually coordinated game localization before. Forgive me if this approach is already a standard, or if there's a glaring design flaw that will bite me once I cross that bridge.

Do I Have a Choice?

Another unique bit about NMLore...

Each story/narration format I've encountered allows the writer to display a menu to the player, such as a list of available conversations. These options directly shape the format's syntax and runtime, potentially spiraling into more and more advanced features to support every exceptional situation...

Well in NetMission Engine, I said no to all that!

To me, menus just seem too specialized toward specific genres to be worth all the built-in overhead. They almost force games to be ready for that scenario at any moment, even when it wouldn't make sense! If an NME game wants that, it can provide its own functions, such as this one I made up called showChoice():

Troid92: Are you still reading this blog post?
{choice = showChoice("Yes","No")}
#if {choice == "Yes"}
  Troid92: That's awesome!
#else
  Troid92: Are you sure?
#end

Yeah, it's bulkier than native choice syntax. But it's also very freeing. Maybe I'll change my mind on this down the road.

(If you're curious, I took the same scripting language that powers NME's games (Lua) and enabled it inside Lore, too, but in a "sandboxed" way that lets the game kinda become its own engine within NME. For Lua nerds, the reader object returned by readLore() is conveniently its own Lore environment, and this system is coroutine-friendly, meaning showChoice() could play animations and wait for the player to push a button before proceeding.)


Final Score!

There's not much else worth mentioning. NMLore has standard features one might expect, like "chapters" and "jumping" and voiceover hooks. Here's the quick rundown of how it all turned out:

  • Made in my spare time in January 2025: 1 wk designing, 1 wk implementing, 1 wk debugging.
  • Tried to keep memory allocations low-ish (it's basically 1 per Lore file at load time).
  • NME "bakes" Lore into an optimized runtime format (e.g. custom bytecode, de-duplicated strings).
  • In total adds 0.016MB to the final exe, plus 0.044MB to the developer exe for "baking".
  • Lore became an essential part of gamedev in NME. Even level-building got easier from it!
  • I don't use AI, if that's relevant to mention.

It'd be nice to share my Lore system with the world, but that'd be difficult. It leverages NME's existing (private) infrastructure such that there's no clear boundary line. Plus I'm still tinkering with the syntax now and then.

On the bright side, it wasn't exactly a large endeavor. I've spent at least 10x as many hours making this write-up & artwork than I ever spent on the corresponding code! So this is my way of sharing it.

If you're itching to get ahold of NMLore, instead try out the many delightful narrative formats I listed previously. Alternatively, it's a pretty fun challenge to build your own, molding it into precisely what would help your creative expression the most! No cheating.


A final note on the concept of resistance... I've really only scratched the surface here. Resistance can shift paradigms far beyond the scope of programming, and it's become my core engineering ethos from micro to macro. Sadly there's a hands-on nature to it that I may never be able to truly convey through words. I'll keep trying!

For now, take whatever bits you can from this write-up and go finish something cool.

~ Troid92

P.S. At least I'm no longer calling it the "Musty Smell"! That's a story for another day. Settled on the name "Resistance" back in July 2020. 

Thursday, August 6, 2026

If Only I Could Translate My Games


Some video games don't have much text.

Maybe just a title logo and some menu options.

Other games might have over 316,000 words
inside 338 collectible manuscripts,
each translated to 8 other languages!

An average game is probably somewhere in the middle.
The latest game I worked on has around 4,300 words.

 


So how do games manage all that text? How is it structured? Where is it stored? How was it drafted?

I was curious... because I'd always been lazily sticking text directly in my games' code!

if (Player.Pressed(Button)) ShowMessage("Something like this.");

This "hard-coded" approach can only go so far, and last year I was reaching my limit. I wanted my little engine to have some better ideas...


The Shoestring Strategies

Get it?
To be fair, there's nothing wrong with hard-coding the words in a small-enough project. It's the most immediate way to get your text working, and if that's all you need, great!

The problems only arise once you accumulate too much writing and want to perform operations on it. Operations like... translating to a new language, or asking a non-programmer to edit the messages. How will you gather each scene's dialogue and strip away the code? How long will it take you to incorporate the writer's changes back in? Did you miss a spot? And so on.

This and back, times a thousand, would really add up.

It's possible your engine of choice already has a good system for managing text, or maybe these tasks will never bubble up enough to cause any trouble.

Still, as my homemade projects grow, I'd like to avoid any tedious chores or big "oopsie" moments later on.


So the next step might be using external data files, perhaps with a hand-typed structure like json, csv, ini, xml, toml, yaml, etc.

Since these files isolate the writing into a dedicated location, a programmer can easily process just the text in various ways, like reformatting it or measuring the word count. In theory even non-programmers could get involved, so now you have writers writing, translators translating, and voice actors voice acting.

With that, many games could comfortably stop here!

...But my thoughts still weren't settled.

Take a moment to imagine placing game data, like images and sounds, directly inside the source code. Not as text or numbers, but in their natural forms. Like, what if some programming language out there is made of colored pixels, and you write your logical flow with a pencil tool! Then we'd probably end up embedding little images instead of words in our source files.

It just so happens that code and text usually share the same representation, so it's all too tempting to let them intermingle in the same editor screen. That's all that's happening, right?

Or... is there something more going on, something inherent to text that sets it apart?

 

Is Text Actually Just Text?

We've chosen external data files for the moment, but what exactly are we extracting from the code? Just any "strings" of text that may appear to the player?

That could work, but we'll need to link them back to the code somehow. Maybe we give them all labels, as in key-value pairs. In other words, the code asks for "welcome_hero_to_village" (key) and then receives "Welcome to our village, Hero!" (value) from the database, at least when playing in English.

Those labels take extra effort to manage, especially if we use a key-naming scheme like "area1_scene3_line5" to automate the order. So we'll probably have to build custom tools to organize all our keys, which could be a bit of a chore...

Ah, but there's a bigger problem! Between each line of dialogue in most games there might be pauses, reaction animations, musical cues, camera swoops, inventory updates, conditional branches, and maybe even a dragon flyby or two. These things aren't text! And yet there they are, inseparably linked to that text, interwoven through each passage...


The Dilemma

Where do you think those in-between events and descriptors should reside:
with the game code or with the text?

On one hand,

to split one timeline into two different views could get awfully confusing, as you'd have to keep each side in sync. Clearly these types of moment-to-moment controls should simply go alongside the text in the external data files, right?

On the other hand,

we're referencing extremely game-specific mechanics. How could an external data file check on e.g. the scene's current weather to say the matching line? Clearly we should let that type of game logic stay in the game code and request lines as needed, right?

It almost seems like code and text can't always be cleanly separated. Despite focusing on pure text, we've started converging on a slightly different paradigm, one that emerges from the very nature of text, a kind of glue that holds it together. Since text usually flows in order through time... that means the context and presentation may change along with it...

Which means what we're actually looking for is a story-telling or narration format!



Existing Solutions

With the correct realm identified, let's check out what others have accomplished here. Most of the tools below will let you build shareable, playable games with no coding necessary, so maybe you have a fun weekend project ahead! Let your creative juices flow! I'm not affiliated and didn't end up using any of these, but I'll still list what I found.

First up are open-source solutions. These focus on syntax-based specifications for writing your text files, alongside free or paid supporting software to bring those formats to life.

While the above designs don't quite fit my particular style or requirements, I think they're very cool in their own right and worth checking out.

There are also some paid solutions, which aim to have accessible and featureful user interfaces. While I can't speak to their quality, these seem to be the popular ones:

I want to avoid making developers buy 3rd-party tools just to use my game engine, so I had to rule these out at a glance. Again, your needs may be different from mine.

Not listed: Proprietary solutions, and anything else I missed! Let me know what else is out there.

 

The Journey Continues

I have my reasons for not choosing any of the above options. For example, several of them treat localization as an afterthought. That strikes me as so odd! I wanna be able to translate my games someday! Past that, I honestly might just be too stubborn or picky about what I add into my game engine.

Regardless, in January 2025, I set off designing my own proprietary narration format. It took about 3 weeks in my spare time, and it's been a core part of NME's game development process ever since. In the next post, I'll share this format's standout features and how I arrived at them.

~ Troid92

Wednesday, December 31, 2025

From OpenGL to Vulkan

In my last update, I decided to start learning something called Vulkan, or Vk for short. What's Vulkan, again? Why, it's just the hottest new way to put your graphics card to use!


I began climbing the Vulkano (so to speak) in 2023, but despite releasing multiple Vk-powered minigames since then, I'm still climbing it over two years later. Vulkan's reputation for taking nearly 1000 lines of code just to render a single triangle is no joke! But I was ready for the challenge. Let's dive in and see what's been taking me so long.
 

Early Rumblings

 
August 21, 2023: With no idea what I was doing, I searched "vulkan tutorial", clicked on the first result, and got to work. In a single week of leisurely progress, I already had my first triangle in just 845 lines of code (well, with the help of a 4913-line helper system called volk).

I may have just been copy-pasting tutorial segments together, but it felt like I was on a roll! It wouldn't take much work to add more shapes to the scene. Most of those 845 lines were for setting up the program, like establishing a proper connection with the graphics card, reserving the right types of memory, and other mundane details. Soon enough I had two triangles (aka a rectangle)!
 
By week three, the window could be resized (including full-screen mode!) and my shaders were compiling automatically without external software (don't worry if you don't know what that means). Not long after, I had textures and could even animate the scene!
 
In the past, my games had basically just been lots of shaded textures sliding around, so surely I'd covered most of the features by now. All I had to do was fill in some gaps and incorporate this project into my engine.


The Drought

 
And then the realization gradually dawned on me that I hadn't actually accomplished all that much yet. Vulkan is at its best when, just like in this tutorial demo, you know in advance exactly what your visuals require so you can set every structure in stone. When you do this, your GPU turns into The Sphinx and winks at you.

But if anything in your scene needs to change or behave unpredictably, like running scripts from a file, you are left on your own to solve the riddle of moving the entire Egyptian Pyramids around by hand in a well-coordinated improvisational dance.

I had climbed the first hill only to discover the vast desert ahead.

Welcome to Vulkan.
 

Dormancy

 
For the next five months or so, I was stuck in a loop. When I'd start writing the next critical feature, the app would naturally stop working until I finished that part. During this under-construction phase there'd be nothing to see but boring old C++ code, sometimes for weeks. Then I'd pull the feature together, only to be back to exactly where I was before: the spinning squares.
Always those same spinning squares.







I was really stuck with those spinning squares.
 
Spinning squares.

Sure, I could've changed my test app's facade now and then, but that would have been a distraction from the real goals. Every time I arrived back at this scene (you know, the spinning squares), I knew my code was another step closer to solving Vulkan's puzzles. So you may wonder... how on earth could this process be so dizzying for so long?
 

Inside the Magma Chamber


One of Vulkan's early accomplishments was that it "minimizes driver overhead", meaning it removed much of the work required for people to bring Vk support to your hardware and many other devices. That's exciting! The downside is that this work didn't disappear, but instead became the responsibility of every Vk-based program, like NetMission Engine. And that work... is less exciting.
 
To study the level of detail, we could take a magnifying glass and zoom into pretty much any portion of Vk-based code. How about this moment: our game is running at 60 frames per second, and it just directed a new frame for us to display. How do we show that final image on the screen?
WARNING: This section gets somewhat technical. If you begin to experience symptoms of discomfort, feel free to skip to the next section. I think you'll get the point pretty fast.
In OpenGL we'd use a single built-in function, usually something with a name like SwapBuffers(). And we're done! The graphics driver probably does some interesting stuff under the hood there.
 
But in Vulkan? ...

I spent too many hours making this gif.
... Well first, our image isn't in the right format yet. It's been set up to receive new pixels, not display them, which is an important distinction. To convert the image, we just need to fill out a 12-field VkImageMemoryBarrier2 form and a 9-field VkDependencyInfo form. We'll turn in this paperwork to a vkCmdPipelineBarrier2() which, just like the name doesn't suggest, offers image-reformatting services on the side. Be careful: if we're trying to use this GPU to the fullest, our image might be exclusive to only one "queue family" at a time. You see, it's further possible that rendering and displaying might operate on different "queue families", in which case we'll need to submit forms to two such pipeline barriers (warning: three in the case of non-image data) indicating an ownership transfer. Be sure to indicate the correct order twice or it might not go through.
 
Still here? Ah, sorry to say, we haven't even started. Literally! With Vk we tend to operate in a future realm where none of this is happening yet, even our image's rendering. Which means we now have a similar story for the accumulated past data referenced by that image, like the positions & colors of all our objects. The game was generating that info, but soon the GPU needs to read it. This data's properties can vary for complex reasons, so our strategy depends on which memory parts are "host coherent" or not, and/or if any are split between a "host visible" space and "host invisible" space. If you forget to do this data conversion here... it might actually still work on many devices, leaving you with a mystery for the rest. Good luck!
 
Moving forward, it's time to submit all our frame's commands into the correct queues so the GPU can eventually prepare our image. Alert: This is assuming our command-submission model is robust enough to handle all the above synchronization dependencies efficiently, or else we'll have performance warnings or impossible constraints. With this done, at long last we can use vkQueuePresentKHR() to "present" the image, and we'll attach a note saying to wait for the image to be ready first, since it's probably not yet. Whoops, tragedy strikes! Our hard-earned image might get rejected here if the window recently resized, so we'll have to react accordingly by e.g. creating a new "swap chain". Yeah, you get the idea. I'll gloss over that part. ...

... And we're done!

Critical Overload


When I researched my options in 2023, Vulkan was the winner for what I needed. I was craving that extreme degree of control & reliability in my engine's graphics & performance, and there are good reasons why so many other software projects are also switching to Vk.

That being said... Vulkan is... well, frankly it's just a really bad time. I heard its designers had some pressure to get version 1.0 out the door as soon as possible. I wish they'd been given more time to clean it up before it hit the fan.

I'd love to at least be able to rename all the unclear and misleading terminology... or fix the paradigm behind how resources get deleted... Synchronization is so confusing that virtually every Vk tutorial and program got part of it wrong. At the time of this writing, the Official Vk Validation Layer reports that the Official Vk Spinning Cube Demo has a nasty SYNC-HAZARD-WRITE-AFTER-WRITE error, and I actually don't know which side of that to believe. There's so much complexity.

Every software development story has some amount of toil and frustration, and unfortunately my experience with Vk is defined by much of that. But the internet has enough negativity already, so I'll do my best to focus on the fun parts. Plus it's worth knowing that the designers have been listening to the community and improving Vulkan over time, slowly but surely.

Anyway, this part's boring. Let's speed ahead.

Comedy Break


One day, I thought I'd be funny to grab arbitrary regions of GPU memory and interpret them as pixels on the screen. Tada, looks really cool on my laptop!
An artistic rendition
I left the glitchy effect open for a few minutes while I worked on something else. Then I closed out, and... huh, it was still flickering? Even over other windows? On my desktop wallpaper?! Even after rebooting?!?! Uh oh. Screen was busted.
The solution was to fully power-off my laptop for a week to alleviate the charge buildup. That did the trick. Screen was fixed! So I immediately made it impossible for NME to ever do this again.
Yep. Hahaha. Real funny... Anyway...

Magma Formation


February 1, 2024: I was finally able to send my half-year Vulkan experiment to others, and... *drumroll*... the scene ran smoothly on everyone's machines!
 
I added the tutorial's 3D model to keep it interesting

I had come so far, but to be clear there was one crucial stretch left: actually replacing NetMission's OpenGL code with all this Vulkan stuff. At first it was thrilling to demolish so much of NME's old code and restructure everything, but with an April 1 deadline looming, the pressure was on. Cue the montage music!

In a few frantic short weeks, I'd implemented enough to make a new game. Not quite enough to meet every feature of my old games, but that could come later. With a shiny new Vulkan-powered engine at my fingertips, I was finally ready to...

Hold on a second... That isn't montage music we've been listening to. That's more like... boss music?!

Phantom Bug: Final Form

There was a problem. About every 1 in 200 times I'd open my Vulkan-based engine, it would freeze immediately. Empty screen, no response.

Hmm, okay. "Troid," you say, "That's only 1 in 200 times, and only like a dozen people are going to play your upcoming demo anyway. The bug might never even happen. Come on, you're in a time crunch! Fix it later. Can't you just let the player close the window and try again?"

Oh, if only it were that simple. The engine would not close.

I don't mean that it simply wouldn't respond to the [X] button in the corner of its window. For that, the operating system (Windows) would notice and help close it. No, in this case, not even Task Manager could not kill the process. I began resorting to lesser-known PowerShell commands with administrative privileges to try to stab this thing. The world's sharpest knives would phase through this process like it didn't exist. Yet there it was, still clinging onto its ghostly Process ID...

I'm serious. The engine would not close.

Not even shutting down the computer could successfully close my engine. The shutdown screen would just spin its little animation at me until the end of time. I couldn't even put my computer into sleep mode for some reason. No, the only way to kill this process was to hold down the power button and hard-reset. There was no choice on the matter: I couldn't allow any number of my players to experience this phenomenon!

It was truly a finicky, elusive nightmare. Even if I attached a debugger mid-freeze, or if I had the engine output any text to show what it was doing, the bug would temporarily go into hiding. It was still there, but not in a way I could study.

Things were getting down to the wire, and I was scared. This was a total deal-breaker. What was wrong with my code? After so much effort, would I have to call it quits and revert the next game back to OpenGL?

...

Well, no. I figured it out. There's this function called vkQueueSubmit(). I already knew that I shouldn't overlap more than one vkQueueSubmit() at the same time if they share any parameters. But it turns out my machine's graphics drivers had a mistake: I couldn't overlap these at ALL, even with completely different parameters, or else I'd be haunted by the phantom. That would've been nice to know!

I probably could have waited for the next driver update for this problem to go away, but I couldn't wait. It was an easy fix! In code we use things called "mutexes" (which stands for "mutual exclusion") to avoid such simultaneity woes. So to make sure my vkQueueSubmit() functions never overlap, I merged a handful of parameter-specific mutexes into a single general one and called it a day. Slower? Maybe. Peaceful? Yes.

I was free at last.

The Vulkanic Eruption

Artwork by Team SCU

March 24, 2024: I released a preview of NetMission 5.0 (the new Vulkan version) to Team SCU, showcasing the title screen for our next April Fool's game. Other than some folks having to manually update their graphics drivers, the application ran smoothly. All systems go! Let's make a game!

April 2, 2024: Well, we missed the deadline by a day, but our little Vk-powered game was released to the world. Time to find out if the new tech holds up!

...And right away, there was a visual bug on someone's computer. Luckily that was a false alarm: it wasn't related to NetMission 5 or Vulkan. Basically I made a silly mistake in one of the shaders, which was easy to fix.
Artwork by Team SCU. Well, the left side at least. My apologies to anyone who saw the cursed one at full speed before I patched it.

(If you're curious: I was unaware that in GLSL the modulo operator (%) has "undefined behavior" (e.g. could be different from computer to computer) with negative values (which I was using), while the modulo function (mod()) is defined for both positive and negative values (same on all computers). If I could redesign GLSL, I'd make % the reliable one since it's easier to use, and maybe rename the other to positivemod() or fastmod() or something, for people who know what they're doing. Oh well. History is history. Live and learn.)

The Falling Ash


Anyway, we made it. NetMission was running Vulkan instead of OpenGL. How'd we do?

Excluding 3rd-party modules, NetMission had a net gain of 2,705 lines of code in the first draft. All things considered that isn't all that much. The whole engine is around 23,165 lines today, ignoring whitespace and comments.

My previous post focused on an older phantom bug: images failing to load in extremely rare circumstances due to OpenGL-related problems. And guess what? Vulkan ate that bug for breakfast! My engine was healed.

But one of the draws of Vulkan is its performance gains, and my engine... actually got slower! While it's true that the old NME couldn't have rendered 10,000 animated blades of grass like the new one, I could've implemented that feature back in OpenGL, too. For cases we can actually compare, the Vulkan edition was definitely worse: more stuttering, higher CPU and GPU usage, and a few really obvious slowdowns. We were lucky that our first game with it was a retro pixel-art game so it didn't matter much!

Coupled with the fact that NME no longer ran well on Steam Deck, these results were honestly pretty demoralizing. Was this worth all the effort overall? It was hard to say.

...Well the answer was yes. But it was hard to say that with any confidence. I was sad and tired.

Regrowth


With less-than-stellar results, the story couldn't stop there. I picked myself up and continued to refine my Vulkan back end one detail at a time into the present day.
  • Too many command submissions, "semaphores", "barriers", etc.? Fixed.
  • Doesn't display properly on Steam Deck? Fixed.
  • Code-generated shapes (like shadows) are way too slow? Fixed.
  • Can't run my old NME games due to missing features? Fixed.
  • Obscure bugs and slowdowns all over the place? Fixed.
I make it sound quick and easy, but some of those points were fairly large undertakings. Vulkan's systems are intertwined in complex ways, where if I have a discovery or realization about the tiniest footnote in the specifications, the outward ripple effect from that might eventually have me drawing new logic diagrams and upending half my code for another redesign.
 
It's like working within a landscape of microservices that all hinge on each other in specific ways, or watching how video game speedruns change their entire routes over time due to seemingly tiny discoveries

...Ok, fine, I'll admit that this kind of puzzle solving can be invigorating at times.
 

Aftermath

 
Let's end this with a little Q&A.
 
Is the Vulkan-powered NetMission fast enough yet?
Yep! It has generally caught up or surpassed the former OpenGL-based version now, and I could optimize further if needed. Loading is like 2x faster (possibly for unrelated reasons), my games work on Steam Deck again, and I've thoroughly ironed out so many bugs that my engine has never felt more slick. I'm happy.

Are you done adding Vulkan into NetMission?
No, but it's become low priority until a future game needs more. Someday I'd love to finish adding 3D support, as well as "compute shaders" to boost particle simulations, fluid dynamics, lighting effects, fractals, and so on. I also want to improve my usage of "descriptors," and I should probably switch shader languages from GLSL to "slang". Might be fun to experiment with 2D vector styles or shifting a bit further toward a "retained mode" architecture. The list goes on and on.

Would you recommend that others learn Vulkan?
For most solo devs, no. I'm hoping something like a Vk2.0 would be designed better. OpenGL is still (mostly) available and capable, other new APIs exist, and there are free frameworks out there which wrap multiple graphics APIs into a single interface, not to mention the many well-established game engines that further hide the complexity. All of these alternatives are worth considering first.

On the flip side, if cutting-edge performance is crucial to your project and you have the time and resources to grapple with Vulkan directly, then go for it and good luck. I still maintain that Vk was the right choice for me and NME, and little did I know I was in good company. When I first started, Godot had just finished their 5-year evolution into a monumental 4.0 Vk release, Blender was about to begin their nearly 2-year journey to achieve a well-received Vk release, and the floodgates were opening for many others to make the switch. Even FFmpeg recently began using Vk to speed up certain decoders!
 
Is anything else new in NetMission besides Vulkan support?
Yes, luckily I wasn't stuck working on Vulkan this entire time. I completely revamped how spatial level data works, and I invented a new file format for linking text, cutscenes, voiceovers, and even translations in a natural way. Together these two features make it dramatically easier to build game content at scale, and I'm hoping to give them their own write-ups here down the road.
 
As an additional push toward usability, I granted access to a trusted individual (aka my brother) to use NetMission to build a game. So I've had to document my engine, teach it, watch another person use it, and receive their feedback. That process has definitely helped me transform NME for the better in innumerable little ways. More on that another day as well.

In other news, there's no Windows-specific code left in the engine, so in theory I could compile it for other platforms (Mac, Linux, iOS, Android, Switch, ...) if I had the time (I don't). So that's cool. Looks like I'm crossing off many items from the shortcomings list in my 2023 engine overview!
 

 
Anyway, that's all for now. Thanks for reading, whoever you are! See you in the next post.

Sunday, December 29, 2024

Fixing a Bug the Hard Way

The day was April 2, 2024. Team SCU had just unveiled a new mystery download link for any and all who dare peek inside. The few brave souls who clicked open the executable file were greeted by not one, not two, but ten-thousand blades of grass blowing in the wind...


That's right, my game engine was now on Version 5.0! Woohoo! And following that announcement was of course a playable demo for Team SCU's stealth video game.

So what did the new version mean exactly? It meant I finally decided to increase the number to 5.0!

Oh, and there was a pretty huge change, too. Our story begins eight months prior...

Lost in the Woods

It was an ordinary Wednesday afternoon in Camp Troidia when three hikers journeyed into the woods. "We'll head this way. Just keep walking," said the leader.

Not long had passed before the hikers wondered if they were headed the right direction. Suddenly they heard a sequence of snapping sounds, growing more fierce then stopping with a heavy thud. It was as if a tree had fallen in the forest, and everyone was around to hear it! Yet, within the resulting plume of dust, there was no toppled timber in sight. "Just keep walking," said the leader.

Hours passed, and the party grew concerned. Where were all the birds and ferns that should be here? The hikers turned a corner and stopped with a jolt. There was a vast angular ravine beneath them, and they'd nearly stepped over the edge! But with the blink of an eye, the ravine was gone. The ground was flat as far as they could see. "Just keep walking," said the leader.

As the sun descended into the horizon, the hikers knew they were lost. In the final moments of light, they pulled out their Book of Maps. "That's strange. The cover just says 'of Maps' now. I guess the other word must've rubbed off in my pack." They flipped through the pages trying to find something relevant, but all the important panels had become scrambled and indecipherable.

"Wait, where's my arm?!" By now it was too late. Within seconds the hikers were consumed... by darkness!

When the search party arrived the next day, they found only two bodies. The leader was was nowhere in sight.

"Oh I'm right here," a mysterious voice interjected, startling the two sleeping hikers awake. "We're totally fine. I'm just invisible right now. Once in a blue moon, Troid's engine doesn't load an image properly for some reason, so weird visual artifacts can happen."

Yep. That's annoying. But the occurrence is almost too infrequent to care. Well, almost...

Finding the Trail

Building a new feature into my engine is a huge commitment, often spanning several months to a year, so I must always choose my next steps wisely. But in Summer 2023, there were suddenly too many important paths forward, and no deadlines to point me toward any in particular. I was feeling as lost as those fictional hikers...

So I organized my thoughts into a full summary of my engine, highlighting the good, the bad, and the ugly. The hope was that this outline would make it obvious to me how to proceed. Spoiler: it worked! Let's walk through some of the shortcomings to discover the pattern I saw:

"Making games in NME means mostly just writing code, with minimal support for visual editors."
"It'd be nice to have model animations back, using standard formats instead of my custom system."

It takes a lot of code to make a game scene work, and as the one who writes the code, I become the de facto author of many core aspects of the game. In other words, I'm wearing too many hats! The more hats I can offload to others, the better.

However, I'm very picky about data formats I will support in NME. For one, I never want developers to feel they must pay money for 3rd-party software to get the most out of my engine. This rules out the most popular 2D skeletal animation tools out there like Spline and Spriter Pro.

A sequence of forces and procedural reactions allow these abstract shapes to glide around believably. I think it's some kind of bear's head maybe? Who knows.

If anyone knows of a robust free solution for playing with that kind of data, let me know! For now all these 2D figures will continue to be animated with my code and/or with more hand-drawn frames by the artists. In my head I was pondering the idea of projecting 3D formats into 2D space so that we could leverage programs like Blender to make game assets. Hey, that reminds me of another thing...

"I believe NME has a lot to offer as a 3D engine, but certain modules restrict it to 2D for now."

If I wish to start supporting 3D models in my engine, I might as well go all in! NME isn't too far off from going full 3D, but it would certainly take some time. The biggest barrier would be my own understanding of graphics APIs, as NME 4.4 uses only the most basic OpenGL features possible to achieve its results. (Note: a graphics API is how games display things to the screen, and OpenGL is the one I've always used). Which reminds me of another thing...

"The graphics capabilities are not reaching their full potential. NME runs well on older hardware, but someday I'd like to take better advantage of modern GPU architectures. I'm behind the times."

Over two decades behind to be more specific! Well, in a sense. I'm somewhat ashamed to admit that all my games had been using something called Immediate Mode for everything.

Sorry, I cut some corners on this one...

What is Immediate Mode? Pretend we're in a restaurant. I'm a chef and you're a waiter. The customer just ordered a bowl of rice from their phone, so I call you over to deliver it to them.

Now, I could just scoop the rice into a bowl and hand it to you, but that would be too sophisticated! Instead, I reach into the cooker and start handing over one grain at a time. I've gotten weirdly fast at this, but you'll still need some patience. I don't provide you with a bowl, and I don't even tell you in advance how much rice was ordered, so you first have to guess what size receptacle to use.

If you guess too small, the customer has to receive their order in multiple batches. But guess too large and that oversized bowl could've been used for something else, so go do some dishes! Anyway that's what Immediate Mode is like for your graphics driver. Many grains. Much improvisation.

In my defense, NME's games don't exactly have much data to send, so Immediate Mode can actually make a lot of sense as it's very flexible. The first room of Prime 2D renders 1640 triangles (from 3258 vertices), which might sound like a lot, but nowadays a single "low-detail character" in a typical console game might already have 10,000-40,000 triangles (source).

Unfortunately going beyond OpenGL's Immediate Mode is a daunting task for a hobbyist like me, even if the concepts are simple to use once understood. Poorly-named terms like Vertex Array Objects, Vertex Buffer Objects, and Element Buffer Objects (all different things) can easily scare someone away. Still, there was no excuse to put it off anymore. By procrastinating on this next layer, I was preventing NME from displaying detailed 3D models or tons of particles, not to mention that Immediate Mode isn't even available on mobile devices.

Hey, speaking of other platforms...

"NME is Windows-only. Native support for other platforms (Apple, Linux, web) would be nice."

I've tried to keep NME as cross-platform as possible, so there are only a few key locations in the engine that still require Windows. One of these locations relates to spinning up multiple OpenGL graphics threads, for tasks like loading several images at the same time. OpenGL was never designed for multi-threading, so this aspect required me to read through incomplete documentation of "WGL functions", discover best practices through old or obscure message boards, and do some plain old trial and error. Which reminds me of another thing...

"There are two phantom bugs that are incredibly rare but have been haunting me for years."

One of those bugs... is the one where images don't always load correctly! When tens of thousands of players tried one of NME's games, the bug was bound to show up a few times.

Did I look into it? Absolutely. The problem happens only with images, so I meticulously studied every image-related shared-state access. I experimented with extra command flushes and synchronization guards, but I wasn't able to narrow it down. It's possible I was hitting an obscure behavior of OpenGL context sharing, WGL functions, or maybe just certain drivers or hardware, but still, I couldn't shake the feeling that it was somehow my fault. I had no idea how to tiptoe around it, other than making load times longer by reducing image submissions to a single thread, just to stop this once-in-a-blue-moon phenomenon. I didn't want to do that, and my time was spread thin, so the bug continued to haunt me.

But... given all of the above shortcomings of my engine, and the path I just walked through... there was another way. Another way to make everything better.

I could switch to Vulkan.

Beneath the Crust

Vulkan (Vk for short) is a modern alternative to OpenGL. All the crumbs for this fell into place so neatly. Working backward:

Vulkan was designed with multi-threading in mind, so my phantom bug would surely go away. I would need fewer OS-specific calls to initiate that, so the codebase would be a step closer to being cross-platform (assuming MoltenVK stays strong). I'd be forced out of Immediate Mode and pulled closer to present-day GPU architecture, now with fancy techniques like Compute Shaders available at my fingertips. NME would take a big step toward eventual 3D capabilities, which would also add future potential to connect with many standard game asset formats.

Don't get me wrong: OpenGL is fine. Its latest flavors are still relevant and can handle the above. But my particular old-school use of it finally rang clear in my head as the #1 thing holding my engine back. I had two options: learn more advanced OpenGL features, or switch to a newer API.

It wasn't a decision to make lightly. I'd be dropping support for older hardware, and if Vulkan lost its traction in the coming years I'd have taken a big misstep. It's significantly more fine-grained than OpenGL, but that's exactly the kind of control I wanted. It seemed I was ready for such a challenge. It was time for NetMission to overcome its greatest weakness and join the world of cutting-edge graphics!

Luckily this switch would probably only take me a month or two, right? Everything would be up and running in no time. What could go wrong?

TO BE CONTINUED...