A browsable, searchable catalog of the audio plugins installed on this machine — metadata, and eventually a picture of each plugin's GUI.
Get the latest PlugBrowser-<version>-win-x64.zip from the
Releases page, unzip it anywhere, and run
PlugBrowser.App.exe. Windows 10 or 11, 64-bit. Nothing else needs installing: .NET and the C++
runtime are included.
The download is not code-signed yet, so Windows SmartScreen may warn about an unrecognized app the first time it runs; choose More info → Run anyway.
Discovery, catalog, and browser UI are complete. Scanning finds 418 plugins on the development machine (196 VST3, 222 VST2) in about 300 ms, without loading a single plugin.
Capture works, at scale. A full run over this machine's library with the native worker:
418 found · 194 loaded · 199 screenshots · 2 failed · 12 min
That is 99% of installed VST3 plugins photographed — effects, instruments and copy-protected titles
alike — every one via PrintWindow off screen. That machine is a hybrid-graphics laptop, which is why:
on a desktop GPU the same library lost all 87 Arturia editors to blank captures until the on-screen
fallback below was added.
For comparison, the managed worker on the same library managed 190 loaded · 189 screenshots · 15 failed in 22 minutes. Most of that extra time was the 45-second timeout burned by each plugin it could
not load.
Two plugins still fail: KORG MS-20 V dies with an access violation, and VocalSynth 2 stops responding. Both are contained by the sandbox and both are worth retrying — see the note on non-reproducibility below.
Those failures are also not reproducible run to run. The same bx_* plugin variously hangs until
the watchdog kills it, dies by native fail-fast, or fails cleanly in 270ms with "load failed" — which is
what licence-server interaction looks like from the outside. So a failed plugin is worth retrying; the
catalog keeps the state and the reason so a later scan can pick it up.
Distinguishing the two cases needed care. The worker's watchdog ends the process with
Environment.FailFast, and a plugin crashing via native __fastfail produces a nearly identical
exit code, so a hang and a crash were indistinguishable from the parent. The watchdog now writes an
explicit timed-out report before it pulls the plug, and the parent trusts that over the exit code.
Scanning is wired into the app. Scan & Options lists every scan root — conventional, registered by an installer, or added by you — with per-root enable switches and plugin counts, and runs the scan from the same dialog with live progress. Captured screenshots appear in the gallery at each plugin's own aspect ratio.
Filters, the chosen vendor, and window geometry are remembered between runs.
- Category facets built from the subcategory tags the plugins themselves report — Delay, Dynamics, Reverb, Modulation and so on, each with a count. Ticking several asks for any of them, which is how a facet list reads; requiring all would return almost nothing, since a plugin rarely claims two.
- Every facet count cross-filters. Counts are computed against the rest of the active query, so typing "arturia" takes the sidebar from Delay (8) to Delay (3) and Drum (4) to Drum (0). Each group's count excludes its own selections — otherwise ticking Delay would show "Delay (1)" and zero beside every sibling, which is the opposite of what a facet list is for.
- Result count above the gallery, tracking the current filter.
- Full-size view: double-click a card, or click the detail pane's image. Click anywhere or press Escape to dismiss.
- Clear filters resets everything including the search; the search box carries its own ✕.
- Unique only hides plugins that are also installed in another format — see below.
- Multi-select with the usual Ctrl/Shift gestures. The detail pane then shows only traits the selection genuinely shares — a differing one reports "(3 different)" rather than quietly showing the first plugin's value, which would be a claim about plugins it does not describe.
Scan & Options → View all plugins opens every catalogued file as one spreadsheet-style grid: plugin, format, which formats it is installed as, type, vendor, version, architecture, size, whether it has a screenshot, and the binary's location. Every column sorts from its header. A search box matches the text columns, and each column with a fixed set of values (format, installed as, type, vendor, architecture, screenshot) has its own drop-down filter. Copy as spreadsheet puts the rows shown, in the order shown, on the clipboard as TSV, and Export CSV… saves the same rows to a file. Every column header has a tooltip explaining it.
A VST2 and a VST3 build of the same plugin are usually installed together by one installer, so most catalogs contain each plugin twice. On the development machine that is 139 products and 278 files — 278 of 418 entries, leaving 140 unique. The grid's Installed as column shows it per row ("VST3 + VST2"); the Unique only facet hides those plugins in the main gallery, and Redundant only shows just them.
Matching is by a normalised product name, because the same product is named inconsistently across its own builds — "Addictive Drums 2" as VST3 against "Addictive Drums 2 x64" as VST2, "Comp FET-76" against "Comp FET 76". The normalisation is deliberately conservative: a false duplicate would invite someone to delete a plugin they still need, which is far worse than missing one. Audited against the real catalog, every group it found was a genuine match.
Some plugins show a registration or trial prompt in their editor, so the stored screenshot is a picture of that dialog rather than the interface — every Audio Plugin Union plugin on the development machine did this. Select those plugins and use Reload & recapture: it re-probes just that selection, bypassing the unchanged-file check, since nothing about the file changed — only the licence state outside it. A recapture that fails keeps the previous image rather than leaving the plugin with none.
Two VST3 subcategory groups are deliberately kept out of the category list. Fx and Instrument
duplicate the Type facet, and listing them twice lets the two controls be set into a contradiction that
silently returns nothing. OnlyRT, NoOfflineProcess and the channel-layout markers are processing
capabilities the spec packs into the same string as musical categories — real, but noise in a list
meant for picking "delay" or "reverb".
src/PlugBrowser.Core/ discovery, PE parsing, SQLite catalog, scan orchestration
native/ PlugBrowser.NativeWorker - the C++ loader/capture worker (preferred)
src/PlugBrowser.Worker/ the managed worker, used as a fallback
src/PlugBrowser.App/ Avalonia 11 browser UI
tests/PlugBrowser.Tests/ xUnit suite
external/NetPlugHost/ the VST3 host (git submodule)
tools/ make-icon.ps1, which regenerates the app icon from icon.png
Needs the .NET 9 SDK and Visual Studio 2022 (or Build Tools) with the Desktop development with C++ workload, which brings MSVC and CMake.
git clone --recursive https://github.com/Bezmont/PlugBrowser.git
cd PlugBrowser
.\build-native.ps1 # NetPlugHost's native DLL and the native worker
dotnet build PlugBrowser.sln # the app, with both workers copied next to it
dotnet test # the test suite
.\publish.ps1 -Version 1.0.0 # the release zip, under artifacts\NetPlugHost, the VST3 host, is a git submodule at
external/NetPlugHost, and it pins the Steinberg VST3 SDK in turn, hence --recursive. Already cloned
without it? git submodule update --init --recursive. To build against a NetPlugHost checkout of your
own instead, pass -NetPlugHost <path> to build-native.ps1 and -p:NetPlugHostDir=<path>\ to
dotnet build.
Without the C++ workload the app still builds and catalogs plugins from their files, but cannot load them or take screenshots, since the VST3 host itself is native code.
Both native binaries link the C++ runtime statically, so the release runs on machines without the Visual C++ Redistributable.
.github/workflows/build.yml builds and tests every push. Pushing a version tag also packages the app
and publishes a GitHub Release with the zip attached:
git tag v1.0.0
git push origin v1.0.0A tag with a suffix, such as v1.1.0-beta.1, is published as a pre-release.
The app never loads a plugin itself. PlugBrowser.Worker is launched once per bundle and killed —
process tree and all — if it overruns its budget. Large sample-library and amp-sim plugins do hang on
load; out of process that costs one catalog row marked "timed out" instead of a frozen UI.
Inside the worker, capture is: create an off-desktop top-level window, parent the plugin's editor into a
child of it, pump messages until two consecutive captures come back identical (so splash screens and
fades finish), then PrintWindow with PW_RENDERFULLCONTENT.
Two details that are easy to get wrong:
- Re-query the editor size after attaching. Most plugins only know their real size then.
- A blank capture is a silent failure, not an error. An OpenGL or Direct3D editor returns solid black and a success code, so the only way to detect it is to inspect the pixels — hence the distinct-colour check that rejects flat images.
- Off screen is not the same everywhere. OpenGL lets a driver skip drawing pixels that are on no
monitor. A hybrid laptop draws the whole frame anyway and copies it in, so the off-desktop window
captures fine; a desktop GPU driving the display directly skips every pixel and the capture is blank.
So a blank capture is retried with the window on the primary monitor, first as a click-through window
at 1/255 opacity (
OnScreenTransparent, invisible in practice), then fully visible (OnScreenVisible). On the desktop machine that recovered every blank editor: 88 through the transparent rung, and exactly one — Arturia's Bus PEAK — that drew only once the window was genuinely visible, which is what keeps the last rung in the ladder.--onscreenmakes the native worker skip straight to those for testing.
Plugin Alliance's bx_* titles, elysia, SPL and Knifonium hang forever in the .NET worker while loading
fine in any DAW. The cause is not the plugin or the ABI: NetPlugHost's own native C++ smoke host
loads every one of them in 2–5 seconds. The only difference is a CLR in the process.
During a hang the process burns a full core, has WININET/CRYPT32/WS2_32 loaded and no network
connections — copy protection spinning in a local check loop, not waiting on a licence server. These
titles use PACE/iLok anti-tamper, which probes for debuggers and instrumentation; a CLR process presents
exactly that shape (vectored exception handlers, hardware exceptions for null checks, threads suspended
for GC), so the protection goes defensive. It also explains the nondeterminism — the same plugin
variously hangs, fails fast, or fails cleanly depending on how the race falls.
Fixed by PlugBrowser.NativeWorker, a C++ worker with no runtime in its process. All eight
previously-unloadable plugins now load and capture in 3–7 seconds:
bx_solo Fx|Analyzer 7 params 380x114
bx_subfilter Fx|EQ|Filter 8 params 481x211
bx_masterdesk Classic Fx|EQ|Dynamics 12 params 630x389
bx_console N Fx|EQ|Dynamics 55 params 700x785
bx_oberhausen Instrument|Synth 269 params 850x683
elysia niveau filter Fx|EQ 5 params 455x174
SPL Free Ranger Fx|EQ 8 params 308x450
Knif Audio Knifonium Instrument|Synth 317 params 1200x541
The worker talks to plugins through NetPlugHost's flat C ABI rather than the VST3 SDK directly, so all
the hosting logic stays in one place and the worker stays a thin shell around windowing, capture and
JSON — no SDK headers, no submodule, three source files. It emits the same result.json the managed
worker does, so nothing downstream changed.
WorkerRunner picks the native worker whenever it is present and falls back to the managed one
otherwise, because building it needs a C++ toolchain.
The VST3 spec requires IPlugView::setFrame(IPlugFrame*) before attached(). NetPlugHost never
called it, and the omission is not benign: plugins are entitled to call plugFrame->resizeView() from
inside attached() when their editor sizes or scales itself, and one that does not null-check its frame
dereferences null and takes the host process down.
That is what every Native Instruments plugin was doing. Battery 4 and Kontakt died inside attached()
with NI's own crash dialog; Super 8 produced a bare 0xC0000005 access violation. All of them load fine
in any DAW, because every DAW implements this interface. Fixed in NetPlugHost by implementing a minimal
IPlugFrame that resizes the parent window and forwards to onSize().
One wrinkle: the SDK declares IPlugFrame's IID but defines it in none of the translation units this
target compiles, so the host has to provide it with DEF_CLASS_IID(IPlugFrame).
Known limitation. A few plugins draw an information or licence screen inside their own editor rather than in a separate window — every Elektron Overbridge plugin does this — so the capture is of that notice, not the interface. Foreign-window detection cannot catch it, because there is no foreign window. These are visible in the gallery and can be recaptured individually.
Reported editor size is not always the size drawn: Arturia's Comp FET-76 reports 960x508, the height it needs with its "Advanced" panel open, but paints only the top 289px while collapsed. Uniform edge bands are cropped off, so stored images match the visible interface and the gallery can lay every card out at the plugin's true proportions.
dotnet test # the test suite
dotnet run --project src/PlugBrowser.App # the browserThe catalog lives at %LOCALAPPDATA%\PlugBrowser\catalog.db. Deleting it forces a full rescan.
Every scan and reload appends to %LOCALAPPDATA%\PlugBrowser\scan.log: each bundle's outcome, every
class's editor size and capture method, the reason any screenshot was not taken, and whatever the plugin
printed to the console. It rotates to scan.previous.log past 4 MB. Start there when a plugin has no picture.
No plugin is loaded. Everything shown today comes from the filesystem:
- Traversal. A
.vst3entry is one plugin whether it is a file or a directory, and a.vst3directory is never descended into — doing so rediscovers its ownContents/x86_64-win/Name.vst3binary as a phantom second plugin. - PE version resource. The dependable source of vendor and version. On this machine 147 of 151 flat VST3 files carry one, and VST2 DLLs carry product names too.
- Exports.
GetPluginFactorymarks VST3,VSTPluginMain/mainmarks VST2 — but the file extension must be part of the test, because every Arturia VST3 also exportsmain. moduleinfo.json. Rich when present (CIDs, categories, SDK version) but rare: 3 bundles of ~229. Note the SDK writes"Sub Categories"with a space and emits trailing commas.- Vendor snapshots.
Contents/Resources/Snapshots/<CID>_snapshot.pngis part of the VST3 spec and would be the ideal plugin image — but zero plugins on this machine ship one, so it is a bonus path, not a foundation.
Plugins are addressed by bundle path and factory index. Exposing each class's CID through NetPlugHost would let them be addressed by identity instead: index-based addressing can re-point cached data at the wrong plugin when a vendor ships an update that reorders a bundle's classes. Worth doing, but not blocking, since nearly every bundle exposes exactly one class.
An earlier reading of NetPlugHost suggested its kVstAudioEffectClass filter hid instruments, making
the rebuild urgent. That is wrong, and worth recording so nobody re-derives it: in VST3,
kVstAudioEffectClass is the component category for every audio processor. Instruments included.
The instrument/effect distinction lives in the subcategory string — "Fx|Dynamics" versus
"Instrument|Synth" — which the v1 ABI already reports. Instruments enumerate, instantiate and
screenshot today.
- NetPlugHost is the VST3 host PlugBrowser loads plugins through: a C++ wrapper around the Steinberg VST3 SDK with a C# P/Invoke layer. It loads modules, instantiates plugins, and opens their editors into a host-supplied window, which is the hard half of screenshot capture. It is included here as a submodule.
- StreamRecorder uses the same host, which is why NetPlugHost is only ever extended additively: its existing exports keep their exact meaning, and new ones sit alongside them.
GPLv3; see LICENSE. NetPlugHost is GPLv3 because it links the Steinberg VST3 SDK under
that SDK's GPLv3 option, and linking NetPlugHost makes PlugBrowser GPLv3 too.
VST is a registered trademark of Steinberg Media Technologies GmbH.
VST2 is catalogued but never loaded, and gets no screenshots. Steinberg no longer licenses the VST2 SDK, so hosting it would require a clean-room header. The metadata available without loading turns out to be good — vendor, product name, version, architecture — so VST2 entries are not second-class in the browser.