Version 1.1.13, CRT Shaders, Text Shaping


Version 1.1.13 of Starlight includes a new shader pipeline with a CRT shader, as well as text shaping and font rendering improvements. It should support RTL languages and multi-width characters.

The rest of this post gets into the technical details, so feel free to stop reading here if that doesn’t interest you.

The first thing I decided to tackle for this version was further improving text rendering and text shaping. Given that I wrote the game in English, text shaping wasn’t really necessary, but I’d like to support other languages in case someone ever wants to write a mod using them. I tried out a few libraries before settling on swash for improved text rendering and rustybuzz for shaping.

Swash has shaping builtin, but it doesn’t appear to have utilities for examining the input text to determine the script, while rustybuzz does so automatically, making it much easier to use. I also trust rustybuzz’s implementation of shaping more given that it’s copied from HarfBuzz and passes most of its test suite.

Performing shaping required partially re-writing the renderer to perform layout and painting of whole lines of text rather than processing cell by cell. I sped this up considerably by caching the result of shape(...) calls and re-using them, making it so that screen borders and going back to previous screens wouldn’t have to redo shaping (which is rather slow). I also record the actual drawn character post-shaping and do a check while rendering to avoid re-rendering it if it hasn’t changed.

During this process, I discovered that ratatui’s diffing algorithm skips diffing cells which come after multi-width characters. This resulted in text from old screens being drawn under the extra cells taken up by those characters. I’m not sure if I should file a bug report for this as I’m not entirely sure the behavior is wrong. I worked around this by performing my own diffing - examining updated cells during the draw call, and blanking them out if they come after multi-width cells.

The next thing I tackled was adding a CRT shader effect. I knew I wanted the game to have more of a retro feel, and such a shader would give it the perfect look. After looking at various pre-existing shaders, I settled on the one by Timothy Lottes.

I then updating my rendering code to use OpenGL directly via glow, giving me the ability to build a full shader pipeline. I also took this as an opportunity to change text rendering to use a shader for blitting characters from the atlas to the screen rather than using the CPU. By using a framebuffer to retain the results of the last draw call for the text, I am able to only pass vertex data for updated characters to the shader rather than redrawing the full screen. The resulting texture is then passed through the CRT shader for the final effect.

I was rather happy with this, but decided I wanted harder scanlines and looked around again at various shaders - finding a shader that had the exact scanline pattern I wanted. The resulting pipeline ended up being quite a bit more complex due to the extra framebuffers and swaps involved, but the effect was really pretty. I then merged in the grille effect from lottes, since I really liked that look, and added corner rounding since I liked it better than the shader’s default vignette.

While working on this new shader, I at one point had a bug where I used the accumulation buffer rather than the text buffer for the final paint, giving screen transitions a fadeout effect. I liked it so much that I made it a toggle in settings, although it defaults to off.

All of this work using OpenGL for drawing uncovered a bug in window z-order updates. I was unable to trace down the exact cause, but the basic gist is that the game stopped respecting z-order updates as a result of switching to drawing with OpenGL (even failing to re-order in response to direct calls to the win32 API requesting a z-level change). In addition to just being bad behavior, this broke the file dialogs in the game as they were opening up behind the game and were inaccessible.

Fixing this took a lot of experimenting. I eventually figured out that it happened only when the window was either created fullscreen or borderless and set to the screen dimensions. I tried various ways of setting the window styles and even created my own window using win32 to pass in to SDL in an attempt to get precise control over the window creation and styling in the hopes that it was an SDL bug. Nothing worked.

I eventually worked around the issue by creating the window borderless, with the screen dimensions plus 1 pixel in the y direction. This kept the app from being considered fullscreen, and allowed it to respect the z-order again. In order to prevent the window from covering apps up if they were on a screen below, I set the window to be transparent and updated the gl viewport to take into account the height offset. This effectively hid the extra 1 pixel and allowed Windows to properly perform hittesting on windows overlapping the border.

Get Starlight

Download NowName your own price

Leave a comment

Log in with itch.io to leave a comment.