Sitar118 commited on
Commit
0548359
·
1 Parent(s): 0cf3d62

fix: replace CARemoteLayerServer with CAContext+CALayerHost for cross-process GUI

Browse files

Three bugs killed the GUI:
1. CARemoteLayerServer does not capture a hidden NSWindow's content cross-process.
Replaced with CAContext (server) + CALayerHost (client) — the correct private-but-stable
macOS API for cross-process layer embedding, stable from 10.10 through Sequoia.
2. NSWindow was never orderFront: — WindowServer never composited its content.
Fixed: window positioned at (-32000,-32000) and orderFront: called.
3. NSMachBootstrapServer (deprecated on 13+) no longer needed.
contextId (uint32_t) is sent directly over the existing IPC socket.

New file: Source/Shared/PrivateCA.h — declares CAContext, CALayerHost, CGSMainConnectionID.

SYNC.md CHANGED
@@ -2,107 +2,90 @@
2
 
3
  ---
4
 
5
- ## MODE: FULL DEBUG (active until GUI works)
6
 
7
- You have FULL permission to:
8
- - Read all source files
9
- - Add DBG prints
10
- - Write code
11
- - Fix bugs
12
- - Push to this HF repo
13
-
14
- **Do NOT just build. Debug and fix.**
15
 
16
  ---
17
 
18
- ## THE PROBLEM
 
 
 
 
 
 
 
 
 
19
 
20
- Plugin loads successfully (name + param count shows in top bar) but **GUI does not appear** inside PluginBridge's editor.
 
21
 
22
- ## ARCHITECTURE (v0.4)
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  ```
25
  PluginBridge.vst3 (inside Ableton)
26
  ├── PluginBridgeProcessor.cpp — spawns Helper, shared memory audio, IPC
27
- ├── PluginBridgeEditor.mm — should display CARemoteLayerClient layer
28
  ├── HelperConnection.cpp — Unix socket IPC to Helper
29
  ├── McpServer.cpp — HTTP MCP on port 16620
30
  └── Blocklist.h — persistent blocklist
31
 
32
  PluginBridgeHelper.app (separate crash-safe process)
33
  ├── main.cpp — JUCE app, --pid argument
34
- ├── HelperPluginHost.mm — loads plugin, audio, CARemoteLayerServer GUI
35
  ├── HelperIPC.cpp — receives commands, sends notifications
36
  └── SharedAudioBuffer.h — shared memory
37
  ```
38
 
39
- ## THE CHAIN THAT SHOULD HAPPEN
40
-
41
- ```
42
- 1. User selects plugin → Processor::loadPlugin() → Helper loads it ✅ WORKS
43
- 2. Processor::onPluginLoaded fires → Editor calls showPluginGui()
44
- 3. showPluginGui() → sends {"cmd":"show_gui"} to Helper
45
- 4. Helper HelperPluginHost::showGui() →
46
- - Creates plugin editor (NSView)
47
- - Creates hidden NSWindow (layer-backed)
48
- - Sets up CARemoteLayerServer
49
- - Registers Mach port via NSMachBootstrapServer
50
- - Calls onGuiCreated callback
51
- 5. onGuiCreated → HelperIPC::sendGuiReady(portName, width, height)
52
- 6. HelperConnection receives {"notify":"gui_ready",...} → calls onGuiReady
53
- 7. Processor stores remoteLayerPortName + size → triggers Editor onGuiReady
54
- 8. Editor::connectRemoteLayer() →
55
- - Looks up Mach port via NSMachBootstrapServer
56
- - Creates CARemoteLayerClient with port
57
- - Adds client.layer to JUCE NSView
58
- - Resizes editor to fit
59
- ```
60
-
61
- **Something in steps 2-8 is failing silently.**
62
 
63
- ## HOW TO DEBUG
64
 
65
- 1. Check Ableton log:
66
- ```bash
67
- grep -i "pluginbridge\|helper\|GUI\|layer\|port\|gui_ready" ~/Library/Preferences/Ableton/Live\ 12.4/Log.txt | tail -50
68
- ```
69
 
