The EffectPlugin Trait
EffectPlugin is the central abstraction of rustjay-engine. It's a trait you implement once per app, and the engine calls its methods at the right times during setup and the render loop.
#![allow(unused)] fn main() { pub trait EffectPlugin: Send + Sync + 'static { type State: Default + Send + Sync + Serialize + DeserializeOwned + 'static; type Uniforms: bytemuck::Pod + bytemuck::Zeroable; // Required fn shader_source(&self) -> &'static str; fn build_uniforms(&self, state: &Self::State, engine: &EngineState) -> Self::Uniforms; // Common optional overrides fn app_name(&self) -> &str { "rustjay" } fn default_state(&self) -> Self::State { Default::default() } fn parameters(&self) -> Vec<ParameterDescriptor> { vec![] } fn hidden_tabs(&self) -> Vec<GuiTab> { vec![] } // Dynamic parameter lists fn parameters_dirty(&self) -> bool { false } fn clear_parameters_dirty(&mut self) {} // Lifecycle hooks fn init(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {} fn prepare(&mut self, state: &mut Self::State, engine: &EngineState, device: &wgpu::Device, queue: &wgpu::Queue) {} // Rendering overrides fn render_graph(&self) -> Option<RenderGraph> { None } fn mesh_descriptor(&self, state: &Self::State) -> Option<MeshDescriptor> { None } fn vertex_reads_texture(&self) -> bool { false } fn compute_shader(&self) -> Option<&'static str> { None } fn render(&mut self, ...) -> bool { false } } }
Associated types
State
Your app's mutable runtime state. The engine owns one instance of this, passes &State to build_uniforms() every frame, and passes &mut State to prepare() and to your custom GUI tab's draw() method.
Requirements:
Default— the engine creates the initial state withdefault_state()(which callsDefault::default()unless you override it)Serialize + DeserializeOwned— the preset system serialises this to JSON when saving and restores it when loading
A typical state struct:
#![allow(unused)] fn main() { #[derive(Default, serde::Serialize, serde::Deserialize)] struct MyState { intensity: f32, hue_shift: f32, enabled: bool, } }
Uniforms
The GPU-side data block uploaded to @group(1) @binding(0) every frame. Must be:
#[repr(C)]— stable field layout for bytemuckbytemuck::Pod + bytemuck::Zeroable— safe transmute to bytes
#![allow(unused)] fn main() { #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] struct MyUniforms { intensity: f32, hue_shift: f32, _pad: [f32; 2], // pad to 16-byte alignment } }
Alignment: wgpu requires uniform buffers to be 16-byte aligned. If your struct's size isn't a multiple of 16 bytes, add padding fields.
Required methods
shader_source()
Returns the WGSL source for your fragment shader. Use include_str! to embed a file at compile time:
#![allow(unused)] fn main() { fn shader_source(&self) -> &'static str { include_str!("shaders/my_effect.wgsl") } }
build_uniforms()
Called every frame. Reads from your State and EngineState to produce the Uniforms value that gets uploaded to the GPU:
#![allow(unused)] fn main() { fn build_uniforms(&self, s: &MyState, engine: &EngineState) -> MyUniforms { MyUniforms { intensity: engine.get_param("intensity").unwrap_or(s.intensity), hue_shift: s.hue_shift, _pad: [0.0; 2], } } }
Call engine.get_param(id) to read a parameter with LFO and audio modulations already applied. See EngineState for the full API.
Lifecycle hooks
init(device, queue)
Called once after the wgpu device is ready. Use this to create extra textures, bind groups, or pipelines that the default single-pass setup can't express.
#![allow(unused)] fn main() { fn init(&mut self, device: &wgpu::Device, _queue: &wgpu::Queue) { let texture = device.create_texture(&wgpu::TextureDescriptor { /* ... */ }); self.extra_tex = Some(texture); } }
prepare(state, engine, device, queue)
Called every frame, before the render pass. Use this for per-frame GPU resource updates — writing to a texture, updating a compute buffer — that aren't handled by the uniform upload.
parameters_dirty() / clear_parameters_dirty()
For effects whose parameter list can change at runtime (e.g. a shader hot-reloader that swaps inputs when a new file loads), implement these two methods together:
#![allow(unused)] fn main() { fn parameters_dirty(&self) -> bool { self.params_changed } fn clear_parameters_dirty(&mut self) { self.params_changed = false; } }
When parameters_dirty() returns true, the engine re-calls parameters(), swaps out EngineState::param_descriptors, and resizes the parameter value arrays — preserving existing values for any param IDs that survive the change. clear_parameters_dirty() is called immediately after so the flag is reset before the next frame.
Set the flag inside init() (after a successful pipeline rebuild) so the engine picks up the new list on the very next frame:
#![allow(unused)] fn main() { fn init(&mut self, device: &wgpu::Device, _queue: &wgpu::Queue) { // ... compile pipeline ... self.params_changed = true; } }
Declaring parameters
#![allow(unused)] fn main() { fn parameters(&self) -> Vec<ParameterDescriptor> { vec![ ParameterDescriptor::float( "intensity", "Intensity", // id, display name ParamCategory::Color, // tab grouping 0.0, 1.0, 0.5, 0.01, // min, max, default, step ), ParameterDescriptor::int( "blend_mode", "Blend Mode", ParamCategory::Motion, 0, 7, 0, 1, ), ] } }
Declared parameters:
- Appear as sliders in the built-in control UI
- Can be targeted by LFO banks
- Can be mapped to MIDI CC via learn mode
- Are addressable as OSC messages at
/rustjay/<id> - Receive audio-reactive modulation from the routing matrix
Read them back in build_uniforms() via engine.get_param(id), which returns the base value plus all active modulations.
Hiding built-in tabs
If your effect doesn't use colour parameters, you can hide the Color tab to keep the UI clean:
#![allow(unused)] fn main() { fn hidden_tabs(&self) -> Vec<GuiTab> { vec![GuiTab::Color] } }
Available tabs: GuiTab::Input, GuiTab::Audio, GuiTab::Lfo, GuiTab::Midi, GuiTab::Osc, GuiTab::Output, GuiTab::Presets, GuiTab::Color, GuiTab::Sync.
Entry points
Two engine entry points are available:
#![allow(unused)] fn main() { // Simple — no custom tabs rustjay_engine::run(MyEffect) // With custom control-window tabs rustjay_engine::run_with_tabs(MyEffect, vec![Box::new(MyTab)]) }
See Custom Tabs for how to implement AnyGuiTab.