| # PluginBridge β Failed Approaches & Architecture Decisions Log |
| > Everything we tried that didn't work, why it failed, and what we did instead. |
| > Use this to avoid repeating mistakes or to revisit with better solutions later. |
|
|
| --- |
|
|
| ## 1. Out-of-Process GUI via Floating Window (Sprint 2, v0.2) |
|
|
| **What we tried:** |
| Host the plugin in a separate Helper process. Show the plugin's GUI as a floating window from the Helper process, overlaying Ableton. |
|
|
| **What worked:** |
| - Audio routing via shared memory β
|
| - Crash safety (Helper crashes, Ableton survives) β
|
| - GUI showed correctly when Ableton was in WINDOWED mode β
|
|
|
| **What broke:** |
| When Ableton was in macOS FULLSCREEN, the plugin GUI opened in a separate Space (different virtual desktop), not over Ableton. |
|
|
| **Fixes attempted (all failed in fullscreen):** |
|
|
| | Attempt | Code | Result | |
| |---|---|---| |
| | `NSApplicationActivationPolicyAccessory` | Set in `main.cpp initialise()` | No Dock icon β
but broke fullscreen behavior β | |
| | `setLevel: 3` (NSFloatingWindowLevel) | In `showAsFloatingPanel()` | Not high enough β fullscreen apps are above level 3 β | |
| | `setLevel: 25` (NSStatusWindowLevel) | In `showAsFloatingPanel()` | Still opens in separate Space β | |
| | `setCollectionBehavior: canJoinAllSpaces + fullScreenAuxiliary` (257) | Before `setVisible(true)` | Still separate Space β | |
| | `setCollectionBehavior` + `stationary` + `ignoresCycle` | Full flags (1<<0 \| 1<<4 \| 1<<6 \| 1<<8) | Still separate Space β | |
| | `orderFrontRegardless` | After setting level | Window appears but wrong Space β | |
| | `setHidesOnDeactivate: 0` | Combined with above | No effect on Space problem β | |
| | `setAlwaysOnTop(true)` (JUCE method) | Reverted to original approach | Was original working code but didn't fix fullscreen either β | |
|
|
| **Root cause:** |
| macOS fullscreen creates a dedicated compositor Space. Apple prevents other processes from injecting windows into a fullscreen app's Space. No combination of window level, collection behavior, or activation policy reliably works for cross-process window display in fullscreen. This is a macOS platform limitation, not a code bug. |
|
|
| **What we did instead:** |
| Switched to in-process hosting (v0.3). Plugin GUI is embedded inside PluginBridge's own editor. Ableton manages the window. No separate process window needed. |
|
|
| --- |
|
|
| ## 2. X Button (Close) Not Working on Helper Window (Sprint 2, v0.2) |
|
|
| **What happened:** |
| The Helper's floating window had minimize and close (X) buttons visible, but clicking X did nothing. |
|
|
| **Root cause:** |
| When the Helper ran as `NSApplicationActivationPolicyAccessory`, macOS doesn't deliver close-button clicks to accessory-policy processes the same way. The native title bar's close button was visible but non-functional. |
|
|
| **Fix by Claude Code (commit 0632681):** |
| Removed `makeAccessoryProcess()`, added `showAsFloatingPanel()` with objc_msgSend level tricks. This fixed the X button but broke the fullscreen behavior (see #1 above). |
| |
| **What we did instead:** |
| In v0.3 (in-process hosting), there's no separate window at all. The plugin editor is a child component inside PluginBridge's editor. No close button needed β the DAW manages the window lifecycle. |
| |
| --- |
| |
| ## 3. Plugin Crashes Crashing Ableton (v0.3 initial) |
| |
| **What happened:** |
| After moving to in-process hosting, loading Kickstart 2 crashed Ableton (because the plugin crash happens inside Ableton's process). |
| |
| **Root cause:** |
| `loadPlugin()` called `createPluginInstance()` directly without first testing if the plugin was safe. Some plugins (Kickstart 2, Ozone 12, Gullfoss) call `abort()` or show `NSAlert` during loading, which kills the host process. |
| |
| **Fix attempted that was missing:** |
| The design called for Helper-based test loading BEFORE in-process load, but it was left as a `// TODO` stub. |
| |
| **What we did:** |
| Implemented `testPluginSafety()` β spawns `PluginBridgeHelper --test "/path"` before loading. If Helper survives (exit 0) β safe to load in-process. If Helper crashes β add to blocklist β show "Not compatible" β Ableton stays alive. |
| |
| --- |
| |
| ## 4. In-Process Hosting = No Crash Safety for Audio Processing |
| |
| **Current limitation (v0.3):** |
| If a plugin passes the test-load but crashes during `processBlock()` later (rare but possible), Ableton will still crash. The Helper test only validates loading, not runtime stability. |
| |
| **Potential future solutions:** |
| - Wrap `processBlock()` in a signal handler (fragile, not recommended) |
| - Use `setjmp`/`longjmp` around processBlock (undefined behavior with C++) |
| - Move back to out-of-process audio routing BUT keep GUI in-process (hybrid approach) |
| - Accept the risk β plugins that crash during audio processing are very rare |
| |
| **Status:** Accepted risk. Most crashes happen during loading, not during audio processing. |
| |
| --- |
| |
| ## 5. Plugin Scanning via PluginDirectoryScanner (Early research, never shipped) |
| |
| **What was planned:** |
| Use JUCE's `PluginDirectoryScanner` to scan all installed plugins on first run. |
| |
| **Why it was rejected:** |
| Some plugins execute code during scanning (show NSAlert dialogs, call abort()). This would crash the scanner β or worse, crash Ableton if scanning happened in-process. |
| |
| **What we did instead:** |
| Read `.vst3` and `.component` filenames from disk (no code execution). Only actually load a plugin when the user selects it (after Helper test). |
| |
| --- |
| |
| ## 6. `addDefaultFormats()` β Deleted in JUCE 8 |
| |
| **What happened:** |
| Early code used `formatManager.addDefaultFormats()` which doesn't exist in JUCE 8. |
| |
| **Fix:** |
| Explicitly register formats: |
| ```cpp |
| formatManager.addFormat(new juce::VST3PluginFormat()); |
| formatManager.addFormat(new juce::AudioUnitPluginFormat()); |
| ``` |
| |
| --- |
| |
| ## 7. `MSG_NOSIGNAL` β Doesn't Exist on macOS |
|
|
| **What happened:** |
| Socket code used `MSG_NOSIGNAL` flag which is Linux-only. |
|
|
| **Fix:** |
| ```cpp |
| signal(SIGPIPE, SIG_IGN); // Global |
| int nosigpipe = 1; |
| setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); // Per-socket |
| ``` |
|
|
| --- |
|
|
| ## 8. `juce::Thread::setCurrentThreadPriority()` β Removed in JUCE 8 |
|
|
| **What happened:** |
| Audio thread priority setting via JUCE API no longer exists. |
|
|
| **Fix:** |
| Use native pthread: |
| ```cpp |
| struct sched_param param; |
| param.sched_priority = sched_get_priority_max(SCHED_FIFO); |
| pthread_setschedparam(pthread_self(), SCHED_FIFO, ¶m); |
| ``` |
|
|
| --- |
|
|
| ## 9. `create_audio_track()` Serialization Error (Ableton LOM, not PluginBridge) |
|
|
| **Context:** From the broader AI Music Production system, not PluginBridge-specific. |
|
|
| **What happened:** |
| `song.create_audio_track()` in Ableton's LOM returns a "not JSON serializable" error. |
|
|
| **Reality:** |
| The track IS created. The error is in the return value serialization, not the action. |
|
|
| **Workaround:** |
| Always check `len(song.tracks)` to confirm track was created, ignore the error message. |
|
|
| --- |
|
|
| ## Architecture Evolution Summary |
|
|
| ``` |
| v0.1 (Sprint 1): Plugin shell + MCP server only (no hosting) |
| v0.2 (Sprint 2): Out-of-process hosting via shared memory |
| β
Crash-safe audio |
| β GUI in separate window β fullscreen broken |
| v0.3 (Sprint 3): In-process hosting (like SnappySnap) |
| β
GUI embedded in Ableton's window |
| β
Helper tests plugin safety before loading |
| β No crash safety during audio processBlock (accepted risk) |
| ``` |
|
|
| --- |
|
|
| ## 10. CAContext + CALayerHost β Cross-Process GUI Embedding (v0.4, 2026-05) |
|
|
| **What we tried:** |
| After the floating-window fullscreen failure (see #1), we tried the macOS private API for sharing a `CALayer` tree across processes: |
| - **Helper (server):** `CAContext contextWithCGSConnection:options:` β wraps the plugin's GUI layer and exposes it as a `uint32_t contextId` |
| - **Plugin (client):** `CALayerHost.contextId = contextId` β a `CALayer` subclass that renders the remote context inline in JUCE's NSView |
|
|
| The contextId was sent over the existing IPC socket. No Mach port bootstrapping needed. |
|
|
| Also fixed a secondary bug: the Helper's NSWindow was never ordered front. Fix was to position it off-screen at `(-32000, -32000)` and call `orderFront:`. |
|
|
| **What happened:** |
| - Build succeeded, API calls compiled and ran without errors |
| - `CALayerHost` was created and added as a sublayer to JUCE's NSView |
| - The plugin GUI never appeared inside the PluginBridge editor window |
| - Debug showed: `contextId` was valid (non-zero), `CALayerHost` was attached, but nothing rendered |
|
|
| **Why it failed:** |
| Ableton Live runs inside the macOS app sandbox / process isolation model. `CAContext` cross-process layer sharing requires both processes to be in the same WindowServer session with compatible entitlements. Ableton's process environment appears to block this β the compositor receives the layer tree but doesn't render it into a foreign process's view hierarchy. |
|
|
| This is the same fundamental limitation as the floating-window approach: Apple does not provide a reliable, supported API for one process to embed rendering from another process's view hierarchy. |
|
|
| **Attempts made:** |
| | Attempt | Result | |
| |---|---| |
| | `CAContext` + off-screen NSWindow + `orderFront:` at (-32000,-32000) | API worked, nothing rendered β | |
| | `CALayerHost` added as sublayer to JUCE NSView root layer | Layer hierarchy correct, blank β | |
| | Explicit `setNeedsDisplay` / `setNeedsLayout` on CALayerHost | No effect β | |
| | Sending contextId immediately after `createEditor()` | Same result β | |
|
|
| **Files changed (then reverted):** |
| - `Source/Shared/PrivateCA.h` β declared `CAContext`, `CALayerHost`, `CGSMainConnectionID` |
| - `Source/Helper/HelperPluginHost.mm` β `showGui()` using `CAContext` |
| - `Source/Plugin/PluginBridgeEditor.mm` β `connectRemoteLayer()` using `CALayerHost` |
|
|
| **What we did instead:** |
| Moved to the **hybrid safety-scan architecture** (v0.5): |
| - Plugins that pass a safety scan are loaded **in-process** β their `createEditor()` runs inside PluginBridge's process, producing a standard JUCE `AudioProcessorEditor*` that embeds natively |
| - Plugins that fail the scan are loaded only in Helper for audio-only mode (no GUI) |
| - No cross-process GUI required at all |
|
|
| This trades crash isolation for reliability: safe plugins (99% of the library) get full GUI; only crash-prone plugins (iZotope Ozone, Neutron, a few others) are audio-only. |
|
|
| --- |
|
|
| ## Revisit Candidates (Future) |
|
|
| | Problem | Possible Future Solution | Difficulty | |
| |---|---|---| |
| | Fullscreen floating window from separate process | Wait for Apple to provide a supported API, or use private `CGSOrderWindow` API | Hard, fragile | |
| | Runtime crash safety (processBlock) | Hybrid: audio in Helper, GUI in-process. Two instances synced via IPC. | Very hard | |
| | Blocklist is manual (per-machine) | Share blocklist via cloud / community database | Medium | |
| | Test-load adds 2-3 second delay first time | Cache results aggressively, pre-test in background on startup | Medium | |
| | Some plugins crash in createEditor() not createInstance() | Test GUI creation in Helper too (spawn with --test-gui flag) | Medium | |
|
|