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}