pragma_core 0.6.0

Solid, high-performance multithreaded core in pure Rust for game engines and frameworks
Documentation

pragma.core

CI Version MSRV License Status

pragma.core is a pure-Rust, solid and high-performance multithreaded core for razed.production's game engines and frameworks.

It is the layer under an engine: it owns the threads, schedules the work of a frame and runs background I/O, so that ECS, rendering, physics and audio can be written on top of it without each inventing their own thread pool.

Status

0.6.0 — feature-complete for 1.0, public API reviewed. Milestones M0–M5 are done: the work-stealing job system, the per-frame task graph, async tasks, observability, tuning and the hardening pass (determinism suite, benchmark gate and semver checks in CI). M6 is in progress: the 1.0 API review, the move of all documentation to English and the MSRV and versioning policy, and complete rustdoc with examples and an embedding checklist are done; the crates.io release is next. Until 1.0, minor versions may break the API; every break is listed in the changelog. See the roadmap.

Features

Area Crate Facade
Typed errors, generational handles (Handle, HandlePool), simulation ticks pragma_base base
Sync primitives: latches, counters, task labels (plus the scheduler's internal building blocks) pragma_sync sync
Platform layer: thread names, priorities, core affinity (Windows/Linux) pragma_sys sys
Work-stealing pool: join, scope, spawn, parallel_for, parallel_reduce, priorities pragma_jobs jobs
Per-frame task graph with declared resource access, validated before the first frame pragma_graph graph
Async tasks on the pool, bounded blocking pool for synchronous I/O pragma_async tasks
Vertical slice: 100k entities, 9-system graph, background load, per-tick state hash examples/frame_sim —

Highlights:

  • Help-while-wait. Every wait (join, scope, JobHandle::join) runs other jobs, so nested parallelism never deadlocks, even on a single worker.
  • Panics are errors. A panicking job, graph task or future returns JobError::Panicked / AsyncError::Panicked; the worker survives and nothing hangs.
  • Conflicts are caught at build time. A graph whose tasks touch the same resource without an explicit order fails in build() with GraphError::AmbiguousOrder. It cannot become a data race in frame 10 000.
  • Allocation-free frames. After the warm-up frame, a 50-task graph runs with zero heap allocations, with or without a tracing subscriber.
  • Deterministic. Results do not depend on which thread ran what: frame_sim produces the same per-tick hash on 1, 2 and N workers, and CI checks this on every PR.
  • Observable. PoolStats snapshots, tracing spans, optional puffin scopes, a debug-only watchdog for stuck jobs, and an allocation-free "which task is running here" query for panic hooks and crash reports.
  • Bounded memory. Input-sized allocations go through try_reserve or a hard limit: you get GraphError::TooLarge / BaseError::OutOfMemory instead of an abort.

Quick look

use pragma_core::prelude::*;

struct Velocity(f32);
struct Positions(Vec<f32>);

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let pool = ThreadPool::new(PoolConfig::default())?;

    // Fork-join and data-parallel loops; jobs may borrow from the stack.
    let (a, b) = pool.join(|| 1 + 1, || 2 + 2)?;
    assert_eq!((a, b), (2, 4));

    let mut data = vec![1_u32; 10_000];
    pool.parallel_for(&mut data, Grain::Auto, |x| *x *= 2)?;

    // Frame graph: declare what each task reads and writes; ordering is validated once.
    let mut g = FrameGraph::builder();
    g.task("input", |t| t.writes::<Velocity>(), |ctx| -> Result<(), GraphError> {
        ctx.write::<Velocity>()?.0 = 0.5;
        Ok(())
    });
    g.task("move", |t| t.reads::<Velocity>().writes::<Positions>().after("input"), |ctx| {
        let v = ctx.read::<Velocity>()?.0;
        let mut pos = ctx.write::<Positions>()?;
        ctx.pool().parallel_for(&mut pos.0, Grain::Auto, |p| *p += v)?;
        Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
    });
    let mut graph = g.build()?;

    let mut world = Resources::new();
    world.insert(Velocity(0.0));
    world.insert(Positions(vec![0.0; 100_000]));
    graph.run(&pool, &mut world)?; // one frame

    // Background work: async on the pool, blocking I/O in a bounded side pool.
    let exec = Executor::new(&pool, AsyncConfig::default())?;
    let io = exec.clone();
    let mut load = exec.spawn_async(async move {
        let bytes = io.unblock(|| vec![0_u8; 64]).await?; // std::fs::read(...) in a real game
        Ok::<usize, AsyncError>(bytes.len())
    });
    let size = loop {
        if let Some(result) = load.poll_ready() {
            break result??; // check once per frame, never block the frame
        }
        std::thread::yield_now();
    };
    assert_eq!(size, 64);
    Ok(())
}

More examples are in the crate docs (cargo doc --open -p pragma_core) and in examples/frame_sim.

Using it

pragma.core is not on crates.io yet (planned for 1.0). Depend on it via git:

[dependencies]

pragma_core = { git = "https://github.com/razedproduction/pragma.core" }

