DISTRHO/DPF

How to correctly handle external UI with scale factor

Closed this issue · 17 comments

Hi, FalkTX!

I've migrated rncbc's synthv1 to DPF. It has a Qt-based UI. But it doesn't behave well on Windows 10, with 125% scale factor.

Just like this screenshot describes, REAPER's host window size is still at 100% scale factor, so the UI cannot be fully shown. All of VST2, VST3 and CLAP have this issue.

image

I've tried to set host window size on UI constructor, but in vain. Window size is still untouched:

  • Call UI::setWidth() and UI::setHeight()
  • Use Win32 API SetWindowPos(): SetWindowPos((HWND)getParentWindowHandle(), HWND_TOP, 0, 0, 1800, 1000, SWP_NOMOVE);
  • Use QWindow::fromWinId() to get parent window's QWindow instance, then call setWidth() and setHeight()

I've also tried modifying DISTRHO_UI_DEFAULT_WIDTH and DISTRHO_UI_DEFAULT_HEIGHT, but they are not flexible with different scale factors.

So how can I solve this?


What's more, setGeometryConstraints(), and DISTRHO::UI constructor param automaticallyScaleAndSetAsMinimumSize do not work with external UI.

falkTX commented

just use https://github.com/DISTRHO/DPF/blob/main/distrho/extra/ExternalWindow.hpp#L205 ?
and pass this into to the plugin you embed, in this case find a way to set the Qt scale factor on a per-plugin basis

I tried getScaleFactor() but still in vain. Here's my code, in UI constructor (source file):

	const QSize& hint = fWidget->sizeHint();
	UI::setWidth(hint.width() * getScaleFactor());
	UI::setHeight(hint.height() * getScaleFactor());

The host window size is still small.

falkTX commented

did you log it to see if the scale factor is not 1?

anyway sorting out the high-dpi needs to be a plugin-side thing, your code doesnt look right. the plugin needs to scale according to the host, not the other way around.
and in here you will want a setSize to do both width and height at the same time.

did you log it to see if the scale factor is not 1?

image

I've checked the scale factor. It's 1.2500.

falkTX commented

then all is fine from DPF side. what you need here is to get the Qt side to apply the same.

On Qt the size hints are merely hints, they do not set the size. Even the resize call does not always do what we expect, for such cases you need setFixedSize() to force it.

But the problem is: the "initial" window size is not fit with scale factors larger than 100%. (I have DISTRHO_UI_DEFAULT_WIDTH and DISTRHO_UI_DEFAULT_HEIGHT configured.)

In my plugin, Qt UI is clipped by host. I want to change the host display size (not Qt itself). My effort in the code is to change the host display size.

falkTX commented

that is mostly already there.
when you change the size https://github.com/DISTRHO/DPF/blob/main/distrho/extra/ExternalWindow.hpp#L306
it triggers the sizeChanged callback https://github.com/DISTRHO/DPF/blob/main/distrho/extra/ExternalWindow.hpp#L408
which should do what is needed https://github.com/DISTRHO/DPF/blob/main/distrho/src/DistrhoUI.cpp#L423

but in any case since you are using an external UI, you already have access to the display factor right on the createUI call, per https://github.com/DISTRHO/DPF/blob/main/distrho/DistrhoUI.hpp#L232
so you can create the Qt window with the correct display factor from the beginning. no need to create a window with 1x scale and change it after

Seems that APIs like sizeChanged() is triggered from host side. But my problem is stll unsolved.

The Qt window itself is created in the right scale factor. But the host window is not: still limited to its default size, and cannot be changed at runtime.

What I want to change is the host display size itself, just the window below.

image

falkTX commented

The Qt window itself is created in the right scale factor. But the host window is not

this is not the right way to look at it. it is not the host that creates the window, we are. the UI size is defined on the plugin side, the host cannot dictate what the size is going to be.

