1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
//! # pragma.core
//!
//! A solid, high-performance multithreaded core in pure Rust for
//! game engines and frameworks.
//!
//! The facade: re-exports of the subsystems behind feature flags, and a `prelude`.
//!
//! | Module | Crate | Feature |
//! |---|---|---|
//! | [`base`] | `pragma_base` | always |
//! | [`sync`] | `pragma_sync` | always |
//! | `jobs` | `pragma_jobs` | `jobs` |
//! | `graph` | `pragma_graph` | `graph` |
//! | `tasks` | `pragma_async` | `async` |
//! | `sys` | `pragma_sys` (via `pragma_jobs::sys`) | `jobs` |
//! | `puffin` | `puffin` 0.19 (a re-export of the same version) | `puffin` |
//!
//! The `puffin` feature adds profiler scopes to `scope`/`parallel_*` and to graph tasks;
//! the application marks frames (`puffin::GlobalProfiler::lock().new_frame()`), and recording is
//! enabled with `puffin::set_scopes_on(true)`.
//!
//! ## Semver guarantees
//!
//! The public API of all modules follows semver, except for:
//! - the `puffin` feature and the [`puffin`](https://docs.rs/puffin) re-export: the puffin version
//! is bumped in a minor release (puffin is not 1.0 yet);
//! - items hidden from the documentation (`#[doc(hidden)]`, for example `pragma_sync::__private`):
//! these are internals for the core's own crates.
//!
//! The core logs through `tracing` 0.1: the application installs a subscriber through its own
//! `tracing` dependency.
//!
//! ```
//! use pragma_core::prelude::*;
//!
//! let mut pool = HandlePool::new();
//! let h = pool.insert(42_u32)?;
//! assert_eq!(pool.get(h), Some(&42));
//! # Ok::<(), pragma_core::base::BaseError>(())
//! ```
//!
//! ## Integrating into an engine
//!
//! The core is a library, not a framework: it has no main loop and no global state, and the
//! engine decides how the pieces are wired. These are the decisions to make, with the defaults the
//! core is designed and tested for. The design behind them is in
//! [`docs/architecture.md`](https://github.com/razedproduction/pragma.core/blob/main/docs/architecture.md)
//! §3–6; [`examples/frame_sim`](https://github.com/razedproduction/pragma.core/tree/main/examples/frame_sim)
//! is a complete headless engine loop built this way.
//!
//! 1. **Pool ownership and the number of workers.** Create **one** [`ThreadPool`](jobs::ThreadPool)
//! per process at startup, keep it in the engine's top-level struct and lend `&ThreadPool` to
//! subsystems. Several pools oversubscribe the cores and cannot help each other while waiting.
//! The default config has cores − 1 workers because the thread that calls `FrameGraph::run`,
//! `scope` or `join` runs tasks itself while it waits; if the engine keeps another busy thread
//! of its own (a render thread), subtract it too with
//! [`PoolConfig::with_workers`](jobs::PoolConfig::with_workers). Thread priority and core pinning
//! are optional: a refusal by the OS is a warning, not an error.
//! 2. **The graph in the game loop.** Build the [`FrameGraph`](graph::FrameGraph) once at startup
//! (or when the set of systems changes): ordering mistakes surface there as a
//! [`GraphError`](graph::GraphError) before the first frame. The engine owns the
//! [`Resources`](graph::Resources); between frames it changes them directly through
//! [`get_mut`](graph::Resources::get_mut), during a frame tasks reach them only through
//! [`TaskContext`](graph::TaskContext) and only as declared. A frame that returns an error leaves
//! the graph usable: log it and run the next frame. The graph does not own the resources, so it
//! can also run over an "extract" copy of the state for rendering.
//! 3. **Async results enter the frame through `poll_ready`, not `wait`.** At the start of a frame,
//! before `run`, check the pending [`AsyncHandle`](tasks::AsyncHandle)s with
//! [`poll_ready`](tasks::AsyncHandle::poll_ready) and move finished results into the resources.
//! A blocking [`wait`](tasks::AsyncHandle::wait) belongs to a loading screen between frames; from
//! pool work it returns `AsyncError::WouldBlock` instead of stalling the frame (DECISIONS #25).
//! Never block inside a task: file reads and other synchronous I/O go through
//! [`Executor::unblock`](tasks::Executor::unblock).
//! 4. **Background work stays in `Background`.** Streaming, baking and decompression are
//! `spawn(Priority::Background, ..)` or `spawn_async` (always background). At most
//! [`with_background_limit`](jobs::PoolConfig::with_background_limit) workers (half by default)
//! run them at once, and the main thread never picks them up while it waits, so a frame always
//! has workers. Frame work needs no explicit priority: graph tasks and their `scope`/`join`/
//! `parallel_*` subtasks are `Frame`.
//! 5. **A panic hook that names the task.** Install a hook at startup that reads
//! [`current_task`](jobs::current_task) (no allocations, no locks) and writes the label chain
//! into the crash report; the hook runs on the panicking thread before unwinding. Name your own
//! long-running work with [`labeled`](jobs::labeled) and
//! [`spawn_named`](jobs::ThreadPool::spawn_named) so the report says more than `spawn`.
//! 6. **`panic = "unwind"` or `"abort"`.** With unwinding (the default), a panic in a task is caught
//! and comes back as `JobError::Panicked`, `GraphError::TaskPanicked` or `AsyncError::Panicked`;
//! the worker survives and the engine chooses whether to skip the frame or shut down. With
//! `panic = "abort"` nothing is caught: the process ends, and the panic hook (item 5) is the only
//! place to report the culprit. Either way, a panic in the thread that is waiting inside
//! `scope`/`join` aborts on purpose, because the tasks borrow its stack (DECISIONS #15).
//! 7. **Shutdown order.** Stop starting frames, then drop (cancel) or
//! [`detach`](tasks::AsyncHandle::detach) the pending async handles, drop the
//! [`Executor`](tasks::Executor) (it waits for `unblock` closures that are already running, so
//! keep them short), then the graph and the resources, and the pool last. Dropping the pool
//! drains its queues, or drops them with [`ShutdownPolicy::Cancel`](jobs::ShutdownPolicy::Cancel),
//! and joins every worker. Struct fields drop in declaration order, so declaring them in this
//! order is enough. Never drop the pool from inside one of its own tasks.
//! 8. **Determinism.** The core gives the same result on any number of workers: the
//! [`Grain`](jobs::Grain) split ignores the worker count, `parallel_reduce` combines in chunk
//! order, and the graph orders every conflict. Keep the engine's side the same way: count time
//! in [`Tick`](base::Tick)s rather than `Instant`, do not let results depend on
//! `current_worker_index` or on completion order, and apply an async result at a tick fixed in
//! advance rather than in whichever frame it happens to finish (DECISIONS #26). Then check it the
//! way `frame_sim --verify` does: the same scenario on 1 and N workers must give the same state
//! hash.
//!
//! The skeleton of such an engine, with the fields in shutdown order:
//!
//! ```
//! use pragma_core::graph::{FrameGraph, GraphError, Resources};
//! use pragma_core::jobs::{PoolConfig, ThreadPool};
//! use pragma_core::tasks::{AsyncConfig, AsyncHandle, Executor};
//!
//! struct Level(Vec<u8>);
//! struct Bodies(Vec<f32>);
//!
//! struct Engine {
//! loading: Option<AsyncHandle<Vec<u8>>>, // dropped first: cancels the load
//! exec: Executor,
//! graph: FrameGraph,
//! world: Resources,
//! pool: ThreadPool, // dropped last: joins the workers
//! }
//!
//! impl Engine {
//! fn new() -> Result<Self, Box<dyn std::error::Error>> {
//! std::panic::set_hook(Box::new(|info| {
//! pragma_core::jobs::current_task(|task| match task {
//! Some(task) => eprintln!("panic in {task}: {info}"),
//! None => eprintln!("panic: {info}"),
//! });
//! }));
//! let pool = ThreadPool::new(PoolConfig::default().with_name_prefix("engine"))?;
//! let exec = Executor::new(&pool, AsyncConfig::default())?;
//!
//! let mut g = FrameGraph::builder();
//! g.task("physics", |t| t.reads::<Level>().writes::<Bodies>(), |ctx| -> Result<(), GraphError> {
//! let gravity = f32::from(ctx.read::<Level>()?.0.first().copied().unwrap_or(0));
//! let mut bodies = ctx.write::<Bodies>()?;
//! ctx.pool().parallel_for(&mut bodies.0, Default::default(), |v| *v -= gravity)?;
//! Ok(())
//! });
//! let graph = g.build()?; // ordering mistakes fail here, at startup
//!
//! let mut world = Resources::new();
//! world.insert(Level(Vec::new()));
//! world.insert(Bodies(vec![0.0; 10_000]));
//! // std::fs::read("level.bin") in a real engine
//! let loading = Some(exec.unblock(|| vec![1_u8, 2, 3]));
//! Ok(Self { loading, exec, graph, world, pool })
//! }
//!
//! fn frame(&mut self) -> Result<(), GraphError> {
//! // 1. Async results enter the frame between frames, without blocking.
//! if let Some(result) = self.loading.as_mut().and_then(AsyncHandle::poll_ready) {
//! self.loading = None;
//! match result {
//! Ok(bytes) => {
//! self.world.insert(Level(bytes));
//! }
//! Err(err) => eprintln!("level failed to load: {err}"),
//! }
//! }
//! // 2. The frame itself: the calling thread works too.
//! self.graph.run(&self.pool, &mut self.world)
//! }
//! }
//!
//! let mut engine = Engine::new()?;
//! for _ in 0..3 {
//! if let Err(err) = engine.frame() {
//! eprintln!("frame failed: {err}"); // the graph stays usable
//! }
//! }
//! while engine.loading.is_some() {
//! engine.frame()?;
//! }
//! assert!(engine.world.get::<Level>().is_some_and(|l| l.0 == [1, 2, 3]));
//! drop(engine); // fields in declaration order: handles, executor, graph, resources, pool
//! # let _ = std::panic::take_hook();
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
pub use pragma_base as base;
/// Synchronization primitives (the stable part of `pragma_sync`).
pub use pragma_jobs as jobs;
/// The platform layer: thread names, priorities, core pinning.
pub use sys;
/// The `puffin` version the core is built with: mark frames through it.
pub use puffin;
pub use pragma_graph as graph;
pub use pragma_async as tasks;
/// Frequently used types in a single import.
///
/// ```
/// use pragma_core::prelude::*;
///
/// let pool = ThreadPool::new(PoolConfig::default().with_workers(2))?;
/// let mut values = vec![1_u64; 1024];
/// pool.parallel_for(&mut values, Grain::Auto, |v| *v *= 3)?;
/// let total = pool.spawn(Priority::Normal, || 6_u64).join()?;
/// assert_eq!(values.iter().sum::<u64>() + total, 3078);
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```