70
- 2. Check if Helper process is running:
71
  ```bash
72
- ps aux | grep PluginBridgeHelper
 
73
  ```
74
 
75
- 3. Add DBG() prints at each step in the chain to find where it breaks.
76
-
77
- 4. Possible failure points:
78
- - `showPluginGui()` never called (step 2)
79
- - `show_gui` command never reaches Helper (step 3)
80
- - `showGui()` fails silently — editor not created, or CARemoteLayerServer init fails (step 4)
81
- - Mach port registration fails (NSMachBootstrapServer may be deprecated/broken on modern macOS)
82
- - `gui_ready` notification never sent or never received (steps 5-6)
83
- - `connectRemoteLayer()` fails — port lookup returns nil (step 8)
84
- - `CARemoteLayerClient initWithServerPort:` fails or layer is empty
85
 
86
- 5. **Key question:** Does `CARemoteLayerServer` / `CARemoteLayerClient` actually work on macOS 14+ Sonoma/Sequoia? The API exists but may not function as documented for cross-process layer sharing without App Groups or XPC. If it doesn't work, we need a different approach (IOSurface or pixel copy).
 
 
 
87
 
88
- ## KEY FILES
89
-
90
- | File | Role |
91
- |---|---|
92
- | `Source/Plugin/PluginBridgeEditor.mm` | `connectRemoteLayer()` — CARemoteLayerClient side |
93
- | `Source/Helper/HelperPluginHost.mm` | `showGui()` — CARemoteLayerServer side |
94
- | `Source/Helper/HelperIPC.cpp` | Wires `onGuiCreated` → `sendGuiReady()` |
95
- | `Source/Plugin/HelperConnection.cpp` | `handleNotification()` receives `gui_ready` |
96
- | `Source/Plugin/PluginBridgeProcessor.cpp` | `onGuiReady` callback stores port name |
97
-
98
- ## BUILD
99
 
 
100
  ```bash
101
- cd ~/pluginbridge && git pull
102
- cd build && cmake --build . --config Release 2>&1 | tail -30
103
  ```
104
 
105
- ## AFTER FIXING
 
 
106
 
107
- Push all changes to: https://huggingface.co/RAM2118/pluginbridge
108
- Update this SYNC.md with what you fixed and current status.
 
2
 
3
  ---
4
 
5
+ ## STATUS: v0.4.1 — GUI fix pushed (2026-05-17)
6
 
7
+ Build: ✅ Clean
8
+ GUI: Awaiting first real test in Ableton
 
 
 
 
 
 
9
 
10
  ---
11
 
12
+ ## WHAT WAS FIXED
13
+
14
+ ### Root cause: Wrong cross-process layer API + invisible window
15
+
16
+ Three bugs in the original v0.4 GUI approach:
17
+
18
+ **Bug 1 — Wrong API (main bug):**
19
+ `CARemoteLayerServer` + `NSMachBootstrapServer` does NOT share a hidden NSWindow's content to another process. The correct private-but-stable API is:
20
+ - **Helper (server):** `CAContext contextWithCGSConnection:options:` — wraps a `CALayer` and exposes it as a `uint32_t contextId`
21
+ - **Host (client):** `CALayerHost.contextId = contextId` — a `CALayer` subclass that renders the remote context inline
22
 
23
+ **Bug 2 — NSWindow never visible:**
24
+ The window was created but `orderFront:` was never called. Even if CARemoteLayerServer had worked, the WindowServer wouldn't composite a window that isn't ordered front. Fix: window is positioned off-screen at `(-32000, -32000)` and `orderFront:` is called.
25
 
