Every once in a while I get asked this question, usually in the context of bug analysis. What does Wesnoth use to render text? While at first glance it seems to be a really simple question, the answer turns out to be highly convoluted.
Let’s start from the basics. There are two text rasterization pipelines or APIs in Wesnoth, each relying on a different set of dependencies:
- Legacy/SDL_ttf-based: as the name suggests, this component employs the SDL_ttf library, itself a wrapper around the ubiquitous FreeType library. FreeType is most commonly used in X11 environments such as the most popular Linux distributions, as well as other Unix-type operating systems with a graphical user interface; a notable exception being Apple OS X.
- ttext: I call it
ttextbecause that’s the name of the C++ class encapsulating the pipeline for all code requiring its services. Internally, it uses Pango to select a font and apply styles and markup-based formatting, and renders the result with Cairo. The result is then decoded and copied into a native SDL surface.
This duplication of functionality is first and foremost a historical artifact. The legacy pipeline does pretty much nothing that the newer
ttext pipeline doesn’t already do.
ttext uses Pango markup for formatting arbitrary spans of characters where the older code uses homegrown markup that operates on a line-by-line fashion.
ttext also has purportedly support for right-to-left languages than its predecessor, and recognizes certain Unicode ranges that SDL_ttf does not. It also provides some degree of integration with the operating environment by leveraging Fontconfig’s functionality through Cairo.
ttext doesn’t have that the legacy API does is caching of rendering results. The legacy API caches up to 50 text surfaces unless running the (default) MP lobby, which raises the limit to 1000.
Now, here is where things get frustratingly complicated.
GUI1 and GUI2
Two different user interface APIs co-exist in Wesnoth due to neither one implementing a superset of the other’s functionality.
GUI1 is the older API that was already in use in Wesnoth 1.4.x back in 2008. It implements static labels, push buttons, toggle buttons (normally found in check box form), menus (used to implement list boxes, combo boxes, and pop-up menus), progress bars, scroll bars, sliders, and single and multi-line text boxes. It also includes a very basic framework for setting up generic dialogs with a limited set of widgets. Most notably, all layout is done using absolute positioning on the screen, and there is no built-in ability to ensure widgets don’t overlap each other or overflow their dialog. List boxes lack horizontal scrolling, tab pages and other form of pagination do not exist, and both widget and dialog layouts are hard-coded into the game’s C++ code with magic numbers all over the place.
Development on the GUI2 framework commenced in 2008, with the intention of replacing GUI1 entirely once completed. One of the first tangible fruits of the endeavor was the new WML
[message] display introduced in Wesnoth 1.6, which was in turn designed for the specific purpose of enabling the use of larger, transparent portraits for speaking units. GUI2 revolves around using a dynamic grid-based layout method for both dialogs and widgets, leaving almost all of the presentational details to WML, and even defining the entirety of a dialog’s layout in this language. One of the original design goals is support for user-defined themes; in theory, this would allow campaigns and other user-made content to completely reshape Wesnoth’s UI to suit the content’s flavor or address shortcomings in the stock theme.
In practice, however, GUI2 is an incomplete experiment that just happened to become the de facto foundation for most user interface components introduced in Wesnoth 1.6.x and later; this is because most of the aforementioned features are just too convenient. However:
- The dynamic grid-based layout is both a curse and a blessing, as there are barely any officially-sanctioned mechanisms to force a specific layout independent of textual contents.
- Furthermore, the ‘dynamic’ part only refers to automatically rearranging the grid when a widget’s contents or state changes. While it is possible to remove or add widgets at runtime, it essentially requires violating the framework’s established protocol and messing with implementation details. This happens to be why GUI2 does not implement any kind of grid swapping mechanism that would enable us to have tabbed dialogs and such.
- Certain functionality from GUI1 has not been ported yet, in some cases due to flaws in GUI2’s current implementation preventing a ‘clean’ way to add such things, and in others because it was considered low priority to do so. In particular, combo boxes, pop-up menus, and multi-line text boxes are still missing.
- GUI2 theme support does not ‘officially’ exist yet, even though most of the underpinnings are there.
If we ignore for a moment the matter of which parts of the game UI are written using GUI1 or GUI2, the answer to the opening question seems relatively simple, if needlessly bifurcated. GUI2 uses
ttext for rendering text — in fact, it was specifically introduced for use by GUI2 before it crept into other components (more on that later). GUI1 uses the SDL_ttf pipeline instead, and only because nobody bothered to switch it to
ttext — which, although doable, is not a trivial task.
Since Wesnoth 1.10.x, GUI2 is used to implement pretty much all non-fullscreen dialogs, as well as the title screen with the main menu. The most notable exceptions include the Load Game dialog, the Add-ons Manager, the Preferences and Hotkey Preferences dialogs; and the in-game Help browser, Advance Unit, Attack Unit, Status Table, and Unit List. Finally, while the (default) multiplayer lobby screen does not use the stock GUI1 dialog code, it makes extensive use of GUI1 widgets.
So what’s the catch, then?
The two chimeras
The in-game user interface — displaying unit stats, the minimap, and the menu bar — is the heart and soul of Wesnoth’s UI. It is themeable, using an obtuse layout format in WML. It is used by the built-in map editor, whose UI is essentially a special theme with some hard-coded behavior. It also combines stock GUI1 widgets — like the menu bar buttons and End Turn button — with custom user interface elements describing the highlighted unit, as well as providing interaction with the game map. Strictly speaking, it belongs neither to the GUI1 nor GUI2 APIs, but rather sits on top of the former and beside the latter.
And here’s where the elegant dichotomy from earlier falls apart.
Everything in this screenshot, with the exception of the Menu, Actions, and End Turn buttons, is rendered using
ttext. Who would’ve thought, right?
The themeable game UI (or “Theme UI” for short) is not alone. Back in Wesnoth 1.7.x, I took it upon myself to write a new implementation for our campaign story screens enabling the use of Pango markup and a couple of additional options for text positioning. The result is that
ttext is used both for the text heading (most often seen on the last story screen before starting a scenario) and the story text proper at the bottom. The advance/skip buttons are stock GUI1 widgets, however, which means they use the legacy font API.
Finally, sitting alone in its corner, we have the loading screen with the Wesnoth logo on a black background and the progress bar below. It’s not a GUI1 dialog, it’s not a GUI2 dialog — indeed, it’s not a dialog at all. Because Wesnoth is a single-threaded application for most intents and purposes, it wouldn’t do much good to have the loading screen run its own event loop like other parts of the GUI.
Instead, the loading screen exposes its own API to other components like the WML preprocessor and parser, and leaves it to them to perform a status update every time something happens. The visuals — including the fancy progress bar — are all done by hand in the loading screen’s implementation, meaning that no GUI1 or GUI2 widgets are at play here. Thus, the loading screen calls the legacy font rendering code directly.
Since Wesnoth initializes both font rendering APIs early on before the loading screen first comes up, there is really no reason for us to prefer one API over the other for the loading screen. Much like GUI1’s, the loading screen’s choice is merely a historical artifact.
Mixing calls to both APIs in the same visual context often leads to jarring results.
ttext is subject to Cairo’s use of the Fontconfig library to obtain font rendering settings from the system, in addition to calling a system-specific rasterizer such as ClearType on Windows. The legacy SDL_ttf-based API calls FreeType on all systems with its own hard-coded settings instead. While this means
ttext can benefit from platform features where SDL_ttf can’t, it also exposes bugs in
ttext, as well as bugs in Pango/Cairo/Fontconfig themselves:
- Bug #21648: ClearType’s subpixel hinting isn’t handled well by
- Bug #20337: Essentially, subpixel hinting isn’t handled well by
ttext, platform-independent edition.
- Bug #23560: Pango/Cairo/Fontconfig refuse to use our fonts on Apple OS X.
Thus, a single screen can contain multiple text elements in the same font and size, and maybe even with identical contents, but looking either slightly or completely different due to
ttext’s bugs resulting in different output compared to SDL_ttf.
Where do we go from here, though? Right now, it’s highly unlikely that GUI2’s development will progress any further in Wesnoth 1.13.x, but this does not mean that the old API used by GUI1 can’t be removed once and for all in favor of
ttext. Whether this can be done basically depends on two things:
- Our ability to fix the aforementioned
ttext-specific bugs, or at least work around them somehow.
- Determining whether the legacy API’s only unique feature — the render cache — is actually useful, and porting it to work transparently with
ttextinstead. It should be noted that GUI2 does suffer from inefficiencies with large amounts of text, but due to the additional layers of complexity it adds on top of font rendering, it’s hard to tell how much it would benefit from this feature.
Of course, #2 above basically rests on me at the moment.