improve vst refresh rate on windows

This commit is contained in:
Boof2015
2026-06-02 10:46:35 -04:00
parent f25b0b671d
commit e7cc6f12fd
4 changed files with 91 additions and 18 deletions
+40 -2
View File
@@ -6,6 +6,7 @@
#include "VectorscopeEngine.h"
#include "SpectrogramEngine.h"
#include "WaveformEngine.h"
#include <cmath>
#include <cstring>
#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<ScopeEngine> 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));
+16 -8
View File
@@ -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<juce::WebBrowserComponent> 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)
};
+22 -4
View File
@@ -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<K extends ScopeKind> {
export default function ScopeApp<K extends ScopeKind>({ kind, renderScope }: ScopeAppProps<K>): JSX.Element {
const { settings, resolvedTheme, handleUpdate } = useScopeHostSync(kind)
const [settingsOpen, setSettingsOpen] = useState(false)
const [lockedViewportHeight, setLockedViewportHeight] = useState<number | null>(null)
const viewportRef = useRef<HTMLDivElement>(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 (
<div className="spectrum-app">
<div className="spectrum-app__viewport">
<div className={`spectrum-app ${settingsOpen ? 'has-settings' : ''}`.trim()} style={appStyle}>
<div ref={viewportRef} className="spectrum-app__viewport">
{renderScope(settings, resolvedTheme)}
<button
type="button"
className={`spectrum-app__gear ${settingsOpen ? 'is-active' : ''}`.trim()}
onClick={() => setSettingsOpen((open) => !open)}
onClick={toggleSettings}
aria-label="Settings"
title="Settings"
>
+13 -4
View File
@@ -26,6 +26,11 @@ body {
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
}
.spectrum-app.has-settings {
overflow-y: auto;
}
/* The scope fills everything above the (optional) bottom settings panel. */
@@ -35,6 +40,11 @@ body {
min-height: 0;
}
.spectrum-app.has-settings .spectrum-app__viewport {
flex: 0 0 var(--spectrum-viewport-height, 100%);
height: var(--spectrum-viewport-height, 100%);
}
.spectrum-scope {
position: absolute;
inset: 0;
@@ -84,10 +94,9 @@ body {
border-color: var(--control-border-active, rgba(56, 189, 248, 0.4));
}
/* Settings panel along the bottom (like the app). The editor window grows by exactly
this height when it opens, so the scope viewport above keeps its size. The fixed
basis MUST match PANEL_HEIGHT in ScopeApp.tsx (the grow delta), or the scope would
shift. Content taller than this scrolls. */
/* Settings panel along the bottom. The editor asks the host to grow by this height,
but Windows DAWs may delay or constrain that resize, so the viewport locks to its
previous height and this panel overflows instead of shrinking the scope. */
.spectrum-app__panel {
flex: 0 0 280px;
width: 100%;