Progress Update: The unsung Heroes of 3DS Emulation

Seemingly mundane tasks often turn out to be anything but trivial to implement: Here are the cornerstones of an emulator that doesn't merely work, but feels right.

Progress Update: The unsung Heroes of 3DS Emulation

It's time for one of these again! We're seeing a lot of curiosity about what's taking so long to get Mikage out of the door. Let's clear up some dust of mystery that often surrounds emulator development: Many assume there's a straightforward path to bringing games from one platform to another. In practice, it's far from simple.

In this update, we wanted to highlight some of the seemingly mundane tasks that turn out to be anything but trivial to implement in an emulator. Being small in scope but immense in technical depth, these things don't make headlines: Yet they are the cornerstone of creating an emulator that doesn't merely work, but feels right.

We'll get back to big, flashy features and compatibility highlights another day. For a moment, let's put those aside and take a journey through the small, intricate puzzles that make up the backbone of 3DS emulation.

Bootstrapping a Virtual 3DS

Being focused on excellent compatibility right out of the box, Mikage requires a number of different system files that a real 3DS normally works with. For a long time, this involved a convoluted mix of dumping files from a console, decrypting them by hand, and putting them in the correct locations. Doing this from scratch would often take days. Clearly, that wasn't the user experience we intended for production.

During initial development, it wasn't clear which files were needed in the first place, and how the user should get this data in the first place. Making the wrong choices here means users won't be able to get the emulator working, or they would be bothered with adding new files after each update. Now that the emulator core has become sufficiently mature, we've settled for a good approach that has been working well in our experience so far. But implementing this smoother onboarding process would still require a mixture of new emulator features, additional interfaces in the frontend, and a well-integrated setup GUI.

The elephant in the room is decryption support: Virtually all 3DS system files are encrypted with one of 4 increasingly complicated schemes (called Secure1 - Secure4). We've based the cryptographic algorithms themselves on the powerful Crypto++ library to produce more reliable results with less work, however building an understanding of the encryption schemes themselves in face of lacking documentation about them took the most time here.

Another can of worms opens up if you consider decryption keys themselves. What format should the keys be stored in? Which of the over 50 keys do we need? What tools should we provide for dumping them? Luckily, just as we were about to tackle this, developer Steveice10 published a GodMode9 script for exactly this purpose. We tried it out: The dumping process is super easy for anyone with a hacked 3DS, and the dumped data includes everything we need for accurate 3DS emulation.

With crypto keys and decryption algorithms in place, we can load title installation files to set up 3DS system files. This requires implementing support for reading CIA files - a container format that's simple on its own but which wraps a set of other files in different formats. Implementing support for all of them wasn't terribly difficult, but in combination it adds up quickly, especially if you need to flesh out all the edge cases.

Now that we had all these tech-heavy things implemented, all that was left was to bundle it all up in a nice user interface that automates the process. Our new setup wizard will handle each of the steps above for you. If you hadn't read this blog post, you'd never guess how much is happening behind the scenes!

Our new first-time setup GUI in action!

It's worth noting none of this was feasible back when we started work on Citra, which led to a really poor experience: Games wouldn't launch unless dumped with special tools we had to develop from scratch (braindump, later uncart); data from physical consoles (e.g. Miis) couldn't be imported; and many features were outright not available (Amiibos, system apps, ...). With the benefit of hindsight and easy methods for dumping the required keys, designing Mikage as decrypting-by-default yields a much better experience that supports all features you'll ever want to touch without needing to go through annoying extra setup.

Don't panic! Unless it's about Sound...

In the last progress update, we've been showing off our promising initial audio support. It has been moving forward well and has seen general optimizations since, notably a big speed-up to the "warm-up" required for the DSP emulator upon the first launch of a game. But all that aside, we want to highlight one issue in particular here, which is buggy audio emulation.

Imagine this scene: You've been eagerly playing your favorite 3DS game for hours at a time. You've just made it to the door leading to the final boss. You walk through the door in anticipation, and all of a sudden the sound in your earphones becomes... deafening. The audio is painfully loud and distorted beyond recognition. You rush to turn down the volume, but it's too late. With your ears still ringing, you're left wondering what just happened. What's clear though is this glitch was so intense that it ruined the entire experience for you. This almost happened to us during development:

Just looking at this is scary enough: We'll spare you the real audio sample!

While ideally we'd fix whatever underlying bugs cause this problem in the first place, emulator bugs are just a part of reality: We'll never be able to rule out that after 2 hours of gameplay some sort of subtle integer overflow will cause such issues. As far as we know, virtually all emulators take this as a fact and hence refer to a "Use at your own risk" disclaimer. Potentially causing lasting damage to your ears isn't what we're here for though, so instead we're going the extra mile and adding a layer of protection to detect cases were the audio engine clearly went off course. A "panic mode", so to speak, that immediately lowers all volume to prevent the worst.

