Transports
In-Process Transport
Section titled “In-Process Transport”System Intelligence and PCP Core share the compositor process. All communication between them uses direct Rust trait calls. No serialization layer, no IPC, no socket.
When the SI layer queries the element tree, it calls pcp.elements_tree(surface_id) and receives a Vec<Element> back. That return value is a direct reference to live data, not a serialized copy decoded from bytes. The same applies to capability invocations, event subscriptions, and session context queries. Everything is a function call with Rust-native types.
// SI calls PCP Core directly — no JSON, no socket, no codeclet elements: Vec<Element> = pcp.elements_tree(surface_id).await?;
for element in &elements { if element.role == Role::Button && element.label.contains("Send") { pcp.invoke_action(&element.id, Action::Activate, None).await?; }}Data that needs to cross call boundaries is shared through Arc<T> references rather than copied. The SI layer holds an Arc<PcpCore> trait object that wraps the full PCP API. Calling a method on that trait object is indistinguishable from calling any other Rust function. The compiler checks argument types, return types, and lifetime correctness at build time.
This design has three practical consequences. First, latency is sub-microsecond. A trait call completes in nanoseconds, bounded only by the work the callee does. Second, there is no allocation for message framing, no serialization pass, and no copy step. The data stays where it is. Third, every call is type-safe. If PCP Core changes the signature of elements_tree, the SI code fails to compile rather than failing silently at runtime.
Shared Memory for Frame Capture
Section titled “Shared Memory for Frame Capture”Pixel data doesn’t fit the “direct reference” model. Screenshots, screen capture, and any other raster output can be tens of megabytes, and that data needs to move from the adapter (which owns the buffer) to the SI layer (which consumes it). Copying that through an in-process call would work, but passing a file descriptor is cheaper.
PCP uses memory-mapped shared memory for these payloads. The adapter allocates a memory region, writes the pixel data into it, and passes the file descriptor to the SI layer via OwnedFd.
use std::os::unix::io::OwnedFd;
pub struct FrameCapture { pub format: PixelFormat, pub width: u32, pub height: u32, pub stride: u32, /// File descriptor for the shared memory region holding pixel data. pub shm_fd: OwnedFd, pub shm_size: usize,}
pub enum PixelFormat { Bgra8888, Rgba8888, Xrgb8888,}The adapter writes pixels into the shared memory region, then hands the OwnedFd to PCP Core. PCP Core passes it to the SI layer. The SI layer maps the region, reads the pixels, and closes the fd when done. At no point is the pixel data copied or serialized. The shm_size field lets the receiver validate the mapping before reading, and the stride field handles any padding between rows.
This pattern keeps large binary payloads on the fast path without sacrificing the in-process model for everything else.
Why Not IPC?
Section titled “Why Not IPC?”A common alternative for this kind of integration is JSON-RPC over a Unix domain socket, or a D-Bus interface, or some other out-of-process transport. PCP chose in-process calls instead. The reason is performance.
A direct trait call in Rust costs less than a microsecond. The call overhead is a vtable dispatch and a stack frame. By contrast, an IPC round trip over a Unix socket involves serializing the request to bytes, writing those bytes to the kernel buffer, context-switching to the receiving process, deserializing the bytes, doing the actual work, then serializing the response and repeating the path in reverse. That round trip typically exceeds 100 microseconds.
For operations that run once per user gesture, 100 microseconds is invisible. But PCP’s SI layer performs chains of calls in rapid succession: query the element tree, resolve a target, invoke an action, check the result, handle an error. A voice-driven window management command might issue five or six calls in a single turn. Over IPC, that’s half a millisecond of pure transport overhead. Over direct calls, it’s negligible.
The latency difference also matters for continuous operations. Real-time text manipulation, live element tracking, and streaming event subscriptions all involve repeated calls at high frequency. At 60 Hz, each frame has a 16 millisecond budget. IPC overhead doesn’t dominate that budget, but it adds up across multiple subsystems, and it introduces jitter that makes frame timing unpredictable.
Beyond latency, there is type safety. With in-process Rust calls, the compiler enforces the contract between PCP Core and its callers. A wrong field name, a missing parameter, or a type mismatch is a compile error. With IPC, those same mistakes become runtime errors: malformed JSON, missing fields, type coercion failures. Catching bugs at compile time is always cheaper than catching them in production.
The external transport (JSON-RPC over a socket) still exists for debugging tools and test harnesses that run outside the compositor process. But for the production path between SI and PCP Core, in-process calls are the only transport.