Architecture¶
Subsystem Dependency Direction¶
Pulp's subsystems form a directed acyclic graph. Lower-level subsystems do not depend on higher-level ones.
platform (leaf — no dependencies)
^
runtime (depends on: platform)
^
+------+------+------+------+------+
| | | | | |
events state audio midi canvas osc
^ ^ ^ ^
| | | |
+--+---+-----+ +--- render (depends on: runtime, canvas)
|
format (depends on: state, audio, midi)
|
view (depends on: canvas, events, state)
signal (header-only, no link dependencies)
Key rules:
platformhas no internal dependencies (OS detection, native types)runtimedepends onplatformonlystatedepends onruntimeonlyformatdepends onstate,audio,midi-- not onvieworrenderviewandrenderare optional -- plugins can be built and tested headlesssignalis a header-only leaf -- pure DSP, no framework link dependenciesoscdepends onruntimefor logging and assertions
Public vs Internal Surfaces¶
Each subsystem exposes public headers under core/<subsystem>/include/pulp/<subsystem>/. These are the consumer API.
Internal implementation lives in core/<subsystem>/src/. Nothing outside the subsystem should include from src/.
The convention:
core/format/
include/pulp/format/ <-- public headers (consumers include from here)
processor.hpp
clap_entry.hpp
vst3_entry.hpp
au_v2_entry.hpp
headless.hpp
src/ <-- internal implementation
clap_adapter.cpp
vst3_adapter.cpp
au_v2_adapter.cpp
standalone.cpp
The Processor Interface¶
The central abstraction is pulp::format::Processor. Plugin developers subclass it and implement:
descriptor()-- returns plugin metadata (name, manufacturer, category, bus layout)define_parameters()-- registers parameters with the state storeprepare()-- called once before processing beginsprocess()-- called on the real-time audio thread every buffer cycle
Format adapters (VST3, AU, CLAP) wrap a Processor instance and translate between the format's API and Pulp's interface. The developer writes one processor; the build system creates multiple format targets.
Thread Model¶
Pulp uses three thread contexts:
| Thread | What Runs | Rules |
|---|---|---|
| Audio thread | Processor::process(), parameter reads |
No allocation, no locks, no exceptions, no I/O |
| UI thread | Widget updates, event loop, change listeners | May allocate, may lock (briefly) |
| Host thread | prepare(), release(), state serialization |
Full access |
Communication between threads uses lock-free primitives from runtime/:
| Primitive | Purpose |
|---|---|
std::atomic<T> |
Single values, latest-wins (parameter values) |
SeqLock<T> |
Multi-field coherent reads (transport state) |
TripleBuffer<T> |
Large data swaps (wavetables, IR buffers, meter data) |
SPSCQueue<T> |
Ordered event streams (MIDI events, UI commands) |
Build System¶
CMake is the build system. The key function is pulp_add_plugin(), which creates:
${target}_Core-- shared processor code (object or interface library)${target}_VST3-- VST3 bundle (.vst3)${target}_AU-- AU v2 component (.component, macOS only)${target}_CLAP-- CLAP bundle (.clap)${target}_Standalone-- standalone app target (.appbundle on macOS; executable elsewhere)
Each format target links against the core library and its format-specific adapter. The developer provides small entry-point files (vst3_entry.cpp, clap_entry.cpp, au_v2_entry.cpp, main.cpp) that use one-line macros.
GPU Rendering Stack¶
The rendering stack (experimental) layers:
- Dawn -- WebGPU implementation providing GPU access via Metal (macOS), D3D12, Vulkan
- Skia Graphite -- 2D rendering on top of Dawn's GPU context
- Canvas -- abstraction layer over Skia and CoreGraphics, with SVG and post-processing
- View -- widgets, layout, themes, JS scripting engine, hot-reload
This stack is optional. Plugins can be built, tested, and shipped without any UI code in the dependency tree.
JavaScript Engine Abstraction¶
The view system uses a JsEngine abstraction that supports multiple JavaScript backends:
| Engine | Use Case | Features |
|---|---|---|
| QuickJS | Default for plugins | Fast cold-start, low memory, portable |
| V8 | Three.js, heavy workloads | JIT compilation, typed arrays, promises, full ES module support |
| JavaScriptCore | Apple platforms | Native Apple integration, available on macOS and iOS |
The engine is selected at build time or runtime via JsEngineType. Your UI code doesn't change — the same JS runs on any backend. QuickJS is always available. V8 requires an external library (Node.js or standalone V8 monolith). JSC is available on Apple platforms.
Key APIs: ScriptEngine (high-level wrapper), JsEngine (backend interface), WidgetBridge (JS-to-native widget binding).
The Three.js native demo uses V8 for full WebGPU compatibility — Three.js needs typed arrays, promises, and ES module import support that QuickJS doesn't provide.
Platform Isolation¶
Platform-specific code lives in core/platform/ and behind #ifdef guards or platform subdirectories. The rest of the codebase uses platform-agnostic APIs.
On Apple, additional Swift code lives in apple/. C++/Swift interop uses direct interop (Swift 5.9+), not Objective-C++ bridges.