Audio processing is a very specific field on its own, so we've been asking for help on Mastodon: Within just a day, plenty of people more knowledgeable in the field than us reached out to help us figure out how to approach this important problem. While we haven't gotten around actually implementing the panic mode itself, we've got it fairly well-mapped out now, making the effort much more predictable. For the time being, this allows us to keep improving the audio emulation itself again. Thanks again to everyone who contributed their knowledge to help us move forward here!

The Art of Displaying Pictures

Emulating the complex 3D graphics pipeline implemented in an embedded GPU like the PICA200 is a tremendous challenge with lots of side tasks. Even ignoring all this complexity, just getting rendered images to the screen ended up being a massive rabbit hole.

Think about it: Ideally emulation should run at roughly 60 FPS to match the refresh rate of a 3DS display, but in practice this isn't always the case. What happens if the monitor is configured for 144 Hz? What if the audio engine lags behind the core emulation, or if there are temporary frame drops? Handling any of these poorly will give you stuttery audio, frame lags, or desynchronized audio/video. While we've yet to solve the issue in a fully satisfying way, we put together a framework for asynchronous presentation that'll fix the most egregious types of problems while allowing for more fine-tuning later. There's a lot of unexplored territory here even among giants like Dolphin. If you're interested in technical details, the redream folks have outlined many useful ideas in far more detail than we could cover here.

In addition to this, we've restructured our Vulkan code behind the scenes to allow sharing rendering code between the emulator core and the frontend. Our goal was to avoid the immersion-breaking effect you have in most other emulators, where opening the GUI to change some emulator setting throws you into a completely different interface. Mikage's experience on the other hand feels much more streamlined thanks to the closer interplay between GUI and emulation. We're probably the first emulator for a modern console to do this, so stay tuned to see what this enables in practice.

Leave it as you found it

Every good thing comes to an end, including a good emulation session. But what does actually happen in an emulator when the user hits the stop button? This isn't a trivial matter if you consider the many emulated components running in parallel, each optimized to handle countless operations per second. But adding exit checks everywhere would add costly overhead for something that happens only once. Sometimes it's the only option, such as when the emulated GPU has already shut down while the Display subsystem was still waiting for new frames to arrive. Finding each of these dependencies was tricky enough on its own.

In the majority of cases, we're instead using C++ exceptions to exit out of hot loops without impacting emulation performance or requiring complex code refactorings. However we must be careful when throwing exceptions in a coroutine or from just-in-time compiled code (... or the latter running in the former), as these environments can't easily propagate exceptions to their parent. It can be done, but there's some fun trickery involved (look up std::exception_ptr if you're interested in technical details!).

With that in place, we've successfully stopped the emulator and are presented with the main user interface again. There's more problems to face once the user decides to play another game. The UI might still carry state about the emulation core that wasn't properly reset. This is relatively easy to fix but can be somewhat time-consuming. Fortunately, we've designed our emulator with a focus on localizing state and use C++'s resource management features extensively, so we don't have the same issues in our emulator core that many other projects often face.

Finally, there's quality-of-life issues to worry about: Emulator bugs are no fun for anyone, but at least we want to display a friendly popup and return to the UI instead of crashing the entire program. After all, there's no such thing as a bug-free emulator, so we'd like to counter an unpleasant surprise with a somewhat more comforting error message to tell the user what happened.

Big features next!

Many more things happened in 2023 that we'll outline in our next update, from new features, major compatibility improvements, and new developer tools. For this one, we wanted to do things a little bit different. It's easy to underestimate how seemingly little things blow up to fundamental problems that require extensive research work. This is a common pattern in emulation, but we're pleased with the solutions we've discovered, even if we can only implement so many of them with our time constraints.

Despite frequent request, we're not eager to release Mikage just for the sake of it. Most other 3DS emulator releases have been met with lukewarm responses. Simply dropping some code on GitHub doesn't automatically foster a community around the project. For us, this underlines how important defining a clear and practical value-add is.

We've been listening closely to community feedback to see where that value-add sits, and we're continuously putting in the effort to get there. Sadly, a small but vocal (and not always well-intentioned) part of our community has taken this extra-care as reason to question the project's purpose in general. As much as we appreciate the eagerness to try out Mikage, this kind of criticism is not constructive and does not help us improve the project. Other conversations have been much more fruitful. From other emulator developers, game programmers, content creators, homebrew people, 3DS enthusiasts, and others: The folks that truly care about the 3DS platform are those who inspire us, motivate us to keep going, and help us refine the gap Mikage is here to fill.

As usual, thanks for reading, and until next time!

Special thanks to the following people, who generously supported Mikage at Sponsor-level on Patreon:

  • Francisco Garcia
  • Mr. Madness
  • Pretendo Network
  • Rodrigo Copetti