When I first looked at Stellarium for a GSoC project, I wanted to work on one of graphics projects proposed on the Stellarium wiki, as graphics is what I’m most interested in.
After taking a closer look, I noticed that while these projects weren’t difficult in concept, I would spend most time fighting with the existing code; OpenGL is often used directly instead of being isolated into a coherent subsystem, and overall, the graphics code is quite messy.
There are some classes wrapping OpenGL but they are rather inconsistent or incomplete. For example, drawing is often done with StelPainter but one-two direct GL calls must be done to achieve the intended effect, defeating encapsulation and duplicating graphics code all over the place.
This makes things like debugging and switching OpenGL versions difficult. There’s no single location to look at if there’s a graphics bug. Inconsistent compiler switches are used to switch between GL1, GL2 and GL ES, and GL calls in various locations don’t even obey those.
Supporting new OpenGL versions (ES 3.0 is on the horizon) or alternative graphics APIs would be even harder, and might require duplicating much of the code without any clean separation.
So, that’s what my project is about. I intend to isolate graphics code behind a set of interface classes and only use graphics through these. Stellarium shouldn’t even know how the graphics are drawn - it could be GL1, GL2 or even a software rasterizer.
Once I’m done it should be easy to e.g. add OpenGL 3 support - you just need to write a new backend based on OpenGL 3, without changing any code that uses graphics. Debugging and profiling should also be easier, as there will be a clearly separated subsystem to look at, and each graphics API will have its own backend, separate from others.
I’m not adding any significant new functionality. There might be some new OpenGL features used on the backend (e.g. VBOs), but the point is to make the code more maintainable.
Current graphics code contains functions or data members that are never used, but “might be used in the future”. Many of these are unmaintained and not in sync with other changes done since they were introduced. Sometimes, they are not even functional. Member functions of classes often duplicate each other’s functionality or implement features that would require other - nonexistent - methods to actually work.
APIs are often unintuitive. StelTextureMgr functions createTexture() and createTextureThread() have similar names and documentation - but they behave differently. Only createTexture() can load a PVR texture and only createTextureThread() looks in the textures directory as a fallback.
I want to ensure there is always one way to do something and that way is obvious and easy to use. A function should have an intuitive name and do no more and no less than what its documentation states.
If I see a feature that is not used and unmaintained, I’m not rewriting it in the refactored code - it can be added once it is actually needed. The point of refactoring is that it should make it easy to add new features.
Graphics code should be a clearly isolated subsystem. I’m separating graphics code into classes/interfaces in a specific directory (src/core/renderer). Any new graphics features should be added by adding to these interfaces.
When the graphics code is separated behind a few interface classes (and has no significant global state), it can be switched at run time. For instance, if a GL2 backend fails to load, we can fall back to a GL1 backend - without restarting the application.
I’m making a very clear distinction between interface and implementation, or frontend and backend. When the user draws something, they use the interface. Implementation is inaccessible and hidden from the user.
Interface might be an abstract class (from which an implementation class is derived), a wrapper class (which can internally swap implementation classes or functions), a naming convention used by a template, and so on. The latter is not an option as it is compile-time only.
In this project I’m concentrating on the interface. Once the implementation is isolated behind an interface, it can be improved without changing code that uses it.
Stellarium uses OpenGL 1.x (for old machines and some drivers) as well as OpenGL 2.x and OpenGL ES 2 (for mobile devices). Each of these needs an implementation (although OpenGL 2.0 and ES might share most of it due to their similarity).
Different implementations might be needed in future. For instance a software renderer as a fallback, OpenGL 4 or OpenGL ES 3. This project should make it possible to create such implementations.
After I’m done with refactoring do I plan to look at performance. Once implementation is behind a stable interface optimizations can be done without breaking user code. Thanks to tools like APITrace graphics performance can be measured without running the application itself, which can help detect which bottlenecks are on the CPU and which on the GPU. It should also be possible to use VBO (Vertex Buffer Objects) to store some or all vertex data, which might result in a large speedup.
It is counterintuitive that moving performance-sensitive code behind a bunch of virtual function calls can improve performance, but as the graphics code is no longer all over the place, it’s easier to keep track of in a profiler. If a particular piece of code is a significant bottleneck, it can be made a special case and separately optimized (even interally breaking the interface/implementation distinction - but without breaking the interface itself).
Another advantage of well separated graphics subsystem is that it should be easy to gather statistics about how it is used. For instance how many vertex buffers are used, how many shader/texture/buffer binds, how many vertices and so on. If I have any time left I’d like to look at that as well.
Right now I’m still not working full time on this project. This is because I’m still finishing some tests in the university.
I’m not a proponent of detailed planning in programming. From my experience planning in detail anything larger than a class or two results in hitting unexpected issues that haven’t been planned for, at which point the plan becomes useless. It helps to plan on high-level, but determining every function or data member in advance is usually counterproductive.
With this project I’m’ taking a prototyping approach. This basically means I’m coding as fast as I can trying to discover issues by actually hitting them. Once I’m done with that, some code will work, some won’t. Anything that won’t work will be thrown out and replaced.
This is especially important when rewriting code used throughout the project. I can’t keep track of every way graphics code is used and write something that will magically fit everywhere at the first try. So I design an interface, try to use it, and once I find there is some place I can’t use it I modify it or throw it away.
Another important thing is to test the code as I go - I could try to replace everything at once, but that way I’d only have running code in a month or two, and would end up a huge mass of accumulated bugs. So instead I’m replacing code bit by bit, always keeping it in running state. However, this requires writing temporary code only to keep the old and new code working together.