The Indulgence of Engine Porting
After last week's self-reflection, this week probably should have been a fresh new start applying those lessons.
Instead, I honestly felt a bit emotionally bruised. I took a 'week off' and went back to what I find most comfortable - messing around in the guts of my custom engine.
Specifically, the Linux port.
Text Rendering is Hard
One of my engine's biggest achievements is its text layout and rendering system. It's also been one of the hardest parts to get right. In today's world you can't ignore internationalization, and Unicode is complex.
Getting text on the screen on Linux has meant resurrecting the FreeType / HarfBuzz backend. As usual, things had rotted, and I ended up having to do another pass through the layout and line breaking code.
HarfBuzz is an amazing piece of software, but it's only the final piece of the puzzle when it comes to arranging a block of text.
Before you can start shaping glyphs, you have to do a bunch of analysis on your text in order to break it into pieces that all have the same properties:
- Each piece has to have a single font.
- Each piece has to be a single language.
- You need to identify the script.
- In case of Arabic or Hebrew, you need to run the dreaded bidi algorithm.
- Unicode codepoints aren't characters, so to position a cursor you have to identify clusters. Have fun with 'emoji zwj sequences'!
- Word wrap means yet more break analysis.
- And after shaping, the possibility of ligatures mean you have to reconstruct missing cursor positions between glyphs.
The Tower of Babel has a lot to answer for.
The Contrarian's Rule of 3D APIs
The contrarian's rule of 3D APIs states:
If there is more than one way to arrange the mathematics of 3D graphics, each individual 3D API will pick a different way.
We see this with left-handed and right-handed coordinate systems, column-major and row-major matrices, and pre- and post-multiplication of matrices and vectors.
Clip space is the final coordinate system before vertices are actually drawn. In the age of vertex shaders, it's the only coordinate system that actually matters, as everything before it is essentially up to the shader programmer.
The contrarian's rule is in full force:
The shaded pixel in each diagram represents the location of the first texel in a render target, and the origin of uv coordinates.
There are good reasons to prefer a reversed z-buffer, where the geometry closest to the camera has depth buffer value 1.0, and the geometry farthest away has value 0.0.
In OpenGL, you can only really properly achieve this using ARB_clip_control.
There's nothing more inevitable and nothing more disheartening than firing up your rendering code on a new system and being greeted by a black screen. One of this week's causes was finally running on a system that supports ARB_clip_control.
Still, some bug-fixing later, and both codepaths, with and without the extension, were working well enough to get visible geometry.
Engine Clip Space
My engine uses OpenGL, but the lowest rendering layers provide a layer of abstraction in case I need to switch 3D APIs.
This isn't as paranoid as you might think - OpenGL 2.0-style code using
glEnd was once ubiquitous, but is now both deprecated and
clunky in comparison to the Core Profile. Apple has made a big push for Metal,
and it's not hard to imagine a future where Vulkan or DirectX 12 is the API
that is most well-integrated with a particular windowing system.
The engine configures (or emulates) a clip space that looks like this:
This matches the Vulkan convention, save that the depth range is reversed by default.
There are good reasons to pick this clip space:
- The origin is in the top left.
- The minimum coordinate in uv space maps to the minimum xy coordinate in clip space.
- All display hardware scans out framebuffers starting in the top left.
- Image files store the top-left pixel first.
- Y increases downwards.
- Both uv and xy coordinates increase in the same direction.
- All windowing systems (except Cocoa on Mac) have Y increasing downwards.
- Successive lines of text are placed at increasing Y coordinate.
- We have reversed-Z.
The one disadvantage to having Y downwards is that it can be slightly confusing when visualizing coordinates mathematically. The first quadrant is in the bottom right rather than the top right, and conventionally expressed rotations turn clockwise rather than anticlockwise.
XQuartz and xcb
I like to develop on my laptop, which is the hipster's choice - a Mac. I also hang out in coffee shops far too much.
So this week I had the great idea that I could develop some of the X11 code required for Linux without actually being in front of a Linux machine - just install XQuartz and build the X11 code under macOS!
More Blank Screens
The problem with this brilliant plan was that OpenGL refused to render to my XQuartz window. I ended up with black windows or corrupt views of VRAM.
Eventually I tracked this down to the fact that my X11 backend uses xcb to communicate with the X server. Switch to old-style Xlib event processing, and OpenGL started working again.
Event 67 and Apple-DRI
It's problems like these which make you glad for open source. I eventually tracked down the problem to this part of the XQuartz stack.
It seems that XQuartz provides an X11 extension called Apple-DRI to support the low-level infrastructure for OpenGL. Part of this extension hooks into Xlib event decoding in order to route update events - for example, when the window is moved or resized - to the OpenGL context in the client process.
Since xcb bypasses Xlib event decoding, these update events were showing up in my event loop as unknown event number 67.
The solution was to query the X11 server for the Apple-DRI extension and perform the routing for these update events manually.
No more blank screen.
This week has been very productive, but also very self-indulgent. I need to force myself out of my low-level comfort zone and get some ugly pixels on the screen if I'm going to make a game rather than a tech demo.
Wish me luck.