t is not the host that creates the window, we are. the UI size is defined on the plugin side,

So how should I create the window with the right scale factor? Would you love to take a look at my code?

Sounds like our window size is defined either by DISTRHO_UI_DEFAULT_WIDTH and DISTRHO_UI_DEFAULT_HEIGHT, or by constructor of DISTRHO::UI. Can I change our window size at runtime?

NOTE: Assume that the Qt widget window (not our DPF window) itself has fixed size.

falkTX commented

Ignore the DISTRHO_UI_DEFAULT_WIDTH for now and just try with the methods I mentioned.
How to get Qt to accept this I am not sure, but worst case you set an env var to force the qt scale factor as that should confirm if it is working as intended.

After you have the Qt side scaling as needed then see about dynamically changing the size during runtime.

Thanks! I'll try your methods mentioned above.

Sounds like I have forgotten a critical thing: my project does not create window (QWindow) explicitly, instead the main Qt UI is a QWidget.

This is the init code in DISTRHO::UI constructor (partial, modified):

	fWidget = std::make_unique<synthv1widget_dpf>(fDspInstance->getSynthesizer(), this);

	m_widgetSize = fWidget->sizeHint();
	m_widgetSize.setHeight(m_widgetSize.height() * getScaleFactor());
	m_widgetSize.setWidth(m_widgetSize.width() * getScaleFactor());

	fWidget->setMinimumSize(m_widgetSize);

	// Explicitly set window position to avoid misplace on some platforms (especially Windows)
	fWidget->move(0, 0);

	// Embed plug-in UI into host window.
	fParent = (WId) getParentWindowHandle();
	fWinId = fWidget->winId();	// Must require WinID first, otherwise plug-in will crash!
	if (fParent)
	{
		fWidget->windowHandle()->setParent(QWindow::fromWinId(fParent));
	}

	// Explicitly show UI. This is required when using external UI mode of DPF.
	fWidget->show();

Does it have problem? Should I create a QWindow first, then set fWidget as its child?

falkTX commented

as far as I know the scale factor is a global Qt setting.

I am closing the issue unless you can prove dpf is doing something wrong here, these details you need to figure them out by yourself.
It is and always was my stance that using Qt for plugins is a bad idea. This is just one of the many reasons why.

Now let's put Qt aside, and discuss the plugin window itself.

