File size: 10,987 Bytes
33d2e82 dcfc9c9 33d2e82 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | # 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 |
|