26
+ **Bug 3 — NSMachBootstrapServer deprecated:**
27
+ Not needed anymore. The `contextId` (uint32_t) is sent directly over the existing IPC socket as the `layerPortName` string field (decimal number). No Mach port bootstrapping required.
28
+
29
+ ### Files changed
30
+ | File | What changed |
31
+ |---|---|
32
+ | `Source/Shared/PrivateCA.h` | **New** — declares `CAContext`, `CALayerHost`, `CGSMainConnectionID` |
33
+ | `Source/Helper/HelperPluginHost.h` | `remoteLayerServer` → `caContext` |
34
+ | `Source/Helper/HelperPluginHost.mm` | `showGui()` uses `CAContext`; `hideGui()` releases properly; window `orderFront:` |
35
+ | `Source/Plugin/PluginBridgeEditor.mm` | `connectRemoteLayer()` uses `CALayerHost`; no Mach port lookup |
36
+
37
+ ---
38
+
39
+ ## ARCHITECTURE (v0.4.1)
40
 
41
  ```
42
  PluginBridge.vst3 (inside Ableton)
43
  ├── PluginBridgeProcessor.cpp — spawns Helper, shared memory audio, IPC
44
+ ├── PluginBridgeEditor.mm — displays CALayerHost (contextId from Helper)
45
  ├── HelperConnection.cpp — Unix socket IPC to Helper
46
  ├── McpServer.cpp — HTTP MCP on port 16620
47
  └── Blocklist.h — persistent blocklist
48
 
49
  PluginBridgeHelper.app (separate crash-safe process)
50
  ├── main.cpp — JUCE app, --pid argument
51
+ ├── HelperPluginHost.mm — loads plugin, CAContext GUI server
52
  ├── HelperIPC.cpp — receives commands, sends notifications
53
  └── SharedAudioBuffer.h — shared memory
54
  ```
55
 
56
+ **How GUI works (v0.4.1):**
57
+ 1. Helper loads plugin → creates editor → embeds NSView in off-screen NSWindow
58
+ 2. NSWindow ordered front at (-32000, -32000) — invisible to user, visible to compositor
59
+ 3. `CAContext.layer = window.contentView.layer` — exposes the layer tree cross-process
60
+ 4. Helper sends `contextId` (uint32_t as decimal string) via `gui_ready` IPC notification
61
+ 5. Host creates `CALayerHost(contextId)` — a CALayer that renders the remote context
62
+ 6. CALayerHost added as sublayer to JUCE editor's NSView → plugin GUI appears embedded
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
+ ---
65
 
66
+ ## BUILD
 
 
 
67
 
 
68
  ```bash
69
+ cd ~/pluginbridge && git pull
70
+ cd build && cmake --build . --config Release 2>&1 | tail -30
71
  ```
72
 
73
+ ## TEST AFTER BUILD
 
 
 
 
 
 
 
 
 
74
 
75
+ 1. Restart Ableton (plugin is auto-installed to `~/Library/Audio/Plug-Ins/VST3/`)
76
+ 2. Clear blocklist: `rm -f ~/Library/PluginBridge/blocklist.txt`
77
+ 3. Load PluginBridge → select Pro-Q 4 → GUI should appear inside the editor window
78
+ 4. Test Kickstart 2 → should NOT crash Ableton (Helper crashes, gets blocklisted)
79
 
80
+ ## IF GUI STILL DOESN'T SHOW
 
 
 
 
 
 
 
 
 
 
81
 
82
+ Check Ableton log for contextId and CALayerHost messages:
83
  ```bash
84
+ grep -i "pluginbridge\|contextId\|CALayerHost\|gui_ready" ~/Library/Preferences/Ableton/Live\ 12.4/Log.txt | tail -30
 
85
  ```
86
 
87
+ The log should show:
88
+ - `HelperPluginHost: GUI ready — contextId=<number> size=<W>x<H>`
89
+ - `PluginBridgeEditor: CALayerHost connected (contextId=<number> <W>x<H>)`
90
 
91
+ If contextId is 0 or either line is missing → the specific step that's failing is identified.
 
Source/Helper/HelperPluginHost.h CHANGED
@@ -87,6 +87,6 @@ private:
87
  juce::String layerPortName;
88
 
89
  // ObjC objects (stored as void* to avoid ObjC in header)
90
- void* remoteLayerServer = nullptr; // CARemoteLayerServer*
91
- void* guiWindow = nullptr; // NSWindow* (hidden, for layer backing)
92
  };
 