Feature Default Enables
jobs ✅ ThreadPool and the platform layer (sys)
graph ✅ FrameGraph, Resources (implies jobs)
async ✅ Executor, AsyncHandle (implies jobs)
puffin — Profiler scopes for pool operations and graph tasks; re-exports puffin

base and sync are always available. Each feature builds and passes clippy on its own (cargo hack --each-feature in CI).

MSRV policy

The minimum supported Rust version is 1.88 (rust-version in [workspace.package], DECISIONS #10), the minimum required by proptest.

  • Checked in CI. The MSRV (1.88) job runs cargo +1.88 check --workspace --all-features on every PR and every push to main. Development itself uses the toolchain pinned in rust-toolchain.toml (1.99.0), so lints are identical locally and in CI.
  • Raised only in a minor release. A higher MSRV is not treated as a breaking change, but it never ships in a patch release: it goes into the next minor version (1.x.0; 0.x.0 before 1.0), with its own line in the changelog and a decision record explaining why (DECISIONS #44).
  • One change, three places. rust-version, the CI msrv job and the badge above move together in the same PR.

Versioning

pragma.core follows SemVer; the rules are in docs/reliability.md §8.

  • Lockstep. All crates share one version (version in [workspace.package], currently 0.6.0): the pragma_core facade exposes all of them, so separate versions would give users nothing (DECISIONS #33). A breaking change in any crate is a breaking release of all of them.
  • Exact internal dependencies. The crates depend on each other with =X.Y.Z, so pragma_jobs 1.0.0 never builds against a pragma_sync 1.0.1 whose internals have moved (DECISIONS #42). Depend on pragma_core, or keep all pragma_* crates at the same version.
  • Outside semver. No compatibility guarantees cover:
    • pragma_sync::__private (hidden from the docs): the scheduler's building blocks, which change with every optimization of join/scope;
    • the puffin feature and the pragma_core::puffin re-export: puffin is not 1.0 yet, and moving to a new puffin version is a minor release.
  • Breaking changes. Before 1.0, a minor version (0.x.0) may break the API, and every break is listed in the changelog. After 1.0, breaks happen only in a major release; an item is first marked #[deprecated(since, note)] for at least one minor release.
  • Checked in CI. cargo semver-checks --workspace runs on every PR against the PR's base on main (not against crates.io or a tag): a breaking change that comes without a version bump fails the PR that introduces it.

Performance

Windows 10, AMD FX-8350 (8 logical cores), cargo bench. Ranges are min…max across A/B rounds. Full method and numbers are in docs/perf.md.

Benchmark pragma.core rayon
single join 284…288 ns 1.47…1.51 µs
fib(20), ~11k nested join 197…200 µs 99…108 µs
parallel_for, 10⁶ elements 454…478 µs 477…508 µs
parallel_reduce, 10⁶ elements 198…214 µs 230…282 µs
frame graph, 50 near-empty tasks 8.3…8.4 µs per frame —
frame_sim, 100k entities ~5 ms/tick on 1 worker, ~2 ms on 11 —

Nested fork-join is the one place where rayon is still ahead (about 2×). A CI gate fails any PR that regresses a benchmark by more than 10% beyond runner noise.

Reliability

Reliability here is a set of properties that CI checks on every PR. Details are in docs/reliability.md.

  • Pure Rust. No C/C++ compiled into the dependency tree; enforced by cargo deny and tools/audit_workspace.py. System FFI (windows-sys, libc) only.
  • No panics in the runtime. unwrap/expect/panic!/unchecked indexing are denied by lints; failures are #[non_exhaustive] typed errors with context.
  • Contained unsafe. Only in pragma_sync (lifetime erasure, on-stack jobs, task labels) and pragma_sys (OS calls). Every occurrence has a budget in layers.toml; any growth needs a new decision record. All of it is reviewed in docs/unsafe.md.
  • Verified concurrency. Every atomic protocol has a loom model. pragma_sync runs under Miri, and a one-worker pool runs under Miri with Tree Borrows.
  • Tested in depth. Unit, property (proptest) and stress tests, plus fuzzing of the graph builder against an independent oracle. Nightly CI runs 30-minute fuzzing and soak tests.
  • Layered. Each crate depends only on lower layers (layers.toml):
L4  pragma_core     facade: re-exports, prelude, features
L3  pragma_graph    frame graph           pragma_async   async tasks
L2  pragma_jobs     work-stealing pool
L1  pragma_sync     sync primitives       pragma_sys     platform layer
L0  pragma_base     errors, handles, ticks

Platforms

Windows and Linux (x86_64) are first-class and tested in CI. Other targets are not supported yet; the API does not rule out a single-threaded mode for wasm later. See DECISIONS.md.

Building

cargo test --workspace --all-features

cargo run -p frame_sim --release -- --ticks 300 --verify 1,4

The toolchain is pinned in rust-toolchain.toml (1.99.0); the minimum supported Rust version is 1.88 (MSRV policy). See CONTRIBUTING.md for the full check list (Miri, loom, fuzz, benches).

Documentation

License

MIT © 2026 razed