fix(host): make the hosted VST3 editor actually embed#6444
Merged
Conversation
A hosted VST3's editor never appeared: create_hosted_editor() returned null, so EditorAttachment::create() returned null and nothing attached. Four separate defects, all on the path from "load a VST3" to "its editor is on screen": 1. No IPlugFrame. IPlugView documents IPlugFrame::resizeView() as callable from inside attached() — it is how a plug-in reports the size it really wants — so a view with no frame either mis-sizes itself or refuses to attach, and a refused attached() is exactly the null the host sees. Install the frame right after createView (before anything else is asked of the view), route resizeView through the container + the slot's resize-request handler + onSize, and clear the frame on every release path so a plug-in can never call a destroyed frame. The slot now publishes its view/container before attached() and reports the post-attach size. 2. Combined-vs-separated was decided by casting both interface pointers to FUnknown* and comparing. A combined plug-in inherits IComponent and IEditController separately, so each carries its own FUnknown base subobject at a different address and every combined plug-in was classified "separated" — which called IPluginBase::terminate() twice on it at unload and stored its state twice. Identity now goes through the FUnknown query, verified against the SDK's SingleComponentEffect. 3. A separated plug-in's two halves were never joined over IConnectionPoint, so its editor could not send its own processor a message. Connect both directions at load, unwind the accepted half if the other refuses, and disconnect before either terminates. 4. IHostApplication::createInstance returned kNotImplemented, so the plug-in could not allocate the IMessage / IAttributeList objects that connection-point traffic is built from. Derive HostApp from the SDK's Vst::HostApplication (which also supplies IPlugInterfaceSupport), keeping the singleton's refcount neutered. Also: push the component state into a separated controller at load (it otherwise opens the editor on its own defaults), and have EditorAttachment own the slot's resize-request channel so a plug-in-driven resize moves the child view the host placed, not just the slot's private container. The three new seams are headless-testable; test_vst3_connection.cpp is new and test_vst3_editor.cpp / test_hosted_editor_migration.cpp grow frame-lifecycle and resize-wiring coverage.
…quest The two formats' no-handler answer is deliberately opposite — VST3 accepts, CLAP denies — because VST3's resizeView is UI-thread-only and reports a plug-in's real size from inside attached(), while CLAP's request_resize is [thread-safe] and can arrive somewhere a native view must not be touched. Write it down so it does not get 'harmonized'.
reuse_shared_git_source accepted ANY existing symlink at external/<dep> as long as the marker file resolved through it. The shared cache directory name embeds the ref, so a re-pin creates a new cache next to the old one and leaves the link aimed at the old one — every later ./setup.sh then prints "linked from shared cache" while the tree stays a version behind, forever. That is how a v3.7.12 VST3 SDK survived the move to the MIT v3.8.0 pin on a machine whose shared cache already held v3.8.0_build_66: the build, the tests, and every plug-in built from that checkout silently used the older, GPL-or-proprietary tree, and only `tools/deps/audit.py --verify-licenses` noticed. A symlink can be repaired without destroying anything, so a stale one is now re-pointed. A real directory is developer-managed (a hand clone, an unpacked release), so it is never deleted — it is reported instead, via fail(), which ERRORS already turns into a non-zero setup exit. git_checkout_matches_cache answers "matches" whenever it cannot tell (no .git, or an unprovisioned cache) so an honest unknown never fails a run.
… parent The free-function seams cover the VST3 negotiation calls in isolation, but not the part that only exists once the slot is holding the IPlugFrame: IPlugView documents IPlugFrame::resizeView() as callable from inside attached(), and honoring it requires the slot to publish its view and container BEFORE the attach call. That ordering is the fix, and nothing was covering the composition. make_vst3_slot() mirrors make_clap_slot(): build a slot around caller-created SDK interfaces, no dlopen and no bundle. The new macOS-only suite drives it with a fake IEditController whose view calls back into the host mid-attach, parented into a real NSWindow content view. Covers: the container really becomes a subview at the plug-in's size; the frame is live during attached(); a resize requested from inside attached() resizes the container, commits with onSize, and is what create_hosted_editor reports; an embedder handler sees the request and can veto it; a refused attach leaves nothing parented and does not wedge the slot; and a combined plug-in is terminated exactly once. Negative controls, each reverting one piece of the fix: dropping setFrame fails 4 of 7, publishing the view/container after attached() fails 3 of 7, and the FUnknown*-cast identity check makes the combined plug-in report terminate_calls == 2.
The guide implied the exposure starts at "deeper analysis by instantiating and processing untrusted plug-ins", which understates it: a plug-in can fault inside its own factory or initialize() and take the host down before load() returns, with no audio processed at all. Observed with a shipping commercial VST3 whose licensing prerequisites are absent on the machine — it segfaults four frames deep inside its own binary during load_vst3_plugin, with PluginSlot::load the only Pulp frame on the stack, and reproduces regardless of Pulp version. Worth naming so the next reader of that crash log does not go hunting for a host-side bug.
vst3_same_object returned false — "separated" — when a plug-in could not answer the FUnknown query identity depends on. That is the wrong default: it lands a non-conformant plug-in on exactly the path this branch just fixed, calling IPluginBase::terminate() on one object twice. The two wrong answers are not equally bad. Treating one object as two is undefined behavior inside the plug-in; treating two objects as one skips a terminate and leaks. An undeterminable identity now reports "same object".
danielraffel
force-pushed
the
fix/vst3-hosted-editor-null
branch
from
July 22, 2026 12:50
b95111a to
2f294bd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reported against SDK 0.691: hosting a VST3 works, but its editor never appears —
create_hosted_editor()returns null, soEditorAttachment::create()returnsnull and nothing attaches.
The report's own cause is not a bug:
v0.691.0was tagged 2026-07-17 andVST3 editor embedding landed 2026-07-18 (
74305e4fb, first shipped inv0.702.0). At their pin the slot was
has_editor() -> falsewith nocreate_hosted_editoroverride at all. That half needs an SDK upgrade, not a fix.Auditing the newer path for the same symptom turned up four real defects, plus
one found reviewing this branch.
Fixed
IPlugView::setFrame()was never called anywhere in the repo.IPlugViewdocuments
IPlugFrame::resizeView()as callable from insideattached()—it is how a plug-in reports the size it really wants. With no frame a plug-in
either mis-sizes itself or refuses to attach, reproducing the reported
null on current builds. The frame is now installed immediately after
createView,resizeViewroutes through the container +onSize, and everyrelease path clears it so a plug-in can never call a destroyed frame.
Combined-vs-separated identity used a
static_cast<FUnknown*>compare. Acombined plug-in inherits
IComponentandIEditControllerseparately, soeach carries its own
FUnknownbase subobject at a different address —every combined VST3 was classified "separated", which called
IPluginBase::terminate()twice on it at unload and stored its statetwice. Verified against the SDK's own
SingleComponentEffect. Identity nowgoes through the
FUnknownquery.Separated halves were never joined over
IConnectionPoint, so aplug-in's editor could not send its own processor a message.
IHostApplication::createInstancereturnedkNotImplemented, so theplug-in could not allocate the
IMessage/IAttributeListobjects (3) runson.
HostAppnow derives from the SDK'sVst::HostApplication.Identity failed toward the crash.
vst3_same_objectreturned"separated" when the
FUnknownquery could not be answered — dropping anon-conformant plug-in onto the double-
terminate()path above. It now failstoward the leak, which is recoverable.
Also: a separated controller now gets
setComponentStateat load (it opened itseditor on its own defaults), and
EditorAttachmentowns the slot'sresize-request channel so a plug-in-driven resize moves the child view the host
placed, not just the slot's private container.
Unrelated fix carried here
setup.sh'sreuse_shared_git_sourceaccepted any existing symlink atexternal/<dep>, so a re-pin left it aimed at the old shared cache forever.That let a
v3.7.12VST3 SDK — GPL-or-proprietary — outlive the move to the MITv3.8.0pin, with the build, tests and plug-ins silently using it. A stalesymlink is now re-pointed; a real directory is reported, never deleted.
Verification
test_vst3_connection.cpp(11 cases) andtest_vst3_hosted_editor.mm(7 cases — the real
Vst3Slotagainst a realNSWindow, with a plug-in thatcalls
resizeViewfrom insideattached()), plus frame-lifecycle andresize-wiring coverage added to two existing suites.
setFramefails4 of 7; publishing the view/container after
attached()fails 3 of 7; theFUnknown*-cast identity makes a combined plug-in reportterminate_calls == 2.setup.shfix; pre-fixsetup.shfails 5 groups.pre-existing
examples/PulpSampler+ a-j6import-design flake, nonereachable from this diff (it touches no
examples/file, andpulp-sampler-testlinks neitherpulp::hostnorvst3-sdk).plugin-host-demo --editoragainst VST3 SDK v3.8.0_build_66.🔎 Provenance
claudem3/Volumes/Workshop/Code/pulp-vst3-editor48c1f115-283b-4780-9bd4-95175eaf79eeResume
Jump to this tab
Relaunch (any agent)
stamped 2026-07-22 12:50 UTC