OpenGL renderer: status update, and "fun" stuff
First of all, we wish you a happy new year. May 2026 be filled with success.

Next, well... DS emulation, oh what fun.

It all started as I was contemplating what was left to be done with the OpenGL renderer. At this point, I've mostly managed to turn this pile of hacks into an actual, proper renderer. While there's still a lot of stuff to verify, clean up, optimize, the core structure is there. It may not support every edge case, but I believe it will do a pretty decent job.

The main remaining things to do are: add mosaic, add the "forced blank" and POWCNT1 2D engine enable flags, and restructure the 2D rendering code. As it turns out, those things are somewhat interconnected.


Mosaic was discussed here. Basically, it applies a pixelation effect to 2D graphics. BG mosaic shouldn't be difficult to add to the OpenGL renderer, but sprite mosaic is going to be tricky, because the way it works makes sense if you're processing pixels from left to right, but that's not how GPU shaders work. It will probably require some shitty code to get it right.

"Forced blank" is a control bit in DISPCNT. What it does is force the 2D engine to render a blank picture, which allows the CPU to access VRAM faster.

POWCNT1 made an appearance here. This register has enable bits for the two 2D engines, which have a bit of a similar effect to the aforementioned "forced blank" bit. However it's not quite the same.

For example, I was testing all those features to make sure I understood them correctly before implementing them, and it highlighted some shortcomings in melonDS's software renderer.

Take affine BG layers. The way they work is that you have reference point registers (BGnX and BGnY) determining which point of the layer will be in the screen's top-left corner, then you have a 2x2 matrix (BGnPA to BGnPD) which transforms screen coordinates to layer coordinates. This is how arbitrary rotation and scaling is created. It is similar to mode 7 on the SNES.

Reference point registers have a peculiarity, though. They are stored in internal registers which are updated at every scanline, and reloaded at the end of the VBlank period (ie. before a new frame). When you write to one of the reference point registers, the value goes in both the internal register and the reload register. This means that if you write to those registers during active display, the values you write set the origin point for the next scanline, not for the first scanline. It's important to get this right, as games may modify the reference point registers with HDMA to do fun effects.

The part melonDS currently misses, is that those reference point registers are updated even if the DISPCNT "forced blank" bit is set. However, they don't get updated if the 2D engine is entirely disabled via POWCNT1.

This means that, say you were to toggle the 2D engine on and off during active display, affine layers would end up displaced, but this wouldn't occur with the "forced blank" bit. It's a bit of an extreme edge case, I don't know of any games that do that, but you never know what they might do...

Mosaic also works in a similar way. Vertical mosaic is implemented with counters which are updated at every scanline, and reset at the end of the VBlank period. The counters simply alter the base Y position used to render BG layers or sprites.

There's also an amusing difference in how they work, which can show if you change the MOSAIC register during active display. For both BG and sprite mosaic, the counter gets reset when it reaches the target value in MOSAIC. However, for BG mosaic, that target value is copied to an internal register, while for sprite mosaic it is always checked directly against MOSAIC. This means that if the counter happens to be greater than the new target value, it will count all the way to 15 and wrap to 0, resulting in some pretty tall "pixels". BG mosaic doesn't exhibit this quirk due to the internal register.

Anyway, similarly to the affine reference point registers described above, messing with POWCNT1 can also cause the 2D engine not to reset its mosaic counters before a new frame, which can cause the mosaic effect to shimmer vertically.

It is also likely that windows are affected in similar ways, but I haven't checked them yet.


All in all, I don't think this research of edge cases is going to be very useful for emulating commercial games, but it's always interesting and helps understand how the hardware is laid out, how everything interacts, and so on. It also helps cover some doubts that are inevitably raised while implementing new features.


Which brings me to the last item: restructuring the 2D renderer code.

The way the renderer code in melonDS was laid out was originally fairly simple: GPU had the "base" stuff (framebuffers, VRAM, video timing emulation), GPU3D had the 3D geometry engine (transforms and lighting, polygon setup), GPU3D_Soft had the 3D rendering engine (rasterizer), and GPU2D received anything pertaining to 2D rendering.

However, things have evolved since then.

While the 3D side was made to be modular, to eventually allow for hardware renderers, the 2D side was pretty much, well, set in stone. It wasn't made to be replaced.

melonDS followed the design of older DS emulators. In the past, it made sense to use OpenGL for 3D rendering, because at a surface level, the DS's 3D functionality maps fairly well to it. However, 2D rendering is another deal entirely. The tile engines do not map well to OpenGL. If today, it's possible to leverage shaders to implement a lot of the functionality on the GPU, this was unthinkable in 2005. So it makes sense the old DS emulators were designed this way: OpenGL for 3D graphics, software scanline-based renderer for 2D graphics.

However, it's not 2005 anymore. In the past, Generic restructured the 2D renderer code to make it modular, so he could implement better renderers for his Switch port.