87
  juce::String layerPortName;
88
 
89
  // ObjC objects (stored as void* to avoid ObjC in header)
90
+ void* caContext = nullptr; // CAContext* — cross-process layer server
91
+ void* guiWindow = nullptr; // NSWindow* — off-screen, ordered-front for compositing
92
  };
Source/Helper/HelperPluginHost.mm CHANGED
@@ -8,6 +8,7 @@
8
  #if JUCE_MAC
9
  #import <Cocoa/Cocoa.h>
10
  #import <QuartzCore/QuartzCore.h>
 
11
  #endif
12
 
13
  HelperPluginHost::HelperPluginHost()
@@ -188,7 +189,7 @@ bool HelperPluginHost::setParams(const std::map<int, float>& values)
188
  return true;
189
  }
190
 
191
- // --- GUI via CARemoteLayerServer ---
192
 
193
  void HelperPluginHost::showGui()
194
  {
@@ -203,19 +204,24 @@ void HelperPluginHost::showGui()
203
  editor.reset(plugin->createEditor());
204
  if (!editor) return;
205
 
206
- guiWidth = editor->getWidth();
207
  guiHeight = editor->getHeight();
 
208
 
209
- // Hidden NSWindow for layer-backed rendering
 
 
210
  NSWindow* window = [[NSWindow alloc]
211
- initWithContentRect:NSMakeRect(0, 0, guiWidth, guiHeight)
212
  styleMask:NSWindowStyleMaskBorderless
213
  backing:NSBackingStoreBuffered
214
  defer:NO];
215
  [window setReleasedWhenClosed:NO];
 
 
216
  [[window contentView] setWantsLayer:YES];
217
 
218
- // Add JUCE editor view
219
  editor->addToDesktop(0, (void*)[window contentView]);
220
  if (auto* peer = editor->getPeer())
221
  {
@@ -224,24 +230,26 @@ void HelperPluginHost::showGui()
224
  [editorView setFrame:[[window contentView] bounds]];
225
  }
226
 
227
- // CARemoteLayerServer
228
- CARemoteLayerServer* server = [CARemoteLayerServer sharedServer];
229
- remoteLayerServer = (__bridge_retained void*)server;
 
230
 
231
- // Register port
232
- mach_port_t port = server.serverPort;
233
- NSMachPort* machPort = [NSMachPort portWithMachPort:port];
234
- NSString* portNameNS = [NSString stringWithUTF8String:layerPortName.toRawUTF8()];
235
- [[NSMachBootstrapServer sharedInstance] registerPort:machPort name:portNameNS];
 
 
236
 
237
- guiWindow = (__bridge_retained void*)window;
238
 
239
- DBG("HelperPluginHost: GUI ready, port: " + layerPortName +
240
- " size: " + juce::String(guiWidth) + "x" + juce::String(guiHeight));
241
 
242
- // NOTIFY HOST that GUI is ready with port name and dimensions
243
  if (onGuiCreated)
244
- onGuiCreated(layerPortName, guiWidth, guiHeight);
245
  });
246
  #endif
247
  }
@@ -253,19 +261,21 @@ void HelperPluginHost::hideGui()
253
  {
254
  editor.reset();
255
 
256
- if (guiWindow)
257
  {
258
- NSWindow* window = (__bridge_transfer NSWindow*)guiWindow;
259
- [window close];
260
- guiWindow = nullptr;
 
261
  }
262
 
263
- if (remoteLayerServer)
264
  {
265
- NSString* portNameNS = [NSString stringWithUTF8String:layerPortName.toRawUTF8()];
266
- [[NSMachBootstrapServer sharedInstance] registerPort:nil name:portNameNS];
267
- CFRelease(remoteLayerServer);
268
- remoteLayerServer = nullptr;
 
269
  }
270
  });
271
  #endif
 
8
  #if JUCE_MAC
9
  #import <Cocoa/Cocoa.h>
10
  #import <QuartzCore/QuartzCore.h>
11
+ #include "PrivateCA.h"
12
  #endif
