Skip to main content

pixelflow_core/
core.rs

1//! Core runtime owner for per-instance registries and plugin loading.
2
3use std::path::PathBuf;
4
5use crate::plugin_host::{load_plugins_from_directories, platform_plugin_directories};
6use crate::{
7    FilterRegistry, LoadedPlugin, Logger, OrderedRender, RenderEngine, RenderExecutorMap,
8    RenderOptions, Result,
9};
10
11/// Configuration used when creating a [`Core`].
12#[derive(Clone)]
13pub struct CoreConfig {
14    auto_load_plugins: bool,
15    plugin_directories: Vec<PathBuf>,
16    logger: Logger,
17    worker_threads: usize,
18}
19
20impl Default for CoreConfig {
21    fn default() -> Self {
22        Self {
23            auto_load_plugins: true,
24            plugin_directories: Vec::new(),
25            logger: Logger::default(),
26            worker_threads: std::thread::available_parallelism().map_or(1, usize::from),
27        }
28    }
29}
30
31impl CoreConfig {
32    /// Enables or disables conventional plugin directory scanning.
33    #[must_use]
34    pub const fn with_auto_load_plugins(mut self, enabled: bool) -> Self {
35        self.auto_load_plugins = enabled;
36        self
37    }
38
39    /// Replaces logger used for plugin diagnostics.
40    #[must_use]
41    pub fn with_logger(mut self, logger: Logger) -> Self {
42        self.logger = logger;
43        self
44    }
45
46    /// Sets per-core worker thread count. Zero becomes one.
47    #[must_use]
48    pub fn with_worker_threads(mut self, worker_threads: usize) -> Self {
49        self.worker_threads = worker_threads.max(1);
50        self
51    }
52
53    /// Returns mutable plugin directory overrides.
54    #[must_use]
55    pub const fn plugin_directories_mut(&mut self) -> &mut Vec<PathBuf> {
56        &mut self.plugin_directories
57    }
58
59    pub(crate) const fn auto_load_plugins(&self) -> bool {
60        self.auto_load_plugins
61    }
62
63    pub(crate) fn plugin_directories(&self) -> &[PathBuf] {
64        &self.plugin_directories
65    }
66
67    pub(crate) const fn logger(&self) -> &Logger {
68        &self.logger
69    }
70
71    /// Returns configured per-core worker thread count.
72    #[must_use]
73    pub const fn worker_threads(&self) -> usize {
74        self.worker_threads
75    }
76}
77
78/// Per-instance PixelFlow host state.
79pub struct Core {
80    registry: FilterRegistry,
81    config: CoreConfig,
82    loaded_plugins: Vec<LoadedPlugin>,
83}
84
85impl Core {
86    /// Creates a core using default configuration.
87    pub fn new() -> Result<Self> {
88        Self::with_config(CoreConfig::default())
89    }
90
91    /// Creates a core using explicit configuration.
92    pub fn with_config(config: CoreConfig) -> Result<Self> {
93        let mut registry = FilterRegistry::new();
94        let mut directories = config.plugin_directories().to_vec();
95        if config.auto_load_plugins() {
96            directories.extend(platform_plugin_directories());
97        }
98        let loaded_plugins =
99            load_plugins_from_directories(&directories, &mut registry, config.logger());
100
101        Ok(Self {
102            registry,
103            config,
104            loaded_plugins,
105        })
106    }
107
108    /// Returns immutable registry access.
109    #[must_use]
110    pub const fn registry(&self) -> &FilterRegistry {
111        &self.registry
112    }
113
114    /// Returns mutable registry access for in-process registration.
115    pub const fn registry_mut(&mut self) -> &mut FilterRegistry {
116        &mut self.registry
117    }
118
119    /// Returns core configuration.
120    #[must_use]
121    pub const fn config(&self) -> &CoreConfig {
122        &self.config
123    }
124
125    /// Returns plugins loaded during core construction.
126    #[must_use]
127    pub fn loaded_plugins(&self) -> &[LoadedPlugin] {
128        &self.loaded_plugins
129    }
130
131    /// Creates render engine using this core's worker configuration.
132    #[must_use]
133    pub const fn render_engine(&self) -> RenderEngine {
134        RenderEngine::new(crate::WorkerPoolConfig::new(self.config.worker_threads()))
135    }
136
137    /// Starts blocking ordered rendering using this core's worker configuration.
138    pub fn render_ordered(
139        &self,
140        graph: crate::Graph,
141        executors: RenderExecutorMap,
142        options: RenderOptions,
143    ) -> Result<OrderedRender> {
144        self.render_engine()
145            .render_ordered(graph, executors, options)
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use std::path::PathBuf;
152
153    use crate::{Core, CoreConfig, FilterDescriptor};
154
155    #[test]
156    fn core_can_disable_plugin_auto_load_for_deterministic_tests() {
157        let core = Core::with_config(CoreConfig::default().with_auto_load_plugins(false))
158            .expect("core should construct without scanning plugin directories");
159
160        assert!(core.registry().filter_names().is_empty());
161    }
162
163    #[test]
164    fn core_preserves_pre_registered_filters_when_plugin_load_fails() {
165        let mut config = CoreConfig::default().with_auto_load_plugins(false);
166        config
167            .plugin_directories_mut()
168            .push(PathBuf::from("/path/that/does/not/exist"));
169        let mut core = Core::with_config(config).expect("core should construct");
170
171        core.registry_mut()
172            .register_filter(FilterDescriptor::new("crop", "pixelflow", "crop"))
173            .expect("built-in filter should register");
174
175        assert!(core.registry().contains_filter("crop"));
176    }
177
178    #[test]
179    fn core_config_defaults_to_available_worker_threads() {
180        let config = CoreConfig::default();
181
182        assert!(config.worker_threads() >= 1);
183    }
184
185    #[test]
186    fn core_config_accepts_explicit_worker_count_for_cli() {
187        let config = CoreConfig::default().with_worker_threads(3);
188
189        assert_eq!(config.worker_threads(), 3);
190    }
191
192    #[test]
193    fn core_config_clamps_zero_workers_to_one() {
194        let config = CoreConfig::default().with_worker_threads(0);
195
196        assert_eq!(config.worker_threads(), 1);
197    }
198
199    #[test]
200    fn core_render_ordered_uses_configured_worker_count() {
201        let core = Core::with_config(
202            CoreConfig::default()
203                .with_auto_load_plugins(false)
204                .with_worker_threads(1),
205        )
206        .expect("core should construct");
207
208        assert_eq!(core.render_engine().worker_threads(), 1);
209    }
210}