Reunited with an old friend: Software rendering is back!
Back when we started development on Citra, all graphics emulation was done using a software renderer: All rendering was done purely on the host CPU and hence is incredibly slow, since it doesn't even attempt to use graphics acceleration. The reason for that was simple: Debugging rendering code on the GPU is notoriously difficult, so back when the 3DS hardware wasn't understood well, the software renderer was ideal for quick analysis and was a flexible code base that could easily be adapted for new findings as they popped up.
Mikage actually had initially reused Citra's software renderer to get graphics showing up before I wrote the Vulkan renderer earlier this year. But these days, the benefits of a software renderer aren't quite as clear-cut: The PICA200 GPU is researched well including even the 3DS-unique features, but, alas, the problem of debugging graphics issues persists. Sometimes you don't even know whether a graphical issue is truly a bug in the rendering code or just a CPU emulation bug manifesting on the user's screen.
That is why I decided to revive the old software rendering code and adapted it for Mikage's new display logic. Along the way, I fixed a big performance issue in the clipping code, and implemented a couple of previously missing features. With this work, I could finally confirm that the missing flame effect as well as the "radioactive water" in Kokiri Forest in TLoZ OoT3D are indeed graphics emulation bugs, since they are magically fixed in the software renderer.
What's left is actually fixing these issues in the Vulkan renderer now ;)
Unit Tests for the graphics core
If you've followed my emulation work, you'll know I'm a strong believer in testing as a means for ensuring long-term success of an emulator project. Having tests that confirm your understanding of hardware not only is good to factually verify how the hardware works, but also ensures that any emulation fixes you make at a later point won't break already tested behavior.
I've seen this problem in Dolphin plenty of times: Fixing one bug would cause other bugs to appear, seemingly out of nowhere. You would run into circles chasing bug fix after bug fix, yet it appeared as if you weren't making any actual progress since new bugs would pop up. Sometimes we were just working sloppily and introduced bugs when writing new code, but other times we in fact uncovered bugs that were already there and just hidden by other code that never worked in the first place. Hardware testing is a good tool of ensuring thanks to hardware testing
Unfortunately, I haven't always been successful at convincing others that putting effort into testing is worthwhile: Citra in particular has always taken a rather lax approach here; some features of the 3DS GPU have had utility homebrew programs written to selectively confirm understanding about their behavior, whereas for other features this was deemed too difficult. This approach is better than nothing, but it's not useful in the long run: None of the tests are automated, and some won't even compile anymore with today's tools. And then most nontrivial features aren't backed by tests, so Citra doesn't actually verify the things they most likely got wrong. Being a bit tongue-in-cheek, I like to say Citra doesn't *really* have tests, for these reasons.
Long story short, Mikage's mission being to do things right, I've naturally invested heavily in testing. The CPU and OS HLE implementations already have a testing framework in place, and in the past weeks I've been setting up something similar for the graphics side of things. I've already uncovered a few small bugs even though only few actual tests have been written so far.
You might now ask yourself: What's in it for you? Well, making the rendering code faster has a high chance of breaking things; in fact, the Vulkan renderer already has a couple of bugs not present with the software renderer as mentioned above. Optimizing the rendering code to be faster has a high chance of introducing even more bugs. Having tests in place enables me to implement these optimizations while making sure the rendered picture is as flawless as seen when playing the game on your actual console at home.
Packaging the 3DS homebrew ecosystem
Once written, we want our tests to be in a state of perpetuity: They verify the behavior of an emulator at one point in time, and to reliably verify this behavior for (possibly years into) the future they mustn't change. After all, if we find the emulator starts failing a test at some point, who would we blame otherwise? Did we introduce a bug in the emulator, or did the test change in a subtle way and now doesn't actually describe the hardware behavior anymore?
And here comes the catch with writing testing code for the 3DS: Tests must be written as 3DS homebrew programs, which are usually compiled using a set of tools collectively called devkitARM, plus a few other bits. These tools are written by people in the 3DS reverse-engineering community and, naturally, are changed very often. Often times you can notice this easily since your code won't compile anymore, but other times things just silently change under the hood. In fact, if you compile any 3DS homebrew title released, say, 2 years ago, you'll find it won't compile anymore.
You may already see how this poses a major threat to reliable long-term testability of an emulator: Of the handful of tests Citra has, most of them don't work anymore! I personally was hit by this once, when the library ctrulib decided to silently swap the order of width and height parameters in various places, hence breaking various tests of mine and wasting a couple of hours debugging until I found out about the library change.
On top of that, devkitARM and libraries such as ctrulib or citro3d can be quite a hassle to set up in an automated environment; it begins with devkitARM using a rather heavy custom package manager (that is only supported on newer versions), and ends with versioning problems (ever tried using an old version of ctrulib? Good luck figuring out which devkitARM version goes with that!).
And finally, ctrulib and citro3d make very specific assumptions about how they're going to be used. Some things frankly cannot be tested with the layers of abstractions they impose on the programmer. In particular, this has made writing custom system modules a rather headache-inducing task for me in the past.
Don't get me wrong though, these tools are certainly good options for homebrew game development. But they do severely lack as a base for emulator development. That's why I've been working towards building an alternative toolchain for the 3DS, based on modern tools such as the package manager Conan and the build system CMake. Earlier this month, I've reached a first mile stone of being able to compile one of the 3ds-examples from scratch - literally: My setup bootstrapped everything starting from the compiler, ctrulib, various 3ds-specific tools, and of course the program source code itself!
Mikage's serialization code at CppCon
I've been at CppCon, one of the biggest C++ conferences in the US, and presented blobify, Mikage's serialization framework!
This post was originally posted on 31 October 2019 for patrons, and released publicly on 18 December 2019.