From e7cc6f12fd7caa5254860283148ba7f3adf41385 Mon Sep 17 00:00:00 2001 From: Boof2015 <75185879+Boof2015@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:46:35 -0400 Subject: [PATCH] improve vst refresh rate on windows --- plugin/Source/PluginEditor.cpp | 42 ++++++++++++++++++++++++++++++++-- plugin/Source/PluginEditor.h | 24 ++++++++++++------- src/plugin-ui/ScopeApp.tsx | 26 +++++++++++++++++---- src/plugin-ui/styles.css | 17 ++++++++++---- 4 files changed, 91 insertions(+), 18 deletions(-) diff --git a/plugin/Source/PluginEditor.cpp b/plugin/Source/PluginEditor.cpp index 8cfc857..496ddb9 100644 --- a/plugin/Source/PluginEditor.cpp +++ b/plugin/Source/PluginEditor.cpp @@ -6,6 +6,7 @@ #include "VectorscopeEngine.h" #include "SpectrogramEngine.h" #include "WaveformEngine.h" +#include #include #if ! PRISM_USE_DEV_SERVER @@ -19,7 +20,24 @@ namespace { const juce::Identifier kRestoreSettingsEvent { "prismRestoreSettings" }; - constexpr int kDrainCapacity = 16384; + constexpr int kDrainCapacity = 1 << 16; +#if JUCE_WINDOWS + constexpr int kFallbackWindowsFrameRateHz = 60; + constexpr int kMaxWindowsFrameRateHz = 240; + + int resolveWindowsFrameRateHz(const juce::Component& component) + { + const auto& displays = juce::Desktop::getInstance().getDisplays(); + if (const auto* display = displays.getDisplayForRect(component.getScreenBounds())) + { + const auto frequency = display->verticalFrequencyHz; + if (frequency.has_value() && std::isfinite(*frequency) && *frequency > 0.0) + return juce::jlimit(30, kMaxWindowsFrameRateHz, (int) std::lround(*frequency)); + } + + return kFallbackWindowsFrameRateHz; + } +#endif std::unique_ptr makeEngine() { @@ -128,9 +146,22 @@ PrismSpectrumEditor::PrismSpectrumEditor(PrismSpectrumProcessor& p) setResizeLimits(pref.minWidth, pref.minHeight, 4096, 4096); setSize(pref.defaultWidth, pref.defaultHeight); - // Drive frames at the display's refresh rate (adapts to 60/120/144 Hz). if (webView != nullptr) + { +#if JUCE_WINDOWS + startTimerHz(resolveWindowsFrameRateHz(*this)); +#else + // Drive frames at the display's refresh rate (adapts to 60/120/144 Hz). vblank = juce::VBlankAttachment(this, [this] { renderFrame(); }); +#endif + } +} + +PrismSpectrumEditor::~PrismSpectrumEditor() +{ +#if JUCE_WINDOWS + stopTimer(); +#endif } void PrismSpectrumEditor::resized() @@ -176,6 +207,13 @@ void PrismSpectrumEditor::showWebViewFallback() addAndMakeVisible(webViewFallback); } +#if JUCE_WINDOWS +void PrismSpectrumEditor::timerCallback() +{ + renderFrame(); +} +#endif + void PrismSpectrumEditor::onSettingsPanel(juce::var payload) { const int height = juce::jmax(0, (int) payload.getProperty("height", 0)); diff --git a/plugin/Source/PluginEditor.h b/plugin/Source/PluginEditor.h index 3d6fc95..ce3acab 100644 --- a/plugin/Source/PluginEditor.h +++ b/plugin/Source/PluginEditor.h @@ -9,20 +9,26 @@ /** * Hosts the React webview UI and bridges the reused Prism spectrum DSP to it. * - * Driven by a VBlankAttachment (message thread, synced to the display's refresh - * rate). Each vblank it drains stereo audio buffered by the processor, runs - * Visualizer::Spectrum, and emits mid + side magnitudes to the webview as a - * "spectrumFrame" event. Receives "prismConfig" (settings + fftSize/smoothing) - * and "prismReady" events from the UI. + * Drains stereo audio buffered by the processor, feeds the selected scope engine, + * and emits frame payloads to the webview. macOS uses display vblank; Windows uses + * a steady message-thread timer because some DAW/WebView2 embeddings deliver + * vblank callbacks too slowly for data-driven scopes. */ class PrismSpectrumEditor : public juce::AudioProcessorEditor +#if JUCE_WINDOWS + , private juce::Timer +#endif { public: explicit PrismSpectrumEditor(PrismSpectrumProcessor&); - ~PrismSpectrumEditor() override = default; + ~PrismSpectrumEditor() override; void resized() override; +#if JUCE_WINDOWS + void timerCallback() override; +#endif + // Called by the webview event listeners (message thread). void onPrismConfig(juce::var payload); void onPrismReady(); @@ -64,9 +70,11 @@ private: std::unique_ptr webView; juce::Label webViewFallback; - // Declared last so it is destroyed first — no vblank callback can fire into - // a partially-destroyed editor. + // Declared last so it is destroyed first; no vblank callback can fire into a + // partially-destroyed editor. Windows uses juce::Timer instead. +#if ! JUCE_WINDOWS juce::VBlankAttachment vblank; +#endif JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PrismSpectrumEditor) }; diff --git a/src/plugin-ui/ScopeApp.tsx b/src/plugin-ui/ScopeApp.tsx index 0fb1017..8540427 100644 --- a/src/plugin-ui/ScopeApp.tsx +++ b/src/plugin-ui/ScopeApp.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, type JSX, type ReactNode } from 'react' +import { useEffect, useRef, useState, type CSSProperties, type JSX, type ReactNode } from 'react' import type { ScopeKind } from '../types/scope' import type { ScopeSettings } from '../types/settings' import type { PrismResolvedTheme } from '../types/theme' @@ -26,20 +26,38 @@ interface ScopeAppProps { export default function ScopeApp({ kind, renderScope }: ScopeAppProps): JSX.Element { const { settings, resolvedTheme, handleUpdate } = useScopeHostSync(kind) const [settingsOpen, setSettingsOpen] = useState(false) + const [lockedViewportHeight, setLockedViewportHeight] = useState(null) + const viewportRef = useRef(null) useEffect(() => { emitToHost('prismSettingsPanel', { height: settingsOpen ? PANEL_HEIGHT : 0 }) }, [settingsOpen]) + const toggleSettings = (): void => { + if (settingsOpen) { + setSettingsOpen(false) + setLockedViewportHeight(null) + return + } + + const rect = viewportRef.current?.getBoundingClientRect() + setLockedViewportHeight(rect && rect.height > 0 ? Math.ceil(rect.height) : null) + setSettingsOpen(true) + } + + const appStyle = lockedViewportHeight === null + ? undefined + : ({ '--spectrum-viewport-height': `${lockedViewportHeight}px` } as CSSProperties) + return ( -
-
+
+
{renderScope(settings, resolvedTheme)}