# GR6 fast-fail — PoC results + revised diagnosis Date: 2026-05-13 Branch: `diag/vst-trace` (unchanged) + new scratch dir `C:\Users\byron\vst-poc\` on Win11 VM PoC binary: `C:\Users\byron\vst-poc\build\Release\vst_poc.exe` ## TL;DR — the original diagnosis was half right The original `DIAG-REPORT.md` proposed Fix Path 1 ("sandboxed plugin-host process with QApplication"). The PoC confirms **the sandboxed-process direction is correct**, but **QApplication is not needed at all** — the entire root cause is JUCE's message thread not being the OS main thread. | Mode | Process | JUCE MessageManager thread | QApplication? | GR6 editor opens? | |---|---|---|---|---| | Baseline | `node load-gr6.js` | JUCE-internal (TID ≠ main) | no | **crashes** in 1–2 s with `0xC0000409` | | PoC #1 | `vst_poc.exe` | OS main thread (TID == main) | yes (Qt 5.15.2) | **clean**, editor 1110×780, runs 10 s, exit 0 | | PoC #2 | `vst_poc.exe POC_NO_QAPP=1` | OS main thread (TID == main) | **no** | **clean**, editor 1110×780, runs 10 s, exit 0 | PoC #2 is the load-bearing experiment: dropping QApplication entirely still works as long as JUCE owns the OS main thread. The "no QCoreApplication instance" Qt warnings the original report flagged don't even fire in PoC #2 — GR6's static-linked Qt5 is happy as long as the calling thread has the OS-main-thread properties it expects. ## How the PoC reproduces / refutes the bug ```cpp // main.cpp (essentials) int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { // OPTIONAL — POC_NO_QAPP env disables this leg. Same outcome either way. QApplication qapp(qArgc, qArgv); juce::ScopedJuceInitialiser_GUI juceInit; // (a) MessageManager bound to TID=WinMain VSTHost host; auto plugin = host.loadPlugin(GR6_PATH, 48000.0, 256, err); auto editor = plugin->createEditor(); // (b) runs on the same TID editor->setSize(1000, 600); EditorWindow window(...); window.setVisible(true); while (alive) { // (c) message pump on main thread juce::MessageManager::getInstance()->runDispatchLoopUntil(50); } } ``` The three pieces above are necessary and sufficient. Compare to today's Slopsmith addon: ```cpp // src/audio/NodeAddon.cpp::OpenPluginEditor juce::MessageManager::callAsync([processor, ...]() { auto* editor = processor->createEditor(); // runs on JUCE's worker thread, ... // which is NOT the V8/main thread }); ``` Inside a Node native addon, V8 owns the OS main thread; JUCE silently spins up its own "message thread" via `MessageManager::startup()` to give itself a place to run UI work. That thread has a Win32 message queue but it is not a `QThread` and is not the OS main thread. GR6's embedded Qt5 starts timers and connects QML signals on whatever thread created its editor, hits Qt's thread-affinity guards (`Timers can only be used with threads started with QThread`, `Illegal attempt to connect ... different thread than the QML engine`), then `__fastfail`s. ## Revised recommended fix `slopsmith-vst-host.exe` — a sandbox-process plugin host. **Same shape as the PoC, without the Qt link.** Concretely: 1. **Subprocess binary**: standalone Win32 exe with `WinMain`. Loads exactly one VST3 via the existing JUCE `VSTHost`. Runs JUCE's MessageManager on its main thread, opens the editor on its main thread, owns the editor HWND. 2. **No QApplication, no Qt link**: PoC #2 proves it isn't needed for GR6. Drop the ~50 MB of Qt deps the original report assumed. 3. **IPC for audio + control**: shared-memory ring buffer for audio, named pipe for control messages (load, setParameter, openEditor, etc.). Same surface as the current in-process `VSTHost`, so `SignalChain` doesn't have to know. 4. **Detection**: only route through the sandbox for plugins that fail in-process. The PoC plus the Qt warnings in the JUCE-thread case strongly suggest any Qt-using plugin (Reaktor 6, Massive X, Komplete Kontrol, Battery 4, etc.) has the same issue; non-Qt plugins (most JUCE-based ones) are fine in-process. ## What is dropped vs. the original plan - **Qt dependency entirely.** ~2 weeks of "make Qt cohabit with JUCE" work doesn't happen because we don't need Qt. - The "Qt main loop must own the message pump" anxiety in the original §"Why fixing this in-process is hard" doesn't apply — Win32 message pump (drained by JUCE's `runDispatchLoopUntil`) is enough. - The original cost estimate of ~2 weeks for single-plugin sandbox / ~1 month for pooled is still roughly right, dominated by the IPC layer, not Qt. ## Followups - **Editor window reparenting**: the production sandbox needs to reparent the plugin's HWND into the Electron renderer (or pop a separate top-level window owned by Electron). Standard pattern — `SetParent` from the sandbox after sending the HWND over IPC. - **Audio latency budget**: one extra IPC hop adds ~1–2 ms at 48 kHz/256 frames. Fine for guitar processing; reconsider for low-latency monitoring. Use a shared-memory ring + futex-style signalling (Windows events) to avoid syscall round-trips per buffer. - **Crash isolation**: a plugin in the sandbox crashing only takes down its own process; Slopsmith Desktop can detect, log, restart. Bigger win than just fixing GR6. ## Reproducing the baseline crash The in-process load that motivated this work is reproduced via the existing smoke harness on a Windows host with GR6 installed: ```bat cd node scripts\dev\load-gr6.js ``` Before this PR's sandbox patch, that command crashed with `STATUS_STACK_BUFFER_OVERRUN (0xC0000409)` from the Qt5 GR6 path. The single-file PoC that confirmed the QApplication-on-message-thread fix lived under a developer-local scratch tree during investigation; the findings are summarised above and the production fix is the out-of-process sandbox in this PR. There is no reproducible artefact to ship for the PoC — the experiment is fully captured by the diagnostic narrative above.