Direct2D performance regression in develop vs 8.0.12

Hi. I’d like to report two major direct2d performance regressions somewhere in develop.

Test: 16 instances of a new plugin I am working on, on screen at the same time. A number of controls have animated 60fps repaints. Direct2d.

Things run fine with one instance on screen; the following issues only occur with lots of Editors on screen at once:

Fast AMD, Nvidia 4070 video. Profiler sees lots of time spent in IDXGISwapChain4::Present1 (although this is at the most recent build; not sure if the first of two issues here is related to that):

On develop:

cf3dbc8d8829e4cca6167707a87e2096091789ed - app runs fine. Smooth enough.
5f032cff631c1e7d8f797e29b7a8fd91109c4da7 - window drags start chugging down to about 4fps.

(edited)

At bf41bcc939ebca4c0c8f993bfc9dadeef8145c8b, a larger regression comes in - too many repaints, and the whole UI gets laggy and horrible. This is worse than the other one in terms of impact.

What can I provide to help diagnose/repair these ones? With the latter case, the 16 separate Editors are probably generating something like 60fps x 16 x 5 control repaints per second - all super super optimized code though, but if it’s choking somewhere out of my control there’s not much I can do directly. Rolling back to 8.0.12 is buttery-smooth again.

Thanks.

CCing @reuk @anthony-nicholls in case there’s a major release about to go out, sorry for the ping.

I don’t like AI very much, but it does have the advantage of being able to see my codebase.

It came up with plausible explanations for both issues. I put them up externally, so as not to clutter up the forum with slop. I have not verified if these are just hallucinations. ● I now have the full picture of both regressions. Let me write the analysis. - Pastebin.com

Thanks.

The removal of DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT looks like an oversight on our behalf. Does putting that back improve things?

i don’t have a lot of time this evening, but i can confirm that on my setup, reverting these lines in 5f032cff makes window drags run a lot better:

swapChainDescription.Flags = 0; → swapChainDescription.Flags = swapChainFlags;

if (const auto hr = chain->ResizeBuffers(0, (UINT)scaledSize.getWidth(),
(UINT)scaledSize.getHeight(),
DXGI_FORMAT_B8G8R8A8_UNORM, 0);
FAILED(hr))
return hr;
—>
if (const auto hr = chain->ResizeBuffers(0, (UINT)scaledSize.getWidth(),
(UINT)scaledSize.getHeight(),
DXGI_FORMAT_B8G8R8A8_UNORM, swapChainFlags);
FAILED(hr))
return hr;

and reinstate

static constexpr uint32 swapChainFlags =
DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT;

…but I’m not a graphics driver person, so I have no idea what the actual implications of those changes are. Those changes are the difference between smooth vs awful window drags in 5f032cff631c1e7d8f797e29b7a8fd91109c4da7 though.

The second issue seems more complex.

Cheers.

Thank you for testing that out, and for the detailed bug reporting.

Yes, there’s much more to think about for the second issue. We’ll look at it now.

let me know if you need more info on how to repro btw. hopefully it’s something as simple as “lots of separate Editors, each with 3-5 medium size components with very simple paint() functions getting sent repaint at 60fps”. i was able to trigger it with a single fillRect per paint.

thanks.

1 Like

I’m investigating on a machine with

  • Windows 11 25H2
  • Intel i7-11700
  • Nvidia GeForce RTX 3070

I’m using a plugin with the following editor:

struct Rainbow : public juce::Component
{
    void paint (juce::Graphics& g) override
    {
        const auto colour = juce::Colour::fromHSL (phase, 0.5f, 0.5f, 1.0f);
        g.fillAll (colour);
    }
    float initialOffset = juce::Random::getSystemRandom().nextFloat();
    float phase = 0.0f;
    juce::VBlankAttachment attachment { this, [this] (auto diff)
    {
        diff += initialOffset;
        phase = + (float) (diff - (int) diff);
        repaint();
    } };
};

//==============================================================================
class AudioPluginAudioProcessorEditor final : public juce::AudioProcessorEditor
{
public:
    AudioPluginAudioProcessorEditor (AudioPluginAudioProcessor& p)
        : AudioProcessorEditor (&p), processorRef (p)
    {
        juce::ignoreUnused (processorRef);
        // Make sure that before the constructor has finished, you've set the
        // editor's size to whatever you need it to be.
        setSize (800, 600);

        for (auto& c : comps)
            addAndMakeVisible (c);
    }

    void resized() override
    {
        juce::FlexBox fb;

        for (auto& c : comps)
            fb.items.add (juce::FlexItem { c }.withMargin (1).withFlex (1));

        fb.performLayout (getLocalBounds());
    }

    void paint (juce::Graphics& g) override
    {
        g.fillAll (juce::Colours::black);
    }

private:
    Rainbow comps[16];

    // This reference is provided as a quick way for your editor to
    // access the processor object that created it.
    AudioPluginAudioProcessor& processorRef;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioPluginAudioProcessorEditor)
};

To test, I’m loading a REAPER project with 16 instances of this editor on-screen simultaneously.

On the develop branch, the editors are a bit stuttery - not quite 4fps like you reported, but visibly slow to repaint. Reinstating the DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT flag definitely helps to make things run more smoothly again.

If I change the number of rainbow components from 16 to 80, then I see the following behaviours:

  • develop branch: dragging plugin editor windows is a bit stuttery, repaints are slow.
  • develop branch with WAITABLE_OBJECT reinstated: very similar to plain develop.
  • 8.0.12: REAPER fails to load the project completely, and seems to hang after opening all editors. I guess the main thread gets swamped with repaint work, preventing other messages from getting processed.

This seems to be the reverse of what you reported. For me, 8.0.12 is unusable with several heavy editors showing, but develop is much smoother.

I also tried adjusting the Rainbow component to call its repaint() on a timer at 500Hz, but this didn’t seem to have any appreciable performance impact. paint still seems to get called once-per-vblank, so I think the coalescing is working as intended.

Do you have any idea what could be different between my test scenario and yours? Maybe the host has an effect. Which hosts are you using for testing? Do you see the same behaviour in all hosts you’ve tried?

Please could you test out the editor code I provided, and check what results you get on 8.0.12 and develop? If my test plugin is slow on 8.0.12 but fast on develop, then that probably indicates that there’s something specific to your plugin that is causing the inverse behaviour. Otherwise, if the test plugin is fast on 8.0.12 and slow on develop (i.e. the same as your plugin) then that might indicate that the problem is related to your specific hardware, DAW, or some other aspect of your environment.

1 Like

i re-updated to the latest on develop with the frame latency fix. from a quick test, it seems like this sorted out the second regression as well - possibly those two changes interacted in an odd way. it’s saturday here - i’ll do a deep dive on monday.

thanks for (possibly) rectifying!

For future reference, this was the change we made to address the performance regression:

1 Like

btw, i discovered a separate bug in my original code that resulted in more repaints being triggered than expected, which could have explained why i was seeing the problem manifest worse than you did. fwiw, it has seemed clean since that fix went in. will need to do some more testing on user systems but afaict it is ok now. thanks again.