for most intents and purposes

umbral: a language + runtime for games

Background

For the past month, I've been working on a toy programming language called umbral. The goal is for it to be part of a larger suite of tools called lunar, designed for making interactive graphical applications (read: games). Other planned tools include a debugger, text editor, DAW, who knows.

If you're online, every day you hear about some new DSL designed for AI/ML, but rarely for anything else. So I had an idea: what if someone made a DSL specifically for games? What kinds of features could you embed in a language whose sole purpose is to create games? How ergonomic could you make it without building a full engine? These were the questions I set out to explore.

This post is a high-level introduction to the language; subsequent posts will cover specific features in more detail.

Design Decisions

Inspirations + Motivations

Looking at any umbral code, you'll quickly be able to tell that it wears its inspirations on its sleeve. There are obvious allusions to popular languages like C++, Rust, and Zig, and some hints of lesser-known ones like Hare.

The main idea was that I wanted 70% of the power and expressiveness of a low-level systems language, but the simplicity and ergonomics of a high-level one. I consider umbral a high-level language, almost like a scripting language akin to Lua. When designing it, I made several choices to simplify wherever possible, because a programming language for games doesn't need to be general-purpose. For example, there are no true pointers in umbral, only references and slices. As you can imagine, such a choice makes working with the language very difficult in certain areas: how do you interface with the underlying OS and hardware without pointers? That's where umbral-rt comes in.

umbral-rt

What Love2D is to Lua, umbral-rt is to umbral: a runtime that bridges the gap between umbral's high-level abstractions and everything the OS and hardware actually need. It handles the unglamorous stuff: memory management, system timing, windowing, input, audio, and a restricted Vulkan rendering implementation. The goal is Love2D-level ergonomics with C++ performance, and critically, no garbage collector or VM.

Admittedly, a lot of the runtime relies on third-party vendored libraries. I'd love to write every part of lunar myself, but I simply don't have the time. That said, the libraries chosen are as lightweight as possible and are embedded directly into the compiler as a binary blob, then linked with your application at compile time. This, aside from being a security nightmare, is a natural extension of one of umbral's core design decisions:

...including the assets.

I read The Atomic Application by Sherief Farouk and fell in love with the idea of a fully self-contained application (not quite to the extent of containers, but one that ships with all of its dependencies in a compact format). Many a time have I been bitten by installing an application and needing some driver, some .so file, SOMETHING. Therefore, one of the key design decisions behind umbral is to statically link everything by default.

I am aware that this results in increased binary size. Honestly, I don't really care that much. The only way this poses a real issue is if you have several umbral applications on your machine, then you'll have all the common dependencies duplicated in every binary. Worst case, I can eject the common bits into a .so. But I won't do it quietly!

On the "including the assets" part: instead of a filesystem-based approach like Sherief's usage of PhysicsFS VFS, I drew more inspiration from texture atlases. I built ul, an asset "linker" that collects assets referenced by your application (shaders, textures, audio files), optionally compresses them, and packs them into a .umpack file deserialized transparently at runtime. Eventually, the .umpack gets embedded directly in the binary; for now it stays external to keep debugging sane.

Embedded Shader DSL

This is probably my favorite part of the language so far, but it deserves its own post, so I'll keep this brief. Rather than writing shaders in a separate language (GLSL, HLSL, WGSL, some other SL), you write them in umbral alongside the rest of your code. Same types, same syntax, no duplication. More on this next time!

Relevant Technical Details

Some background on umbral and its compiler, now that you have some context:

Bookkeeping Bits

Key Principles

Project State

If you take a look at the examples directory in the lunar repo, you'll get a sense of what the language + runtime can do today:

Not too shabby for this early on.

Language-level features are added as needed, driven by new examples and standard library work. Whenever I hit an obvious gap or annoyance, I look at what's missing from the language to address it.

Future Plans

The main plan is to keep building out examples and the standard library. Off the top of my head, things I want to tackle next:

My view on the standard library is that it should do just enough to make programmer's lives easier without implementing a full-on engine. Therefore, I like to implement high-level primitives for users to manipulate as they please.

On the runtime side, I want to figure out a good solution for file I/O. Ideally I'd want none, but games need some way to store persistent state. I also want to do some performance profiling of the runtime to ensure it is low-overhead. Beyond that, updates will be on a case-by-case basis.

Conclusion

umbral is still a work in progress, but I'm happy with where it's headed. The goal is to write a full game in umbral before moving on to the other tools in the lunar suite. This will be a good end-to-end test to assess how ergonomic the language is and help me nail down the final shape of the language. If any of this interests you, feel free to poke around the lunar codebase or maybe even use it for a project (it's MIT licensed)!

#compilers #gamedev #programming-languages #umbral