13
 
14
  HelperPluginHost::HelperPluginHost()
 
189
  return true;
190
  }
191
 
192
+ // --- GUI via CAContext + CALayerHost (cross-process layer sharing) ---
193
 
194
  void HelperPluginHost::showGui()
195
  {
 
204
  editor.reset(plugin->createEditor());
205
  if (!editor) return;
206
 
207
+ guiWidth = editor->getWidth();
208
  guiHeight = editor->getHeight();
209
+ if (guiWidth <= 0 || guiHeight <= 0) { guiWidth = 800; guiHeight = 600; }
210
 
211
+ // Off-screen NSWindow positioned far off-screen so it's invisible to the user,
212
+ // but orderFront: is required so the WindowServer composites its layer content.
213
+ NSRect frame = NSMakeRect(-32000, -32000, guiWidth, guiHeight);
214
  NSWindow* window = [[NSWindow alloc]
215
+ initWithContentRect:frame
216
  styleMask:NSWindowStyleMaskBorderless
217
  backing:NSBackingStoreBuffered
218
  defer:NO];
219
  [window setReleasedWhenClosed:NO];
220
+ [window setOpaque:NO];
221
+ [window setBackgroundColor:[NSColor clearColor]];
222
  [[window contentView] setWantsLayer:YES];
223
 
224
+ // Embed JUCE editor NSView inside the off-screen window
225
  editor->addToDesktop(0, (void*)[window contentView]);
226
  if (auto* peer = editor->getPeer())
227
  {
 
230
  [editorView setFrame:[[window contentView] bounds]];
231
  }
232
 
233
+ // OrderFront so the WindowServer allocates a backing store and composites content.
234
+ // The window is off-screen so the user never sees it.
235
+ [window orderFront:nil];
236
+ guiWindow = (__bridge void*)window; // alloc/init already retains; no extra retain needed
237
 
238
+ // CAContext wraps the content view's backing CALayer and exposes it to other
239
+ // processes via a uint32_t contextId sent over our existing IPC socket.
240
+ // We explicitly retain because we're not using ARC and this is a factory method.
241
+ uint32_t cgsConn = CGSMainConnectionID();
242
+ CAContext* ctx = [[CAContext contextWithCGSConnection:cgsConn options:@{}] retain];
243
+ ctx.layer = [window contentView].layer;
244
+ caContext = (__bridge void*)ctx;
245
 
246
+ uint32_t ctxId = ctx.contextId;
247
 
248
+ DBG("HelperPluginHost: GUI ready — contextId=" + juce::String(ctxId) +
249
+ " size=" + juce::String(guiWidth) + "x" + juce::String(guiHeight));
250
 
 
251
  if (onGuiCreated)
252
+ onGuiCreated(juce::String(ctxId), guiWidth, guiHeight);
253
  });
254
  #endif
255
  }
 
261
  {
262
  editor.reset();
263
 
264
+ if (caContext)
265
  {
266
+ CAContext* ctx = (__bridge CAContext*)caContext;
267
+ ctx.layer = nil;
268
+ [ctx release];
269
+ caContext = nullptr;
270
  }
271
 
272
+ if (guiWindow)
273
  {
274
+ NSWindow* window = (__bridge NSWindow*)guiWindow;
275
+ [window orderOut:nil];
276
+ [window close];
277
+ [window release];
278
+ guiWindow = nullptr;
279
  }
280
  });
281
  #endif
Source/Plugin/PluginBridgeEditor.mm CHANGED
@@ -3,6 +3,7 @@
3
  #if JUCE_MAC
4
  #import <Cocoa/Cocoa.h>
5
  #import <QuartzCore/QuartzCore.h>
 
6
  #endif
7
 
8
  #include "json.hpp"
@@ -137,15 +138,12 @@ void PluginBridgeEditor::resized()
137
  mcpStatusLabel.setBounds(topBar);
138
 
139
  #if JUCE_MAC
140
- // Resize the remote CALayer to fill the area below the top bar
141
  if (layerConnected && remoteLayerClient)
