I revisited the Windows Direct2D renderer recently and I think it’s in pretty good shape now; see the previous thread for more context.
I was stuck on the window resizing before, but I believe I’ve finally found a good solution. The trick was to use Direct2D on top of DirectComposition. I found this forum posting on gamedev.net:
My thanks to “jbatez”, the original poster; that was very helpful.
If you’d like to check it out, I made a music visualizer VST plugin that can switch between software rendering and Direct2D. Here’s the repository with build instructions and examples:
Pretty well. CPU usage is much lower in Direct2D mode and text looks very nice. Plus, you can paint on-demand from a timer callback, VBlankAttachment callback, or a dedicated thread, which makes a big difference for animation.
The Direct2D renderer is now disabling the redirection surface used for old-school GDI painting and is instead rendering to a DirectComposition visual that fills the entire window.
The Direct2D renderer requires Direct2D 1.2 (Windows 8.1 or later).
- Implemented partial window repainting using dirty rectangles
- DPI scaling is now done by the Direct2D device context
- Fixed restoring the Direct2D color brush opacity when restoring the saved state
- Added support for variable refresh rate displays (https://learn.microsoft.com/en-us/windows/win32/direct3ddxgi/variable-refresh-rate-displays)
- Fixed drawGlyph with gradient brush
- Each window now has its own DirectX factory to avoid the global Direct2D lock (cleaned up all sorts of lock contention delays)
The software renderer is great; you can run at 60 FPS in a 1000x1000 window and it keeps up just fine. Here’s a screenshot of the plugin doing just that:
However, the software renderer starts to struggle with a larger window; for example, here’s 3440x1350 window at 60 FPS:
Zooming in on the statistics in the lower left corners shows:
It’s hitting around 50 FPS; rendering each frame is taking about 90% of the frame time on average. That means the message thread is mostly busy painting.
Now here’s the same window in Direct2D mode:
It’s easily keeping up at 60 FPS; rendering is now taking about 11% of each frame.
And here’s Direct2D at 120 FPS:
Performance will, of course, be highly dependent on your CPU & GPU.
Good question. Here’s 20 instances of the plugin rendering butter-smooth Direct2D at 100 FPS in the JUCE plugin host:
Zooming in the stats:
The frame rate could probably go higher; in this case, the the threaded renderer is driven by the WASAPI 10 msec block size, so 100 Hz is the practical limit.
The software renderer in this case will handle about 30 FPS and cannot keep up at 60 FPS.
You can also get 60 FPS with Direct2D painting on the message thread painting in the VBlankAttachment callback, but it’s entirely possible to clog the message thread and render the app unresponsive.
Even in Direct2D mode, the juce Graphics class still does significant work in software before calling the LowLevelGraphicsContext. For example, stroking a complex Path often involves the Graphics class breaking the original Path into small piecewise segments, then creating a second Path, which is then in turn passed to the low-level renderer (check out Path::addCentredArc and Graphics::strokePath). Ideally all of that work would be done in the GPU.
Also - allocating GPU resources is expensive; it’s much better to set up and reuse the same Direct2D objects (Improving the performance of Direct2D apps - Win32 apps | Microsoft Learn). Say you call Graphics::fillRoundedRectangle; the Graphics class will allocate a Path object, add a rounded rectangle to the Path, tell the renderer to draw a filled path, then free the original Path. In Direct2D mode, that Path is converted to a Geometry, which is a Direct2D GPU resource. The Geometry is then rendered and freed. That all makes path rendering much more costly than it needs to be; creating the Path takes time, the Path to Geometry conversion takes more time, and pushing the Geometry into the GPU even longer. Pre-creating, retaining, and reusing the Geometry would be much more efficient.
A JUCE LowLevelGraphicsContext really only does a few things; it can fill rectangles, fill a Path, draw an Image, draw a Line, or draw text. The rest of the methods handle clipping and transparency and such. To get better Direct2D performance, the LowLevelGraphicsContext will need to be extended to handle more drawing operations and support retained GPU resources.
These changes would make a big difference with Direct2D, but of course there’s the questions of portability and breaking existing code. More on this topic to come…
Direct2D allows you to paint from any thread on demand. So instead of waiting for a timer or VBlank notification, calling repaint, and then waiting for the Windows to tell you it’s time to paint, you just do it right away. This alone makes a big difference in reducing frame jitter and cuts down the load on the message loop.
Mozilla Firefox switched to off-message-thread painting a few years ago for similar reasons: Off-Main-Thread Painting – Mozilla Gfx Team Blog
But, of course, the JUCE component hierarchy is definitely not thread safe and painting off the message thread is a huge change. The demo plugin does support painting on a dedicated thread, but it’s largely experimental to show that it can be done.
- If Nvidia G-Sync is enabled for windowed apps, the mouse will stutter and lag when moving over a Direct2D-enabled window (not a JUCE-specific issue). I recommend turning G-Sync off for Windowed mode; there may be some way to register an app with the Nvidia driver to disable G-Sync for that app.
-There may still be bugs lurking with creating and destroying windows and the DPI not matching; need to investigate further.
-Colors look slightly different in Direct2D mode; it may just require a gamma adjustment
Let me know what you all think. I could definitely use more testers! I'll have more to show soon.