glitcher_api/lib.rs
1//! # Glitcher API
2//!
3//! WebAssembly Interface Types for Glitcher Engine render node plugins.
4//!
5//! ## Overview
6//!
7//! This crate provides the WIT-generated Rust types that define the contract
8//! between the Glitcher Engine host and WASM plugins. Plugins act as "Definition
9//! Providers" - they supply node metadata and WGSL shader source code.
10//!
11//! ## Architecture
12//!
13//! ```text
14//! WASM Plugin (Guest) Glitcher Engine (Host)
15//! ┌─────────────────┐ ┌─────────────────────┐
16//! │ get-manifest() │────WIT──>│ Load metadata │
17//! │ get-shader() │────WIT──>│ Compile WGSL │
18//! └─────────────────┘ │ Execute on GPU │
19//! └─────────────────────┘
20//! ```
21//!
22//! ## Usage
23//!
24//! ### For Plugin Authors (Guest)
25//!
26//! ```rust,ignore
27//! use glitcher_api::*;
28//!
29//! wit_bindgen::generate!({
30//! world: "plugin",
31//! exports: {
32//! "glitcher:engine/render-node": MyNode,
33//! }
34//! });
35//!
36//! struct MyNode;
37//!
38//! impl Guest for MyNode {
39//! fn get_manifest() -> NodeManifest {
40//! NodeManifest {
41//! api_version: 1,
42//! display_name: "My Effect".into(),
43//! model: ExecutionModel::FragmentShader,
44//! parameters: vec![
45//! ShaderParam {
46//! name: "strength".into(),
47//! data_type: ParamType::F32,
48//! widget: WidgetConfig::Slider(SliderConfig {
49//! min: 0.0,
50//! max: 1.0,
51//! step: 0.01,
52//! }),
53//! }
54//! ],
55//! textures: vec![
56//! TexturePort {
57//! name: "input".into(),
58//! binding_slot: 0,
59//! writable: false,
60//! }
61//! ],
62//! output_resolution_scale: 1.0,
63//! }
64//! }
65//!
66//! fn get_shader_source() -> String {
67//! r#"
68//! struct Params {
69//! strength: f32,
70//! }
71//!
72//! @group(1) @binding(0) var<uniform> params: Params;
73//! @group(2) @binding(0) var input_texture: texture_2d<f32>;
74//!
75//! @fragment
76//! fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
77//! // Your shader code here
78//! return textureSample(input_texture, sampler, uv);
79//! }
80//! "#.into()
81//! }
82//! }
83//! ```
84//!
85//! ### For Host Implementation
86//!
87//! ```rust,ignore
88//! use glitcher_api::*;
89//! use wasmtime::*;
90//!
91//! // Load and instantiate WASM plugin
92//! let module = Module::from_file(&engine, "plugin.wasm")?;
93//! let instance = linker.instantiate(&mut store, &module)?;
94//!
95//! // Call WIT functions
96//! let get_manifest = instance
97//! .get_typed_func::<(), NodeManifest>(&mut store, "get-manifest")?;
98//! let manifest = get_manifest.call(&mut store, ())?;
99//!
100//! // Validate and use manifest
101//! assert_eq!(manifest.api_version, 1);
102//! ```
103//!
104//! ## Type System
105//!
106//! All types are strongly typed through WIT, eliminating string-based parsing:
107//!
108//! - [`ParamType`]: WGSL-compatible parameter types (f32, vec3, mat4, etc.)
109//! - [`WidgetConfig`]: UI widget configurations (slider, color-picker, etc.)
110//! - [`ExecutionModel`]: Fragment shader or compute shader
111//! - [`NodeManifest`]: Complete node definition
112//!
113//! ## Memory Layout
114//!
115//! The host calculates GPU buffer layouts using std140 rules based on [`ParamType`].
116//! Plugins don't need to worry about alignment or padding.
117//!
118//! ## Binding Conventions
119//!
120//! Shaders must follow these binding group conventions:
121//!
122//! - **Group 0**: System globals (time, resolution, mouse) - Host-managed
123//! - **Group 1**: Node parameters - Single uniform buffer at binding 0
124//! - **Group 2**: Textures - Bindings match [`TexturePort::binding_slot`]
125//!
126//! ## API Versioning
127//!
128//! The [`NodeManifest::api_version`] field ensures compatibility:
129//!
130//! - Current version: **1**
131//! - Host rejects plugins with mismatched versions
132//! - Breaking changes increment the version number
133
134// Re-export WIT-generated types
135wit_bindgen::generate!({
136 path: "wit",
137 world: "plugin",
138});
139
140// Re-export from sub-interfaces for backwards compatibility
141pub use exports::glitcher::engine::actions::{
142 ActionConfig, ActionDef, ActionError, BeatInfo, ParamComputeConfig, ParamRangeError, ParamRef,
143 ParamUpdate,
144};
145pub use exports::glitcher::engine::ports::{
146 ControlOutput, NodePort, PortConfig, TextureStorageConfig,
147};
148pub use exports::glitcher::engine::render_node::{
149 CustomCategory, CustomOutputHint, EmbeddedTexture, Guest, NodeCategory, NodeManifest,
150 OutputHint,
151};
152pub use exports::glitcher::engine::types::{
153 ExecutionModel, ParamType, ParamValue, StorageTextureFormat, WorkgroupSize,
154};
155pub use exports::glitcher::engine::widgets::{
156 ButtonGroupConfig, ControlPreviewConfig, CustomWidget, DropdownConfig, ShaderParam,
157 SliderConfig, WidgetConfig,
158};
159
160/// Current API version
161///
162/// This constant should match the version used by the host.
163/// Plugins should use this in their manifest:
164///
165/// ```rust,ignore
166/// NodeManifest {
167/// api_version: glitcher_api::CURRENT_API_VERSION,
168/// // ...
169/// }
170/// ```
171pub const CURRENT_API_VERSION: u32 = 1;
172
173/// Standard action implementations
174pub mod actions;
175
176/// Action registry for name→ID mapping and dispatch
177pub mod registry;
178
179/// Hook system for action execution events
180pub mod hooks;
181
182/// Builtin plugin provider interface
183pub mod builtin;
184
185/// Helper to create a slider widget config
186pub fn slider(min: f32, max: f32, step: f32) -> WidgetConfig {
187 WidgetConfig::Slider(SliderConfig { min, max, step })
188}
189
190/// Helper to create a dropdown widget config
191///
192/// Best for: Many options or detailed work (combobox style)
193pub fn dropdown(options: Vec<String>) -> WidgetConfig {
194 WidgetConfig::Dropdown(DropdownConfig { options })
195}
196
197/// Helper to create a button group widget config
198///
199/// Best for: Live performance with quick visual selection (2-6 options)
200pub fn button_group(options: Vec<String>) -> WidgetConfig {
201 WidgetConfig::ButtonGroup(ButtonGroupConfig { options })
202}
203
204/// Helper to create a compute shader execution model
205pub fn compute_shader(x: u32, y: u32, z: u32) -> ExecutionModel {
206 ExecutionModel::ComputeShader(WorkgroupSize { x, y, z })
207}
208
209/// Helper to create a fragment shader execution model
210pub fn fragment_shader() -> ExecutionModel {
211 ExecutionModel::FragmentShader
212}
213
214// =============================================================================
215// Port Helpers
216// =============================================================================
217
218/// Helper to create a texture input port
219///
220/// # Example
221/// ```rust,ignore
222/// let port = texture_input("input", 0);
223/// ```
224pub fn texture_input(name: &str, binding_slot: u32) -> NodePort {
225 NodePort {
226 name: name.to_string(),
227 config: PortConfig::TextureInput(binding_slot),
228 }
229}
230
231/// Helper to create a texture output port
232///
233/// Note: Most nodes have implicit output (their rendered texture).
234/// Explicit output ports are used for compute shaders with multiple outputs.
235pub fn texture_output(name: &str, binding_slot: u32) -> NodePort {
236 NodePort {
237 name: name.to_string(),
238 config: PortConfig::TextureOutput(binding_slot),
239 }
240}
241
242/// Helper to create a texture storage port (read/write for compute shaders)
243///
244/// # Example
245/// ```rust,ignore
246/// let port = texture_storage("trail_map", 0, StorageTextureFormat::Rgba8Unorm);
247/// let port = texture_storage("agent_map", 1, StorageTextureFormat::Rgba32Float);
248/// ```
249pub fn texture_storage(name: &str, binding_slot: u32, format: StorageTextureFormat) -> NodePort {
250 NodePort {
251 name: name.to_string(),
252 config: PortConfig::TextureStorage(TextureStorageConfig {
253 binding_slot,
254 format,
255 is_input: false,
256 }),
257 }
258}
259
260/// Helper to create a texture storage input port (read/write for compute shaders, accepts connections)
261///
262/// Use this for compute shader ports that need to receive input from other nodes.
263/// The engine will automatically blit from the connected texture output to this storage texture.
264///
265/// # Example
266/// ```rust,ignore
267/// let port = texture_storage_input("input", 0, StorageTextureFormat::Rgba8Unorm);
268/// ```
269pub fn texture_storage_input(
270 name: &str,
271 binding_slot: u32,
272 format: StorageTextureFormat,
273) -> NodePort {
274 NodePort {
275 name: name.to_string(),
276 config: PortConfig::TextureStorage(TextureStorageConfig {
277 binding_slot,
278 format,
279 is_input: true,
280 }),
281 }
282}
283
284/// Helper to create a control output port
285///
286/// Use this for ParamCompute MODs that generate control signals (LFO, envelope, etc.)
287///
288/// ## Note: No control_input helper
289///
290/// All parameters are implicitly controllable - any parameter can receive
291/// modulation from a control-output port without explicit declaration.
292/// The target parameter is selected via UI (ControlTargetSelector widget),
293/// not hardcoded in the MOD.
294///
295/// # Example
296/// ```rust,ignore
297/// let port = control_output_port("lfo_out");
298/// ```
299pub fn control_output_port(name: &str) -> NodePort {
300 NodePort {
301 name: name.to_string(),
302 config: PortConfig::ControlOutput,
303 }
304}
305
306// =============================================================================
307// PortConfig Helper Methods
308// =============================================================================
309
310/// Extension trait for PortConfig helpers
311pub trait PortConfigExt {
312 /// Check if this is a texture port (input, output, or storage)
313 fn is_texture(&self) -> bool;
314 /// Check if this is a texture input port
315 fn is_texture_input(&self) -> bool;
316 /// Check if this is a texture output port
317 fn is_texture_output(&self) -> bool;
318 /// Check if this is a texture storage port
319 fn is_texture_storage(&self) -> bool;
320 /// Check if this is a control output port
321 fn is_control_output(&self) -> bool;
322 /// Check if this is an input port (texture input only - control input is implicit)
323 fn is_input(&self) -> bool;
324 /// Check if this is an output port (texture output, storage, or control output)
325 fn is_output(&self) -> bool;
326 /// Get binding slot (for texture ports)
327 fn binding_slot(&self) -> Option<u32>;
328}
329
330impl PortConfigExt for PortConfig {
331 fn is_texture(&self) -> bool {
332 matches!(
333 self,
334 PortConfig::TextureInput(_)
335 | PortConfig::TextureOutput(_)
336 | PortConfig::TextureStorage(_)
337 )
338 }
339
340 fn is_texture_input(&self) -> bool {
341 matches!(self, PortConfig::TextureInput(_))
342 }
343
344 fn is_texture_output(&self) -> bool {
345 matches!(self, PortConfig::TextureOutput(_))
346 }
347
348 fn is_texture_storage(&self) -> bool {
349 matches!(self, PortConfig::TextureStorage(_))
350 }
351
352 fn is_control_output(&self) -> bool {
353 matches!(self, PortConfig::ControlOutput)
354 }
355
356 fn is_input(&self) -> bool {
357 // Note: No ControlInput - all parameters are implicitly controllable
358 match self {
359 PortConfig::TextureInput(_) => true,
360 PortConfig::TextureStorage(config) => config.is_input,
361 _ => false,
362 }
363 }
364
365 fn is_output(&self) -> bool {
366 match self {
367 PortConfig::TextureOutput(_) => true,
368 PortConfig::ControlOutput => true,
369 // Storage textures with is_input=false are treated as outputs
370 PortConfig::TextureStorage(config) => !config.is_input,
371 _ => false,
372 }
373 }
374
375 fn binding_slot(&self) -> Option<u32> {
376 match self {
377 PortConfig::TextureInput(slot) => Some(*slot),
378 PortConfig::TextureOutput(slot) => Some(*slot),
379 PortConfig::TextureStorage(config) => Some(config.binding_slot),
380 PortConfig::ControlOutput => None,
381 }
382 }
383}
384
385// =============================================================================
386// Test Helpers (cfg(test) only)
387// =============================================================================
388
389/// Test-only builder for NodeManifest with sensible defaults.
390///
391/// Use this in tests to avoid updating every test when new fields are added.
392/// Search for `test_manifest_builder` to find all usages when adding new fields.
393///
394/// # Example
395/// ```rust,ignore
396/// let manifest = test_manifest_builder()
397/// .display_name("My Test Node")
398/// .category(NodeCategory::Effector)
399/// .build();
400/// ```
401#[cfg(any(test, feature = "test-utils"))]
402pub fn test_manifest_builder() -> TestManifestBuilder {
403 TestManifestBuilder::default()
404}
405
406#[cfg(any(test, feature = "test-utils"))]
407#[derive(Default)]
408pub struct TestManifestBuilder {
409 display_name: Option<String>,
410 category: Option<NodeCategory>,
411 parameters: Vec<ShaderParam>,
412 ports: Vec<NodePort>,
413 actions: Vec<ActionDef>,
414}
415
416#[cfg(any(test, feature = "test-utils"))]
417impl TestManifestBuilder {
418 pub fn display_name(mut self, name: &str) -> Self {
419 self.display_name = Some(name.to_string());
420 self
421 }
422
423 pub fn category(mut self, cat: NodeCategory) -> Self {
424 self.category = Some(cat);
425 self
426 }
427
428 pub fn parameters(mut self, params: Vec<ShaderParam>) -> Self {
429 self.parameters = params;
430 self
431 }
432
433 pub fn ports(mut self, ports: Vec<NodePort>) -> Self {
434 self.ports = ports;
435 self
436 }
437
438 pub fn actions(mut self, actions: Vec<ActionDef>) -> Self {
439 self.actions = actions;
440 self
441 }
442
443 pub fn build(self) -> NodeManifest {
444 NodeManifest {
445 api_version: CURRENT_API_VERSION,
446 display_name: self.display_name.unwrap_or_else(|| "Test Node".to_string()),
447 version: "1.0.0".to_string(),
448 author: "Test".to_string(),
449 description: "Test node".to_string(),
450 category: self.category.unwrap_or(NodeCategory::Effector),
451 tags: vec![],
452 model: ExecutionModel::FragmentShader,
453 parameters: self.parameters,
454 ports: self.ports,
455 output_resolution_scale: 1.0,
456 output_hint: None,
457 actions: self.actions,
458 embedded_textures: vec![],
459 }
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 #[test]
468 fn test_api_version_constant() {
469 assert_eq!(CURRENT_API_VERSION, 1);
470 }
471
472 #[test]
473 fn test_slider_helper() {
474 let widget = slider(0.0, 1.0, 0.01);
475 match widget {
476 WidgetConfig::Slider(config) => {
477 assert_eq!(config.min, 0.0);
478 assert_eq!(config.max, 1.0);
479 assert_eq!(config.step, 0.01);
480 }
481 _ => panic!("Expected slider variant"),
482 }
483 }
484
485 #[test]
486 fn test_compute_shader_helper() {
487 let model = compute_shader(8, 8, 1);
488 match model {
489 ExecutionModel::ComputeShader(size) => {
490 assert_eq!(size.x, 8);
491 assert_eq!(size.y, 8);
492 assert_eq!(size.z, 1);
493 }
494 _ => panic!("Expected compute shader variant"),
495 }
496 }
497
498 #[test]
499 fn test_fragment_shader_helper() {
500 let model = fragment_shader();
501 assert!(matches!(model, ExecutionModel::FragmentShader));
502 }
503
504 #[test]
505 fn test_dropdown_helper() {
506 let options = vec![
507 "Normal".to_string(),
508 "Add".to_string(),
509 "Multiply".to_string(),
510 ];
511 let widget = dropdown(options.clone());
512 match widget {
513 WidgetConfig::Dropdown(config) => {
514 assert_eq!(config.options.len(), 3);
515 assert_eq!(config.options[0], "Normal");
516 assert_eq!(config.options[1], "Add");
517 assert_eq!(config.options[2], "Multiply");
518 }
519 _ => panic!("Expected dropdown variant"),
520 }
521 }
522
523 #[test]
524 fn test_button_group_helper() {
525 let options = vec!["A".to_string(), "B".to_string(), "C".to_string()];
526 let widget = button_group(options.clone());
527 match widget {
528 WidgetConfig::ButtonGroup(config) => {
529 assert_eq!(config.options.len(), 3);
530 assert_eq!(config.options[0], "A");
531 }
532 _ => panic!("Expected button_group variant"),
533 }
534 }
535
536 // Action System types tests
537 #[test]
538 fn test_action_config_variants() {
539 let _trigger = ActionConfig::Trigger;
540 let _toggle = ActionConfig::Toggle;
541 let _beat_sync = ActionConfig::BeatSync;
542 }
543
544 #[test]
545 fn test_action_def_creation() {
546 let action = ActionDef {
547 id: "reset".to_string(),
548 label: "Reset".to_string(),
549 config: ActionConfig::Trigger,
550 };
551 assert_eq!(action.id, "reset");
552 assert_eq!(action.label, "Reset");
553 }
554
555 #[test]
556 fn test_param_value_variants() {
557 let _f32_val = ParamValue::ScalarF32(1.0);
558 let _i32_val = ParamValue::ScalarI32(42);
559 let _u32_val = ParamValue::ScalarU32(100);
560 let _bool_val = ParamValue::ScalarBool(true);
561 let _vec2_val = ParamValue::Vec2F32((1.0, 2.0));
562 let _vec3_val = ParamValue::Vec3F32((1.0, 2.0, 3.0));
563 let _vec4_val = ParamValue::Vec4F32((1.0, 2.0, 3.0, 4.0));
564 }
565
566 #[test]
567 fn test_param_update_creation() {
568 let update = ParamUpdate {
569 param_index: 0,
570 value: ParamValue::ScalarF32(0.5),
571 };
572 assert_eq!(update.param_index, 0);
573 }
574
575 #[test]
576 fn test_beat_info_creation() {
577 let beat = BeatInfo {
578 bpm: 120.0,
579 phase: 0.5,
580 bar_position: 0.25,
581 };
582 assert_eq!(beat.bpm, 120.0);
583 assert_eq!(beat.phase, 0.5);
584 assert_eq!(beat.bar_position, 0.25);
585 }
586
587 #[test]
588 fn test_action_error_variants() {
589 let _not_found = ActionError::ActionNotFound("test".to_string());
590 let _invalid_state = ActionError::InvalidState("test".to_string());
591 let _exec_failed = ActionError::ExecutionFailed("test".to_string());
592 let _out_of_range = ActionError::ParameterOutOfRange(ParamRangeError {
593 param: "test".to_string(),
594 value: "1.0".to_string(),
595 });
596 }
597}
598
599// =============================================================================
600// System Globals
601// =============================================================================
602
603/// System globals shared across all nodes
604///
605/// Bound to @group(0) @binding(0) in all shaders.
606/// This provides common values that are the same for all nodes in a render pass.
607///
608/// Layout follows std140 rules:
609/// - vec2 = 8 bytes, 8-byte aligned
610/// - vec4 = 16 bytes, 16-byte aligned
611/// - Total size must be multiple of 16 bytes
612#[repr(C)]
613#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
614pub struct SystemGlobals {
615 /// Time since engine start in seconds
616 pub time: f32,
617 /// Delta time since last frame in seconds
618 pub delta_time: f32,
619 /// Current frame count
620 pub frame_count: u32,
621 /// Padding for std140 alignment (vec4 boundary)
622 _padding1: u32,
623
624 /// Screen resolution in pixels (width, height)
625 pub resolution: [f32; 2],
626 /// Mouse position normalized to 0.0-1.0 (x, y)
627 pub mouse: [f32; 2],
628
629 /// Audio analysis: overall volume (0.0-1.0)
630 pub audio_volume: f32,
631 /// Audio analysis: bass frequencies (0.0-1.0)
632 pub audio_bass: f32,
633 /// Audio analysis: mid frequencies (0.0-1.0)
634 pub audio_mid: f32,
635 /// Audio analysis: high frequencies (0.0-1.0)
636 pub audio_high: f32,
637
638 // === Beat Sync ===
639 /// Current BPM (beats per minute)
640 pub beat_bpm: f32,
641 /// Beat phase (0.0-1.0, 0.0 = on beat)
642 pub beat_phase: f32,
643 /// Cumulative beat count
644 pub beat_count: u32,
645 /// Padding for std140 alignment
646 _padding2: u32,
647
648 // === External Inputs (Creative Coding / SNS) ===
649 /// External input 1 (OSC, sensor, SNS, etc.)
650 pub external_input_1: f32,
651 /// External input 2 (OSC, sensor, SNS, etc.)
652 pub external_input_2: f32,
653 /// External input 3 (OSC, sensor, SNS, etc.)
654 pub external_input_3: f32,
655 /// External input 4 (OSC, sensor, SNS, etc.)
656 pub external_input_4: f32,
657}
658
659impl Default for SystemGlobals {
660 fn default() -> Self {
661 Self {
662 time: 0.0,
663 delta_time: 0.0,
664 frame_count: 0,
665 _padding1: 0,
666 resolution: [1920.0, 1080.0],
667 mouse: [0.5, 0.5],
668 audio_volume: 0.0,
669 audio_bass: 0.0,
670 audio_mid: 0.0,
671 audio_high: 0.0,
672 beat_bpm: 120.0,
673 beat_phase: 0.0,
674 beat_count: 0,
675 _padding2: 0,
676 external_input_1: 0.0,
677 external_input_2: 0.0,
678 external_input_3: 0.0,
679 external_input_4: 0.0,
680 }
681 }
682}
683
684impl SystemGlobals {
685 /// Size in bytes (must be multiple of 16 for std140)
686 /// Layout: time(4) + delta_time(4) + frame_count(4) + pad(4) = 16
687 /// resolution(8) + mouse(8) = 16
688 /// audio_volume(4) + audio_bass(4) + audio_mid(4) + audio_high(4) = 16
689 /// beat_bpm(4) + beat_phase(4) + beat_count(4) + pad(4) = 16
690 /// external_input_1(4) + external_input_2(4) + external_input_3(4) + external_input_4(4) = 16
691 /// Total: 80 bytes
692 pub const SIZE: u64 = 80;
693
694 /// Convert to byte array for GPU upload
695 pub fn as_bytes(&self) -> &[u8] {
696 bytemuck::bytes_of(self)
697 }
698}
699
700// =============================================================================
701// External Trigger State
702// =============================================================================
703
704/// Per-node external trigger state
705///
706/// Bound to @group(4) @binding(0) for nodes with external trigger actions.
707/// Similar to BeatSync, but for external events (OSC, SNS, HTTP, etc.)
708///
709/// ## Memory Layout (std140)
710/// - 4 floats = 16 bytes total
711#[repr(C)]
712#[derive(Clone, Copy, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
713pub struct ExternalTriggerState {
714 /// Time when this node was last triggered (seconds since engine start)
715 pub last_trigger_time: f32,
716
717 /// Trigger type/source identifier (e.g., 0=OSC, 1=SNS mention, 2=HTTP webhook)
718 pub trigger_type: f32,
719
720 /// Intensity/value at trigger time (0.0-1.0, can be > 1.0 for boosted events)
721 pub trigger_intensity: f32,
722
723 /// Custom parameter for event-specific data
724 pub custom_param: f32,
725}
726
727impl ExternalTriggerState {
728 /// Size in bytes (16 bytes, std140 aligned)
729 pub const SIZE: u64 = 16;
730
731 /// Convert to byte array for GPU upload
732 pub fn as_bytes(&self) -> &[u8] {
733 bytemuck::bytes_of(self)
734 }
735}
736
737// =============================================================================
738// Parameter Modulation Types
739// =============================================================================
740
741/// Modulation waveform types for LFO
742#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
743pub enum ModWaveform {
744 /// No modulation
745 #[default]
746 Off,
747 /// Sine wave (smooth oscillation)
748 Sin,
749 /// Sawtooth wave (linear ramp)
750 Saw,
751 /// Square wave (on/off)
752 Square,
753 /// Random (sample & hold)
754 Random,
755}
756
757impl ModWaveform {
758 /// Get display name
759 pub fn name(&self) -> &'static str {
760 match self {
761 ModWaveform::Off => "Off",
762 ModWaveform::Sin => "Sin",
763 ModWaveform::Saw => "Saw",
764 ModWaveform::Square => "Sq",
765 ModWaveform::Random => "Rnd",
766 }
767 }
768
769 /// Get all waveform variants
770 pub fn all() -> &'static [ModWaveform] {
771 &[
772 ModWaveform::Off,
773 ModWaveform::Sin,
774 ModWaveform::Saw,
775 ModWaveform::Square,
776 ModWaveform::Random,
777 ]
778 }
779}
780
781/// Beat division for modulation timing
782#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
783pub enum BeatDivision {
784 /// 1/4 beat
785 Quarter,
786 /// 1/2 beat
787 Half,
788 /// 1 beat (default)
789 #[default]
790 One,
791 /// 2 beats
792 Two,
793 /// 4 beats (1 bar)
794 Four,
795}
796
797impl BeatDivision {
798 /// Get display name
799 pub fn name(&self) -> &'static str {
800 match self {
801 BeatDivision::Quarter => "1/4",
802 BeatDivision::Half => "1/2",
803 BeatDivision::One => "1",
804 BeatDivision::Two => "2",
805 BeatDivision::Four => "4",
806 }
807 }
808
809 /// Get beat multiplier (how many beats per cycle)
810 pub fn multiplier(&self) -> f32 {
811 match self {
812 BeatDivision::Quarter => 0.25,
813 BeatDivision::Half => 0.5,
814 BeatDivision::One => 1.0,
815 BeatDivision::Two => 2.0,
816 BeatDivision::Four => 4.0,
817 }
818 }
819
820 /// Get all beat division variants
821 pub fn all() -> &'static [BeatDivision] {
822 &[
823 BeatDivision::Quarter,
824 BeatDivision::Half,
825 BeatDivision::One,
826 BeatDivision::Two,
827 BeatDivision::Four,
828 ]
829 }
830}
831
832/// Audio frequency band for audio-reactive modulation
833#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
834pub enum AudioBand {
835 /// Low frequencies (bass)
836 #[default]
837 Bass,
838 /// Mid frequencies
839 Mid,
840 /// High frequencies (treble)
841 High,
842 /// Overall volume (average of all bands)
843 Volume,
844}
845
846impl AudioBand {
847 /// Get display name
848 pub fn name(&self) -> &'static str {
849 match self {
850 AudioBand::Bass => "Bass",
851 AudioBand::Mid => "Mid",
852 AudioBand::High => "High",
853 AudioBand::Volume => "Vol",
854 }
855 }
856
857 /// Get all audio band variants
858 pub fn all() -> &'static [AudioBand] {
859 &[
860 AudioBand::Bass,
861 AudioBand::Mid,
862 AudioBand::High,
863 AudioBand::Volume,
864 ]
865 }
866}
867
868/// Audio-reactive modulation configuration
869#[derive(Debug, Clone, Copy, Default)]
870pub struct AudioReactConfig {
871 /// Which frequency band to react to
872 pub band: AudioBand,
873 /// Sensitivity multiplier (0.0 - 2.0, 1.0 = normal)
874 pub sensitivity: f32,
875 /// Smoothing factor (0.0 = instant, 1.0 = very smooth)
876 pub smoothing: f32,
877 /// Whether audio modulation is enabled
878 pub enabled: bool,
879}
880
881/// Audio-reactive parameters from audio analysis
882#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
883pub struct AudioReactiveParams {
884 /// Detected BPM
885 pub bpm: f32,
886 /// Beat intensity (0.0-1.0)
887 pub beat_intensity: f32,
888 /// Low frequency energy (bass)
889 pub low_freq: f32,
890 /// Mid frequency energy
891 pub mid_freq: f32,
892 /// High frequency energy (treble)
893 pub high_freq: f32,
894 /// Time since last beat
895 pub time_since_beat: f32,
896 /// Whether we're currently on a beat
897 pub on_beat: bool,
898}
899
900/// Modulation settings for a single parameter
901///
902/// Supports both LFO (beat-synced oscillation) and audio-reactive modulation.
903/// These can be combined for complex parameter animation.
904#[derive(Debug, Clone, Copy, Default)]
905pub struct ParamModulation {
906 /// LFO waveform type
907 pub waveform: ModWaveform,
908 /// Beat division for LFO timing
909 pub division: BeatDivision,
910 /// LFO modulation depth (0.0 - 1.0)
911 pub depth: f32,
912 /// Minimum value for modulation range
913 pub min: f32,
914 /// Maximum value for modulation range
915 pub max: f32,
916 /// Random state (for Random waveform)
917 pub random_value: f32,
918 /// Audio-reactive modulation config
919 pub audio_react: AudioReactConfig,
920 /// Smoothed audio value (internal state for exponential smoothing)
921 pub smoothed_audio: f32,
922}
923
924impl ParamModulation {
925 /// Create a new modulation with specified range
926 pub fn new(min: f32, max: f32) -> Self {
927 Self {
928 waveform: ModWaveform::Off,
929 division: BeatDivision::One,
930 depth: 1.0,
931 min,
932 max,
933 random_value: 0.5,
934 audio_react: AudioReactConfig::default(),
935 smoothed_audio: 0.0,
936 }
937 }
938
939 /// Calculate modulated value based on beat phase (0.0 - 1.0)
940 pub fn calculate(&self, phase: f32, base_value: f32) -> f32 {
941 let mut result = base_value;
942 let range = self.max - self.min;
943
944 // Apply LFO modulation
945 if self.waveform != ModWaveform::Off {
946 let lfo_value = match self.waveform {
947 ModWaveform::Off => 0.5,
948 ModWaveform::Sin => (phase * std::f32::consts::TAU).sin() * 0.5 + 0.5,
949 ModWaveform::Saw => phase,
950 ModWaveform::Square => {
951 if phase < 0.5 {
952 0.0
953 } else {
954 1.0
955 }
956 }
957 ModWaveform::Random => self.random_value,
958 };
959
960 let lfo_mod = self.min + lfo_value * range * self.depth;
961 result = base_value * (1.0 - self.depth) + lfo_mod * self.depth;
962 }
963
964 // Apply audio-reactive modulation (additive)
965 if self.audio_react.enabled {
966 let audio_mod = self.smoothed_audio * self.audio_react.sensitivity * range;
967 result = (result + audio_mod).clamp(self.min, self.max);
968 }
969
970 result
971 }
972
973 /// Calculate the full modulated value with audio input
974 ///
975 /// Updates internal smoothed audio state and applies all modulation sources.
976 pub fn calculate_with_audio(
977 &mut self,
978 phase: f32,
979 base_value: f32,
980 audio: &AudioReactiveParams,
981 delta_time: f32,
982 ) -> f32 {
983 // Update smoothed audio value
984 if self.audio_react.enabled {
985 let raw_audio = match self.audio_react.band {
986 AudioBand::Bass => audio.low_freq,
987 AudioBand::Mid => audio.mid_freq,
988 AudioBand::High => audio.high_freq,
989 AudioBand::Volume => (audio.low_freq + audio.mid_freq + audio.high_freq) / 3.0,
990 };
991
992 // Exponential smoothing
993 let smooth_factor = 1.0 - self.audio_react.smoothing;
994 let alpha = 1.0 - (-delta_time * 10.0 * smooth_factor).exp();
995 self.smoothed_audio = self.smoothed_audio * (1.0 - alpha) + raw_audio * alpha;
996 }
997
998 self.calculate(phase, base_value)
999 }
1000
1001 /// Check if any modulation is active
1002 pub fn is_active(&self) -> bool {
1003 self.waveform != ModWaveform::Off || self.audio_react.enabled
1004 }
1005}
1006
1007// =============================================================================
1008// Loader Types (shared between glitcher-loader and glitcher-engine)
1009// =============================================================================
1010
1011/// Decoded embedded texture data (ready for GPU upload)
1012///
1013/// This is the result of decoding an embedded texture from a WASM plugin.
1014/// The pixel data is in RGBA8 format, ready to be uploaded to the GPU.
1015#[derive(Debug, Clone)]
1016pub struct DecodedTexture {
1017 /// Unique key for runtime lookup (matches `EmbeddedTexture::key`)
1018 pub key: String,
1019 /// Decoded RGBA8 pixel data
1020 pub pixels: Vec<u8>,
1021 /// Width in pixels
1022 pub width: u32,
1023 /// Height in pixels
1024 pub height: u32,
1025}
1026
1027impl DecodedTexture {
1028 /// Create a new decoded texture
1029 pub fn new(key: impl Into<String>, pixels: Vec<u8>, width: u32, height: u32) -> Self {
1030 Self {
1031 key: key.into(),
1032 pixels,
1033 width,
1034 height,
1035 }
1036 }
1037
1038 /// Get the expected byte size for RGBA8 format
1039 pub fn expected_size(&self) -> usize {
1040 (self.width * self.height * 4) as usize
1041 }
1042
1043 /// Validate that pixel data size matches dimensions
1044 pub fn is_valid(&self) -> bool {
1045 self.pixels.len() == self.expected_size()
1046 }
1047}
1048
1049/// A loaded WASM plugin with extracted metadata
1050///
1051/// This is the output of `PluginLoader::load()` and the input to
1052/// `GlitchEngine::register_plugin()`. It contains everything needed
1053/// to create GPU resources for the plugin.
1054///
1055/// ## Usage
1056///
1057/// ```rust,ignore
1058/// // In glitcher-loader
1059/// let loaded = loader.load(&wasm_bytes)?;
1060///
1061/// // In glitcher-engine
1062/// let node_id = engine.register_plugin(&loaded)?;
1063/// ```
1064#[derive(Debug, Clone)]
1065pub struct LoadedPlugin {
1066 /// Node manifest from `get-manifest()` WIT function
1067 pub manifest: NodeManifest,
1068 /// WGSL shader source from `get-shader-source()` WIT function
1069 pub shader_source: String,
1070 /// Decoded embedded textures (ready for GPU upload)
1071 pub embedded_textures: Vec<DecodedTexture>,
1072}
1073
1074impl LoadedPlugin {
1075 /// Create a new loaded plugin
1076 pub fn new(
1077 manifest: NodeManifest,
1078 shader_source: String,
1079 embedded_textures: Vec<DecodedTexture>,
1080 ) -> Self {
1081 Self {
1082 manifest,
1083 shader_source,
1084 embedded_textures,
1085 }
1086 }
1087
1088 /// Get the display name from manifest
1089 pub fn display_name(&self) -> &str {
1090 &self.manifest.display_name
1091 }
1092
1093 /// Check if this plugin has embedded textures
1094 pub fn has_embedded_textures(&self) -> bool {
1095 !self.embedded_textures.is_empty()
1096 }
1097}