142
  {
143
- CARemoteLayerClient* client = (__bridge CARemoteLayerClient*)remoteLayerClient;
144
- if (client.layer)
145
- {
146
- auto body = getLocalBounds().withTrimmedTop(kTopBarHeight);
147
- client.layer.frame = CGRectMake(0, 0, body.getWidth(), body.getHeight());
148
- }
149
  }
150
  #endif
151
  }
@@ -195,51 +193,37 @@ void PluginBridgeEditor::connectRemoteLayer()
195
  #if JUCE_MAC
196
  disconnectRemoteLayer();
197
 
 
198
  auto portName = processorRef.getRemoteLayerPortName();
199
  if (portName.isEmpty()) return;
200
 
201
- int guiW = processorRef.getRemoteGuiWidth();
202
- int guiH = processorRef.getRemoteGuiHeight();
203
- if (guiW <= 0 || guiH <= 0) return;
204
-
205
- // Look up the Mach port from the bootstrap server
206
- NSString* nsPortName = [NSString stringWithUTF8String:portName.toRawUTF8()];
207
- NSMachPort* machPort = (NSMachPort*)[[NSMachBootstrapServer sharedInstance]
208
- portForName:nsPortName];
209
-
210
- if (!machPort)
211
  {
212
- DBG("PluginBridgeEditor: Failed to look up Mach port: " + portName);
213
  return;
214
  }
215
 
216
- mach_port_t port = [machPort machPort];
217
-
218
- // Create remote layer client
219
- CARemoteLayerClient* client = [[CARemoteLayerClient alloc] initWithServerPort:port];
220
- if (!client)
221
- {
222
- DBG("PluginBridgeEditor: Failed to create CARemoteLayerClient");
223
- return;
224
- }
225
 
226
- remoteLayerClient = (__bridge_retained void*)client;
 
 
 
 
 
 
227
 
228
- // Get our NSView and make it layer-backed
229
  if (auto* peer = getPeer())
230
  {
231
  NSView* nsView = (NSView*)peer->getNativeHandle();
232
  [nsView setWantsLayer:YES];
233
 
234
- // Add remote layer as sublayer, positioned below the top bar
235
- CALayer* remoteLayer = client.layer;
236
- if (remoteLayer)
237
- {
238
- remoteLayer.frame = CGRectMake(0, 0, guiW, guiH);
239
- // Position at the bottom of the view (below top bar)
240
- // NSView coordinates are flipped (0,0 = bottom-left)
241
- [nsView.layer addSublayer:remoteLayer];
242
- }
243
  }
244
 
245
  layerConnected = true;
@@ -247,7 +231,8 @@ void PluginBridgeEditor::connectRemoteLayer()
247
  // Resize editor to fit plugin GUI + top bar
248
  setSize(std::max(guiW, kDefaultWidth), guiH + kTopBarHeight);
249
 
250
- DBG("PluginBridgeEditor: Remote layer connected (" + juce::String(guiW) + "x" + juce::String(guiH) + ")");
 
251
  #endif
252
  }
253
 
@@ -256,9 +241,9 @@ void PluginBridgeEditor::disconnectRemoteLayer()
256
  #if JUCE_MAC
257
  if (remoteLayerClient)
258
  {
259
- CARemoteLayerClient* client = (__bridge_transfer CARemoteLayerClient*)remoteLayerClient;
260
- if (client.layer)
261
- [client.layer removeFromSuperlayer];
262
  remoteLayerClient = nullptr;
263
  }
264
  layerConnected = false;
 
3
  #if JUCE_MAC
4
  #import <Cocoa/Cocoa.h>
5
  #import <QuartzCore/QuartzCore.h>
6
+ #include "PrivateCA.h"
7
  #endif
8
 
9
  #include "json.hpp"
 
138
  mcpStatusLabel.setBounds(topBar);
139
 
140
  #if JUCE_MAC
141
+ // Resize the CALayerHost to fill the area below the top bar
142
  if (layerConnected && remoteLayerClient)
