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, &param);
```

---

## 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 |