And now, I'm doing the same thing. As I've discussed before, my old approach to OpenGL rendering was a hack - it allowed me to reuse the old 2D renderer with minimal modifications, but it was pretty limited, which is why I've since changed gears, and started implementing an actual OpenGL renderer.

And I'm not a fan of how things work now. Right now, the emulator frontend is responsible for creating appropriate 3D and 2D renderers and passing them to the core. It allows to support platform-specific renderers (like the Deko3D renderers on the Switch), but I don't like that it's technically possible to create mismatched renderers. Because the renderer I'm working on is only compatible with the OpenGL-based 3D renderers, it makes no sense to be able to mix and match them. My idea was to have a general renderer object which includes both, and avoids this possibility.


This also brings me to another fact: the rendering pipeline in melonDS doesn't accurately model DS hardware. I mean, it does its job quite well, but the way the modules are laid out doesn't really match the DS. I've been aware of this for years, but at the time it was easy to ignore - but now, it's more evident than ever.

Basically, in the DS, you find the following basic blocks (bar the 3D side):

* 2D engines A and B: those are the base tile engines. They composite BG layers and sprites, with effects such as blending, fade, windows, mosaic.

* LCD controller (LCDC): receives input from the 2D engines and sends it to the screens. It can apply its own fade effect (master brightness). It also handles VRAM display, the main memory display FIFO, and display capture.

The split is pretty clear if you observe which features are affected or not by POWCNT1. And it makes it clearer than ever, for example, that display capture is not part of 2D engine A, and doesn't belong in GPU2D.

Thus, it appears that restructuring this code properly will kill two birds with one stone.

This code restructuring may not sound very exciting, but it's good to have if it helps keep things clear, and often, adhering to the hardware's basic layout makes it easier to model things accurately.


As a bonus: since I was busy testing things on hardware, we went down the DS hardware rabbit hole again, and ended up testing things related to the VCOUNT register.

You might be already aware if you follow this blog, but: on the DS, the VCOUNT register is writable. This register keeps track of the vertical retrace position, but it is possible to modify it. The typical use is for syncing up consoles during local multiplayer, but it can also be used to alter the video framerate. The basic way it works is that writing to VCOUNT sets the value it will take after the current scanline ends.

melonDS has basic support for this, but we haven't dared dig into the details. And for good reason.

A few fun details, for example.

VCOUNT is 9 bits wide. The range is 0-262. You would think that if you write a value outside this range, it would detect the error, right?

Right?

Hahahahahahahahahahahahaahahaahh.

In typical DS fashion, this register accepts whatever values. You can set it to anything between 263 and 511. It will just keep counting until 511, and wrap to 0. We even found out that doing so can suppress the next frame's VBlank IRQ. Things like this happen because many video-related events are tied to VCOUNT reaching specific values.

Basically, the writable VCOUNT register is an endless rabbit hole of weirdness, and I'm not sure how much of it is realistic to emulate. VCOUNT affects not only the rendering logic, but also the signal that gets sent to the LCDs. Some manipulations can cause weird scanline effects, or cause the LCDs to fade out. How would one even emulate that? Even if we ignore the visual effects, we might need to look at the LCD sync signals with an oscilloscope to figure out what is happening.
DJT says:
Jan 5th 2026
Hopefully this new version will address the graphical glitches I've been experiencing with some of the games...
Sul says:
Jan 5th 2026
I hope that MelonDS will someday have a UI with a game list ala Dolphin and PCSX2!! (that, and Vulkan, if possible)
Zyute says:
Jan 5th 2026
I'm sure it will come together when the time is right. I have to tip my hat to you Arisotura cause every time I read one of these updates my mind wants to spontaneously combust due to the complexity 😅. Drown out all the noise and bullshit life throws and I know you'll smash it.
LEGO_Vince says:
Jan 5th 2026
Ah sweet, a post on my birthday!
Klauserus says:
Jan 5th 2026
That sounds like you're very hardworking. Wonderful, I'm happy for you. And when you're working on it, it's often a sign that you're doing well. Thank you.

@LEGO_Vince

Happy Birthday
Tagger101 says:
Jan 6th 2026
Is there any chance we'll see a gamelist GUI (potentially with the option to add an image like box art) in 1.2 or beyond?
redmandanno says:
Jan 7th 2026
hope i can install it
a thankful user says:
Jan 8th 2026
dude, your emu is the best one for playing pokemons. it had inspired me to buy gamepad for pc. while i experienced some unusual renders in pkmn white2 e.g. with title screen in opengl mode, i hope you will find out the right implementation to make things best. happy X-mas and ny!
Tim Higgins says:
Jan 9th 2026
I am not sure what you are modding but if it will play all DS games on tv (Hopefully something that is plug and play or buy the ds games I dont care but this sounds great thjhiggins1@gmail.com
THX says:
Jan 9th 2026
Keep updating Android too pls!!
cuddles says:
Jan 12th 2026
What's so special about the month of May to make it full of success?
Post a comment
Name:
DO NOT TOUCH