143
  {
144
+ CALayerHost* hostLayer = (__bridge CALayerHost*)remoteLayerClient;
145
+ auto body = getLocalBounds().withTrimmedTop(kTopBarHeight);
146
+ hostLayer.frame = CGRectMake(0, 0, body.getWidth(), body.getHeight());
 
 
 
147
  }
148
  #endif
149
  }
 
193
  #if JUCE_MAC
194
  disconnectRemoteLayer();
195
 
196
+ // portName is now the CAContext contextId sent as a decimal string by the Helper
197
  auto portName = processorRef.getRemoteLayerPortName();
198
  if (portName.isEmpty()) return;
199
 
200
+ uint32_t contextId = (uint32_t)portName.getLargeIntValue();
201
+ if (contextId == 0)
 
 
 
 
 
 
 
 
202
  {
203
+ DBG("PluginBridgeEditor: Invalid contextId in portName: " + portName);
204
  return;
205
  }
206
 
207
+ int guiW = processorRef.getRemoteGuiWidth();
208
+ int guiH = processorRef.getRemoteGuiHeight();
209
+ if (guiW <= 0 || guiH <= 0) return;
 
 
 
 
 
 
210
 
211
+ // CALayerHost embeds the Helper's CAContext layer into our view hierarchy.
212
+ // No Mach port lookup needed — contextId is the only thing required.
213
+ // Explicitly retain — no ARC in this target, and layer is a factory method.
214
+ CALayerHost* hostLayer = [[CALayerHost layer] retain];
215
+ hostLayer.contextId = contextId;
216
+ hostLayer.frame = CGRectMake(0, 0, guiW, guiH);
217
+ remoteLayerClient = (__bridge void*)hostLayer;
218
 
219
+ // Make JUCE's NSView layer-backed and add the host layer as a sublayer
220
  if (auto* peer = getPeer())
221
  {
222
  NSView* nsView = (NSView*)peer->getNativeHandle();
223
  [nsView setWantsLayer:YES];
224
 
225
+ if (nsView.layer)
226
+ [nsView.layer addSublayer:hostLayer];
 
 
 
 
 
 
 
227
  }
228
 
229
  layerConnected = true;
 
231
  // Resize editor to fit plugin GUI + top bar
232
  setSize(std::max(guiW, kDefaultWidth), guiH + kTopBarHeight);
233
 
234
+ DBG("PluginBridgeEditor: CALayerHost connected (contextId=" + juce::String(contextId) +
235
+ " " + juce::String(guiW) + "x" + juce::String(guiH) + ")");
236
  #endif
237
  }
238
 
 
241
  #if JUCE_MAC
242
  if (remoteLayerClient)
243
  {
244
+ CALayerHost* hostLayer = (__bridge CALayerHost*)remoteLayerClient;
245
+ [hostLayer removeFromSuperlayer];
246
+ [hostLayer release];
247
  remoteLayerClient = nullptr;
248
  }
249
  layerConnected = false;
Source/Shared/PrivateCA.h ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ // Private QuartzCore + CoreGraphics APIs for cross-process layer hosting.
3
+ // CAContext/CALayerHost have been stable from macOS 10.10 through Sequoia (15.x).
4
+ // No entitlements needed — works for same-user processes.
5
+
6
+ #import <Foundation/Foundation.h>
7
+ #import <QuartzCore/QuartzCore.h>
8
+
9
+ // WindowServer connection ID for the current process
10
+ extern "C" uint32_t CGSMainConnectionID(void);
11
+
12
+ // CAContext — server side: wraps a CALayer and exposes it to other processes via contextId
13
+ @interface CAContext : NSObject
14
+ + (instancetype)contextWithCGSConnection:(uint32_t)cid options:(NSDictionary *)opts;
15
+ @property (readonly) uint32_t contextId;
16
+ @property (retain) CALayer *layer;
17
+ @end
18
+
19
+ // CALayerHost — client side: a CALayer subclass that renders a remote CAContext by ID
20
+ @interface CALayerHost : CALayer
21
+ @property uint32_t contextId;
22
+ @end