The "plugin window" is the window created by plugin API (e.g. VST2's VST_EFFECT_OPCODE_WINDOW_GETRECT call).

Seems that the discussions above had mistake the "window" as Qt window.


I think DPF may need some improvements about creating plugin window in different scale factor. Let me describe it.

In DISTRHO::UI constructor, there's a param called automaticallyScaleAndSetAsMinimumSize:

UI::UI(const uint width, const uint height, const bool automaticallyScaleAndSetAsMinimumSize)
    : UIWidget(UI::PrivateData::createNextWindow(this,
              #ifdef DISTRHO_UI_DEFAULT_WIDTH
               width == 0 ? DISTRHO_UI_DEFAULT_WIDTH :
              #endif
               width,
              #ifdef DISTRHO_UI_DEFAULT_HEIGHT
               height == 0 ? DISTRHO_UI_DEFAULT_HEIGHT :
              #endif
               height,
              #ifdef DISTRHO_UI_DEFAULT_WIDTH
               width == 0
              #else
               false
              #endif
               )),
      uiData(UI::PrivateData::s_nextPrivateData)
{
    /* main code ... */
}

I tried to set automaticallyScaleAndSetAsMinimumSize to true, and set width to 0, in order to enable scaling on window creation.

And, in function UI::PrivateData::createNextWindow(), it really scaled the window width and height, then stored it into private data.

UI::PrivateData::createNextWindow(UI* const ui, uint width, uint height, const bool adjustForScaleFactor)
{
    UI::PrivateData* const pData = s_nextPrivateData;
   #if DISTRHO_PLUGIN_HAS_EXTERNAL_UI
    const double scaleFactor = d_isNotZero(pData->scaleFactor) ? pData->scaleFactor : getDesktopScaleFactor(pData->winId);

    if (adjustForScaleFactor && d_isNotZero(scaleFactor) && d_isNotEqual(scaleFactor, 1.0))
    {
        width *= scaleFactor;
        height *= scaleFactor;
    }

    pData->window = new PluginWindow(ui, pData->app);
    ExternalWindow::PrivateData ewData;
    ewData.parentWindowHandle = pData->winId;
    ewData.width = width;
    ewData.height = height;
    ewData.scaleFactor = scaleFactor;

But the plugin window creation seems to ignore the size assigned by UI::PrivateData::createNextWindow().

For example, in VST2 VST_EFFECT_OPCODE_WINDOW_GETRECT call (see comments in this code section):

        case VST_EFFECT_OPCODE_WINDOW_GETRECT:
            if (fVstUI != nullptr)
            {
               // =====================================================================
               // If works well, fVstUI should be available, and get the right window size.
               // =====================================================================
                fVstRect.right  = fVstUI->getWidth();
                fVstRect.bottom = fVstUI->getHeight();
# ifdef DISTRHO_OS_MAC
                const double scaleFactor = fVstUI->getScaleFactor();
                fVstRect.right /= scaleFactor;
                fVstRect.bottom /= scaleFactor;
# endif
            }
            else
            {
               // =====================================================================
               // However, fVstUi is not available, so it fallback to DISTRHO_UI_DEFAULT_(WIDTH|HEIGHT).
               // That's why I open this issue.
               // =====================================================================
                double scaleFactor = fLastScaleFactor;
               #if defined(DISTRHO_UI_DEFAULT_WIDTH) && defined(DISTRHO_UI_DEFAULT_HEIGHT)
                fVstRect.right = DISTRHO_UI_DEFAULT_WIDTH;
                fVstRect.bottom = DISTRHO_UI_DEFAULT_HEIGHT;
                if (d_isZero(scaleFactor))
                    scaleFactor = 1.0;
               #else
                UIExporter tmpUI(nullptr, 0, fPlugin.getSampleRate(),
                                 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, d_nextBundlePath,
                                 fPlugin.getInstancePointer(), scaleFactor);
                fVstRect.right = tmpUI.getWidth();
                fVstRect.bottom = tmpUI.getHeight();
                scaleFactor = tmpUI.getScaleFactor();
                tmpUI.quit();
               #endif
               #ifdef DISTRHO_OS_MAC
                fVstRect.right /= scaleFactor;
                fVstRect.bottom /= scaleFactor;
               #endif
            }
            *(vst_rect**)ptr = &fVstRect;
            return 1;

Sounds like VST_EFFECT_OPCODE_WINDOW_GETRECT is called before VST_EFFECT_OPCODE_WINDOW_CREATE, so the fVstUI in VST_EFFECT_OPCODE_WINDOW_GETRECT is empty. As a result, the size specified by DISTRHO::UI has no effect.

falkTX commented

The external-ui does not make use of adjustForScaleFactor, because that is something that DPF tries to handle internally (scaling up the opengl/cairo context). This cannot be done automatically for external-uis since they are by their very nature external.

The issue you are having is due to hosts asking for window size before creating the window, this is one of the targets of DISTRHO_UI_DEFAULT_WIDTH so that we can skip creating a temporary window just to know the correct initial size.
Host-provided scale factor is applied to this size, which in theory makes the creation of the actual UI start with the correct size.

I think the best approach here is to code something as an example plugin that is simple and can be easily tested.
So far the examples that use external-ui have all been very simplistic and plugins using external-ui are overly complex (zynaddsubfx and hiphop-based html views).
But coding all the OS support by hand is also too much..
So, in my opinion, we need to create a plugin example that makes use of a super light toolkit that allows embed, one that is not pugl. Stuff like GLFW, SDL or vstgui. Might be that vstgui is the best option, since it has similar goals in mind.

in my opinion, we need to create a plugin example that makes use of a super light toolkit that allows embed, one that is not pugl

Great idea! This will help developers who are interested in external UI.

Host-provided scale factor is applied to this size, which in theory makes the creation of the actual UI start with the correct size.

However, by now the scale factor is not applied to initial window size. If it can apply scale factor correctly, my issue can be solved.

Is it possible to modify the code in VST_EFFECT_OPCODE_WINDOW_GETRECT as below?

                double scaleFactor = __A_FUNCTION_TO_GET_SYSTEM_SCALE_FACTOR__();
               #if defined(DISTRHO_UI_DEFAULT_WIDTH) && defined(DISTRHO_UI_DEFAULT_HEIGHT)
                 if (d_isZero(scaleFactor))
                    scaleFactor = 1.0;
                fVstRect.right = DISTRHO_UI_DEFAULT_WIDTH * scaleFactor;
                fVstRect.bottom = DISTRHO_UI_DEFAULT_HEIGHT * scaleFactor;
                
               if (fLastScaleFactor != scaleFactor)
                    fLastScaleFactor = scaleFactor;

BTW, as I described on the previous reply, by setting DISTRHO::UI() constructor param width to 0, I can make DPF automatically scale window size, but the size is not applied to plugin's initial size.

Here's my modification, to let initial window apply the scaled size (VST2 only)
        case VST_EFFECT_OPCODE_WINDOW_GETRECT:
            if (fVstUI != nullptr)
            {
                fVstRect.right  = fVstUI->getWidth();
                fVstRect.bottom = fVstUI->getHeight();
# ifdef DISTRHO_OS_MAC
                const double scaleFactor = fVstUI->getScaleFactor();
                fVstRect.right /= scaleFactor;
                fVstRect.bottom /= scaleFactor;
# endif
            }
            else
            {
                UIVst* tmpVstUI = new UIVst(fAudioMaster, fEffect, this, &fPlugin, (intptr_t)ptr, fLastScaleFactor);

                if (tmpVstUI != nullptr) {
                    d_stderr("    Created temp VstUI. Size: (%d x %d)", tmpVstUI->getWidth(), tmpVstUI->getHeight());

                    fVstRect.right = tmpVstUI->getWidth();
                    fVstRect.bottom = tmpVstUI->getHeight();

                    delete tmpVstUI;
                    tmpVstUI = nullptr;
                } else {
                    double scaleFactor = fLastScaleFactor;
                   #if defined(DISTRHO_UI_DEFAULT_WIDTH) && defined(DISTRHO_UI_DEFAULT_HEIGHT)
                    fVstRect.right = DISTRHO_UI_DEFAULT_WIDTH;
                    fVstRect.bottom = DISTRHO_UI_DEFAULT_HEIGHT;
                    if (d_isZero(scaleFactor))
                        scaleFactor = 1.0;
                   #else
                    UIExporter tmpUI(nullptr, 0, fPlugin.getSampleRate(),
                                    nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, d_nextBundlePath,
                                    fPlugin.getInstancePointer(), scaleFactor);
                    fVstRect.right = tmpUI.getWidth();
                    fVstRect.bottom = tmpUI.getHeight();
                    scaleFactor = tmpUI.getScaleFactor();
                    tmpUI.quit();
                   #endif                
                }

               #ifdef DISTRHO_OS_MAC
                fVstRect.right /= scaleFactor;
                fVstRect.bottom /= scaleFactor;
               #endif
            }
            *(vst_rect**)ptr = &fVstRect;
            return 1;

BTW2, VST and VST3 seem to have APIs for adjusting window size on the plugin side.

For example, Xhip synthesizer has VSTi edition. It makes use of an API to adjust window size dynamically.