I’ve encountered another regression on Windows related to commits b4c28db and fcf1971. This issue occurs specifically in FL Studio 2025 when dragging a VST3 plugin with a detached window between monitors with different DPI scaling factors.
On FL Studio, when dragging a detached VST3 window from a screen with 100% scaling (Screen A) to a screen with a higher scaling factor, such as 200% (Screen B), the plugin’s scaling is never updated. It remains stuck at the original 100% scaling on Screen B. This behavior was not present prior to these commits and occurs with JUCE_WIN_PER_MONITOR_DPI_AWARE enabled.
In other hosts like Studio One, dragging a VST3 window between screens with different scaling correctly triggers setContentScaleFactor() in juce_audio_plugin_client_VST3.cpp, which updates the UI. However, in FL Studio with detached windows, this function is never called during the drag.
I’ve investigated PluginScaleFactorManager in juce_PluginScaleFactorUtilities.h and found issues with how the timer callback interacts with FL Studio’s detached window behavior. When FL Studio switches a plugin to “detached” mode, it destroys the original editor and creates a new one. This calls startObserving() again, but unlike the initial creation, startTimer() is seemingly not called, preventing the periodic scale check.
Even after modifying the code to ensure the timer remains active, a second issue persists: getPlatformScaleFactor() returns 1.0 even when the detached window is on the 200% screen. In the older code, the timer callback in juce_audio_plugin_client_VST3.cpp called checkHostWindowScaleFactor(). This internally called getScaleFactorForWindow(), which correctly returned the updated scaling value (e.g., 2.0) in this specific detached scenario, whereas the new getPlatformScaleFactor() implementation fails to reflect the change.
Below is a comparison between the current develop branch (up to commit fcf1971) and the older working code (up to commit 83e3cd8).
New Code:
Old Code:
Here is the code I used to for testing:
#pragma once
#include <JuceHeader.h>
#include "PluginProcessor.h"
class ScaleTestAudioProcessorEditor : public juce::AudioProcessorEditor
{
public:
ScaleTestAudioProcessorEditor (ScaleTestAudioProcessor& p)
: AudioProcessorEditor (&p), audioProcessor (p)
{
setResizable (true, true);
setSize (400, 300);
}
void paint (juce::Graphics& g) override
{
g.fillAll (juce::Colours::black);
auto bounds = getLocalBounds();
int checkerSize = 20;
for (int y = 0; y < bounds.getHeight(); y += checkerSize)
{
for (int x = 0; x < bounds.getWidth(); x += checkerSize)
{
if (((x / checkerSize) + (y / checkerSize)) % 2 == 0)
{
g.setColour (juce::Colours::darkgrey);
g.fillRect (x, y, checkerSize, checkerSize);
}
}
}
auto centerBox = juce::Rectangle<float>(300, 100).withCentre(bounds.toFloat().getCentre());
g.setColour (juce::Colours::black.withAlpha (0.7f));
g.fillRect (centerBox);
g.setColour (juce::Colours::white);
g.drawRect (centerBox, 2.0f);
g.setFont (juce::FontOptions (20.0f));
double scale = g.getInternalContext().getPhysicalPixelScaleFactor();
juce::String text;
text << "Drawing Scale: " << scale << "\n";
text << "Bounds: " << getWidth() << "x" << getHeight();
g.drawFittedText (text, centerBox.toNearestInt(), juce::Justification::centred, 10);
}
void resized() override {}
private:
ScaleTestAudioProcessor& audioProcessor;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScaleTestAudioProcessorEditor)
};